mirror of
https://gitlab.com/cable-mc/cobblemon.git
synced 2026-10-27 12:36:24 -04:00
Compare commits
436
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58d5478b30 | ||
|
|
2bde3058dc | ||
|
|
ffdbbe0e21 | ||
|
|
760f48d850 | ||
|
|
bf296b4921 | ||
|
|
49886389cf | ||
|
|
643bf9068b | ||
|
|
53f3258246 | ||
|
|
1e9895ec84 | ||
|
|
52dacb03ae | ||
|
|
85b0f3f393 | ||
|
|
a04a91bf24 | ||
|
|
5e814f7ab5 | ||
|
|
2780f491d6 | ||
|
|
2d3ec7aff1 | ||
|
|
cb7baf2696 | ||
|
|
5cc069df78 | ||
|
|
9222476c1f | ||
|
|
f220755f99 | ||
|
|
d55bbf237c | ||
|
|
83a9d44c3a | ||
|
|
c5a802eaf6 | ||
|
|
2e27875460 | ||
|
|
cd66887faa | ||
|
|
d665c87c21 | ||
|
|
044d3102b6 | ||
|
|
c421a93b06 | ||
|
|
2865aa2272 | ||
|
|
7c4aae3e9d | ||
|
|
d9f671f2a8 | ||
|
|
69733b530b | ||
|
|
d4471c30bc | ||
|
|
185f7e03f5 | ||
|
|
a45eb0eb21 | ||
|
|
ed67142379 | ||
|
|
13169c6848 | ||
|
|
8be134ee2f | ||
|
|
8c3206a413 | ||
|
|
4d4c071b95 | ||
|
|
b82c27754f | ||
|
|
63afec184e | ||
|
|
65699f045e | ||
|
|
d02441a273 | ||
|
|
f76ab784b5 | ||
|
|
a566e36754 | ||
|
|
ef1479fde6 | ||
|
|
d6c7b271e4 | ||
|
|
df9acfa0d1 | ||
|
|
9d7fc8435e | ||
|
|
ab4492ac5c | ||
|
|
cb78dc8d88 | ||
|
|
020adabe26 | ||
|
|
08aca7a274 | ||
|
|
2a9ef2b9fe | ||
|
|
90dc489547 | ||
|
|
5ec5cfbc4a | ||
|
|
1ef65449a8 | ||
|
|
c6ff1ea72e | ||
|
|
cbfa8d3e78 | ||
|
|
5098ff3cfd | ||
|
|
7c32b07d3f | ||
|
|
b2142fed1d | ||
|
|
fef21516e9 | ||
|
|
6fba0f55c1 | ||
|
|
95d43de79f | ||
|
|
0be617b8ab | ||
|
|
c02d22f449 | ||
|
|
846b645856 | ||
|
|
25d93a771c | ||
|
|
f16c782336 | ||
|
|
ed0f315e41 | ||
|
|
d20b03f4c5 | ||
|
|
a3920413ac | ||
|
|
92bccf620f | ||
|
|
15f8a9ee71 | ||
|
|
dfb9ee755e | ||
|
|
68f4117e29 | ||
|
|
f74a24a056 | ||
|
|
e5750cbba0 | ||
|
|
c6482c82cd | ||
|
|
b4c23a9132 | ||
|
|
c470894bd3 | ||
|
|
a940e32f8d | ||
|
|
b6b59eccad | ||
|
|
b520b94fae | ||
|
|
9af99d16f7 | ||
|
|
2cc17d3280 | ||
|
|
20b2bcaa5a | ||
|
|
d1ada605fd | ||
|
|
d37f73626d | ||
|
|
73a94cb02e | ||
|
|
5a48945e50 | ||
|
|
476448a2ce | ||
|
|
bcba4f7fb7 | ||
|
|
20e9608ff3 | ||
|
|
3e5733ceaf | ||
|
|
af29c99b03 | ||
|
|
60d596a24f | ||
|
|
3f993e0466 | ||
|
|
9e3d22ee54 | ||
|
|
e86cb1981f | ||
|
|
e94f265adb | ||
|
|
b3fc35240a | ||
|
|
d74618bddb | ||
|
|
8b54a8d459 | ||
|
|
c17104a0fd | ||
|
|
88ca3ebbf3 | ||
|
|
4003456e5c | ||
|
|
1d21a52d28 | ||
|
|
2d49c62f85 | ||
|
|
20fac724f6 | ||
|
|
0807d73646 | ||
|
|
3013302f35 | ||
|
|
879e7bef21 | ||
|
|
557623dfe2 | ||
|
|
a28d1e176d | ||
|
|
f3c52db70d | ||
|
|
525575b0ba | ||
|
|
a55efeb378 | ||
|
|
da18b082a6 | ||
|
|
7511facebc | ||
|
|
1486f5cfc2 | ||
|
|
f8a16e5081 | ||
|
|
558e2d89ff | ||
|
|
ea7b688655 | ||
|
|
fdafa3435c | ||
|
|
f17c90f6ea | ||
|
|
1dc45669bb | ||
|
|
f3658614a4 | ||
|
|
cad39d8802 | ||
|
|
422f60f587 | ||
|
|
8190ddf0e9 | ||
|
|
af281d0bde | ||
|
|
00905b8c32 | ||
|
|
79b3cb8611 | ||
|
|
797e908621 | ||
|
|
3df3c395de | ||
|
|
b2f682ebb1 | ||
|
|
754e9bc158 | ||
|
|
2df8504139 | ||
|
|
2838f97c56 | ||
|
|
68c75deeeb | ||
|
|
3bc2659740 | ||
|
|
89bba8a67a | ||
|
|
cbd5cdaf92 | ||
|
|
5b9d5cbb55 | ||
|
|
6d6e13be83 | ||
|
|
899bec77b6 | ||
|
|
6dd8325772 | ||
|
|
e9bffd3ad7 | ||
|
|
ff890307c2 | ||
|
|
dda9d3e790 | ||
|
|
1acc02a2ec | ||
|
|
460c472c55 | ||
|
|
69c3b6142d | ||
|
|
9a036a4c01 | ||
|
|
0fbe206893 | ||
|
|
3cea6dd175 | ||
|
|
2fa053f833 | ||
|
|
ea4f858919 | ||
|
|
2aa4ab4b52 | ||
|
|
113977990e | ||
|
|
d86021e2a1 | ||
|
|
79faadef59 | ||
|
|
9b6629db7f | ||
|
|
d151a1ce65 | ||
|
|
d9ca7b483d | ||
|
|
708f7fc1b8 | ||
|
|
621db85845 | ||
|
|
b09066c580 | ||
|
|
7c45f28827 | ||
|
|
9c1b2cae22 | ||
|
|
16b2f81bd4 | ||
|
|
1969af65b9 | ||
|
|
3bd3ce5688 | ||
|
|
108f0e3d88 | ||
|
|
4344c127eb | ||
|
|
881a87216b | ||
|
|
1d473b82af | ||
|
|
6e0c005587 | ||
|
|
5955547c6f | ||
|
|
7a3a1cb8de | ||
|
|
1674cb0913 | ||
|
|
152ce4e923 | ||
|
|
3a6d024217 | ||
|
|
3b722068b0 | ||
|
|
f2fcc5ba79 | ||
|
|
db0fb8e287 | ||
|
|
dae36a67d1 | ||
|
|
6f99862288 | ||
|
|
0377747e1e | ||
|
|
6bc974f90f | ||
|
|
aca53de259 | ||
|
|
41bde38727 | ||
|
|
a4b7ee3b38 | ||
|
|
154b653c57 | ||
|
|
04489bc969 | ||
|
|
25499b3e7a | ||
|
|
bb6bda54dc | ||
|
|
9f3dbdc2d8 | ||
|
|
932148338c | ||
|
|
e0d0c38232 | ||
|
|
a0e732dc6e | ||
|
|
309f674853 | ||
|
|
ea20c2aa4c | ||
|
|
da5272c473 | ||
|
|
dc705c7720 | ||
|
|
a785e574e0 | ||
|
|
4b236b0268 | ||
|
|
1cd86ae5d6 | ||
|
|
b7489a0b14 | ||
|
|
f7554425dc | ||
|
|
9bfc577444 | ||
|
|
217d4cda42 | ||
|
|
08daa43aae | ||
|
|
184d889831 | ||
|
|
c6e0f431ae | ||
|
|
93daeab840 | ||
|
|
f132b297c6 | ||
|
|
858d2e908a | ||
|
|
b61e0b0c82 | ||
|
|
4d9a6e71c9 | ||
|
|
a0010de504 | ||
|
|
cb73a30e23 | ||
|
|
905d63e098 | ||
|
|
1ea8ab7dcc | ||
|
|
a45d21b279 | ||
|
|
3ca6d350fe | ||
|
|
2997ce8457 | ||
|
|
ff279d8310 | ||
|
|
9a7536b068 | ||
|
|
bd973e702e | ||
|
|
ede1851d34 | ||
|
|
cdb8ee0d8e | ||
|
|
e7d86a382c | ||
|
|
eb911df830 | ||
|
|
1c30acc53c | ||
|
|
32b0699235 | ||
|
|
e9c6580230 | ||
|
|
f893eb48fd | ||
|
|
232e2f18bf | ||
|
|
c266c99710 | ||
|
|
2a0e8f6d47 | ||
|
|
e6bd8ec529 | ||
|
|
911a95e873 | ||
|
|
447cfc2ae5 | ||
|
|
8dfeb0d260 | ||
|
|
caa7b19833 | ||
|
|
91d6fb62ba | ||
|
|
8f4a5b2015 | ||
|
|
07604c9a3b | ||
|
|
64dd8ae193 | ||
|
|
156ee6f290 | ||
|
|
c0a524a718 | ||
|
|
fe433d1417 | ||
|
|
efe8f34413 | ||
|
|
0dc662d295 | ||
|
|
081e370bfd | ||
|
|
3ef0567287 | ||
|
|
3cad9e5481 | ||
|
|
dc8b7908be | ||
|
|
85085a1eca | ||
|
|
bff59dee6e | ||
|
|
bb5828531a | ||
|
|
cd08e98c28 | ||
|
|
6bb23582c4 | ||
|
|
56ccb43997 | ||
|
|
bf7d40c9b5 | ||
|
|
5abb3d7f97 | ||
|
|
975ad8186a | ||
|
|
26b6419f04 | ||
|
|
331f7ca425 | ||
|
|
f445c05333 | ||
|
|
71c9ac0ff0 | ||
|
|
1404cc0ab5 | ||
|
|
a62485fc1f | ||
|
|
044f3080ff | ||
|
|
28b0cab906 | ||
|
|
82f34d651c | ||
|
|
6ca908a91d | ||
|
|
0948b8da9a | ||
|
|
22e04a53b3 | ||
|
|
df3217b41b | ||
|
|
9ff15a1913 | ||
|
|
89e43945a3 | ||
|
|
bd8dcddf35 | ||
|
|
dd372f3160 | ||
|
|
c56aab28d4 | ||
|
|
ca58801794 | ||
|
|
38fa5e2628 | ||
|
|
fae1ae7124 | ||
|
|
3ec5f76c73 | ||
|
|
5fbbcb5707 | ||
|
|
5bb8aaba55 | ||
|
|
23ba41e981 | ||
|
|
29b049452b | ||
|
|
0b0f665804 | ||
|
|
5d53fba2ce | ||
|
|
92b0ef84e9 | ||
|
|
dc8d18384c | ||
|
|
44c056ecd5 | ||
|
|
dcc707c20d | ||
|
|
14cc851562 | ||
|
|
7223d1fd68 | ||
|
|
75f52e4cf4 | ||
|
|
55446dd4b1 | ||
|
|
580a24eac3 | ||
|
|
07b1b22c2c | ||
|
|
23078eccba | ||
|
|
8b825d558f | ||
|
|
8a82f35edf | ||
|
|
bec664c073 | ||
|
|
9a7f97f11f | ||
|
|
95b8d62e4a | ||
|
|
3e61424d8f | ||
|
|
c1606c6fc9 | ||
|
|
e0108cc99f | ||
|
|
c0de8a2591 | ||
|
|
b8733eed52 | ||
|
|
4aa30efec7 | ||
|
|
4354c3fb03 | ||
|
|
f25a676bd5 | ||
|
|
a9906762a0 | ||
|
|
7b5b88eca2 | ||
|
|
291682ba52 | ||
|
|
546d403b45 | ||
|
|
c9f40e32a1 | ||
|
|
d642b8dd07 | ||
|
|
549a11116e | ||
|
|
b5468a4a4f | ||
|
|
c5d278ccb7 | ||
|
|
bf5e15de45 | ||
|
|
f4af417267 | ||
|
|
69c04acaeb | ||
|
|
83a6bb188f | ||
|
|
494a8b5e91 | ||
|
|
c8021f2f08 | ||
|
|
8cdf3af047 | ||
|
|
c90a24e1ab | ||
|
|
83e3cfc34e | ||
|
|
133dd4e5b7 | ||
|
|
a8cca14b86 | ||
|
|
9b0f356fa4 | ||
|
|
f2b0a102b6 | ||
|
|
a7d01b230a | ||
|
|
2a7db4b9b8 | ||
|
|
77e5f2501d | ||
|
|
e6513717bd | ||
|
|
0e0c98f693 | ||
|
|
528bfa4e22 | ||
|
|
83c87779d7 | ||
|
|
dc33235144 | ||
|
|
935c658c5f | ||
|
|
0426f4fbf9 | ||
|
|
7930ec52fa | ||
|
|
57eb306e4f | ||
|
|
bd0511a9fb | ||
|
|
c14f3292aa | ||
|
|
46e771a062 | ||
|
|
4c0095844b | ||
|
|
10a2ef27d6 | ||
|
|
3c7637515c | ||
|
|
0a5abdb331 | ||
|
|
4ed7413dd2 | ||
|
|
40b66c24fc | ||
|
|
20a35de3d8 | ||
|
|
8fb2b1cf3d | ||
|
|
5c7d1cbe10 | ||
|
|
3348bfc362 | ||
|
|
8db9252434 | ||
|
|
d9d4fe15cc | ||
|
|
8453e2d514 | ||
|
|
4d3070c0c2 | ||
|
|
15f273a9ef | ||
|
|
007c079cd6 | ||
|
|
9851b1e8ad | ||
|
|
c65c2610cd | ||
|
|
2be08a3fcc | ||
|
|
4cdf440eee | ||
|
|
1545ee8fc2 | ||
|
|
a009fabfe4 | ||
|
|
46e4bfdd13 | ||
|
|
e24c82860d | ||
|
|
1291dee1e1 | ||
|
|
69732be09b | ||
|
|
8ce4ed7dee | ||
|
|
33d3cc0104 | ||
|
|
33b6b519ed | ||
|
|
5ec849f4e3 | ||
|
|
6f4a2d2cd5 | ||
|
|
0bca89e77d | ||
|
|
8f9f162011 | ||
|
|
ac3bea3d5e | ||
|
|
178d4f199b | ||
|
|
933819380d | ||
|
|
d68131e1b9 | ||
|
|
ffbc379c0b | ||
|
|
7053d12508 | ||
|
|
d41442200f | ||
|
|
d2564c343e | ||
|
|
d1c19e8837 | ||
|
|
0ec3bfabad | ||
|
|
43ffb04621 | ||
|
|
eeaedf76a9 | ||
|
|
120fb4829a | ||
|
|
12b93e53a7 | ||
|
|
00b17b9764 | ||
|
|
06192f809d | ||
|
|
d09eb0d3ff | ||
|
|
416321679c | ||
|
|
72c4e6e54f | ||
|
|
d3738e3424 | ||
|
|
2e183db903 | ||
|
|
fa174b88dd | ||
|
|
0b4abe6d16 | ||
|
|
759fb0ecd8 | ||
|
|
429e927c3f | ||
|
|
643aed9133 | ||
|
|
e701642de2 | ||
|
|
a3a03e3da6 | ||
|
|
5dd3d5b552 | ||
|
|
1bae246e6c | ||
|
|
c94ef10189 | ||
|
|
dfacf1f9df | ||
|
|
06c5e34f48 | ||
|
|
1ab8b35e98 | ||
|
|
261b61ee6c | ||
|
|
5abf8e1bab | ||
|
|
7c0b463637 | ||
|
|
e156ab2432 | ||
|
|
0b46a7163b | ||
|
|
21b76c008a | ||
|
|
b7e9f8248d | ||
|
|
8b21e76a36 | ||
|
|
515471185f | ||
|
|
628e8f4e5e |
@@ -157,6 +157,9 @@ build:
|
||||
paths:
|
||||
- neoforge/build/libs
|
||||
- fabric/build/libs
|
||||
exclude:
|
||||
- neoforge/build/libs/*-dev-shadow.jar
|
||||
- fabric/build/libs/*-dev-shadow.jar
|
||||
|
||||
publish:
|
||||
stage: deploy
|
||||
|
||||
+19
-25
@@ -1,45 +1,39 @@
|
||||
# Changelog
|
||||
## [1.8.2 (MONTH xth, 2026)](#1-8-2)
|
||||
## [1.8.3 (MONTH xth, 2026)](#1-8-3)
|
||||
|
||||
### Additions
|
||||
- The following items can now be placed on the ground as blocks:
|
||||
- King's Rock, Galarica Cuff, Unremarkable Teacup, Masterpiece Teacup, Chipped Pot, Cracked Pot, Sachet, Metal Powder, Quick Powder, Bright Powder, Icy Rock, Heat Rock, Smooth Rock, Damp Rock, Destiny Knot, Luminous Moss, Never-Melt Ice, Wide Lens, Scope Lens, and Zoom Lens.
|
||||
- Iron Ball, Metal Coat, Throat Spray, HP Up, Protein, Iron, Carbos, Calcium, Zinc, PP Up, PP Max, Upgrade, Dubious Disc, and Loaded Dice.
|
||||
- The following items can now be placed on various surfaces:
|
||||
- Galarica Wreath, Magnet, Flame Orb, Life Orb, Toxic Orb, Light Ball, and Smoke Ball.
|
||||
- The following items can now be placed as blocks, and have additional functionality:
|
||||
- Ring Target, Metronome, and Eject Button.
|
||||
- The following items can now be worn by Players and Pokémon:
|
||||
- Galarica Wreath, Cleanse Tag, Spell Tag, Sharp Beak, Covert Cloak, Shell Helmet, and Never-Melt Ice.
|
||||
- Wearable items can now be equipped by a player by holding it and right-clicking (or rebound use/interact)
|
||||
- Wearable items that can be placed down are exempt from this behaviour.
|
||||
|
||||
### New rideable Pokémon
|
||||
|
||||
### Pokémon Added
|
||||
#### Generation 4
|
||||
|
||||
#### Regional Forms
|
||||
#### Generation 5
|
||||
|
||||
#### Generation 6
|
||||
|
||||
#### Generation 7
|
||||
|
||||
#### Generation 8
|
||||
|
||||
#### Generation 9
|
||||
|
||||
### Model updates for the following Pokémon
|
||||
|
||||
### Animation updates for the following Pokémon
|
||||
|
||||
#### Added sounds to the following moves
|
||||
|
||||
### Changes
|
||||
|
||||
### Fixes
|
||||
- Correctly grants "They're Minerals!" advancement when planting a type gem
|
||||
- Type Gem Blocks now have advancements to give you the recipe in the recipe book (the data for the advancements were inside the recipes somehow??)
|
||||
- Corrected the TM Recipes for Charge Beam and Payday
|
||||
- Fixed datapacks with item interaction evolutions that don't specify an item crashing the world on load.
|
||||
- Fixed the Pokédex evolution tree crashing when a datapack's dex entry is missing an evolution's form.
|
||||
- Fixed the Pokédex evolution tree crashing with a stack overflow on evolutions that loop back on themselves.
|
||||
- Fixed form-changing evolutions sometimes resolving to the source form in the Pokédex evolution tree.
|
||||
- Broken datapack evolutions are now logged and skipped in the Pokédex evolution tree instead of crashing.
|
||||
- Fixed `/spectatebattle` not starting the battle theme music, unlike spectating through the interaction wheel
|
||||
- Fixed spectating not being cancelled when a battle starts for the spectator, which merged the two battles into an unplayable state, and fixed spectating a second battle leaving the first one still sending updates
|
||||
|
||||
### Developer
|
||||
|
||||
### Molang & Datapacks
|
||||
|
||||
### Particles
|
||||
|
||||
#### New Move Animations
|
||||
|
||||
### Localization
|
||||
- Updated translations for:
|
||||
- Updated translations for:
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
- Some kind of particle effect for them? they're meant to have glowing eyes, so maybe some red particles on the eye locator
|
||||
--- global posers? stacked posers?
|
||||
--- red texture seems to be what some people wanted.
|
||||
|
||||
/- shoulder mounts part of it
|
||||
- omni boost
|
||||
- roar when player comes within reach, adjust levels
|
||||
|
||||
- Riding settings to control what the min scale is for a pokemon to be ridable.
|
||||
+5
-2
@@ -1,3 +1,6 @@
|
||||
import utilities.gitBranch
|
||||
import utilities.gitShortCommit
|
||||
|
||||
plugins {
|
||||
id("cobblemon.root-conventions")
|
||||
}
|
||||
@@ -6,6 +9,6 @@ version = "${project.property("mod_version")}+${project.property("mc_version")}"
|
||||
|
||||
val isSnapshot = project.property("snapshot")?.equals("true") == true
|
||||
if (isSnapshot) {
|
||||
val fixedBranchName = versioning.info.branch.substringAfter("/")
|
||||
version = "$version-${fixedBranchName}-${versioning.info.build}"
|
||||
val fixedBranchName = gitBranch().substringAfter("/")
|
||||
version = "$version-${fixedBranchName}-${gitShortCommit()}"
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
- Added `cobblemon:catch_alpha_pokemon` advancement trigger and `totalAlphaCaptureCount` stat for captured Alpha Pokémon.
|
||||
|
||||
### New rideable Pokémon
|
||||
- Pidgeot
|
||||
- Crobat
|
||||
- Skarmory
|
||||
- Milotic
|
||||
@@ -106,7 +107,9 @@
|
||||
- Avalugg (Hisuian Form)
|
||||
|
||||
### Model updates for the following Pokémon
|
||||
|
||||
- Pidgey
|
||||
- Pidgeotto
|
||||
- Pidgeot
|
||||
- Sandshrew
|
||||
- Sandslash
|
||||
- Nidoking
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
# Changelog
|
||||
## [1.8.2 (October 11th, 2026)](#1-8-2)
|
||||
|
||||
### Additions
|
||||
- The following items can now be placed on the ground as blocks:
|
||||
- King's Rock, Galarica Cuff, Unremarkable Teacup, Masterpiece Teacup, Chipped Pot, Cracked Pot, Sachet, Metal Powder, Quick Powder, Bright Powder, Soft Sand, Electirizer, Magmarizer, Icy Rock, Heat Rock, Smooth Rock, Damp Rock, Destiny Knot, Luminous Moss, Never-Melt Ice, Wide Lens, Scope Lens, and Zoom Lens.
|
||||
- I wasn't finished. Iron Ball, Metal Coat, Throat Spray, Terrain Extender, Medicinal Brew, Moomoo Milk, HP Up, Protein, Iron, Carbos, Calcium, Zinc, PP Up, PP Max, Upgrade, Dubious Disc, and Loaded Dice.
|
||||
- The following items can now be placed on various surfaces:
|
||||
- Galarica Wreath, Magnet, Flame Orb, Life Orb, Toxic Orb, Light Ball, and Smoke Ball.
|
||||
- The following items can now be placed as blocks and have additional functionality:
|
||||
- Ring Target, Metronome, and Eject Button.
|
||||
- The following items can now be worn by Players and Pokémon:
|
||||
- Galarica Wreath, Cleanse Tag, Spell Tag, Sharp Beak, Covert Cloak, Shell Helmet, and Never-Melt Ice.
|
||||
- Wearable items can now be equipped by a player by holding it and right-clicking (or whatever you've rebound use/interact to).
|
||||
- Wearable items that can be placed down are exempt from this behaviour.
|
||||
- Mint Leaves can now be placed on the ground as Leaf Litter, or in Flower Pots.
|
||||
- The recall animation now renders the ball in hand with the player's arm raised.
|
||||
- Added Oricorio Flowers.
|
||||
- Added Terrain Plants, four plants grown from Electric, Grassy, Misty, and Psychic Seeds, which glow under specific environmental conditions and can be farmed for more of their seeds.
|
||||
- Berries can now be placed in Flower Pots.
|
||||
- The evolution tree for a Pokémon can now be viewed in the Pokédex, showing the Pokémon's full family line and what's needed to evolve into each stage.
|
||||
- Added custom sounds to a bunch of items that can be placed as blocks: liquid bottles, pill bottles, powders, and dice.
|
||||
- Added paper as a new cosmetic item for Pikachu.
|
||||
- Burmy changes its cloak after a battle it took part in, based on the biome.
|
||||
- Trash Cloak in the Deep Dark, Nether and End.
|
||||
- Plant Cloak in lush biomes such as forests, jungles, plains and swamps.
|
||||
- Sandy Cloak in sandy biomes, beaches and caves.
|
||||
|
||||
### Pokémon Added
|
||||
#### Generation 4
|
||||
- Snover
|
||||
- Abomasnow
|
||||
- Burmy
|
||||
- Wormadam
|
||||
- Mothim
|
||||
|
||||
#### Generation 5
|
||||
- Tympole
|
||||
- Palpitoad
|
||||
- Seismitoad
|
||||
|
||||
#### Generation 6
|
||||
- Helioptile
|
||||
- Heliolisk
|
||||
|
||||
#### Generation 7
|
||||
- Grubbin
|
||||
- Charjabug
|
||||
- Vikavolt
|
||||
- Minior
|
||||
|
||||
#### Generation 8
|
||||
- Eternatus
|
||||
|
||||
#### Generation 9
|
||||
- Greavard
|
||||
- Houndstone
|
||||
|
||||
### Model updates for the following Pokémon
|
||||
- Sudowoodo
|
||||
- Spheal
|
||||
- Cacnea
|
||||
- Cacturne
|
||||
- Binacle
|
||||
- Barbaracle
|
||||
- Glimmet
|
||||
- Glimmora
|
||||
- Deerling
|
||||
- Sawsbuck (All forms)
|
||||
- Absol
|
||||
- Poliwag
|
||||
- Politoed
|
||||
- Ekans
|
||||
- Oddish
|
||||
- Gloom
|
||||
- Vileplume
|
||||
- Bellossom
|
||||
- Shroomish
|
||||
- Chespin
|
||||
- Quilladin
|
||||
- Chesnaught
|
||||
- Fennekin
|
||||
- Braixen
|
||||
- Delphox
|
||||
- Froakie
|
||||
- Frogadier
|
||||
- Greninja
|
||||
- Shinx
|
||||
- Luxio
|
||||
- Luxray
|
||||
- Slakoth
|
||||
- Riolu
|
||||
- Lucario
|
||||
- Corsola
|
||||
- Mime Jr.
|
||||
|
||||
### Animation updates for the following Pokémon
|
||||
- Treecko (New Shoulder Mount!)
|
||||
- Bonsly (New Shoulder Mount!)
|
||||
- Sudowoodo
|
||||
- Cacnea
|
||||
- Cacturne
|
||||
- Ekans
|
||||
- Arbok
|
||||
- Oddish
|
||||
- Gloom
|
||||
- Vileplume
|
||||
- Bellossom
|
||||
- Pichu
|
||||
- Pikachu
|
||||
- Raichu (+ Alolan)
|
||||
- Greninja
|
||||
- Slakoth
|
||||
|
||||
### Changes
|
||||
- Slowed the speed of Type Gem growth, and revamped their generation to grow in a more crystal-like shape.
|
||||
- Terrain Seeds can now be planted.
|
||||
- Terrain Seeds are now consumed during PvP and NPC battles.
|
||||
- Glow Item Frames can be crafted using Bright Powder as an alternative to Glow Ink Sacs.
|
||||
- Bright Powder can be used on Signs and Hanging Signs to make text glow, as an alternative to Glow Ink Sacs.
|
||||
- Updated various recipes to display in Recipe Groups within the Recipe Book.
|
||||
- Updated most recipes to display more logically and consistently within the Recipe Book's categories.
|
||||
- Updated the following recipes
|
||||
- Never-Melt Ice, Focus Sash, Blunder Policy, Weakness Policy, Blank TM.
|
||||
- Made ride rotations less jittery in multiplayer.
|
||||
- Changes Disc Shelf TMs to use textures instead of tinting and overlays to line up with item display.
|
||||
- Blank TM can now be inserted into the Disc Shelf.
|
||||
- Added a TM creative tab.
|
||||
- Made regional and Vivillon split-evo locations mutually exclusive.
|
||||
- Vivillon's patterns are now viewable as forms with form names and unique Pokédex entries displayed.
|
||||
- Item evolutions now unlock **all** locked evolutions whose requirements are met instead of stopping after the first.
|
||||
- Exceptions being "forced" evolutions which stop after the first one.
|
||||
- Removed Mega Drain as a TM.
|
||||
- PokeSnacks (and other area spawns) will now start the vertical offset checking from the center rather than base coordinate
|
||||
- This will help PokeSnacks to work inside caves or tighter spaces better.
|
||||
|
||||
### Fixes
|
||||
- Type Gem blocks now properly generate clusters off of the block.
|
||||
- Correctly grant "They're Minerals!" advancement when planting a Type Gem.
|
||||
- Shoulder mounted Pokémon now follow player body animations, improving their positioning with custom player animations and default actions.
|
||||
- Shoulder mounted Pokémon now sit slightly higher with chest armor equipped for better alignment, with a configurable blacklist.
|
||||
- Type Gem Blocks now have advancements to give you the recipe in the recipe book (the data for the advancements were inside the recipes somehow??).
|
||||
- Some other recipes without advancements have also been corrected.
|
||||
- Corrected the TM Recipes for Charge Beam and Payday.
|
||||
- Starter Screen Categories will no longer stop rendering if they are bigger than the viewport and you are not viewing the top or bottom edges.
|
||||
- The Random Starter option no longer runs off into other categories or becomes unaccessible due to scroll limits if it causes a new row.
|
||||
- Mochi and Feathers no longer drop glass bottles on use.
|
||||
- Fixed a rare set of concurrency bugs related to reloading packs on NeoForge; fixes a ModernFix compatibility issue.
|
||||
- Removed unnecessary conditions for Mimikyu's standing pose. Hopefully fixes the rare instance of it using its shoulder animations during battle in multiplayer scenarios.
|
||||
- Fixed Rockruff line not blinking.
|
||||
- Fixed Flygon sometimes using its running animation instead of walking.
|
||||
- Fixed Bramblin always sleeping outside of battle.
|
||||
- Fixed Dragonair using its battle idle as a normal idle.
|
||||
- Fixed Lycanroc's head movement.
|
||||
- Added head movement to Archaludon.
|
||||
- Fixed Duraludon line not having any sleep animations.
|
||||
- Fixed Diglett line not having their faint animations.
|
||||
- Fixed Kingambit moving its legs while walking.
|
||||
- Fixed Alolan Diglett's cry not playing.
|
||||
- Fixed shiny alpha Snorlax using the non-shiny texture.
|
||||
- Fixed Crabominable's head breaking when looking around.
|
||||
- Fixed Shiny Dipplin and Hydrapple's textures.
|
||||
- Various structure fixes.
|
||||
- Fixed shiny Pokémon under certain circumstances forgetting taught TMs.
|
||||
- Fixed certain Pokémon sounds sometimes being doubled when played at specific times in their animations.
|
||||
- Fixed ball capture beam origin coming from behind the ball.
|
||||
- Fixed Safari Ball catch rate outside of battle.
|
||||
- Fixed alpha eye (and other) particles not respecting the client particle density setting in video options.
|
||||
- Fixed the TM Machine UI not closing when player is moving away from it.
|
||||
- Moved the config category for alpha/shiny chance config fields for fossil machine and honey log slathering to Spawning.
|
||||
- Fixed crashes for addon species with non-Cobblemon namespaces in evolution, fishing bait, and Pokédex lookups.
|
||||
- Bees will now grow Apricorns and the Galarica Nut Bush, which were in the bee growables tag but never handled, and correctly spend their nectar when they do.
|
||||
- Saccharine Leaves can now be bone mealed.
|
||||
- Fixed equipping a stack of wearable items instead of one item of a stack.
|
||||
- Fixed TM stack consumption in TM Machine screen when a player teaches a move to a Pokémon.
|
||||
- Fixed every Miltank giving mushroom stew, not only mushroom variants.
|
||||
- Medicinal Leeks can now be eaten when they aren't being planted, matching their food properties.
|
||||
- Sweeping attacks no longer set owned Pokémon on fire (e.g. with Fire Aspect) or knock them back. Friendly Fire is now off.
|
||||
- Fixed Pokémon not losing their previous form's signature moves when changing forms (e.g. Rotom keeping Overheat after leaving its Heat form).
|
||||
- Form-specific moves gained from a form change are now only granted if the Pokémon is high enough level to learn them, and no longer create duplicates in benched moves.
|
||||
- Spinda now learns Teeter Dance at level 32 instead of via TM.
|
||||
- Fixed Oricorio Pa'u form description in the Pokédex.
|
||||
- Fixed PC playing sound on close when opened via command.
|
||||
|
||||
### Developer
|
||||
- Evolution requirements can now set `pokedexIcon` and `pokedexTooltip` directly in their JSON to control how they're shown on the Pokédex's evolution tree, for example:
|
||||
```json
|
||||
{
|
||||
"variant": "attack_defence_ratio",
|
||||
"pokedexIcon": "minecraft:diamond_sword",
|
||||
"pokedexTooltip": "cobblemon.ui.pokedex.requirement.attack_defence_ratio.tooltip"
|
||||
}
|
||||
```
|
||||
- Fixed the wrong order of operations in the code flow prior to the `FOSSIL_REVIVED` event causing Pokémon to appear without the entity link.
|
||||
- Fixed TestPartySlot command not correctly comparing the provided spec with the target Pokémon.
|
||||
|
||||
### Molang & Datapacks
|
||||
- A new `negate` variant for evolution requirements has been added, which will negate (invert) the provided inner condition.
|
||||
- Added Molang functions for individual Pokédex ids, wherein a subdex is passed in as a string to `dex_caught_count` etc.
|
||||
- Added a `battle_ended` callback, backed by the new `BATTLE_ENDED` event, which fires whenever a battle ends for any reason, alongside a multitude of flags not previously covered by `BATTLE_VICTORY`.
|
||||
- Added `battle_pokemon` to battle actor structs and `has_faced_opponents` to battle Pokémon structs.
|
||||
- Added the `cobblemon:form_change/bagworm_cloak/plant_cloak`, `sandy_cloak` and `trash_cloak` biome tags, which control Burmy's cloak after battle.
|
||||
|
||||
### Localization
|
||||
- Updated translations for:
|
||||
- Brazilian Portuguese
|
||||
- Canadian French
|
||||
- Japanese
|
||||
- Korean
|
||||
- Russian
|
||||
- Ukrainian
|
||||
+15
-8
@@ -1,9 +1,8 @@
|
||||
|
||||
import utilities.isSnapshot
|
||||
import utilities.version
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import utilities.gitBranch
|
||||
import utilities.gitCommit
|
||||
|
||||
/*
|
||||
*
|
||||
@@ -66,14 +65,20 @@ dependencies {
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
// Static GL mocks must not load a graphics driver on headless CI.
|
||||
systemProperty("org.lwjgl.opengl.explicitInit", "true")
|
||||
testLogging {
|
||||
setEvents(listOf("failed"))
|
||||
setExceptionFormat("full")
|
||||
}
|
||||
}
|
||||
|
||||
// Git info lives in its own output dir (not processResources) so a new commit only rewrites this one file.
|
||||
val writeGitInfo by tasks.registering(WriteProperties::class) {
|
||||
destinationFile = layout.buildDirectory.file("generated/gitInfo/cobblemon-git.properties")
|
||||
property("commit", gitCommit())
|
||||
property("branch", gitBranch())
|
||||
}
|
||||
sourceSets.main { output.dir(mapOf("builtBy" to writeGitInfo), layout.buildDirectory.dir("generated/gitInfo")) }
|
||||
|
||||
tasks.processResources {
|
||||
exclude("data/**/README.md")
|
||||
}
|
||||
@@ -100,11 +105,13 @@ sourceSets {
|
||||
property("modid", "cobblemon")
|
||||
property("version", project.version())
|
||||
property("isSnapshot", if(rootProject.isSnapshot()) "true" else "false")
|
||||
property("gitCommit", versioning.info.commit)
|
||||
property("branch", versioning.info.branch)
|
||||
System.getProperty("buildNumber")?.let { property("buildNumber", it) }
|
||||
property("timestamp", OffsetDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ofPattern("MM/dd/yyyy hh:mm:ss")) + " UTC")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The platform jars are built from :common:jar, so the remapped common jars are only needed by `publish`
|
||||
tasks.assemble {
|
||||
setDependsOn(dependsOn.filterNot { (it as? TaskProvider<*>)?.name in setOf("remapJar", "remapSourcesJar") })
|
||||
}
|
||||
|
||||
@@ -8,54 +8,42 @@
|
||||
|
||||
package com.cobblemon.mod.common.mixin;
|
||||
|
||||
import com.cobblemon.mod.common.block.SaccharineLeafBlock;
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.animal.Bee;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.BonemealableBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
|
||||
|
||||
|
||||
@Mixin(targets = "net.minecraft.world.entity.animal.Bee$BeeGrowCropGoal")
|
||||
public abstract class BeeEntityMixin {
|
||||
|
||||
@Unique
|
||||
private BlockState cobblemon$result = null;
|
||||
|
||||
@Inject(method = "tick()V", at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lnet/minecraft/world/level/block/BonemealableBlock;performBonemeal(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/util/RandomSource;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)V",
|
||||
shift = At.Shift.BY,
|
||||
by = 2
|
||||
),
|
||||
locals = LocalCapture.CAPTURE_FAILHARD
|
||||
)
|
||||
private void injectCustomGrowth(CallbackInfo ci, int i, BlockPos blockPos, BlockState blockState, Block block) {
|
||||
if (block instanceof SaccharineLeafBlock) {
|
||||
|
||||
int age = blockState.getValue(SaccharineLeafBlock.Companion.getAGE());
|
||||
boolean waterlogged = blockState.getValue(BlockStateProperties.WATERLOGGED);
|
||||
|
||||
if (age < 2 && !waterlogged) {
|
||||
this.cobblemon$result = blockState.setValue(SaccharineLeafBlock.Companion.getAGE(), age + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@Shadow(aliases = {"this$0", "field_20373"}) @Final private Bee bee;
|
||||
|
||||
// Grows bonemealable blocks vanilla's hardcoded checks miss, returning the state so it handles the rest.
|
||||
@ModifyVariable(method = "tick", at = @At(value = "LOAD", ordinal = 0), index = 5)
|
||||
private BlockState applyCustomBlockState(BlockState before) {
|
||||
if(this.cobblemon$result != null && before == null) {
|
||||
BlockState result = this.cobblemon$result;
|
||||
this.cobblemon$result = null;
|
||||
return result;
|
||||
private BlockState cobblemon$growBonemealableBlocks(BlockState grownState, @Local(ordinal = 0) BlockState blockState, @Local BlockPos pos) {
|
||||
if (grownState != null || blockState.is(Blocks.CAVE_VINES) || blockState.is(Blocks.CAVE_VINES_PLANT)) {
|
||||
return grownState;
|
||||
}
|
||||
|
||||
return before;
|
||||
if (!(blockState.getBlock() instanceof BonemealableBlock bonemealable)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ServerLevel level = (ServerLevel) this.bee.level();
|
||||
if (!bonemealable.isValidBonemealTarget(level, pos, blockState)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
bonemealable.performBonemeal(level, this.bee.getRandom(), pos, blockState);
|
||||
return level.getBlockState(pos);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.IntTag;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
@@ -45,4 +46,24 @@ public class ChunkStorageMixin {
|
||||
int curVersion = vanillaFixed.contains(VERSION_KEY) ? vanillaFixed.getInt(VERSION_KEY) : 0;
|
||||
return DataFixTypes.CHUNK.update(CobblemonSchemas.getDATA_FIXER(), vanillaFixed, curVersion, CobblemonSchemas.DATA_VERSION);
|
||||
}
|
||||
|
||||
// Our update on the same MC version still needs its own fixes so run them here in that case.
|
||||
@Inject(
|
||||
method = "upgradeChunkTag(Lnet/minecraft/resources/ResourceKey;Ljava/util/function/Supplier;Lnet/minecraft/nbt/CompoundTag;Ljava/util/Optional;)Lnet/minecraft/nbt/CompoundTag;",
|
||||
at = @At("HEAD"),
|
||||
cancellable = true
|
||||
)
|
||||
private void cobblemon$doChunkFixOnCurrentVersion(
|
||||
ResourceKey<Level> levelKey,
|
||||
Supplier<DimensionDataStorage> storage,
|
||||
CompoundTag chunkData,
|
||||
Optional<ResourceKey<MapCodec<? extends ChunkGenerator>>> chunkGeneratorKey,
|
||||
CallbackInfoReturnable<CompoundTag> cir
|
||||
) {
|
||||
if (ChunkStorage.getVersion(chunkData) != SharedConstants.getCurrentVersion().getDataVersion().getVersion()) {
|
||||
return;
|
||||
}
|
||||
int curVersion = chunkData.contains(VERSION_KEY) ? chunkData.getInt(VERSION_KEY) : 0;
|
||||
cir.setReturnValue(DataFixTypes.CHUNK.update(CobblemonSchemas.getDATA_FIXER(), chunkData, curVersion, CobblemonSchemas.DATA_VERSION));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,13 +35,16 @@ import com.cobblemon.mod.common.util.DataKeys;
|
||||
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.EntityDimensions;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
@@ -50,6 +53,7 @@ import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.food.FoodProperties;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.enchantment.EnchantmentHelper;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -371,4 +375,33 @@ public abstract class PlayerMixin extends LivingEntity implements ScannableEntit
|
||||
this.cobblemon$rideEyePos = rideEyePos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeping attacks apply post-attack enchantment effects (e.g. Fire Aspect) to every entity caught
|
||||
* in the sweep, even if the damage itself was blocked. Skip them for Pokémon immune to the attack.
|
||||
*/
|
||||
@WrapOperation(
|
||||
method = "attack",
|
||||
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/enchantment/EnchantmentHelper;doPostAttackEffects(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/damagesource/DamageSource;)V")
|
||||
)
|
||||
private void cobblemon$skipPostAttackEffectsOnImmunePokemon(ServerLevel level, Entity target, DamageSource source, Operation<Void> original) {
|
||||
if (target instanceof PokemonEntity pokemonEntity && pokemonEntity.isInvulnerableTo(source)) {
|
||||
return;
|
||||
}
|
||||
original.call(level, target, source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeping attacks also knock back every entity caught in the sweep regardless of whether it took damage.
|
||||
*/
|
||||
@WrapOperation(
|
||||
method = "attack",
|
||||
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;knockback(DDD)V")
|
||||
)
|
||||
private void cobblemon$skipKnockbackOnImmunePokemon(LivingEntity target, double strength, double x, double z, Operation<Void> original, @Local DamageSource source) {
|
||||
if (target instanceof PokemonEntity pokemonEntity && pokemonEntity.isInvulnerableTo(source)) {
|
||||
return;
|
||||
}
|
||||
original.call(target, strength, x, z);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,11 +10,13 @@ package com.cobblemon.mod.common.mixin.client;
|
||||
|
||||
import com.cobblemon.mod.common.OrientationControllable;
|
||||
import com.cobblemon.mod.common.client.render.player.MountedPlayerRenderer;
|
||||
import com.cobblemon.mod.common.client.render.player.RecallArmAnimationTracker;
|
||||
import com.cobblemon.mod.common.duck.RidePassenger;
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
|
||||
import net.minecraft.client.model.HumanoidModel;
|
||||
import net.minecraft.client.model.geom.ModelPart;
|
||||
import net.minecraft.client.player.AbstractClientPlayer;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
@@ -96,4 +98,14 @@ public class HumanoidModelMixin {
|
||||
MountedPlayerRenderer.INSTANCE.animate((HumanoidModel<AbstractClientPlayer>)(Object)this, pokemonEntity, player, netHeadYaw, headPitch, ageInTicks, limbSwing, limbSwingAmount);
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "setupAnim*", at = @At("TAIL"))
|
||||
public void cobblemon$poseRecallArm(LivingEntity entity, float limbSwing, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch, CallbackInfo ci) {
|
||||
if (!(entity instanceof AbstractClientPlayer) || entity.getVehicle() instanceof PokemonEntity) return;
|
||||
float raise = RecallArmAnimationTracker.INSTANCE.raiseFactor(entity.getUUID());
|
||||
if (raise <= 0f) return;
|
||||
this.rightArm.xRot = Mth.lerp(raise, this.rightArm.xRot, (float) (-Math.PI / 2.0));
|
||||
this.rightArm.yRot = Mth.lerp(raise, this.rightArm.yRot, 0f);
|
||||
this.rightArm.zRot = Mth.lerp(raise, this.rightArm.zRot, 0f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.mixin.client;
|
||||
|
||||
import com.cobblemon.mod.common.client.render.player.RecallArmAnimationTracker;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.entity.layers.ItemInHandLayer;
|
||||
import net.minecraft.world.entity.HumanoidArm;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(ItemInHandLayer.class)
|
||||
public class ItemInHandLayerMixin {
|
||||
@Inject(method = "renderArmWithItem", at = @At("HEAD"), cancellable = true)
|
||||
private void cobblemon$hideRecallHandItem(LivingEntity entity, ItemStack itemStack, ItemDisplayContext itemDisplayContext, HumanoidArm humanoidArm, PoseStack poseStack, MultiBufferSource multiBufferSource, int light, CallbackInfo ci) {
|
||||
if (humanoidArm == HumanoidArm.RIGHT && RecallArmAnimationTracker.INSTANCE.isRecalling(entity.getUUID())) {
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -9,6 +9,7 @@
|
||||
package com.cobblemon.mod.common.mixin.client;
|
||||
|
||||
import com.cobblemon.mod.common.OrientationControllable;
|
||||
import com.cobblemon.mod.common.client.render.player.FirstPersonRecallBallRenderer;
|
||||
import com.cobblemon.mod.common.item.PokedexItem;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import net.minecraft.client.Minecraft;
|
||||
@@ -31,6 +32,9 @@ public class ItemInHandRendererMixin {
|
||||
if (abstractClientPlayer.isUsingItem() && abstractClientPlayer.getUseItem().getItem() instanceof PokedexItem) {
|
||||
ci.cancel();
|
||||
}
|
||||
else if (interactionHand == InteractionHand.MAIN_HAND && FirstPersonRecallBallRenderer.INSTANCE.render(poseStack, multiBufferSource, j, f)) {
|
||||
ci.cancel();
|
||||
}
|
||||
else if (abstractClientPlayer instanceof OrientationControllable controllable && controllable.getOrientationController().isActive()) {
|
||||
|
||||
}
|
||||
|
||||
@@ -1,30 +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.mixin.client;
|
||||
|
||||
import com.cobblemon.mod.common.client.render.RenderAlphaScope;
|
||||
import net.minecraft.client.renderer.RenderStateShard;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(RenderStateShard.class)
|
||||
public abstract class RenderStateShardMixin {
|
||||
@Inject(method = "setupRenderState", at = @At("RETURN"))
|
||||
private void cobblemon$applyScopedAlpha(CallbackInfo ci) {
|
||||
if ((Object) this instanceof RenderType) RenderAlphaScope.afterMaterialSetup();
|
||||
}
|
||||
|
||||
@Inject(method = "clearRenderState", at = @At("RETURN"))
|
||||
private void cobblemon$restoreScopedAlpha(CallbackInfo ci) {
|
||||
if ((Object) this instanceof RenderType) RenderAlphaScope.afterMaterialClear();
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,12 @@ object CobblemonBuildDetails {
|
||||
const val VERSION = "{{ version }}"
|
||||
const val SNAPSHOT = {{ isSnapshot }}
|
||||
|
||||
const val GIT_COMMIT = "{{ gitCommit | default("Not Specified") }}"
|
||||
const val BRANCH = "{{ branch | default("Not Specified") }}"
|
||||
// Read at runtime rather than templated in, so a new commit doesn't invalidate compiled code.
|
||||
private val git = java.util.Properties().apply {
|
||||
CobblemonBuildDetails::class.java.getResourceAsStream("/cobblemon-git.properties")?.use(::load)
|
||||
}
|
||||
@JvmField val GIT_COMMIT: String = git.getProperty("commit", "Not Specified")
|
||||
@JvmField val BRANCH: String = git.getProperty("branch", "Not Specified")
|
||||
|
||||
fun smallCommitHash(): String {
|
||||
return this.GIT_COMMIT.substring(0, 7)
|
||||
|
||||
@@ -22,8 +22,6 @@ import com.cobblemon.mod.common.api.habitats.HabitatPools
|
||||
import com.cobblemon.mod.common.api.interaction.RequestManager
|
||||
import com.cobblemon.mod.common.api.molang.MoLangLoadedFilesCache
|
||||
import com.cobblemon.mod.common.api.molang.ObjectValue
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiActionRegistry
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiService
|
||||
import com.cobblemon.mod.common.api.permission.PermissionValidator
|
||||
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CaptureCalculator
|
||||
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CaptureCalculators
|
||||
@@ -71,7 +69,6 @@ import com.cobblemon.mod.common.api.storage.player.adapter.TMMoveNbtBackend
|
||||
import com.cobblemon.mod.common.api.storage.player.factory.CachedPlayerDataStoreFactory
|
||||
import com.cobblemon.mod.common.api.tags.CobblemonEntityTypeTags
|
||||
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
|
||||
import com.cobblemon.mod.common.api.vending.VendingMachinePageHandler
|
||||
import com.cobblemon.mod.common.battles.BagItems
|
||||
import com.cobblemon.mod.common.battles.BattleFormat
|
||||
import com.cobblemon.mod.common.battles.BattleRegistry
|
||||
@@ -262,7 +259,6 @@ object Cobblemon {
|
||||
playerDataManager.onPlayerDisconnect(it.player)
|
||||
RequestManager.onLogoff(it.player)
|
||||
serverPlayerStructs.remove(it.player.uuid)
|
||||
MolangUiService.closeAllForPlayer(it.player.uuid)
|
||||
}
|
||||
PlatformEvents.PLAYER_DEATH.subscribe {
|
||||
PCLinkManager.removeLink(it.player.uuid)
|
||||
@@ -325,11 +321,6 @@ object Cobblemon {
|
||||
// Start up the data provider.
|
||||
CobblemonDataProvider.registerDefaults()
|
||||
|
||||
MolangUiActionRegistry.register(
|
||||
VendingMachinePageHandler.ACTION,
|
||||
VendingMachinePageHandler
|
||||
)
|
||||
|
||||
SHINY_ASPECT.register()
|
||||
GENDER_ASPECT.register()
|
||||
COSMETIC_SLOT_ASPECT.register()
|
||||
@@ -459,7 +450,6 @@ object Cobblemon {
|
||||
}
|
||||
|
||||
PlatformEvents.SERVER_STOPPED.subscribe {
|
||||
MolangUiService.closeAll()
|
||||
storage.unregisterAll(it.server.registryAccess())
|
||||
playerDataManager.saveAllStores()
|
||||
playerDataManager.saveExecutor.shutdown()
|
||||
|
||||
@@ -109,4 +109,21 @@ object CobblemonBlockEntities : PlatformRegistry<Registry<BlockEntityType<*>>, R
|
||||
"poke_snack",
|
||||
BlockEntityType.Builder.of(::PokeSnackBlockEntity, CobblemonBlocks.POKE_SNACK, CobblemonBlocks.POKE_CAKE).build(null)
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val TERRAIN_PLANT: BlockEntityType<TerrainPlantBlockEntity> = this.create(
|
||||
"terrain_plant",
|
||||
BlockEntityType.Builder.of(::TerrainPlantBlockEntity,
|
||||
CobblemonBlocks.VOLTACONIA,
|
||||
CobblemonBlocks.LUCIBELL,
|
||||
CobblemonBlocks.VERDROOT
|
||||
).build(null)
|
||||
)
|
||||
@JvmField
|
||||
val MISTLEAF_FLOWER: BlockEntityType<MistleafFlowerBlockEntity> = this.create(
|
||||
"mistleaf",
|
||||
BlockEntityType.Builder.of(::MistleafFlowerBlockEntity,
|
||||
CobblemonBlocks.MISTLEAF
|
||||
).build(null)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,8 +16,7 @@ import com.cobblemon.mod.common.block.MintBlock.MintType
|
||||
import com.cobblemon.mod.common.block.campfirepot.CampfireBlock
|
||||
import com.cobblemon.mod.common.block.campfirepot.CampfirePotBlock
|
||||
import com.cobblemon.mod.common.block.chest.GildedChestBlock
|
||||
import com.cobblemon.mod.common.block.decor.ActivatableDecorationBlock
|
||||
import com.cobblemon.mod.common.block.decor.RingTargetBlock
|
||||
import com.cobblemon.mod.common.block.decor.*
|
||||
import com.cobblemon.mod.common.block.general.BaleBlock
|
||||
import com.cobblemon.mod.common.block.general.HorizontalRotationCarpetBlock
|
||||
import com.cobblemon.mod.common.block.general.HorizontalRotationalBlock
|
||||
@@ -28,6 +27,7 @@ import com.cobblemon.mod.common.block.sign.CobblemonSignBlock
|
||||
import com.cobblemon.mod.common.block.sign.CobblemonWallHangingSignBlock
|
||||
import com.cobblemon.mod.common.block.sign.CobblemonWallSignBlock
|
||||
import com.cobblemon.mod.common.block.tmmachine.TMMachineBlock
|
||||
import com.cobblemon.mod.common.block.general.MintLitterBlock
|
||||
import com.cobblemon.mod.common.mixin.invoker.BlocksInvoker
|
||||
import com.cobblemon.mod.common.mixin.invoker.DoorBlockInvoker
|
||||
import com.cobblemon.mod.common.mixin.invoker.FireBlockInvoker
|
||||
@@ -653,15 +653,21 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
|
||||
val CHIPPED_POT = create("chipped_pot", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DECORATED_POT).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.TEAPOT))
|
||||
@JvmField
|
||||
val CRACKED_POT = create("cracked_pot", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DECORATED_POT).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.TEAPOT))
|
||||
@JvmField
|
||||
val ELECTIRIZER = create("electirizer", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.IZER))
|
||||
@JvmField
|
||||
val MAGMARIZER = create("magmarizer", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.IZER))
|
||||
|
||||
@JvmField
|
||||
val SACHET = create("sachet", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
|
||||
@JvmField
|
||||
val METAL_POWDER = create("metal_powder", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
|
||||
val METAL_POWDER = create("metal_powder", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_POWDER_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
|
||||
@JvmField
|
||||
val QUICK_POWDER = create("quick_powder", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
|
||||
val QUICK_POWDER = create("quick_powder", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_POWDER_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
|
||||
@JvmField
|
||||
val BRIGHT_POWDER = create("bright_powder", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion().lightLevel{3}, DecorativeItemBlock.DecorativeItemBlockType.POUCH))
|
||||
val BRIGHT_POWDER = create("bright_powder", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_POWDER_SOUNDS).mapColor(MapColor.NONE).noOcclusion().lightLevel{3}, DecorativeItemBlock.DecorativeItemBlockType.POUCH))
|
||||
@JvmField
|
||||
val SOFT_SAND = create("soft_sand", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_POWDER_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
|
||||
|
||||
@JvmField
|
||||
val NEVER_MELT_ICE = create("never_melt_ice", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion().friction(0.989F), DecorativeItemBlock.DecorativeItemBlockType.NEVERMELT))
|
||||
@@ -691,7 +697,7 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
|
||||
val EJECT_BUTTON = create("eject_button", EjectButtonBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).pushReaction(PushReaction.DESTROY).noOcclusion().strength(0.125f), 40))
|
||||
|
||||
@JvmField
|
||||
val LOADED_DICE = create("loaded_dice", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.DICE))
|
||||
val LOADED_DICE = create("loaded_dice", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_DICE_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.DICE))
|
||||
@JvmField
|
||||
val IRON_BALL = create("iron_ball", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.IRON_BALL))
|
||||
@JvmField
|
||||
@@ -702,40 +708,46 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
|
||||
val DUBIOUS_DISC = create("dubious_disc", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.UPGRADE))
|
||||
|
||||
@JvmField
|
||||
val THROAT_SPRAY = create("throat_spray", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.THROAT_SPRAY))
|
||||
val THROAT_SPRAY = create("throat_spray", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.THROAT_SPRAY))
|
||||
@JvmField
|
||||
val TERRAIN_EXTENDER = create("terrain_extender", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_METAL_CAN_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.EXTENDER))
|
||||
@JvmField
|
||||
val MOOMOO_MILK = create("moomoo_milk", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_JAR_GLASS_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.GLASS_BOTTLE))
|
||||
@JvmField
|
||||
val MEDICINAL_BREW = create("medicinal_brew", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_JAR_GLASS_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.GLASS_BOTTLE))
|
||||
|
||||
@JvmField
|
||||
val FULL_HEAL = create("full_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
|
||||
val FULL_HEAL = create("full_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
|
||||
@JvmField
|
||||
val ANTIDOTE = create("antidote", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
|
||||
val ANTIDOTE = create("antidote", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
|
||||
@JvmField
|
||||
val AWAKENING = create("awakening", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
|
||||
val AWAKENING = create("awakening", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
|
||||
@JvmField
|
||||
val BURN_HEAL = create("burn_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
|
||||
val BURN_HEAL = create("burn_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
|
||||
@JvmField
|
||||
val ICE_HEAL = create("ice_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
|
||||
val ICE_HEAL = create("ice_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
|
||||
@JvmField
|
||||
val PARALYZE_HEAL = create("paralyze_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
|
||||
val PARALYZE_HEAL = create("paralyze_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
|
||||
|
||||
@JvmField
|
||||
val ETHER = create("ether", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
|
||||
val ETHER = create("ether", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
|
||||
@JvmField
|
||||
val MAX_ETHER = create("max_ether", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
|
||||
val MAX_ETHER = create("max_ether", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
|
||||
@JvmField
|
||||
val ELIXIR = create("elixir", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
|
||||
val ELIXIR = create("elixir", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
|
||||
@JvmField
|
||||
val MAX_ELIXIR = create("max_elixir", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
|
||||
val MAX_ELIXIR = create("max_elixir", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
|
||||
|
||||
@JvmField
|
||||
val POTION = create("potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.POTION))
|
||||
val POTION = create("potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.POTION))
|
||||
@JvmField
|
||||
val SUPER_POTION = create("super_potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.POTION))
|
||||
val SUPER_POTION = create("super_potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.POTION))
|
||||
@JvmField
|
||||
val HYPER_POTION = create("hyper_potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.POTION))
|
||||
val HYPER_POTION = create("hyper_potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.POTION))
|
||||
@JvmField
|
||||
val MAX_POTION = create("max_potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.MAX_POTION))
|
||||
val MAX_POTION = create("max_potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.MAX_POTION))
|
||||
@JvmField
|
||||
val FULL_RESTORE = create("full_restore", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.MAX_POTION))
|
||||
val FULL_RESTORE = create("full_restore", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.MAX_POTION))
|
||||
|
||||
@JvmField
|
||||
val DIRE_HIT = create("dire_hit", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
|
||||
@@ -755,21 +767,21 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
|
||||
val X_SPEED = create("x_${Stats.SPEED.identifier.path}", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
|
||||
|
||||
@JvmField
|
||||
val HP_UP = create("hp_up", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
val HP_UP = create("hp_up", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_PILLS_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
@JvmField
|
||||
val PROTEIN = create("protein", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
val PROTEIN = create("protein", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_PILLS_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
@JvmField
|
||||
val IRON = create("iron", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
val IRON = create("iron", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_PILLS_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
@JvmField
|
||||
val CARBOS = create("carbos", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
val CARBOS = create("carbos", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_PILLS_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
@JvmField
|
||||
val CALCIUM = create("calcium", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
val CALCIUM = create("calcium", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_PILLS_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
@JvmField
|
||||
val ZINC = create("zinc", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
val ZINC = create("zinc", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_PILLS_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
@JvmField
|
||||
val PP_UP = create("pp_up", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
val PP_UP = create("pp_up", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_PILLS_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
@JvmField
|
||||
val PP_MAX = create("pp_max", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
val PP_MAX = create("pp_max", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_PILLS_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
|
||||
@JvmField
|
||||
val MONITOR = create("monitor", MonitorBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).requiresCorrectToolForDrops().strength(5.0F, 6.0F).lightLevel { if (it.getValue(MonitorBlock.SCREEN) != MonitorBlock.MonitorScreen.OFF) 13 else 0 }))
|
||||
@@ -786,6 +798,7 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
|
||||
@JvmField
|
||||
val PC = create("pc", PCBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).strength(2F).noOcclusion().lightLevel { if ((it.getValue(PCBlock.ON) as Boolean) && (it.getValue(PCBlock.PART) == PCBlock.PCPart.TOP)) 13 else 0 }))
|
||||
|
||||
@JvmField
|
||||
val TM_MACHINE = create(
|
||||
"tm_machine",
|
||||
TMMachineBlock(
|
||||
@@ -808,16 +821,13 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
|
||||
@JvmField
|
||||
val DISPLAY_CASE = create("display_case", DisplayCaseBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.DISPLAY_CASE_SOUNDS).noOcclusion().pushReaction(PushReaction.BLOCK).mapColor(MapColor.STONE).strength(0.3F).requiresCorrectToolForDrops()))
|
||||
|
||||
@JvmField
|
||||
val VENDING_MACHINE = create("vending_machine", VendingMachineBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).strength(2F).requiresCorrectToolForDrops()))
|
||||
|
||||
// TODO after 1.7
|
||||
// @JvmField
|
||||
// val INCENSE_SWEET = create("incense_sweet", SweetIncenseBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.DISPLAY_CASE_SOUNDS).noOcclusion().pushReaction(PushReaction.BLOCK).mapColor(MapColor.STONE).strength(0.3F)))
|
||||
|
||||
@JvmField
|
||||
val PASTURE = create("pasture", PastureBlock(BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_BROWN).sound(SoundType.WOOD).strength(2F).noOcclusion().pushReaction(PushReaction.BLOCK).lightLevel { if ((it.getValue(PastureBlock.ON) as Boolean) && (it.getValue(PastureBlock.PART) == PastureBlock.PasturePart.TOP)) 13 else 0 }))
|
||||
|
||||
@JvmField
|
||||
val RED_MINT = create("red_mint", MintBlock(MintType.RED, BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_RED).noCollission().randomTicks().instabreak().sound(CobblemonSounds.MINT_SOUNDS).pushReaction(PushReaction.DESTROY)))
|
||||
@JvmField
|
||||
val BLUE_MINT = create("blue_mint", MintBlock(MintType.BLUE, BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_BLUE).noCollission().randomTicks().instabreak().sound(CobblemonSounds.MINT_SOUNDS).pushReaction(PushReaction.DESTROY)))
|
||||
@@ -859,6 +869,64 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
|
||||
@JvmField
|
||||
val POTTED_SACCHARINE_SAPLING = this.create("potted_saccharine_sapling", BlocksInvoker.createFlowerPotBlock(SACCHARINE_SAPLING))
|
||||
|
||||
@JvmField
|
||||
val RED_MINT_LEAF_LITTER = this.mintLitter("red_mint_leaf_litter", MapColor.COLOR_RED)
|
||||
@JvmField
|
||||
val POTTED_RED_MINT = this.pottedPlant("potted_red_mint", RED_MINT_LEAF_LITTER)
|
||||
@JvmField
|
||||
val BLUE_MINT_LEAF_LITTER = this.mintLitter("blue_mint_leaf_litter", MapColor.COLOR_BLUE)
|
||||
@JvmField
|
||||
val POTTED_BLUE_MINT = this.pottedPlant("potted_blue_mint", BLUE_MINT_LEAF_LITTER)
|
||||
@JvmField
|
||||
val CYAN_MINT_LEAF_LITTER = this.mintLitter("cyan_mint_leaf_litter", MapColor.COLOR_CYAN)
|
||||
@JvmField
|
||||
val POTTED_CYAN_MINT = this.pottedPlant("potted_cyan_mint", CYAN_MINT_LEAF_LITTER)
|
||||
@JvmField
|
||||
val PINK_MINT_LEAF_LITTER = this.mintLitter("pink_mint_leaf_litter", MapColor.COLOR_PINK)
|
||||
@JvmField
|
||||
val POTTED_PINK_MINT = this.pottedPlant("potted_pink_mint", PINK_MINT_LEAF_LITTER)
|
||||
@JvmField
|
||||
val GREEN_MINT_LEAF_LITTER = this.mintLitter("green_mint_leaf_litter", MapColor.COLOR_GREEN)
|
||||
@JvmField
|
||||
val POTTED_GREEN_MINT = this.pottedPlant("potted_green_mint", GREEN_MINT_LEAF_LITTER)
|
||||
@JvmField
|
||||
val WHITE_MINT_LEAF_LITTER = this.mintLitter("white_mint_leaf_litter", MapColor.SNOW)
|
||||
@JvmField
|
||||
val POTTED_WHITE_MINT = this.pottedPlant("potted_white_mint", WHITE_MINT_LEAF_LITTER)
|
||||
|
||||
@JvmField
|
||||
val LOBAILA = this.create("lobaila", LobailaFlowerBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).noCollission().instabreak().sound(SoundType.GRASS).offsetType(BlockBehaviour.OffsetType.XZ).ignitedByLava().pushReaction(PushReaction.DESTROY)))
|
||||
@JvmField
|
||||
val POMOHANA = this.create("pomohana", PomohanaFlowerBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).noCollission().instabreak().sound(SoundType.GRASS).ignitedByLava().pushReaction(PushReaction.DESTROY)))
|
||||
@JvmField
|
||||
val PAUVILLEA = this.create("pauvillea",PauvilleaFlowerBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).noCollission().sound(SoundType.GRASS).instabreak().ignitedByLava().pushReaction(PushReaction.DESTROY)))
|
||||
@JvmField
|
||||
val WISTENSU = this.create("wistensu", WistensuFlowerHeadBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).noCollission().sound(SoundType.GRASS).instabreak().ignitedByLava().pushReaction(PushReaction.DESTROY).lightLevel{11}))
|
||||
@JvmField
|
||||
val WISTENSU_VINE = this.create("wistensu_vine", WistensuFlowerVineBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).noCollission().sound(SoundType.GRASS).instabreak().ignitedByLava().pushReaction(PushReaction.DESTROY)))
|
||||
|
||||
@JvmField
|
||||
val ELECTRIC_SEED = this.terrainSeed("electric_seed", TerrainSeedBlock.TerrainSeedType.ELECTRIC)
|
||||
@JvmField
|
||||
val PSYCHIC_SEED = this.terrainSeed("psychic_seed", TerrainSeedBlock.TerrainSeedType.PSYCHIC)
|
||||
@JvmField
|
||||
val MISTY_SEED = this.terrainSeed("misty_seed", TerrainSeedBlock.TerrainSeedType.MISTY)
|
||||
@JvmField
|
||||
val GRASSY_SEED = this.terrainSeed("grassy_seed", TerrainSeedBlock.TerrainSeedType.GRASSY)
|
||||
@JvmField
|
||||
val VOLTACONIA = this.terrainPlant("voltaconia") { state -> if (state.getValue(TerrainPlantBlock.GLOWING)) 8 else 0 }
|
||||
@JvmField
|
||||
val VERDROOT = this.terrainPlant("verdroot") { state -> if (state.getValue(TerrainPlantBlock.GLOWING)) 5 else 0 }
|
||||
@JvmField
|
||||
val LUCIBELL = this.terrainPlant("lucibell") { state -> if (state.getValue(TerrainPlantBlock.GLOWING)) 8 else 0 }
|
||||
@JvmField
|
||||
val MISTLEAF = this.create(
|
||||
"mistleaf",
|
||||
MistleafFlowerBlock {
|
||||
if ((it.getValue(MistleafFlowerBlock.GLOWING) >= 1)) it.getValue(MistleafFlowerBlock.GLOWING) + 2 else 0
|
||||
}
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val HEARTY_GRAIN_BALE = this.create("hearty_grain_bale", BaleBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).instrument(NoteBlockInstrument.BANJO).strength(0.5F).sound(CobblemonSounds.HEARTY_GRAIN_BALE_SOUNDS)))
|
||||
@JvmField
|
||||
@@ -1035,6 +1103,147 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
|
||||
val YACHE_BERRY = this.berryBlock("yache")
|
||||
//val BINDING_SOIL = this.create("binding_soil", BindingSoilBlock(BlockBehaviour.Properties.of()))
|
||||
|
||||
@JvmField
|
||||
val POTTED_AGUAV_BERRY = this.create("potted_aguav_berry", BlocksInvoker.createFlowerPotBlock(AGUAV_BERRY))
|
||||
@JvmField
|
||||
val POTTED_APICOT_BERRY = this.create("potted_apicot_berry", BlocksInvoker.createFlowerPotBlock(APICOT_BERRY))
|
||||
@JvmField
|
||||
val POTTED_ASPEAR_BERRY = this.create("potted_aspear_berry", BlocksInvoker.createFlowerPotBlock(ASPEAR_BERRY))
|
||||
@JvmField
|
||||
val POTTED_BABIRI_BERRY = this.create("potted_babiri_berry", BlocksInvoker.createFlowerPotBlock(BABIRI_BERRY))
|
||||
@JvmField
|
||||
val POTTED_BELUE_BERRY = this.create("potted_belue_berry", BlocksInvoker.createFlowerPotBlock(BELUE_BERRY))
|
||||
@JvmField
|
||||
val POTTED_BLUK_BERRY = this.create("potted_bluk_berry", BlocksInvoker.createFlowerPotBlock(BLUK_BERRY))
|
||||
@JvmField
|
||||
val POTTED_CHARTI_BERRY = this.create("potted_charti_berry", BlocksInvoker.createFlowerPotBlock(CHARTI_BERRY))
|
||||
@JvmField
|
||||
val POTTED_CHERI_BERRY = this.create("potted_cheri_berry", BlocksInvoker.createFlowerPotBlock(CHERI_BERRY))
|
||||
@JvmField
|
||||
val POTTED_CHESTO_BERRY = this.create("potted_chesto_berry", BlocksInvoker.createFlowerPotBlock(CHESTO_BERRY))
|
||||
@JvmField
|
||||
val POTTED_CHILAN_BERRY = this.create("potted_chilan_berry", BlocksInvoker.createFlowerPotBlock(CHILAN_BERRY))
|
||||
@JvmField
|
||||
val POTTED_CHOPLE_BERRY = this.create("potted_chople_berry", BlocksInvoker.createFlowerPotBlock(CHOPLE_BERRY))
|
||||
@JvmField
|
||||
val POTTED_COBA_BERRY = this.create("potted_coba_berry", BlocksInvoker.createFlowerPotBlock(COBA_BERRY))
|
||||
@JvmField
|
||||
val POTTED_COLBUR_BERRY = this.create("potted_colbur_berry", BlocksInvoker.createFlowerPotBlock(COLBUR_BERRY))
|
||||
@JvmField
|
||||
val POTTED_CORNN_BERRY = this.create("potted_cornn_berry", BlocksInvoker.createFlowerPotBlock(CORNN_BERRY))
|
||||
@JvmField
|
||||
val POTTED_CUSTAP_BERRY = this.create("potted_custap_berry", BlocksInvoker.createFlowerPotBlock(CUSTAP_BERRY))
|
||||
@JvmField
|
||||
val POTTED_DURIN_BERRY = this.create("potted_durin_berry", BlocksInvoker.createFlowerPotBlock(DURIN_BERRY))
|
||||
@JvmField
|
||||
val POTTED_EGGANT_BERRY = this.create("potted_eggant_berry", BlocksInvoker.createFlowerPotBlock(EGGANT_BERRY))
|
||||
@JvmField
|
||||
val POTTED_ENIGMA_BERRY = this.create("potted_enigma_berry", BlocksInvoker.createFlowerPotBlock(ENIGMA_BERRY))
|
||||
@JvmField
|
||||
val POTTED_FIGY_BERRY = this.create("potted_figy_berry", BlocksInvoker.createFlowerPotBlock(FIGY_BERRY))
|
||||
@JvmField
|
||||
val POTTED_GANLON_BERRY = this.create("potted_ganlon_berry", BlocksInvoker.createFlowerPotBlock(GANLON_BERRY))
|
||||
@JvmField
|
||||
val POTTED_GREPA_BERRY = this.create("potted_grepa_berry", BlocksInvoker.createFlowerPotBlock(GREPA_BERRY))
|
||||
@JvmField
|
||||
val POTTED_HABAN_BERRY = this.create("potted_haban_berry", BlocksInvoker.createFlowerPotBlock(HABAN_BERRY))
|
||||
@JvmField
|
||||
val POTTED_HONDEW_BERRY = this.create("potted_hondew_berry", BlocksInvoker.createFlowerPotBlock(HONDEW_BERRY))
|
||||
@JvmField
|
||||
val POTTED_HOPO_BERRY = this.create("potted_hopo_berry", BlocksInvoker.createFlowerPotBlock(HOPO_BERRY))
|
||||
@JvmField
|
||||
val POTTED_IAPAPA_BERRY = this.create("potted_iapapa_berry", BlocksInvoker.createFlowerPotBlock(IAPAPA_BERRY))
|
||||
@JvmField
|
||||
val POTTED_JABOCA_BERRY = this.create("potted_jaboca_berry", BlocksInvoker.createFlowerPotBlock(JABOCA_BERRY))
|
||||
@JvmField
|
||||
val POTTED_KASIB_BERRY = this.create("potted_kasib_berry", BlocksInvoker.createFlowerPotBlock(KASIB_BERRY))
|
||||
@JvmField
|
||||
val POTTED_KEBIA_BERRY = this.create("potted_kebia_berry", BlocksInvoker.createFlowerPotBlock(KEBIA_BERRY))
|
||||
@JvmField
|
||||
val POTTED_KEE_BERRY = this.create("potted_kee_berry", BlocksInvoker.createFlowerPotBlock(KEE_BERRY))
|
||||
@JvmField
|
||||
val POTTED_KELPSY_BERRY = this.create("potted_kelpsy_berry", BlocksInvoker.createFlowerPotBlock(KELPSY_BERRY))
|
||||
@JvmField
|
||||
val POTTED_LANSAT_BERRY = this.create("potted_lansat_berry", BlocksInvoker.createFlowerPotBlock(LANSAT_BERRY))
|
||||
@JvmField
|
||||
val POTTED_LEPPA_BERRY = this.create("potted_leppa_berry", BlocksInvoker.createFlowerPotBlock(LEPPA_BERRY))
|
||||
@JvmField
|
||||
val POTTED_LIECHI_BERRY = this.create("potted_liechi_berry", BlocksInvoker.createFlowerPotBlock(LIECHI_BERRY))
|
||||
@JvmField
|
||||
val POTTED_LUM_BERRY = this.create("potted_lum_berry", BlocksInvoker.createFlowerPotBlock(LUM_BERRY))
|
||||
@JvmField
|
||||
val POTTED_MAGO_BERRY = this.create("potted_mago_berry", BlocksInvoker.createFlowerPotBlock(MAGO_BERRY))
|
||||
@JvmField
|
||||
val POTTED_MAGOST_BERRY = this.create("potted_magost_berry", BlocksInvoker.createFlowerPotBlock(MAGOST_BERRY))
|
||||
@JvmField
|
||||
val POTTED_MARANGA_BERRY = this.create("potted_maranga_berry", BlocksInvoker.createFlowerPotBlock(MARANGA_BERRY))
|
||||
@JvmField
|
||||
val POTTED_MICLE_BERRY = this.create("potted_micle_berry", BlocksInvoker.createFlowerPotBlock(MICLE_BERRY))
|
||||
@JvmField
|
||||
val POTTED_NANAB_BERRY = this.create("potted_nanab_berry", BlocksInvoker.createFlowerPotBlock(NANAB_BERRY))
|
||||
@JvmField
|
||||
val POTTED_NOMEL_BERRY = this.create("potted_nomel_berry", BlocksInvoker.createFlowerPotBlock(NOMEL_BERRY))
|
||||
@JvmField
|
||||
val POTTED_OCCA_BERRY = this.create("potted_occa_berry", BlocksInvoker.createFlowerPotBlock(OCCA_BERRY))
|
||||
@JvmField
|
||||
val POTTED_ORAN_BERRY = this.create("potted_oran_berry", BlocksInvoker.createFlowerPotBlock(ORAN_BERRY))
|
||||
@JvmField
|
||||
val POTTED_PAMTRE_BERRY = this.create("potted_pamtre_berry", BlocksInvoker.createFlowerPotBlock(PAMTRE_BERRY))
|
||||
@JvmField
|
||||
val POTTED_PASSHO_BERRY = this.create("potted_passho_berry", BlocksInvoker.createFlowerPotBlock(PASSHO_BERRY))
|
||||
@JvmField
|
||||
val POTTED_PAYAPA_BERRY = this.create("potted_payapa_berry", BlocksInvoker.createFlowerPotBlock(PAYAPA_BERRY))
|
||||
@JvmField
|
||||
val POTTED_PECHA_BERRY = this.create("potted_pecha_berry", BlocksInvoker.createFlowerPotBlock(PECHA_BERRY))
|
||||
@JvmField
|
||||
val POTTED_PERSIM_BERRY = this.create("potted_persim_berry", BlocksInvoker.createFlowerPotBlock(PERSIM_BERRY))
|
||||
@JvmField
|
||||
val POTTED_PETAYA_BERRY = this.create("potted_petaya_berry", BlocksInvoker.createFlowerPotBlock(PETAYA_BERRY))
|
||||
@JvmField
|
||||
val POTTED_PINAP_BERRY = this.create("potted_pinap_berry", BlocksInvoker.createFlowerPotBlock(PINAP_BERRY))
|
||||
@JvmField
|
||||
val POTTED_POMEG_BERRY = this.create("potted_pomeg_berry", BlocksInvoker.createFlowerPotBlock(POMEG_BERRY))
|
||||
@JvmField
|
||||
val POTTED_QUALOT_BERRY = this.create("potted_qualot_berry", BlocksInvoker.createFlowerPotBlock(QUALOT_BERRY))
|
||||
@JvmField
|
||||
val POTTED_RABUTA_BERRY = this.create("potted_rabuta_berry", BlocksInvoker.createFlowerPotBlock(RABUTA_BERRY))
|
||||
@JvmField
|
||||
val POTTED_RAWST_BERRY = this.create("potted_rawst_berry", BlocksInvoker.createFlowerPotBlock(RAWST_BERRY))
|
||||
@JvmField
|
||||
val POTTED_RAZZ_BERRY = this.create("potted_razz_berry", BlocksInvoker.createFlowerPotBlock(RAZZ_BERRY))
|
||||
@JvmField
|
||||
val POTTED_RINDO_BERRY = this.create("potted_rindo_berry", BlocksInvoker.createFlowerPotBlock(RINDO_BERRY))
|
||||
@JvmField
|
||||
val POTTED_ROSELI_BERRY = this.create("potted_roseli_berry", BlocksInvoker.createFlowerPotBlock(ROSELI_BERRY))
|
||||
@JvmField
|
||||
val POTTED_ROWAP_BERRY = this.create("potted_rowap_berry", BlocksInvoker.createFlowerPotBlock(ROWAP_BERRY))
|
||||
@JvmField
|
||||
val POTTED_SALAC_BERRY = this.create("potted_salac_berry", BlocksInvoker.createFlowerPotBlock(SALAC_BERRY))
|
||||
@JvmField
|
||||
val POTTED_SHUCA_BERRY = this.create("potted_shuca_berry", BlocksInvoker.createFlowerPotBlock(SHUCA_BERRY))
|
||||
@JvmField
|
||||
val POTTED_SITRUS_BERRY = this.create("potted_sitrus_berry", BlocksInvoker.createFlowerPotBlock(SITRUS_BERRY))
|
||||
@JvmField
|
||||
val POTTED_SPELON_BERRY = this.create("potted_spelon_berry", BlocksInvoker.createFlowerPotBlock(SPELON_BERRY))
|
||||
@JvmField
|
||||
val POTTED_STARF_BERRY = this.create("potted_starf_berry", BlocksInvoker.createFlowerPotBlock(STARF_BERRY))
|
||||
@JvmField
|
||||
val POTTED_TAMATO_BERRY = this.create("potted_tamato_berry", BlocksInvoker.createFlowerPotBlock(TAMATO_BERRY))
|
||||
@JvmField
|
||||
val POTTED_TANGA_BERRY = this.create("potted_tanga_berry", BlocksInvoker.createFlowerPotBlock(TANGA_BERRY))
|
||||
@JvmField
|
||||
val POTTED_TOUGA_BERRY = this.create("potted_touga_berry", BlocksInvoker.createFlowerPotBlock(TOUGA_BERRY))
|
||||
@JvmField
|
||||
val POTTED_WACAN_BERRY = this.create("potted_wacan_berry", BlocksInvoker.createFlowerPotBlock(WACAN_BERRY))
|
||||
@JvmField
|
||||
val POTTED_WATMEL_BERRY = this.create("potted_watmel_berry", BlocksInvoker.createFlowerPotBlock(WATMEL_BERRY))
|
||||
@JvmField
|
||||
val POTTED_WEPEAR_BERRY = this.create("potted_wepear_berry", BlocksInvoker.createFlowerPotBlock(WEPEAR_BERRY))
|
||||
@JvmField
|
||||
val POTTED_WIKI_BERRY = this.create("potted_wiki_berry", BlocksInvoker.createFlowerPotBlock(WIKI_BERRY))
|
||||
@JvmField
|
||||
val POTTED_YACHE_BERRY = this.create("potted_yache_berry", BlocksInvoker.createFlowerPotBlock(YACHE_BERRY))
|
||||
|
||||
init {
|
||||
/**
|
||||
* Makes all blocks in array flammable by adding them to FireBlock's flammableRegistry.
|
||||
@@ -1136,4 +1345,48 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
|
||||
}
|
||||
|
||||
fun typeGemClusters() = typeGemClusters.toMap()
|
||||
|
||||
private fun pottedPlant(name: String, flower: Block) = this.create(name, BlocksInvoker.createFlowerPotBlock(flower))
|
||||
|
||||
private fun mintLitter(name: String, color: MapColor) = this.create(
|
||||
name,
|
||||
MintLitterBlock(
|
||||
BlockBehaviour.Properties.of()
|
||||
.sound(CobblemonSounds.MINT_SOUNDS)
|
||||
.mapColor(color)
|
||||
.noOcclusion()
|
||||
.noCollission()
|
||||
.pushReaction(PushReaction.DESTROY)
|
||||
)
|
||||
)
|
||||
|
||||
private fun terrainSeed(name: String, type: TerrainSeedBlock.TerrainSeedType) = this.create(
|
||||
name,
|
||||
TerrainSeedBlock(
|
||||
type,
|
||||
BlockBehaviour.Properties.of()
|
||||
.pushReaction(PushReaction.DESTROY)
|
||||
.ignitedByLava()
|
||||
.mapColor(MapColor.PLANT)
|
||||
.offsetType(BlockBehaviour.OffsetType.XZ)
|
||||
.noCollission()
|
||||
.randomTicks()
|
||||
.instabreak()
|
||||
)
|
||||
)
|
||||
|
||||
private fun terrainPlant(name: String, lightLevel: (BlockState) -> Int) = this.create(
|
||||
name,
|
||||
TerrainPlantBlock(
|
||||
BlockBehaviour.Properties.of()
|
||||
.mapColor(MapColor.PLANT)
|
||||
.noCollission()
|
||||
.instabreak()
|
||||
.sound(SoundType.GRASS)
|
||||
.offsetType(BlockBehaviour.OffsetType.XZ)
|
||||
.ignitedByLava()
|
||||
.pushReaction(PushReaction.DESTROY)
|
||||
.lightLevel(lightLevel)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -77,6 +77,5 @@ object CobblemonCommands {
|
||||
// Possibly lock down registration if and only if under dev environment or running in an environment
|
||||
// with a certain system environment variable set
|
||||
CobblemonInfoCommand.register(dispatcher)
|
||||
MolangUiCommand.register(dispatcher)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.mojang.brigadier.arguments.ArgumentType
|
||||
import kotlin.reflect.KClass
|
||||
import net.minecraft.commands.synchronization.ArgumentTypeInfo
|
||||
import net.minecraft.core.HolderLookup
|
||||
import net.minecraft.resources.ResourceKey
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.MinecraftServer
|
||||
@@ -168,12 +169,16 @@ interface CobblemonImplementation {
|
||||
* TODO
|
||||
*
|
||||
* @param identifier
|
||||
* @param reloader
|
||||
* @param reloaderFactory
|
||||
* @param type
|
||||
* @param dependencies
|
||||
*/
|
||||
fun registerResourceReloader(identifier: ResourceLocation, reloader: PreparableReloadListener, type: PackType, dependencies: Collection<ResourceLocation>)
|
||||
|
||||
fun registerResourceReloader(
|
||||
identifier: ResourceLocation,
|
||||
type: PackType,
|
||||
dependencies: Collection<ResourceLocation>,
|
||||
reloaderFactory: (HolderLookup.Provider) -> PreparableReloadListener
|
||||
)
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
|
||||
@@ -465,8 +465,6 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
val PASTURE = blockItem("pasture", CobblemonBlocks.PASTURE)
|
||||
@JvmField
|
||||
val DISPLAY_CASE = blockItem("display_case", CobblemonBlocks.DISPLAY_CASE)
|
||||
@JvmField
|
||||
val VENDING_MACHINE = blockItem("vending_machine", CobblemonBlocks.VENDING_MACHINE)
|
||||
// @JvmField
|
||||
// val INCENSE_SWEET = blockItem("incense_sweet", CobblemonBlocks.INCENSE_SWEET) // TODO after 1.7
|
||||
|
||||
@@ -479,8 +477,8 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField val DUBIOUS_DISC = itemNameBlockItem("dubious_disc", CobblemonBlocks.DUBIOUS_DISC)
|
||||
@JvmField val DEEP_SEA_SCALE = noSettingsItem("deep_sea_scale")
|
||||
@JvmField val DEEP_SEA_TOOTH = noSettingsItem("deep_sea_tooth")
|
||||
@JvmField val ELECTIRIZER = noSettingsItem("electirizer")
|
||||
@JvmField val MAGMARIZER = noSettingsItem("magmarizer")
|
||||
@JvmField val ELECTIRIZER = itemNameBlockItem("electirizer", CobblemonBlocks.ELECTIRIZER)
|
||||
@JvmField val MAGMARIZER = itemNameBlockItem("magmarizer", CobblemonBlocks.MAGMARIZER)
|
||||
@JvmField val OVAL_STONE = noSettingsItem("oval_stone")
|
||||
@JvmField val PROTECTOR = noSettingsItem("protector")
|
||||
@JvmField val REAPER_CLOTH = noSettingsItem("reaper_cloth")
|
||||
@@ -713,7 +711,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val PEP_UP_FLOWER = compostableBlockItem("pep_up_flower", CobblemonBlocks.PEP_UP_FLOWER)
|
||||
@JvmField
|
||||
val MEDICINAL_BREW = noSettingsItem("medicinal_brew")
|
||||
val MEDICINAL_BREW = itemNameBlockItem("medicinal_brew", CobblemonBlocks.MEDICINAL_BREW)
|
||||
@JvmField
|
||||
val REMEDY = compostableItem("remedy", RemedyItem(RemedyItem.NORMAL), 0.65f)
|
||||
@JvmField
|
||||
@@ -721,7 +719,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val SUPERB_REMEDY = compostableItem("superb_remedy", RemedyItem(RemedyItem.SUPERB), 1f)
|
||||
@JvmField
|
||||
val MOOMOO_MILK = create("moomoo_milk", MoomooMilk(Item.Properties().craftRemainder(Items.GLASS_BOTTLE).stacksTo((16))))
|
||||
val MOOMOO_MILK = create("moomoo_milk", MoomooMilk(Item.Properties().craftRemainder(Items.GLASS_BOTTLE).stacksTo((16)), CobblemonBlocks.MOOMOO_MILK))
|
||||
|
||||
@JvmField
|
||||
val POTION = create("potion", PotionItem(PotionType.POTION, CobblemonBlocks.POTION))
|
||||
@@ -750,27 +748,27 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val RED_MINT_SEEDS = mintSeed("red", MintType.RED.getCropBlock())
|
||||
@JvmField
|
||||
val RED_MINT_LEAF = mintLeaf("red", MintLeafItem(MintType.RED))
|
||||
val RED_MINT_LEAF = compostableItemNameBlockItem("red_mint_leaf", CobblemonBlocks.RED_MINT_LEAF_LITTER, 0.50f)
|
||||
@JvmField
|
||||
val BLUE_MINT_SEEDS = mintSeed("blue", MintType.BLUE.getCropBlock())
|
||||
@JvmField
|
||||
val BLUE_MINT_LEAF = mintLeaf("blue", MintLeafItem(MintType.BLUE))
|
||||
val BLUE_MINT_LEAF = compostableItemNameBlockItem("blue_mint_leaf", CobblemonBlocks.BLUE_MINT_LEAF_LITTER, 0.50f)
|
||||
@JvmField
|
||||
val CYAN_MINT_SEEDS = mintSeed("cyan", MintType.CYAN.getCropBlock())
|
||||
@JvmField
|
||||
val CYAN_MINT_LEAF = mintLeaf("cyan", MintLeafItem(MintType.CYAN))
|
||||
val CYAN_MINT_LEAF = compostableItemNameBlockItem("cyan_mint_leaf", CobblemonBlocks.CYAN_MINT_LEAF_LITTER, 0.50f)
|
||||
@JvmField
|
||||
val PINK_MINT_SEEDS = mintSeed("pink", MintType.PINK.getCropBlock())
|
||||
@JvmField
|
||||
val PINK_MINT_LEAF = mintLeaf("pink", MintLeafItem(MintType.PINK))
|
||||
val PINK_MINT_LEAF = compostableItemNameBlockItem("pink_mint_leaf", CobblemonBlocks.PINK_MINT_LEAF_LITTER, 0.50f)
|
||||
@JvmField
|
||||
val GREEN_MINT_SEEDS = mintSeed("green", MintType.GREEN.getCropBlock())
|
||||
@JvmField
|
||||
val GREEN_MINT_LEAF = mintLeaf("green", MintLeafItem(MintType.GREEN))
|
||||
val GREEN_MINT_LEAF = compostableItemNameBlockItem("green_mint_leaf", CobblemonBlocks.GREEN_MINT_LEAF_LITTER, 0.50f)
|
||||
@JvmField
|
||||
val WHITE_MINT_SEEDS = mintSeed("white", MintType.WHITE.getCropBlock())
|
||||
@JvmField
|
||||
val WHITE_MINT_LEAF = mintLeaf("white", MintLeafItem(MintType.WHITE))
|
||||
val WHITE_MINT_LEAF = compostableItemNameBlockItem("white_mint_leaf", CobblemonBlocks.WHITE_MINT_LEAF_LITTER, 0.50f)
|
||||
|
||||
val mints = mutableMapOf<String, MintItem>()
|
||||
|
||||
@@ -971,7 +969,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val EJECT_PACK = heldItem("eject_pack")
|
||||
@JvmField
|
||||
val ELECTRIC_SEED = heldItem("electric_seed")
|
||||
val ELECTRIC_SEED = itemNameBlockItem("electric_seed", CobblemonBlocks.ELECTRIC_SEED)
|
||||
@JvmField
|
||||
val EVERSTONE = heldItem("everstone")
|
||||
@JvmField
|
||||
@@ -987,7 +985,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val FOCUS_SASH = heldItem("focus_sash")
|
||||
@JvmField
|
||||
val GRASSY_SEED = heldItem("grassy_seed")
|
||||
val GRASSY_SEED = itemNameBlockItem("grassy_seed", CobblemonBlocks.GRASSY_SEED)
|
||||
@JvmField
|
||||
val GRIP_CLAW = heldItem("grip_claw")
|
||||
@JvmField
|
||||
@@ -1035,7 +1033,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val POWER_WEIGHT = heldItem("power_weight")
|
||||
@JvmField
|
||||
val PSYCHIC_SEED = heldItem("psychic_seed")
|
||||
val PSYCHIC_SEED = itemNameBlockItem("psychic_seed", CobblemonBlocks.PSYCHIC_SEED)
|
||||
@JvmField
|
||||
val PROTECTIVE_PADS = heldItem("protective_pads")
|
||||
@JvmField
|
||||
@@ -1059,7 +1057,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val SILVER_POWDER = heldItem("silver_powder")
|
||||
@JvmField
|
||||
val SOFT_SAND = heldItem("soft_sand")
|
||||
val SOFT_SAND = itemNameBlockItem("soft_sand", CobblemonBlocks.SOFT_SAND)
|
||||
@JvmField
|
||||
val SMOKE_BALL = itemNameBlockItem("smoke_ball", CobblemonBlocks.SMOKE_BALL)
|
||||
@JvmField
|
||||
@@ -1067,7 +1065,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val STICKY_BARB = heldItem("sticky_barb")
|
||||
@JvmField
|
||||
val TERRAIN_EXTENDER = heldItem("terrain_extender")
|
||||
val TERRAIN_EXTENDER = itemNameBlockItem("terrain_extender", CobblemonBlocks.TERRAIN_EXTENDER)
|
||||
@JvmField
|
||||
val THROAT_SPRAY = itemNameBlockItem("throat_spray", CobblemonBlocks.THROAT_SPRAY)
|
||||
@JvmField
|
||||
@@ -1087,13 +1085,13 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val MIRROR_HERB = compostableHeldItem("mirror_herb", null, 0.85F)
|
||||
@JvmField
|
||||
val MISTY_SEED = heldItem("misty_seed")
|
||||
val MISTY_SEED = itemNameBlockItem("misty_seed", CobblemonBlocks.MISTY_SEED)
|
||||
@JvmField
|
||||
val POWER_HERB = compostableHeldItem("power_herb", null, 0.85F)
|
||||
@JvmField
|
||||
val WHITE_HERB = compostableHeldItem("white_herb", null, 0.85F)
|
||||
@JvmField
|
||||
val BRIGHT_POWDER = itemNameBlockItem("bright_powder", CobblemonBlocks.BRIGHT_POWDER)
|
||||
val BRIGHT_POWDER = glowItem("bright_powder", CobblemonBlocks.BRIGHT_POWDER)
|
||||
@JvmField
|
||||
val METAL_POWDER = itemNameBlockItem("metal_powder", CobblemonBlocks.METAL_POWDER)
|
||||
@JvmField
|
||||
@@ -1560,6 +1558,25 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
//@JvmField
|
||||
//val BINDING_SOIL = blockItem("binding_soil", CobblemonBlocks.BINDING_SOIL)
|
||||
|
||||
// Plants, Flora, and Flowers
|
||||
@JvmField
|
||||
val LOBAILA = compostableBlockItem("lobaila", CobblemonBlocks.LOBAILA, 0.65f)
|
||||
@JvmField
|
||||
val POMOHANA = compostableBlockItem("pomohana", CobblemonBlocks.POMOHANA, 0.3f)
|
||||
@JvmField
|
||||
val PAUVILLEA = compostableBlockItem("pauvillea", CobblemonBlocks.PAUVILLEA, 0.65f)
|
||||
@JvmField
|
||||
val WISTENSU = compostableBlockItem("wistensu", CobblemonBlocks.WISTENSU, 0.5f)
|
||||
|
||||
@JvmField
|
||||
val VOLTACONIA = compostableBlockItem("voltaconia", CobblemonBlocks.VOLTACONIA, 0.85f)
|
||||
@JvmField
|
||||
val VERDROOT = compostableBlockItem("verdroot", CobblemonBlocks.VERDROOT, 0.85f)
|
||||
@JvmField
|
||||
val MISTLEAF = compostableBlockItem("mistleaf", CobblemonBlocks.MISTLEAF, 0.85f)
|
||||
@JvmField
|
||||
val LUCIBELL = compostableBlockItem("lucibell", CobblemonBlocks.LUCIBELL, 0.85f)
|
||||
|
||||
private fun blockItem(name: String, block: Block, rarity: Rarity = Rarity.COMMON): BlockItem = this.create(name, BlockItem(block, Item.Properties().rarity(rarity)))
|
||||
|
||||
private fun itemNameBlockItem(name: String, block: Block, rarity: Rarity = Rarity.COMMON): ItemNameBlockItem = this.create(name, ItemNameBlockItem(block, Item.Properties().rarity(rarity)))
|
||||
@@ -1762,4 +1779,6 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
private fun foodItem(nutrition: Int, saturationModifier: Float): Item {
|
||||
return CobblemonItem(Item.Properties().food(FoodProperties.Builder().nutrition(nutrition).saturationModifier(saturationModifier).build()))
|
||||
}
|
||||
|
||||
private fun glowItem(name: String, block: Block): GlowItem = this.create(name, GlowItem(block, Item.Properties()))
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common
|
||||
|
||||
|
||||
import com.cobblemon.mod.common.block.campfirepot.CookingPotMenu
|
||||
import com.cobblemon.mod.common.api.molangui.container.MolangContainerMenu
|
||||
import com.cobblemon.mod.common.block.tmmachine.TMMachineMenu
|
||||
import com.cobblemon.mod.common.platform.PlatformRegistry
|
||||
import net.minecraft.core.Registry
|
||||
@@ -23,10 +22,9 @@ object CobblemonMenuType : PlatformRegistry<Registry<MenuType<*>>, ResourceKey<R
|
||||
|
||||
val COOKING_POT = MenuType.register("cooking_pot_menu", ::CookingPotMenu)
|
||||
val TM_MACHINE = MenuType.register("tm_machine_menu", ::TMMachineMenu)
|
||||
val MOLANG_UI = MenuType.register("molang_ui", ::MolangContainerMenu)
|
||||
|
||||
override val registry: Registry<MenuType<*>>
|
||||
get() = BuiltInRegistries.MENU
|
||||
override val resourceKey: ResourceKey<Registry<MenuType<*>>>
|
||||
get() = Registries.MENU
|
||||
}
|
||||
}
|
||||
@@ -32,10 +32,6 @@ import com.cobblemon.mod.common.client.net.effect.SpawnSnowstormParticleHandler
|
||||
import com.cobblemon.mod.common.client.net.gui.ExpGainedDataPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.gui.InteractPokemonUIPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.gui.PokedexUIPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.molangui.MolangUiCloseHandler
|
||||
import com.cobblemon.mod.common.client.net.molangui.MolangUiCompatibilityRequestHandler
|
||||
import com.cobblemon.mod.common.client.net.molangui.MolangUiOpenHandler
|
||||
import com.cobblemon.mod.common.client.net.molangui.MolangUiUpdateHandler
|
||||
import com.cobblemon.mod.common.client.net.gui.SetActiveTMPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.gui.SetTMMachineContainerDataPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.gui.SummaryUIPacketHandler
|
||||
@@ -87,10 +83,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.molangui.MolangUiClosePacket
|
||||
import com.cobblemon.mod.common.net.messages.client.molangui.MolangUiCompatibilityRequestPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.molangui.MolangUiOpenPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.molangui.MolangUiUpdatePacket
|
||||
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
|
||||
@@ -188,9 +180,6 @@ import com.cobblemon.mod.common.net.messages.server.dialogue.EscapeDialoguePacke
|
||||
import com.cobblemon.mod.common.net.messages.server.dialogue.InputToDialoguePacket
|
||||
import com.cobblemon.mod.common.net.messages.server.habitat.SaveHabitatBlockSettingsPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.npc.SaveNPCPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.molangui.MolangUiActionPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.molangui.MolangUiClosedPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.molangui.MolangUiCompatibilityResponsePacket
|
||||
import com.cobblemon.mod.common.net.messages.server.orientation.ServerboundUpdateOrientationPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.pasture.PasturePokemonPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.pasture.SetPastureConflictPacket
|
||||
@@ -250,9 +239,6 @@ import com.cobblemon.mod.common.net.serverhandling.dialogue.InputToDialogueHandl
|
||||
import com.cobblemon.mod.common.net.serverhandling.evolution.AcceptEvolutionHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.habitat.SaveHabitatBlockSettingsHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.npc.SaveNPCHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.molangui.MolangUiActionHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.molangui.MolangUiClosedHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.molangui.MolangUiCompatibilityResponseHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.orientation.ServerboundUpdateOrientationHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pasture.PasturePokemonHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pasture.SetPastureConflictHandler
|
||||
@@ -439,6 +425,7 @@ object CobblemonNetwork {
|
||||
list.add(PacketRegisterInfo(MovesRegistrySyncPacket.ID, MovesRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
|
||||
list.add(PacketRegisterInfo(BerryRegistrySyncPacket.ID, BerryRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
|
||||
list.add(PacketRegisterInfo(SpeciesRegistrySyncPacket.ID, SpeciesRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
|
||||
list.add(PacketRegisterInfo(DexEvolutionDisplaySyncPacket.ID, DexEvolutionDisplaySyncPacket::decode, DataRegistrySyncPacketHandler()))
|
||||
list.add(PacketRegisterInfo(PropertiesCompletionRegistrySyncPacket.ID, PropertiesCompletionRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
|
||||
list.add(PacketRegisterInfo(StandardSpeciesFeatureSyncPacket.ID, StandardSpeciesFeatureSyncPacket::decode, DataRegistrySyncPacketHandler()))
|
||||
list.add(PacketRegisterInfo(GlobalSpeciesFeatureSyncPacket.ID, GlobalSpeciesFeatureSyncPacket::decode, DataRegistrySyncPacketHandler()))
|
||||
@@ -459,11 +446,6 @@ object CobblemonNetwork {
|
||||
list.add(PacketRegisterInfo(RideSettingsSyncPacket.ID, RideSettingsSyncPacket::decode, RideSettingsSyncHandler))
|
||||
list.add(PacketRegisterInfo(TechnicalMachineRegistrySyncPacket.ID, TechnicalMachineRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
|
||||
|
||||
list.add(PacketRegisterInfo(MolangUiOpenPacket.ID, MolangUiOpenPacket::decode, MolangUiOpenHandler))
|
||||
list.add(PacketRegisterInfo(MolangUiClosePacket.ID, MolangUiClosePacket::decode, MolangUiCloseHandler))
|
||||
list.add(PacketRegisterInfo(MolangUiUpdatePacket.ID, MolangUiUpdatePacket::decode, MolangUiUpdateHandler))
|
||||
list.add(PacketRegisterInfo(MolangUiCompatibilityRequestPacket.ID, MolangUiCompatibilityRequestPacket::decode, MolangUiCompatibilityRequestHandler))
|
||||
|
||||
// Effects
|
||||
list.add(PacketRegisterInfo(SpawnSnowstormParticlePacket.ID, SpawnSnowstormParticlePacket::decode, SpawnSnowstormParticleHandler))
|
||||
list.add(PacketRegisterInfo(SpawnSnowstormEntityParticlePacket.ID, SpawnSnowstormEntityParticlePacket::decode, SpawnSnowstormEntityParticleHandler))
|
||||
@@ -662,10 +644,6 @@ object CobblemonNetwork {
|
||||
list.add(PacketRegisterInfo(SetEntityBehaviourPacket.ID, SetEntityBehaviourPacket::decode, SetEntityBehaviourHandler))
|
||||
list.add(PacketRegisterInfo(DamageOnCollisionPacket.ID, DamageOnCollisionPacket::decode, DamageOnCollisionPacketHandler))
|
||||
|
||||
list.add(PacketRegisterInfo(MolangUiActionPacket.ID, MolangUiActionPacket::decode, MolangUiActionHandler))
|
||||
list.add(PacketRegisterInfo(MolangUiClosedPacket.ID, MolangUiClosedPacket::decode, MolangUiClosedHandler))
|
||||
list.add(PacketRegisterInfo(MolangUiCompatibilityResponsePacket.ID, MolangUiCompatibilityResponsePacket::decode, MolangUiCompatibilityResponseHandler))
|
||||
|
||||
// Habitat blocks
|
||||
list.add(PacketRegisterInfo(SaveHabitatBlockSettingsPacket.ID, SaveHabitatBlockSettingsPacket::decode, SaveHabitatBlockSettingsHandler))
|
||||
|
||||
@@ -679,4 +657,4 @@ object CobblemonNetwork {
|
||||
fun sendPacketToPlayer(player: ServerPlayer, packet: NetworkPacket<*>) {
|
||||
Cobblemon.implementation.networkManager.sendPacketToPlayer(player, packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,6 +148,9 @@ object CobblemonSounds : PlatformRegistry<Registry<SoundEvent>, ResourceKey<Regi
|
||||
@JvmField
|
||||
val MONITOR_GLITCHING = this.create("block.monitor.glitching")
|
||||
|
||||
@JvmField
|
||||
val BRIGHT_POWDER_USE = this.create("block.bright_powder.use")
|
||||
|
||||
@JvmField
|
||||
val FOSSIL_MACHINE_ACTIVATE = this.create("block.fossil_machine.activate")
|
||||
@JvmField
|
||||
@@ -657,5 +660,95 @@ object CobblemonSounds : PlatformRegistry<Registry<SoundEvent>, ResourceKey<Regi
|
||||
SoundEvents.WOOL_FALL
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val ITEM_BLOCK_POWDER_PLACE = this.create("block.item_block.powder.place")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_POWDER_BREAK = this.create("block.item_block.powder.break")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_POWDER_STEP = this.create("block.item_block.powder.step")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_POWDER_SOUNDS = SoundType(1f, 1.1f,
|
||||
ITEM_BLOCK_POWDER_BREAK,
|
||||
ITEM_BLOCK_POWDER_STEP,
|
||||
ITEM_BLOCK_POWDER_PLACE,
|
||||
ITEM_BLOCK_POWDER_STEP,
|
||||
ITEM_BLOCK_POWDER_STEP
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val ITEM_BLOCK_BOTTLE_LIQUID_PLACE = this.create("block.item_block.bottle_liquid.place")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_BOTTLE_LIQUID_BREAK = this.create("block.item_block.bottle_liquid.break")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_BOTTLE_LIQUID_STEP = this.create("block.item_block.bottle_liquid.step")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS = SoundType(1f, 1.1f,
|
||||
ITEM_BLOCK_BOTTLE_LIQUID_BREAK,
|
||||
ITEM_BLOCK_BOTTLE_LIQUID_STEP,
|
||||
ITEM_BLOCK_BOTTLE_LIQUID_PLACE,
|
||||
ITEM_BLOCK_BOTTLE_LIQUID_PLACE,
|
||||
ITEM_BLOCK_BOTTLE_LIQUID_PLACE
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val ITEM_BLOCK_BOTTLE_PILLS_PLACE = this.create("block.item_block.bottle_pills.place")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_BOTTLE_PILLS_BREAK = this.create("block.item_block.bottle_pills.break")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_BOTTLE_PILLS_STEP = this.create("block.item_block.bottle_pills.step")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_BOTTLE_PILLS_SOUNDS = SoundType(1f, 1.1f,
|
||||
ITEM_BLOCK_BOTTLE_PILLS_BREAK,
|
||||
ITEM_BLOCK_BOTTLE_PILLS_STEP,
|
||||
ITEM_BLOCK_BOTTLE_PILLS_PLACE,
|
||||
ITEM_BLOCK_BOTTLE_PILLS_PLACE,
|
||||
ITEM_BLOCK_BOTTLE_PILLS_PLACE
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val ITEM_BLOCK_DICE_PLACE = this.create("block.item_block.dice.place")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_DICE_BREAK = this.create("block.item_block.dice.break")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_DICE_STEP = this.create("block.item_block.dice.step")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_DICE_SOUNDS = SoundType(1f, 1.1f,
|
||||
ITEM_BLOCK_DICE_BREAK,
|
||||
ITEM_BLOCK_DICE_STEP,
|
||||
ITEM_BLOCK_DICE_PLACE,
|
||||
ITEM_BLOCK_DICE_STEP,
|
||||
ITEM_BLOCK_DICE_STEP
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val ITEM_BLOCK_METAL_CAN_PLACE = this.create("block.item_block.metal_can.place")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_METAL_CAN_BREAK = this.create("block.item_block.metal_can.break")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_METAL_CAN_STEP = this.create("block.item_block.metal_can.step")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_METAL_CAN_SOUNDS = SoundType(1f, 1.1f,
|
||||
ITEM_BLOCK_METAL_CAN_BREAK,
|
||||
ITEM_BLOCK_METAL_CAN_STEP,
|
||||
ITEM_BLOCK_METAL_CAN_PLACE,
|
||||
ITEM_BLOCK_METAL_CAN_STEP,
|
||||
ITEM_BLOCK_METAL_CAN_STEP
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val ITEM_BLOCK_JAR_GLASS_LIQUID_PLACE = this.create("block.item_block.jar_glass_liquid.place")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_JAR_GLASS_LIQUID_BREAK = this.create("block.item_block.jar_glass_liquid.break")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_JAR_GLASS_LIQUID_STEP = this.create("block.item_block.jar_glass_liquid.step")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_JAR_GLASS_LIQUID_SOUNDS = SoundType(1f, 1.1f,
|
||||
ITEM_BLOCK_JAR_GLASS_LIQUID_BREAK,
|
||||
ITEM_BLOCK_JAR_GLASS_LIQUID_STEP,
|
||||
ITEM_BLOCK_JAR_GLASS_LIQUID_PLACE,
|
||||
ITEM_BLOCK_JAR_GLASS_LIQUID_STEP,
|
||||
ITEM_BLOCK_JAR_GLASS_LIQUID_STEP
|
||||
)
|
||||
|
||||
private fun create(name: String): SoundEvent = this.create(name, SoundEvent.createVariableRangeEvent(cobblemonResource(name)))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.battles.model
|
||||
|
||||
/**
|
||||
* How a [PokemonBattle] came to an end.
|
||||
*/
|
||||
enum class BattleEndResult {
|
||||
/** One side defeated the other. */
|
||||
VICTORY,
|
||||
/** A wild Pokémon was captured, ending the battle. */
|
||||
CAPTURE,
|
||||
/** An actor forfeited, giving the victory to the other side. */
|
||||
FORFEIT,
|
||||
/** A player fled from a wild Pokémon. */
|
||||
FLED,
|
||||
/** The battle was stopped without a winner, such as by a command, a player disconnecting or an error. */
|
||||
STOPPED
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
|
||||
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.BattleEndedEvent
|
||||
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
|
||||
@@ -124,6 +125,12 @@ open class PokemonBattle(
|
||||
var winners = listOf<BattleActor>()
|
||||
var losers = listOf<BattleActor>()
|
||||
var ended = false
|
||||
/** How the battle ended, or null if it hasn't ended yet. */
|
||||
var endResult: BattleEndResult? = null
|
||||
/** The actor that forfeited the battle, if any. */
|
||||
var forfeitedBy: BattleActor? = null
|
||||
/** Whether a player fled from the battle. */
|
||||
var fled = false
|
||||
// TEMP battle showcase stuff
|
||||
var announcingRules = false
|
||||
var turn: Int = 0
|
||||
@@ -251,6 +258,14 @@ open class PokemonBattle(
|
||||
|
||||
fun end() {
|
||||
ended = true
|
||||
val result = when {
|
||||
winners.isNotEmpty() && showdownMessages.any { "capture" in it } -> BattleEndResult.CAPTURE
|
||||
winners.isNotEmpty() && forfeitedBy != null -> BattleEndResult.FORFEIT
|
||||
winners.isNotEmpty() -> BattleEndResult.VICTORY
|
||||
fled -> BattleEndResult.FLED
|
||||
else -> BattleEndResult.STOPPED
|
||||
}
|
||||
endResult = result
|
||||
val awardToFainted = Cobblemon.config.awardExperienceToFaintedPokemon
|
||||
val awardOnLoss = Cobblemon.config.awardExperienceOnBattleLoss
|
||||
this.actors.forEach { actor ->
|
||||
@@ -332,6 +347,7 @@ open class PokemonBattle(
|
||||
}
|
||||
}
|
||||
}
|
||||
CobblemonEvents.BATTLE_ENDED.post(BattleEndedEvent(this, result, forfeitedBy.takeIf { result == BattleEndResult.FORFEIT }))
|
||||
sendUpdate(BattleEndPacket())
|
||||
BattleRegistry.closeBattle(this)
|
||||
}
|
||||
@@ -502,6 +518,7 @@ open class PokemonBattle(
|
||||
.forEach{it.pokemon.heal()}
|
||||
CobblemonEvents.BATTLE_FLED.post(BattleFledEvent(this, actors.asSequence().filterIsInstance<PlayerBattleActor>().iterator().next()))
|
||||
actors.filterIsInstance<EntityBackedBattleActor<*>>().mapNotNull { it.entity }.forEach { it.sendSystemMessage(battleLang("flee").yellow()) }
|
||||
fled = true
|
||||
stop()
|
||||
}
|
||||
}
|
||||
@@ -539,6 +556,7 @@ open class PokemonBattle(
|
||||
private fun checkForfeit(): Boolean {
|
||||
val forfeit = actors.find { it.responses.any { it is ForfeitActionResponse } }
|
||||
return forfeit?.let {
|
||||
forfeitedBy = it
|
||||
this.dispatchWaiting { this.broadcastChatMessage(battleLang("forfeit", it.getName()).red()) }
|
||||
writeShowdownAction(">forcelose ${it.showdownId}")
|
||||
true
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.data
|
||||
|
||||
import com.cobblemon.mod.common.util.endsWith
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import net.minecraft.core.HolderLookup
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.util.concurrent.ExecutionException
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.packs.PackType
|
||||
import net.minecraft.server.packs.resources.ResourceManager
|
||||
|
||||
/**
|
||||
* A [DataRegistry] that consumes JSON files.
|
||||
* Every deserialized instance is attached to an [Identifier].
|
||||
* For example a file under data/mymod/[resourcePath]/entry.json would be backed by the identifier modid:entry.
|
||||
*
|
||||
* @param T The type of the data consumed by this registry.
|
||||
*
|
||||
* @author Licious
|
||||
* @since August 5th, 2022
|
||||
*/
|
||||
interface ServerJsonDataRegistry<T> : DataRegistry {
|
||||
|
||||
/**
|
||||
* The [TypeToken] of type [T].
|
||||
*/
|
||||
val typeToken: TypeToken<T>
|
||||
|
||||
/**
|
||||
* The folder location for the data this registry will consume.
|
||||
*/
|
||||
val resourcePath: String
|
||||
|
||||
override val type: PackType
|
||||
get() = PackType.SERVER_DATA
|
||||
|
||||
override fun reload(manager: ResourceManager) {
|
||||
//NO-OP, handled by reload(manager, registry)
|
||||
}
|
||||
|
||||
fun reload(manager: ResourceManager, registry: HolderLookup.Provider) {
|
||||
val gson = setupParser(registry)
|
||||
val data = hashMapOf<ResourceLocation, T>()
|
||||
manager.listResources(resourcePath) { path -> path.endsWith(JSON_EXTENSION) }.forEach { (identifier, resource) ->
|
||||
if (identifier.namespace == "pixelmon") {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
resource.open().use { stream ->
|
||||
stream.bufferedReader().use { reader ->
|
||||
val resolvedIdentifier = ResourceLocation.fromNamespaceAndPath(identifier.namespace, File(identifier.path).nameWithoutExtension)
|
||||
data[resolvedIdentifier] = parse(reader, resolvedIdentifier, gson)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reload(data, registry)
|
||||
}
|
||||
|
||||
fun parse(reader: BufferedReader, identifier: ResourceLocation, gson: Gson): T {
|
||||
return try {
|
||||
gson.fromJson(reader, typeToken.type)
|
||||
} catch (exception: Exception) {
|
||||
throw ExecutionException("Error loading JSON for data: $identifier", exception)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the creation of [Gson] instances which have adapters that are registry-aware, e.g. [com.cobblemon.mod.common.pokemon.evolution.adapters.LegacyItemConditionWrapperAdapter]
|
||||
*/
|
||||
fun setupParser(registry: HolderLookup.Provider): Gson
|
||||
|
||||
/**
|
||||
* Reloads this registry from the deserialized data.
|
||||
*
|
||||
* @param data A map of the data associating an instance to the respective identifier from the [ResourceManager].
|
||||
*/
|
||||
fun reload(data: Map<ResourceLocation, T>, registry: HolderLookup.Provider)
|
||||
|
||||
companion object {
|
||||
const val JSON_EXTENSION = ".json"
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ 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.BattleEndedEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.BattleVictoryEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.instruction.FormeChangeEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.instruction.MegaEvolutionEvent
|
||||
@@ -139,6 +140,9 @@ object CobblemonEvents {
|
||||
@JvmField
|
||||
val BATTLE_FAINTED = EventObservable<BattleFaintedEvent>()
|
||||
|
||||
@JvmField
|
||||
val BATTLE_ENDED = EventObservable<BattleEndedEvent>()
|
||||
|
||||
// instructions
|
||||
@JvmField
|
||||
val MEGA_EVOLUTION = EventObservable<MegaEvolutionEvent>()
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.DoubleValue
|
||||
import com.bedrockk.molang.runtime.value.MoValue
|
||||
import com.bedrockk.molang.runtime.value.StringValue
|
||||
import com.cobblemon.mod.common.api.battles.model.BattleEndResult
|
||||
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
|
||||
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
|
||||
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
|
||||
import com.cobblemon.mod.common.util.asArrayValue
|
||||
|
||||
/**
|
||||
* Event fired whenever a [PokemonBattle] ends, regardless of how it ended (victory, fleeing, being stopped, etc).
|
||||
*
|
||||
* [PokemonBattle.winners] and [PokemonBattle.losers] are only populated if the [result] has a winner,
|
||||
* that is [BattleEndResult.VICTORY], [BattleEndResult.CAPTURE] or [BattleEndResult.FORFEIT].
|
||||
*
|
||||
* @param result How the battle ended.
|
||||
* @param forfeitedBy The actor that forfeited, if the [result] is [BattleEndResult.FORFEIT].
|
||||
*/
|
||||
data class BattleEndedEvent(
|
||||
override val battle: PokemonBattle,
|
||||
val result: BattleEndResult,
|
||||
val forfeitedBy: BattleActor?
|
||||
) : BattleEvent {
|
||||
/** Whether one side won the battle, which includes captures and forfeits. */
|
||||
val hasWinner = battle.winners.isNotEmpty()
|
||||
val wasVictory = result == BattleEndResult.VICTORY
|
||||
val wasCapture = result == BattleEndResult.CAPTURE
|
||||
val wasForfeit = result == BattleEndResult.FORFEIT
|
||||
val wasFled = result == BattleEndResult.FLED
|
||||
val wasStopped = result == BattleEndResult.STOPPED
|
||||
|
||||
/** Whether the battle was between players and wild Pokémon. */
|
||||
val isPvW = battle.isPvW
|
||||
/** Whether the battle was between players and NPCs. */
|
||||
val isPvN = battle.isPvN
|
||||
/** Whether the battle was between players. */
|
||||
val isPvP = battle.isPvP
|
||||
|
||||
val context = mutableMapOf<String, MoValue>(
|
||||
"battle" to battle.struct,
|
||||
"result" to StringValue(result.name.lowercase()),
|
||||
"has_winner" to DoubleValue(hasWinner),
|
||||
"was_victory" to DoubleValue(wasVictory),
|
||||
"was_capture" to DoubleValue(wasCapture),
|
||||
"was_forfeit" to DoubleValue(wasForfeit),
|
||||
"was_fled" to DoubleValue(wasFled),
|
||||
"was_stopped" to DoubleValue(wasStopped),
|
||||
"is_pvw" to DoubleValue(isPvW),
|
||||
"is_pvn" to DoubleValue(isPvN),
|
||||
"is_pvp" to DoubleValue(isPvP),
|
||||
"winners" to battle.winners.asArrayValue { it.struct },
|
||||
"losers" to battle.losers.asArrayValue { it.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 }
|
||||
).apply {
|
||||
forfeitedBy?.let { put("forfeited_by", it.struct) }
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ object Fossils: JsonDataRegistry<Fossil> {
|
||||
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
|
||||
.registerTypeAdapter(PokemonProperties::class.java, pokemonPropertiesShortAdapter)
|
||||
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Item::class.java).type, ItemLikeConditionAdapter)
|
||||
.registerTypeAdapter(ItemPredicate::class.java, LegacyItemConditionWrapperAdapter)
|
||||
.registerTypeAdapter(ItemPredicate::class.java, LegacyItemConditionWrapperAdapter())
|
||||
.create()
|
||||
|
||||
override val typeToken: TypeToken<Fossil> = TypeToken.get(Fossil::class.java)
|
||||
|
||||
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.api.gui
|
||||
|
||||
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
|
||||
import net.minecraft.client.gui.components.AbstractWidget
|
||||
import net.minecraft.client.gui.components.Renderable
|
||||
import net.minecraft.client.gui.components.events.GuiEventListener
|
||||
import net.minecraft.client.gui.narration.NarrationElementOutput
|
||||
import net.minecraft.network.chat.Component
|
||||
@@ -55,6 +54,8 @@ abstract class ParentWidget(
|
||||
}
|
||||
|
||||
override fun mouseClicked(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
|
||||
if (!active) return false
|
||||
|
||||
return children.any {
|
||||
it.mouseClicked(pMouseX, pMouseY, pButton)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.gui
|
||||
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
interface TreeNode<T> {
|
||||
val children: List<TreeNode<T>>
|
||||
}
|
||||
|
||||
class TreeDrawingNode<T>(
|
||||
val value: T,
|
||||
val children: List<TreeDrawingNode<T>>,
|
||||
) {
|
||||
var x = 0
|
||||
var y = 0
|
||||
var offset = 0
|
||||
}
|
||||
|
||||
fun <T> createDrawingTree(treeNode: TreeNode<T>): TreeDrawingNode<T> {
|
||||
val root = setup(treeNode, 0, mutableListOf<Int>(), mutableListOf<Int>())
|
||||
addMods(root, 0)
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> setup(
|
||||
treeNode: TreeNode<T>,
|
||||
depth: Int,
|
||||
nexts: MutableList<Int>,
|
||||
offset: MutableList<Int>,
|
||||
): TreeDrawingNode<T> {
|
||||
val children = treeNode.children.map { child ->
|
||||
setup(child, depth + 1, nexts, offset)
|
||||
}
|
||||
|
||||
val treeDrawingNode = TreeDrawingNode<T>(treeNode as T, children)
|
||||
treeDrawingNode.y = depth
|
||||
|
||||
while (nexts.size <= depth) {
|
||||
nexts.add(0)
|
||||
}
|
||||
|
||||
while (offset.size <= depth) {
|
||||
offset.add(0)
|
||||
}
|
||||
|
||||
var place =
|
||||
if (children.isEmpty()) {
|
||||
nexts[depth]
|
||||
} else if (children.size == 1) {
|
||||
// children.first().x - 1
|
||||
children.first().x
|
||||
} else {
|
||||
// (children[0].x + children[1].x) / 2
|
||||
(children.first().x + children.last().x) / 2
|
||||
}
|
||||
|
||||
offset[depth] = max(offset[depth], nexts[depth] - place)
|
||||
|
||||
if (children.isEmpty()) {
|
||||
treeDrawingNode.x = place
|
||||
} else {
|
||||
treeDrawingNode.x = place + offset[depth]
|
||||
}
|
||||
|
||||
nexts[depth] += 2
|
||||
treeDrawingNode.offset = offset[depth]
|
||||
|
||||
return treeDrawingNode
|
||||
}
|
||||
|
||||
private fun <T> addMods(
|
||||
treeNode: TreeDrawingNode<T>,
|
||||
modSum: Int,
|
||||
) {
|
||||
treeNode.x += modSum
|
||||
val newModSum = modSum + treeNode.offset
|
||||
|
||||
treeNode.children.forEach { child ->
|
||||
addMods(child, newModSum)
|
||||
}
|
||||
}
|
||||
|
||||
data class TreeDrawingBounds(val minX: Int, val maxX: Int, val minY: Int, val maxY: Int) {
|
||||
val width = maxX - minX
|
||||
val height = maxY - minY
|
||||
val xCenter = (maxX + minX) / 2.0
|
||||
val yCenter = (maxY + minY) / 2.0
|
||||
}
|
||||
|
||||
fun <T> calculateBounds(treeDrawingNode: TreeDrawingNode<T>): TreeDrawingBounds {
|
||||
var minX = treeDrawingNode.x
|
||||
var maxX = treeDrawingNode.x
|
||||
var minY = treeDrawingNode.y
|
||||
var maxY = treeDrawingNode.y
|
||||
|
||||
for (child in treeDrawingNode.children) {
|
||||
val childBounds = calculateBounds(child)
|
||||
|
||||
minX = min(minX, childBounds.minX)
|
||||
maxX = max(maxX, childBounds.maxX)
|
||||
minY = min(minY, childBounds.minY)
|
||||
maxY = max(maxY, childBounds.maxY)
|
||||
}
|
||||
|
||||
return TreeDrawingBounds(minX, maxX, minY, maxY)
|
||||
}
|
||||
+24
-20
@@ -10,7 +10,7 @@ package com.cobblemon.mod.common.api.interaction
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.conditional.RegistryLikeCondition
|
||||
import com.cobblemon.mod.common.api.data.JsonDataRegistry
|
||||
import com.cobblemon.mod.common.api.data.ServerJsonDataRegistry
|
||||
import com.cobblemon.mod.common.api.molang.ExpressionLike
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
import com.cobblemon.mod.common.api.pokemon.requirement.Requirement
|
||||
@@ -26,6 +26,7 @@ import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import net.minecraft.advancements.critereon.ItemPredicate
|
||||
import net.minecraft.core.HolderLookup
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.server.packs.PackType
|
||||
@@ -36,28 +37,31 @@ import net.minecraft.world.level.block.Block
|
||||
import net.minecraft.world.level.levelgen.structure.Structure
|
||||
import net.minecraft.world.level.material.Fluid
|
||||
|
||||
object PokemonInteractions : JsonDataRegistry<PokemonInteractionSet> {
|
||||
object PokemonInteractions : ServerJsonDataRegistry<PokemonInteractionSet> {
|
||||
override val id = cobblemonResource("pokemon_interactions")
|
||||
override val type = PackType.SERVER_DATA
|
||||
override fun setupParser(registry: HolderLookup.Provider): Gson {
|
||||
return GsonBuilder()
|
||||
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
|
||||
.registerTypeAdapter(PokemonProperties::class.java, pokemonPropertiesShortAdapter)
|
||||
.registerTypeAdapter(ItemPredicate::class.java, LegacyItemConditionWrapperAdapter(registry))
|
||||
.registerTypeAdapter(Requirement::class.java, CobblemonRequirementAdapter)
|
||||
.registerTypeAdapter(InteractionEffect::class.java, InteractionEffectAdapter)
|
||||
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
|
||||
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Biome::class.java).type, BiomeLikeConditionAdapter)
|
||||
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Block::class.java).type, BlockLikeConditionAdapter)
|
||||
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Item::class.java).type, ItemLikeConditionAdapter)
|
||||
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Structure::class.java).type, StructureLikeConditionAdapter)
|
||||
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Fluid::class.java).type, FluidLikeConditionAdapter)
|
||||
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
|
||||
.registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory())
|
||||
.setPrettyPrinting()
|
||||
.create()
|
||||
}
|
||||
|
||||
override val observable = SimpleObservable<PokemonInteractions>()
|
||||
override val typeToken: TypeToken<PokemonInteractionSet> = TypeToken.get(PokemonInteractionSet::class.java)
|
||||
override val resourcePath = "pokemon_interactions"
|
||||
override val gson: Gson = GsonBuilder()
|
||||
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
|
||||
.registerTypeAdapter(PokemonProperties::class.java, pokemonPropertiesShortAdapter)
|
||||
.registerTypeAdapter(ItemPredicate::class.java, LegacyItemConditionWrapperAdapter)
|
||||
.registerTypeAdapter(Requirement::class.java, CobblemonRequirementAdapter)
|
||||
.registerTypeAdapter(InteractionEffect::class.java, InteractionEffectAdapter)
|
||||
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
|
||||
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Biome::class.java).type, BiomeLikeConditionAdapter)
|
||||
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Block::class.java).type, BlockLikeConditionAdapter)
|
||||
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Item::class.java).type, ItemLikeConditionAdapter)
|
||||
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Structure::class.java).type, StructureLikeConditionAdapter)
|
||||
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Fluid::class.java).type, FluidLikeConditionAdapter)
|
||||
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
|
||||
.registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory())
|
||||
.setPrettyPrinting()
|
||||
.create()
|
||||
|
||||
val interactions = mutableListOf<PokemonInteractionSet>()
|
||||
|
||||
@@ -65,7 +69,7 @@ object PokemonInteractions : JsonDataRegistry<PokemonInteractionSet> {
|
||||
PokemonInteractionsSyncPacket(interactions).sendToPlayer(player)
|
||||
}
|
||||
|
||||
override fun reload(data: Map<ResourceLocation, PokemonInteractionSet>) {
|
||||
override fun reload(data: Map<ResourceLocation, PokemonInteractionSet>, registry: HolderLookup.Provider) {
|
||||
interactions.clear()
|
||||
val split = data.entries
|
||||
.partition {
|
||||
@@ -87,4 +91,4 @@ object PokemonInteractions : JsonDataRegistry<PokemonInteractionSet> {
|
||||
.flatMap { it.interactions }
|
||||
.firstOrNull(interactionCheck)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -16,6 +16,7 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
|
||||
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
|
||||
import com.cobblemon.mod.common.battles.actor.PokemonBattleActor
|
||||
import com.cobblemon.mod.common.entity.npc.NPCBattleActor
|
||||
import com.cobblemon.mod.common.util.asArrayValue
|
||||
|
||||
object BattleActorMoLangFunctions : AbstractMoLangFunctionHolder<BattleActor>() {
|
||||
override fun BattleActor.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
|
||||
@@ -25,6 +26,7 @@ object BattleActorMoLangFunctions : AbstractMoLangFunctionHolder<BattleActor>()
|
||||
functions["is_npc"] = { DoubleValue(actor.type == ActorType.NPC) }
|
||||
functions["is_player"] = { DoubleValue(actor.type == ActorType.PLAYER) }
|
||||
functions["is_wild"] = { DoubleValue(actor.type == ActorType.WILD) }
|
||||
functions["battle_pokemon"] = { actor.pokemonList.asArrayValue { it.struct } }
|
||||
|
||||
when (actor) {
|
||||
is NPCBattleActor -> {
|
||||
|
||||
+5
-5
@@ -45,11 +45,6 @@ object GeneralMoLangFunctions {
|
||||
val functions: MutableMap<String, (MoParams) -> Any> = mutableMapOf()
|
||||
|
||||
init {
|
||||
functions["format"] = { params ->
|
||||
val value = params.get<MoValue>(0).asDouble()
|
||||
val decimals = params.get<MoValue>(1).asDouble().toInt().coerceIn(0, 10)
|
||||
StringValue(String.format("%.${decimals}f", value))
|
||||
}
|
||||
functions["print"] = { params ->
|
||||
val message = params.get<MoValue>(0).asString()
|
||||
LOGGER.info(message)
|
||||
@@ -60,6 +55,11 @@ object GeneralMoLangFunctions {
|
||||
struct.map.remove(variable)
|
||||
DoubleValue.ONE
|
||||
}
|
||||
functions["delete_variables"] = { params ->
|
||||
val struct = params.get<VariableStruct>(0)
|
||||
struct.map.clear()
|
||||
DoubleValue.ONE
|
||||
}
|
||||
functions["get_variable"] = { params ->
|
||||
val struct = params.get<VariableStruct>(0)
|
||||
val variable = params.getString(1)
|
||||
|
||||
+22
@@ -16,6 +16,7 @@ import com.cobblemon.mod.common.api.molang.ObjectValue
|
||||
import com.cobblemon.mod.common.api.pokedex.AbstractPokedexManager
|
||||
import com.cobblemon.mod.common.api.pokedex.CaughtCount
|
||||
import com.cobblemon.mod.common.api.pokedex.CaughtPercent
|
||||
import com.cobblemon.mod.common.api.pokedex.Dexes
|
||||
import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
|
||||
import com.cobblemon.mod.common.api.pokedex.PokedexManager
|
||||
import com.cobblemon.mod.common.api.pokedex.SeenCount
|
||||
@@ -74,6 +75,27 @@ object PokedexMoLangFunctions : AbstractMoLangFunctionHolder<AbstractPokedexMana
|
||||
functions["caught_percent"] = { DoubleValue(pokedex.getGlobalCalculatedValue(CaughtPercent)) }
|
||||
functions["seen_percent"] = { DoubleValue(pokedex.getGlobalCalculatedValue(SeenPercent)) }
|
||||
|
||||
functions["dex_caught_count"] = { params ->
|
||||
val dexId = params.getString(0).asIdentifierDefaultingNamespace()
|
||||
if (Dexes.dexEntryMap.containsKey(dexId)) DoubleValue(pokedex.getDexCalculatedValue(dexId, CaughtCount))
|
||||
else DoubleValue.ZERO
|
||||
}
|
||||
functions["dex_seen_count"] = { params ->
|
||||
val dexId = params.getString(0).asIdentifierDefaultingNamespace()
|
||||
if (Dexes.dexEntryMap.containsKey(dexId)) DoubleValue(pokedex.getDexCalculatedValue(dexId, SeenCount))
|
||||
else DoubleValue.ZERO
|
||||
}
|
||||
functions["dex_caught_percent"] = { params ->
|
||||
val dexId = params.getString(0).asIdentifierDefaultingNamespace()
|
||||
if (Dexes.dexEntryMap.containsKey(dexId)) DoubleValue(pokedex.getDexCalculatedValue(dexId, CaughtPercent).toDouble())
|
||||
else DoubleValue.ZERO
|
||||
}
|
||||
functions["dex_seen_percent"] = { params ->
|
||||
val dexId = params.getString(0).asIdentifierDefaultingNamespace()
|
||||
if (Dexes.dexEntryMap.containsKey(dexId)) DoubleValue(pokedex.getDexCalculatedValue(dexId, SeenPercent).toDouble())
|
||||
else DoubleValue.ZERO
|
||||
}
|
||||
|
||||
when (pokedex) {
|
||||
is PokedexManager -> {
|
||||
functions["player_id"] = { StringValue(pokedex.uuid.toString()) }
|
||||
|
||||
@@ -1,18 +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.molangui
|
||||
|
||||
enum class GuiMode {
|
||||
OVERLAY,
|
||||
SCREEN;
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String): GuiMode? = entries.firstOrNull { it.name.equals(value, ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
-50
@@ -1,50 +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.molangui
|
||||
|
||||
import com.cobblemon.mod.common.api.scripting.CobblemonScripts
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
object MolangUiActionRegistry {
|
||||
private val handlers = ConcurrentHashMap<ActionKey, MolangUiActionHandler>()
|
||||
|
||||
fun register(key: ActionKey, handler: MolangUiActionHandler) {
|
||||
check(handlers.putIfAbsent(key, handler) == null) { "Molang UI action '$key' is already registered" }
|
||||
}
|
||||
|
||||
fun unregister(key: ActionKey): Boolean = handlers.remove(key) != null
|
||||
|
||||
internal fun dispatch(
|
||||
session: MolangUiSession,
|
||||
action: ActionKey,
|
||||
parameters: Map<String, ParameterValue>
|
||||
) {
|
||||
val definition = session.contract.actions[action]
|
||||
?: throw IllegalArgumentException("undeclared action '${action.value}'")
|
||||
val registered = handlers[action]
|
||||
if (registered != null) {
|
||||
registered.handle(MolangUiActionContext(session, session.player, parameters))
|
||||
return
|
||||
}
|
||||
val scriptId = definition.handler
|
||||
?: throw IllegalStateException("action '${action.value}' has no registered handler")
|
||||
val script = CobblemonScripts.scripts[scriptId]
|
||||
?: throw IllegalStateException("Molang UI action script '$scriptId' was not loaded")
|
||||
val runtime = MolangUiServerRuntimeFactory.create(session)
|
||||
parameters.forEach { (name, value) -> runtime.environment.setSimpleVariable(name, value.asMoLangValue()) }
|
||||
script.resolve(runtime, emptyMap())
|
||||
}
|
||||
|
||||
internal fun executeOnOpen(session: MolangUiSession) {
|
||||
val scriptId = session.contract.onOpen ?: return
|
||||
val script = CobblemonScripts.scripts[scriptId]
|
||||
?: throw IllegalStateException("Molang UI on_open script '$scriptId' was not loaded")
|
||||
script.resolve(MolangUiServerRuntimeFactory.create(session), emptyMap())
|
||||
}
|
||||
}
|
||||
-201
@@ -1,201 +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.molangui
|
||||
|
||||
import com.google.gson.JsonArray
|
||||
import com.cobblemon.mod.common.api.data.DataRegistry
|
||||
import com.cobblemon.mod.common.api.molangui.container.CollectionConfig
|
||||
import com.cobblemon.mod.common.api.molangui.container.CollectionSource
|
||||
import com.cobblemon.mod.common.api.molangui.container.CloseItemsPolicy
|
||||
import com.cobblemon.mod.common.api.molangui.container.ItemFilterEntry
|
||||
import com.cobblemon.mod.common.api.molangui.container.ShiftClickRule
|
||||
import com.cobblemon.mod.common.api.molangui.container.SlotLayout
|
||||
import com.cobblemon.mod.common.api.reactive.SimpleObservable
|
||||
import com.cobblemon.mod.common.net.messages.client.molangui.MolangUiCompatibilityRequestPacket
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.server.packs.PackType
|
||||
import net.minecraft.server.packs.resources.ResourceManager
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
object MolangUiContractRegistry : DataRegistry {
|
||||
override val id = cobblemonResource("molang_ui_contracts")
|
||||
override val type = PackType.SERVER_DATA
|
||||
override val observable = SimpleObservable<MolangUiContractRegistry>()
|
||||
private val contracts = linkedMapOf<ResourceLocation, MolangUiContract>()
|
||||
private val parseFailures = AtomicLong()
|
||||
private const val DIRECTORY = "molang_ui"
|
||||
|
||||
override fun reload(manager: ResourceManager) {
|
||||
val next = linkedMapOf<ResourceLocation, MolangUiContract>()
|
||||
manager.listResources(DIRECTORY) { it.path.endsWith(".json") }.forEach { (location, resource) ->
|
||||
val id = ResourceLocation.fromNamespaceAndPath(
|
||||
location.namespace,
|
||||
location.path.removePrefix("$DIRECTORY/").removeSuffix(".json")
|
||||
)
|
||||
try {
|
||||
val json = resource.openAsReader().use { JsonParser.parseReader(it).asJsonObject }
|
||||
next[id] = parse(id, json)
|
||||
} catch (error: Exception) {
|
||||
parseFailures.incrementAndGet()
|
||||
Cobblemon.LOGGER.error("Rejected Molang UI contract '$location': ${error.message}")
|
||||
}
|
||||
}
|
||||
require(next.size <= MolangUiPacketLimits.MAX_CONTRACTS) {
|
||||
"Molang UI contract count exceeds ${MolangUiPacketLimits.MAX_CONTRACTS}"
|
||||
}
|
||||
contracts.clear()
|
||||
contracts.putAll(next)
|
||||
Cobblemon.LOGGER.info("Loaded ${contracts.size} validated Molang UI contracts")
|
||||
observable.emit(this)
|
||||
}
|
||||
|
||||
override fun sync(player: ServerPlayer) {
|
||||
MolangUiCompatibilityRequestPacket(versions()).sendToPlayer(player)
|
||||
}
|
||||
|
||||
internal fun parse(
|
||||
id: ResourceLocation,
|
||||
json: JsonObject,
|
||||
itemExists: (ResourceLocation) -> Boolean = BuiltInRegistries.ITEM::containsKey
|
||||
): MolangUiContract {
|
||||
require(!json.has("blocks_input")) { "blocks_input is unsupported; input handling is determined by mode" }
|
||||
val formatVersion = json.get("format_version").requireUiInt("format_version")
|
||||
require(formatVersion == MOLANG_UI_FORMAT_VERSION) { "unsupported format_version $formatVersion" }
|
||||
val contractVersion = json.get("contract_version").requireUiString("contract_version")
|
||||
MolangUiPacketLimits.requireIdentifier(contractVersion, "contract_version")
|
||||
val mode = GuiMode.fromString(json.get("mode").requireUiString("mode"))
|
||||
?: throw IllegalArgumentException("unknown mode '${json.get("mode").asString}'")
|
||||
val parameters = parseParameters(json.getAsJsonObject("parameters"))
|
||||
val actions = parseActions(json.getAsJsonObject("actions"))
|
||||
val collections = parseCollections(json.getAsJsonObject("collections"), itemExists)
|
||||
val shiftRules = parseShiftRules(json.getAsJsonArray("shift_click_rules"), collections.keys)
|
||||
val closePolicy = json.get("on_close_items")?.requireUiString("on_close_items")?.let { raw ->
|
||||
CloseItemsPolicy.fromId(raw) ?: throw IllegalArgumentException("unknown on_close_items policy '$raw'")
|
||||
} ?: CloseItemsPolicy.RETURN
|
||||
return MolangUiContract(
|
||||
id, formatVersion, contractVersion, mode, parameters, actions,
|
||||
json.get("on_open")?.requireUiString("on_open")?.let { ResourceLocation.tryParse(it) ?: error("invalid on_open script id") },
|
||||
collections, shiftRules, closePolicy,
|
||||
json.get("exclusive")?.requireUiBoolean("exclusive") ?: false
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseParameters(json: JsonObject?): Map<String, ParameterDefinition> {
|
||||
require((json?.size() ?: 0) <= MolangUiPacketLimits.MAX_ACTION_FIELDS) { "too many parameters" }
|
||||
return json?.entrySet()?.associate { (name, element) ->
|
||||
MolangUiParameters.requireName(name)
|
||||
val obj = element.asJsonObject
|
||||
val type = ParameterType.fromId(obj.get("type").requireUiString("parameters.$name.type"))
|
||||
?: throw IllegalArgumentException("unknown type for parameter '$name'")
|
||||
val default = obj.get("default")?.let { parseDefault(type, it, "parameters.$name.default") }
|
||||
name to ParameterDefinition(type, obj.get("required")?.requireUiBoolean("parameters.$name.required") ?: (default == null), default)
|
||||
}?.toMap() ?: emptyMap()
|
||||
}
|
||||
|
||||
private fun parseDefault(type: ParameterType, element: JsonElement, path: String): ParameterDefault {
|
||||
if (element.isJsonObject && element.asJsonObject.has("molang")) {
|
||||
require(element.asJsonObject.size() == 1) { "expression '$path' contains unknown fields" }
|
||||
return ParameterDefault.Molang(element.asJsonObject.get("molang").requireUiString("$path.molang"))
|
||||
}
|
||||
val value = when (type) {
|
||||
ParameterType.STRING -> ParameterValue.StringValue(element.requireUiString(path))
|
||||
ParameterType.NUMBER -> ParameterValue.NumberValue(element.requireUiNumber(path))
|
||||
ParameterType.BOOLEAN -> ParameterValue.BooleanValue(element.requireUiBoolean(path))
|
||||
ParameterType.VEC3 -> {
|
||||
require(element.isJsonArray && element.asJsonArray.size() == 3) { "'$path' must be a three-number array" }
|
||||
ParameterValue.Vec3Value(
|
||||
element.asJsonArray[0].requireUiNumber("$path[0]"),
|
||||
element.asJsonArray[1].requireUiNumber("$path[1]"),
|
||||
element.asJsonArray[2].requireUiNumber("$path[2]")
|
||||
)
|
||||
}
|
||||
}
|
||||
MolangUiPacketLimits.requireValue(value.asWireValue(), path)
|
||||
return ParameterDefault.Literal(value)
|
||||
}
|
||||
|
||||
private fun parseActions(json: JsonObject?): Map<ActionKey, ActionDefinition> {
|
||||
val result = json?.entrySet()?.associate { (name, element) ->
|
||||
val key = ActionKey(name)
|
||||
val obj = element.asJsonObject
|
||||
val handler = obj.get("handler")?.requireUiString("actions.$name.handler")?.let { ResourceLocation.tryParse(it) ?: error("invalid handler for action '$name'") }
|
||||
val params = obj.getAsJsonObject("parameters")?.entrySet()?.associate { (paramName, typeElement) ->
|
||||
MolangUiPacketLimits.requireIdentifier(paramName, "action parameter name")
|
||||
paramName to (ParameterType.fromId(typeElement.requireUiString("actions.$name.parameters.$paramName"))
|
||||
?: throw IllegalArgumentException("unknown type for action parameter '$name.$paramName'"))
|
||||
}?.toMap() ?: emptyMap()
|
||||
require(params.size <= MolangUiPacketLimits.MAX_ACTION_FIELDS) { "action '$name' has too many parameters" }
|
||||
key to ActionDefinition(handler, params)
|
||||
}?.toMap() ?: emptyMap()
|
||||
require(result.size <= MolangUiPacketLimits.MAX_ACTION_FIELDS) { "contract has too many actions" }
|
||||
return result
|
||||
}
|
||||
|
||||
private fun parseCollections(
|
||||
json: JsonObject?,
|
||||
itemExists: (ResourceLocation) -> Boolean
|
||||
): Map<String, CollectionConfig> {
|
||||
require((json?.size() ?: 0) <= MolangUiPacketLimits.MAX_CONTRACTS) { "too many container collections" }
|
||||
return json?.entrySet()?.associate { (name, element) ->
|
||||
MolangUiPacketLimits.requireIdentifier(name, "collection name")
|
||||
val obj = element.asJsonObject
|
||||
val source = CollectionSource.fromId(obj.get("source")?.requireUiString("collections.$name.source") ?: "session")
|
||||
?: throw IllegalArgumentException("unknown source for collection '$name'")
|
||||
val filters = obj.getAsJsonArray("allowed_items")?.map { raw ->
|
||||
val filter = ItemFilterEntry.parse(raw.requireUiString("collections.$name.allowed_items"))
|
||||
?: throw IllegalArgumentException("invalid item filter '${raw.asString}' in '$name'")
|
||||
if (filter is ItemFilterEntry.Item) require(itemExists(filter.id)) {
|
||||
"unknown item '${filter.id}' in collection '$name'"
|
||||
}
|
||||
filter
|
||||
}?.toList() ?: emptyList()
|
||||
val layoutObj = obj.getAsJsonObject("layout") ?: throw IllegalArgumentException("collection '$name' is missing layout")
|
||||
val offset = layoutObj.pair("offset", 0, 0)
|
||||
val grid = layoutObj.pair("grid_dimensions", 1, 1)
|
||||
val slotSize = layoutObj.pair("slot_size", 18, 18)
|
||||
val spacing = layoutObj.pair("slot_spacing", 0, 0)
|
||||
val layout = SlotLayout(offset.first, offset.second, grid.first, grid.second, slotSize.first, slotSize.second, spacing.first, spacing.second)
|
||||
name to CollectionConfig(
|
||||
obj.get("size").requireUiInt("collections.$name.size"), filters,
|
||||
obj.get("take_only")?.requireUiBoolean("collections.$name.take_only") ?: false, source,
|
||||
obj.getAsJsonArray("slots")?.map { it.requireUiInt("collections.$name.slots") }?.toList(), layout
|
||||
)
|
||||
}?.toMap() ?: emptyMap()
|
||||
}
|
||||
|
||||
private fun parseShiftRules(array: JsonArray?, collections: Set<String>): List<ShiftClickRule> =
|
||||
array?.map { element ->
|
||||
val obj = element.asJsonObject
|
||||
val from = obj.stringList("from")
|
||||
val to = obj.stringList("to")
|
||||
val valid = collections + ShiftClickRule.PLAYER_SIDE_NAMES
|
||||
require(from.isNotEmpty() && to.isNotEmpty() && (from + to).all { it in valid }) { "shift-click rule references an unknown collection" }
|
||||
ShiftClickRule(from, to)
|
||||
}?.toList() ?: emptyList()
|
||||
|
||||
fun getContract(id: ResourceLocation): MolangUiContract? = contracts[id]
|
||||
fun getAllContracts(): Map<ResourceLocation, MolangUiContract> = contracts.toMap()
|
||||
fun versions(): Map<ResourceLocation, String> = contracts.mapValues { it.value.contractVersion }
|
||||
fun parseFailureCount(): Long = parseFailures.get()
|
||||
}
|
||||
|
||||
private fun JsonObject.pair(name: String, first: Int, second: Int): Pair<Int, Int> {
|
||||
val value = getAsJsonArray(name) ?: return first to second
|
||||
require(value.size() == 2) { "'$name' must have two entries" }
|
||||
return value[0].requireUiInt(name) to value[1].requireUiInt(name)
|
||||
}
|
||||
private fun JsonObject.stringList(name: String): List<String> = getAsJsonArray(name)?.map { it.requireUiString(name) }?.toList()
|
||||
?: throw IllegalArgumentException("missing array '$name'")
|
||||
@@ -1,24 +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.molangui
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.widget.WidgetDefinition
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
|
||||
data class MolangUiDefinition(
|
||||
val guiId: ResourceLocation,
|
||||
val formatVersion: Int,
|
||||
val contractVersion: String,
|
||||
val mode: GuiMode,
|
||||
val parameters: Map<String, ParameterType>,
|
||||
val actions: Set<ActionKey>,
|
||||
val exclusive: Boolean,
|
||||
val root: WidgetDefinition,
|
||||
val variables: Map<String, Double>
|
||||
)
|
||||
@@ -1,26 +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.molangui
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
object MolangUiDiagnostics {
|
||||
private val activeSessions = AtomicLong()
|
||||
private val rejectedActions = AtomicLong()
|
||||
private val compatibilityFailures = AtomicLong()
|
||||
|
||||
internal fun sessionOpened() { activeSessions.incrementAndGet() }
|
||||
internal fun sessionClosed() { activeSessions.updateAndGet { value -> (value - 1).coerceAtLeast(0) } }
|
||||
internal fun actionRejected() { rejectedActions.incrementAndGet() }
|
||||
internal fun compatibilityFailures(count: Long) { compatibilityFailures.addAndGet(count) }
|
||||
|
||||
fun snapshot() = Snapshot(activeSessions.get(), rejectedActions.get(), compatibilityFailures.get(), MolangUiContractRegistry.parseFailureCount())
|
||||
|
||||
data class Snapshot(val activeSessions: Long, val rejectedActions: Long, val compatibilityFailures: Long, val parseFailures: Long)
|
||||
}
|
||||
@@ -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.api.molangui
|
||||
|
||||
import com.google.gson.JsonElement
|
||||
|
||||
internal fun JsonElement?.requireUiString(path: String): String {
|
||||
require(this != null && isJsonPrimitive && asJsonPrimitive.isString) { "'$path' must be a string" }
|
||||
return asString
|
||||
}
|
||||
|
||||
internal fun JsonElement?.requireUiBoolean(path: String): Boolean {
|
||||
require(this != null && isJsonPrimitive && asJsonPrimitive.isBoolean) { "'$path' must be a boolean" }
|
||||
return asBoolean
|
||||
}
|
||||
|
||||
internal fun JsonElement?.requireUiNumber(path: String): Double {
|
||||
require(this != null && isJsonPrimitive && asJsonPrimitive.isNumber) { "'$path' must be a number" }
|
||||
return asDouble.also { require(it.isFinite()) { "'$path' must be finite" } }
|
||||
}
|
||||
|
||||
internal fun JsonElement?.requireUiInt(path: String): Int {
|
||||
require(this != null && isJsonPrimitive && asJsonPrimitive.isNumber) { "'$path' must be an integer" }
|
||||
return try {
|
||||
asBigDecimal.intValueExact()
|
||||
} catch (error: ArithmeticException) {
|
||||
throw IllegalArgumentException("'$path' must be an integer in the 32-bit range", error)
|
||||
}
|
||||
}
|
||||
@@ -1,151 +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.molangui
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.container.SlotLayout
|
||||
import com.cobblemon.mod.common.api.molangui.container.CollectionConfig
|
||||
import com.cobblemon.mod.common.api.molangui.container.CloseItemsPolicy
|
||||
import com.cobblemon.mod.common.api.molangui.container.ShiftClickRule
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import java.util.UUID
|
||||
|
||||
const val MOLANG_UI_FORMAT_VERSION = 1
|
||||
|
||||
enum class ParameterType(val id: String) {
|
||||
STRING("string"),
|
||||
NUMBER("number"),
|
||||
BOOLEAN("boolean"),
|
||||
VEC3("vec3");
|
||||
|
||||
companion object {
|
||||
fun fromId(id: String): ParameterType? = entries.firstOrNull { it.id == id }
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ParameterValue {
|
||||
val type: ParameterType
|
||||
fun asWireValue(): String
|
||||
|
||||
data class StringValue(val value: String) : ParameterValue {
|
||||
override val type = ParameterType.STRING
|
||||
override fun asWireValue() = value
|
||||
}
|
||||
|
||||
data class NumberValue(val value: Double) : ParameterValue {
|
||||
init { require(value.isFinite()) { "number parameters must be finite" } }
|
||||
override val type = ParameterType.NUMBER
|
||||
override fun asWireValue() = value.toString()
|
||||
}
|
||||
|
||||
data class BooleanValue(val value: Boolean) : ParameterValue {
|
||||
override val type = ParameterType.BOOLEAN
|
||||
override fun asWireValue() = value.toString()
|
||||
}
|
||||
|
||||
data class Vec3Value(val x: Double, val y: Double, val z: Double) : ParameterValue {
|
||||
init { require(x.isFinite() && y.isFinite() && z.isFinite()) { "vec3 parameters must be finite" } }
|
||||
override val type = ParameterType.VEC3
|
||||
override fun asWireValue() = "$x,$y,$z"
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun parse(type: ParameterType, value: String): ParameterValue? = when (type) {
|
||||
ParameterType.STRING -> StringValue(value)
|
||||
ParameterType.NUMBER -> value.toDoubleOrNull()?.takeIf(Double::isFinite)?.let(::NumberValue)
|
||||
ParameterType.BOOLEAN -> value.toBooleanStrictOrNull()?.let(::BooleanValue)
|
||||
ParameterType.VEC3 -> value.split(',').takeIf { it.size == 3 }
|
||||
?.map { it.trim().toDoubleOrNull() }
|
||||
?.takeIf { it.all { part -> part != null && part.isFinite() } }
|
||||
?.let { Vec3Value(it[0]!!, it[1]!!, it[2]!!) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ParameterDefinition(
|
||||
val type: ParameterType,
|
||||
val required: Boolean = true,
|
||||
val default: ParameterDefault? = null
|
||||
)
|
||||
|
||||
sealed interface ParameterDefault {
|
||||
data class Literal(val value: ParameterValue) : ParameterDefault
|
||||
data class Molang(val expression: String) : ParameterDefault
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
value class ActionKey(val value: String) {
|
||||
init { require(value.isNotBlank() && value.length <= MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH) }
|
||||
}
|
||||
|
||||
data class ActionDefinition(
|
||||
val handler: ResourceLocation? = null,
|
||||
val parameters: Map<String, ParameterType> = emptyMap()
|
||||
)
|
||||
|
||||
data class MolangUiContract(
|
||||
val id: ResourceLocation,
|
||||
val formatVersion: Int,
|
||||
val contractVersion: String,
|
||||
val mode: GuiMode,
|
||||
val parameters: Map<String, ParameterDefinition>,
|
||||
val actions: Map<ActionKey, ActionDefinition>,
|
||||
val onOpen: ResourceLocation?,
|
||||
val collections: Map<String, CollectionConfig>,
|
||||
val shiftClickRules: List<ShiftClickRule>,
|
||||
val closeItemsPolicy: CloseItemsPolicy,
|
||||
val exclusive: Boolean
|
||||
) {
|
||||
init {
|
||||
require(mode == GuiMode.SCREEN || collections.isEmpty()) { "overlay views cannot have container collections" }
|
||||
SlotLayout.validateCollections(collections.mapValues { it.value.layout })
|
||||
}
|
||||
}
|
||||
|
||||
data class MolangUiOpenRequest(
|
||||
val uiId: ResourceLocation,
|
||||
val parameters: Map<String, ParameterValue> = emptyMap(),
|
||||
val blockPos: BlockPos? = null
|
||||
)
|
||||
|
||||
sealed interface MolangUiOpenResult {
|
||||
data class Opened(val session: MolangUiSessionHandle) : MolangUiOpenResult
|
||||
data class Rejected(val reason: RejectionReason, val detail: String) : MolangUiOpenResult
|
||||
}
|
||||
|
||||
enum class RejectionReason {
|
||||
UNKNOWN_UI,
|
||||
INCOMPATIBLE_CLIENT,
|
||||
INVALID_PARAMETERS,
|
||||
SESSION_LIMIT,
|
||||
INVALID_CONTAINER,
|
||||
INTERNAL_ERROR
|
||||
}
|
||||
|
||||
enum class SessionState { OPENING, ACTIVE, CLOSING, CLOSED }
|
||||
|
||||
interface MolangUiSessionHandle : AutoCloseable {
|
||||
val sessionId: UUID
|
||||
val uiId: ResourceLocation
|
||||
val state: SessionState
|
||||
fun updateParameters(parameters: Map<String, ParameterValue>): Boolean
|
||||
fun updateVariables(variables: Map<String, Double>): Boolean
|
||||
override fun close()
|
||||
}
|
||||
|
||||
data class MolangUiActionContext(
|
||||
val session: MolangUiSessionHandle,
|
||||
val player: ServerPlayer,
|
||||
val parameters: Map<String, ParameterValue>
|
||||
)
|
||||
|
||||
fun interface MolangUiActionHandler {
|
||||
fun handle(context: MolangUiActionContext)
|
||||
}
|
||||
@@ -1,29 +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.molangui
|
||||
|
||||
object MolangUiPacketLimits {
|
||||
const val MAX_CONTRACTS = 256
|
||||
const val MAX_ACTION_FIELDS = 32
|
||||
const val MAX_IDENTIFIER_LENGTH = 128
|
||||
const val MAX_VALUE_LENGTH = 4 * 1024
|
||||
const val MAX_PACKET_BYTES = 256 * 1024
|
||||
|
||||
fun requireIdentifier(value: String, label: String): String {
|
||||
require(value.isNotBlank() && value.length <= MAX_IDENTIFIER_LENGTH) {
|
||||
"$label must contain 1..$MAX_IDENTIFIER_LENGTH characters"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
fun requireValue(value: String, label: String): String {
|
||||
require(value.length <= MAX_VALUE_LENGTH) { "$label exceeds $MAX_VALUE_LENGTH characters" }
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -1,68 +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.molangui
|
||||
|
||||
import com.bedrockk.molang.runtime.value.DoubleValue
|
||||
import com.bedrockk.molang.runtime.value.MoValue
|
||||
import com.bedrockk.molang.runtime.value.StringValue
|
||||
|
||||
internal object MolangUiParameters {
|
||||
val GENERAL_FUNCTIONS = setOf("format", "to_string", "to_number", "to_int", "is_number", "is_int", "is_blank")
|
||||
private val reserved = GENERAL_FUNCTIONS + setOf(
|
||||
"gui_id", "gui_time", "delta", "screen_width", "screen_height", "mouse_x", "mouse_y", "item", "translate", "event"
|
||||
)
|
||||
private val namePattern = Regex("[a-z_][a-z0-9_]*")
|
||||
|
||||
fun requireName(name: String) {
|
||||
MolangUiPacketLimits.requireIdentifier(name, "parameter name")
|
||||
require(namePattern.matches(name) && name !in reserved) { "invalid or reserved parameter name '$name'" }
|
||||
}
|
||||
|
||||
fun decode(types: Map<String, ParameterType>, values: Map<String, String>): Map<String, ParameterValue> {
|
||||
require(values.size <= MolangUiPacketLimits.MAX_ACTION_FIELDS) { "too many parameters" }
|
||||
return values.mapValues { (name, raw) ->
|
||||
requireName(name)
|
||||
val type = types[name] ?: throw IllegalArgumentException("undeclared parameter '$name'")
|
||||
MolangUiPacketLimits.requireValue(raw, name)
|
||||
ParameterValue.parse(type, raw) ?: throw IllegalArgumentException("invalid ${type.id} parameter '$name'")
|
||||
}
|
||||
}
|
||||
|
||||
fun validate(values: Map<String, ParameterValue>) {
|
||||
require(values.size <= MolangUiPacketLimits.MAX_ACTION_FIELDS) { "too many parameters" }
|
||||
values.forEach { (name, value) ->
|
||||
requireName(name)
|
||||
MolangUiPacketLimits.requireValue(value.asWireValue(), name)
|
||||
}
|
||||
}
|
||||
|
||||
fun requireVariableName(name: String): String {
|
||||
MolangUiPacketLimits.requireIdentifier(name, "variable name")
|
||||
return name.removePrefix("v.").also {
|
||||
require(namePattern.matches(it)) { "invalid variable '$name'" }
|
||||
}
|
||||
}
|
||||
|
||||
fun validateVariables(values: Map<String, Double>) {
|
||||
val names = mutableSetOf<String>()
|
||||
values.forEach { (name, value) ->
|
||||
val canonical = requireVariableName(name)
|
||||
require(names.add(canonical) && value.isFinite()) {
|
||||
"invalid or duplicate variable '$name'"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ParameterValue.asMoLangValue(): MoValue = when (this) {
|
||||
is ParameterValue.StringValue -> StringValue(value)
|
||||
is ParameterValue.NumberValue -> DoubleValue(value)
|
||||
is ParameterValue.BooleanValue -> DoubleValue(value)
|
||||
is ParameterValue.Vec3Value -> StringValue(asWireValue())
|
||||
}
|
||||
@@ -1,95 +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.molangui
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.molangui.template.TemplateLibraryRegistry
|
||||
import com.cobblemon.mod.common.api.molangui.widget.WidgetDefinition
|
||||
import com.cobblemon.mod.common.api.molangui.widget.WidgetTree
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.packs.resources.ResourceManager
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
/** Client asset registry. Server code must use [MolangUiContractRegistry]. */
|
||||
object MolangUiRegistry {
|
||||
private const val DIRECTORY = "molang_ui"
|
||||
private val definitions = linkedMapOf<ResourceLocation, MolangUiDefinition>()
|
||||
private val parseFailures = AtomicLong()
|
||||
|
||||
fun reload(manager: ResourceManager) {
|
||||
TemplateLibraryRegistry.reload(manager)
|
||||
val next = linkedMapOf<ResourceLocation, MolangUiDefinition>()
|
||||
manager.listResources(DIRECTORY) { it.path.endsWith(".json") && !it.path.contains("/templates/") }
|
||||
.forEach { (identifier, resource) ->
|
||||
val guiId = ResourceLocation.fromNamespaceAndPath(
|
||||
identifier.namespace,
|
||||
identifier.path.removePrefix("$DIRECTORY/").removeSuffix(".json")
|
||||
)
|
||||
try {
|
||||
val json = resource.openAsReader().use { JsonParser.parseReader(it).asJsonObject }
|
||||
next[guiId] = parse(guiId, json)
|
||||
} catch (error: Exception) {
|
||||
parseFailures.incrementAndGet()
|
||||
Cobblemon.LOGGER.error("Rejected Molang UI view '$identifier': ${error.message}")
|
||||
}
|
||||
}
|
||||
require(next.size <= MolangUiPacketLimits.MAX_CONTRACTS) { "too many Molang UI views" }
|
||||
definitions.clear()
|
||||
definitions.putAll(next)
|
||||
Cobblemon.LOGGER.info("Loaded ${definitions.size} validated Molang UI views")
|
||||
}
|
||||
|
||||
internal fun parse(guiId: ResourceLocation, json: JsonObject): MolangUiDefinition {
|
||||
require(!json.has("blocks_input")) { "blocks_input is unsupported; input handling is determined by mode" }
|
||||
val formatVersion = json.get("format_version").requireUiInt("format_version")
|
||||
require(formatVersion == MOLANG_UI_FORMAT_VERSION) { "unsupported format_version $formatVersion" }
|
||||
val contractVersion = json.get("contract_version").requireUiString("contract_version")
|
||||
MolangUiPacketLimits.requireIdentifier(contractVersion, "contract_version")
|
||||
val mode = GuiMode.fromString(json.get("mode").requireUiString("mode"))
|
||||
?: throw IllegalArgumentException("unknown mode '${json.get("mode").asString}'")
|
||||
val parameterJson = json.getAsJsonObject("parameters")
|
||||
require((parameterJson?.size() ?: 0) <= MolangUiPacketLimits.MAX_ACTION_FIELDS) { "too many parameters" }
|
||||
val parameters = parameterJson?.entrySet()?.associate { (name, value) ->
|
||||
MolangUiParameters.requireName(name)
|
||||
val type = ParameterType.fromId(value.requireUiString("parameters.$name"))
|
||||
?: throw IllegalArgumentException("unknown parameter type for '$name'")
|
||||
name to type
|
||||
}?.toMap() ?: emptyMap()
|
||||
val actions = json.getAsJsonArray("actions")?.map { ActionKey(it.requireUiString("actions")) }?.toSet() ?: emptySet()
|
||||
require(actions.size <= MolangUiPacketLimits.MAX_ACTION_FIELDS) { "too many declared actions" }
|
||||
val variables = json.getAsJsonObject("variables")?.entrySet()?.associate { (name, value) ->
|
||||
MolangUiPacketLimits.requireIdentifier(name, "variable name")
|
||||
name to value.requireUiNumber("variables.$name")
|
||||
}?.toMap() ?: emptyMap()
|
||||
MolangUiParameters.validateVariables(variables)
|
||||
require(!json.has("imports")) { "template imports are unsupported; use qualified template references" }
|
||||
val root = WidgetTree.parse(json)
|
||||
val referencedActions = linkedSetOf<ActionKey>().also { collectActions(root, it) }
|
||||
val undeclared = referencedActions - actions
|
||||
require(undeclared.isEmpty()) { "widgets reference undeclared actions: ${undeclared.joinToString { it.value }}" }
|
||||
return MolangUiDefinition(
|
||||
guiId, formatVersion, contractVersion, mode, parameters, actions,
|
||||
json.get("exclusive")?.requireUiBoolean("exclusive") ?: false,
|
||||
root, variables
|
||||
)
|
||||
}
|
||||
|
||||
private fun collectActions(node: WidgetDefinition, output: MutableSet<ActionKey>) {
|
||||
node.events.values.forEach { output += it.action }
|
||||
node.children.forEach { collectActions(it, output) }
|
||||
node.itemTemplate?.let { collectActions(it, output) }
|
||||
}
|
||||
|
||||
fun getDefinition(guiId: ResourceLocation): MolangUiDefinition? = definitions[guiId]
|
||||
fun getAllDefinitions(): Map<ResourceLocation, MolangUiDefinition> = definitions.toMap()
|
||||
fun getVersionMap(): Map<ResourceLocation, String> = definitions.mapValues { it.value.contractVersion }
|
||||
fun parseFailureCount(): Long = parseFailures.get()
|
||||
}
|
||||
-93
@@ -1,93 +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.molangui
|
||||
|
||||
import com.bedrockk.molang.runtime.MoLangRuntime
|
||||
import com.bedrockk.molang.runtime.value.DoubleValue
|
||||
import com.bedrockk.molang.runtime.value.MoValue
|
||||
import com.bedrockk.molang.runtime.value.StringValue
|
||||
import com.cobblemon.mod.common.api.molang.function.GeneralMoLangFunctions
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.world.item.Item
|
||||
import net.minecraft.world.item.ItemStack
|
||||
|
||||
/** Builds a server runtime with only UI-scoped capabilities. It never exposes q.player. */
|
||||
internal object MolangUiServerRuntimeFactory {
|
||||
fun create(session: MolangUiSession): MoLangRuntime = MoLangRuntime().also { runtime ->
|
||||
GeneralMoLangFunctions.functions.filterKeys { it in MolangUiParameters.GENERAL_FUNCTIONS }.forEach { (name, function) ->
|
||||
runtime.environment.query.addFunction(name) { function(it) }
|
||||
}
|
||||
runtime.environment.query.addFunction("parameter") { params ->
|
||||
session.getParameter(params.getString(0))?.asMoLangValue() ?: DoubleValue.ZERO
|
||||
}
|
||||
runtime.environment.query.addFunction("update_parameter") { params ->
|
||||
val name = params.getString(0)
|
||||
val raw = params.get<MoValue>(1).asString()
|
||||
val type = session.contract.parameters[name]?.type
|
||||
DoubleValue(type != null && ParameterValue.parse(type, raw)?.let { session.updateParameters(mapOf(name to it)) } == true)
|
||||
}
|
||||
runtime.environment.query.addFunction("update_variable") { params ->
|
||||
val value = params.getDouble(1)
|
||||
DoubleValue(value.isFinite() && session.updateVariables(mapOf(params.getString(0) to value)))
|
||||
}
|
||||
runtime.environment.query.addFunction("close_ui") {
|
||||
session.close()
|
||||
DoubleValue.ONE
|
||||
}
|
||||
runtime.environment.query.addFunction("count_item") { params ->
|
||||
val item = validatedItem(params.getString(0)) ?: return@addFunction DoubleValue.ZERO
|
||||
DoubleValue(session.player.inventory.items.filter { it.`is`(item) }.sumOf(ItemStack::getCount).toDouble())
|
||||
}
|
||||
runtime.environment.query.addFunction("remove_items") { params ->
|
||||
val item = validatedItem(params.getString(0)) ?: return@addFunction DoubleValue.ZERO
|
||||
val count = params.getDouble(1).asNonnegativeInt() ?: return@addFunction DoubleValue.ZERO
|
||||
DoubleValue(count > 0 && removeAtomically(session, item, count))
|
||||
}
|
||||
runtime.environment.query.addFunction("slot") { params ->
|
||||
val index = params.getDouble(1).asNonnegativeInt() ?: return@addFunction StringValue("")
|
||||
val stack = session.containerMenu?.getStack(params.getString(0), index)
|
||||
if (stack == null || stack.isEmpty) StringValue("") else StringValue(BuiltInRegistries.ITEM.getKey(stack.item).toString())
|
||||
}
|
||||
runtime.environment.query.addFunction("replace_slot") { params ->
|
||||
val itemId = params.getString(5)
|
||||
validatedItem(itemId) ?: return@addFunction DoubleValue.ZERO
|
||||
val inputIndex = params.getDouble(1).asNonnegativeInt() ?: return@addFunction DoubleValue.ZERO
|
||||
val consumeCount = params.getDouble(2).asNonnegativeInt() ?: return@addFunction DoubleValue.ZERO
|
||||
val outputIndex = params.getDouble(4).asNonnegativeInt() ?: return@addFunction DoubleValue.ZERO
|
||||
val outputCount = params.getDouble(6).asNonnegativeInt() ?: return@addFunction DoubleValue.ZERO
|
||||
val result = session.containerMenu?.replaceSlotAtomically(
|
||||
params.getString(0), inputIndex, consumeCount,
|
||||
params.getString(3), outputIndex, itemId, outputCount
|
||||
) ?: false
|
||||
DoubleValue(result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validatedItem(raw: String) = ResourceLocation.tryParse(raw)
|
||||
?.takeIf(BuiltInRegistries.ITEM::containsKey)
|
||||
?.let(BuiltInRegistries.ITEM::get)
|
||||
|
||||
private fun Double.asNonnegativeInt(): Int? =
|
||||
takeIf { it.isFinite() && it in 0.0..Int.MAX_VALUE.toDouble() && it % 1.0 == 0.0 }?.toInt()
|
||||
|
||||
private fun removeAtomically(session: MolangUiSession, item: Item, count: Int): Boolean {
|
||||
val stacks = session.player.inventory.items.filter { it.`is`(item) }
|
||||
if (stacks.sumOf(ItemStack::getCount) < count) return false
|
||||
var remaining = count
|
||||
stacks.forEach { stack ->
|
||||
if (remaining == 0) return@forEach
|
||||
val removed = minOf(stack.count, remaining)
|
||||
stack.shrink(removed)
|
||||
remaining -= removed
|
||||
}
|
||||
session.player.inventory.setChanged()
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,317 +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.molangui
|
||||
|
||||
import com.bedrockk.molang.runtime.MoLangRuntime
|
||||
import net.minecraft.core.BlockPos
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.molangui.container.CollectionSource
|
||||
import com.cobblemon.mod.common.api.molangui.container.MolangContainerMenu
|
||||
import com.cobblemon.mod.common.api.molangui.container.ServerCollectionSetup
|
||||
import com.cobblemon.mod.common.net.messages.client.molangui.MolangUiClosePacket
|
||||
import com.cobblemon.mod.common.net.messages.client.molangui.MolangUiOpenPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.molangui.MolangUiUpdatePacket
|
||||
import com.cobblemon.mod.common.util.asExpressionLike
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.Container
|
||||
import net.minecraft.world.MenuProvider
|
||||
import net.minecraft.world.SimpleContainer
|
||||
import net.minecraft.world.entity.player.Inventory
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu
|
||||
import java.util.UUID
|
||||
|
||||
internal class MolangUiSession(
|
||||
override val sessionId: UUID,
|
||||
override val uiId: ResourceLocation,
|
||||
val player: ServerPlayer,
|
||||
val contract: MolangUiContract,
|
||||
initialParameters: Map<String, ParameterValue>,
|
||||
val blockPos: BlockPos?
|
||||
) : MolangUiSessionHandle {
|
||||
override var state: SessionState = SessionState.OPENING
|
||||
private set
|
||||
private val parameters = initialParameters.toMutableMap()
|
||||
internal var containerMenu: MolangContainerMenu? = null
|
||||
private var clientWasNotified = false
|
||||
|
||||
override fun updateParameters(parameters: Map<String, ParameterValue>): Boolean {
|
||||
assertServerThread()
|
||||
if (state != SessionState.ACTIVE || !validateParameterUpdate(parameters)) return false
|
||||
MolangUiUpdatePacket(sessionId, parameters.mapValues { it.value.asWireValue() }).sendToPlayer(player)
|
||||
this.parameters.putAll(parameters)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun updateVariables(variables: Map<String, Double>): Boolean {
|
||||
assertServerThread()
|
||||
if (state != SessionState.ACTIVE || variables.size > MolangUiPacketLimits.MAX_ACTION_FIELDS) return false
|
||||
try {
|
||||
MolangUiParameters.validateVariables(variables)
|
||||
} catch (error: IllegalArgumentException) {
|
||||
return false
|
||||
}
|
||||
MolangUiUpdatePacket(sessionId, emptyMap(), variables).sendToPlayer(player)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun close() = closeInternal(notifyClient = true, closeMenu = true)
|
||||
|
||||
internal fun closeFromClient() = closeInternal(notifyClient = false, closeMenu = true)
|
||||
internal fun closeFromMenu() = closeInternal(notifyClient = true, closeMenu = false)
|
||||
|
||||
internal fun activate() {
|
||||
assertServerThread()
|
||||
check(state == SessionState.OPENING)
|
||||
state = SessionState.ACTIVE
|
||||
}
|
||||
|
||||
internal fun notifyOpened(hasContainer: Boolean) {
|
||||
check(state == SessionState.OPENING)
|
||||
clientWasNotified = true
|
||||
MolangUiOpenPacket(
|
||||
sessionId, uiId, contract.contractVersion, contract.mode,
|
||||
parameters.mapValues { it.value.asWireValue() }, contract.exclusive,
|
||||
hasContainer, contract.collections.mapValues { it.value.layout }
|
||||
).sendToPlayer(player)
|
||||
}
|
||||
|
||||
internal fun getParameter(name: String): ParameterValue? = parameters[name]
|
||||
|
||||
private fun validateParameterUpdate(values: Map<String, ParameterValue>): Boolean =
|
||||
values.size <= MolangUiPacketLimits.MAX_ACTION_FIELDS && values.all { (name, value) ->
|
||||
contract.parameters[name]?.type == value.type && value.asWireValue().length <= MolangUiPacketLimits.MAX_VALUE_LENGTH
|
||||
}
|
||||
|
||||
private fun closeInternal(notifyClient: Boolean, closeMenu: Boolean) {
|
||||
assertServerThread()
|
||||
if (state == SessionState.CLOSING || state == SessionState.CLOSED) return
|
||||
state = SessionState.CLOSING
|
||||
val menu = containerMenu
|
||||
containerMenu = null
|
||||
try {
|
||||
cleanup("notify client") {
|
||||
if (notifyClient && clientWasNotified) MolangUiClosePacket(sessionId).sendToPlayer(player)
|
||||
}
|
||||
cleanup("close container") {
|
||||
if (closeMenu && menu != null && player.containerMenu === menu) player.closeContainer()
|
||||
}
|
||||
cleanup("release items") { menu?.releaseOwnedItemsOnce(player) }
|
||||
} finally {
|
||||
state = SessionState.CLOSED
|
||||
MolangUiService.remove(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun cleanup(operation: String, action: () -> Unit) {
|
||||
try {
|
||||
action()
|
||||
} catch (error: Exception) {
|
||||
Cobblemon.LOGGER.error("Failed to $operation while closing Molang UI session $sessionId", error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertServerThread() {
|
||||
check(player.server.isSameThread) { "Molang UI sessions are confined to the server thread" }
|
||||
}
|
||||
}
|
||||
|
||||
object MolangUiService {
|
||||
private const val MAX_OVERLAYS_PER_PLAYER = 8
|
||||
private val sessions = linkedMapOf<UUID, MolangUiSession>()
|
||||
private val disabled = mutableMapOf<UUID, Set<ResourceLocation>>()
|
||||
private val rateLimiters = mutableMapOf<UUID, MolangUiRateLimiter>()
|
||||
|
||||
fun open(player: ServerPlayer, request: MolangUiOpenRequest): MolangUiOpenResult {
|
||||
check(player.server.isSameThread) { "MolangUiService.open must run on the server thread" }
|
||||
val contract = MolangUiContractRegistry.getContract(request.uiId)
|
||||
?: return MolangUiOpenResult.Rejected(RejectionReason.UNKNOWN_UI, "Unknown Molang UI '${request.uiId}'")
|
||||
// Intentionally does not wait for the compatibility round-trip; the client re-validates on open.
|
||||
if (disabled[player.uuid]?.contains(request.uiId) == true) {
|
||||
return MolangUiOpenResult.Rejected(RejectionReason.INCOMPATIBLE_CLIENT, "Client view is missing or incompatible")
|
||||
}
|
||||
val parameters = resolveParameters(contract, request.parameters)
|
||||
?: return MolangUiOpenResult.Rejected(RejectionReason.INVALID_PARAMETERS, "Parameters do not match the contract")
|
||||
val playerSessions = sessions.values.filter { it.player.uuid == player.uuid && it.state != SessionState.CLOSED }
|
||||
if (contract.mode == GuiMode.OVERLAY && playerSessions.count { it.contract.mode == GuiMode.OVERLAY } >= MAX_OVERLAYS_PER_PLAYER) {
|
||||
return MolangUiOpenResult.Rejected(RejectionReason.SESSION_LIMIT, "At most $MAX_OVERLAYS_PER_PLAYER overlays may be active")
|
||||
}
|
||||
val collectionSetup = try {
|
||||
createCollections(player, contract, request.blockPos)
|
||||
?: return MolangUiOpenResult.Rejected(RejectionReason.INVALID_CONTAINER, "The bound block container is unavailable or invalid")
|
||||
} catch (error: Exception) {
|
||||
Cobblemon.LOGGER.error("Failed to prepare Molang UI '${request.uiId}'", error)
|
||||
return MolangUiOpenResult.Rejected(RejectionReason.INTERNAL_ERROR, error.message ?: "Unable to prepare UI")
|
||||
}
|
||||
val session = MolangUiSession(UUID.randomUUID(), request.uiId, player, contract, parameters, request.blockPos)
|
||||
sessions[session.sessionId] = session
|
||||
MolangUiDiagnostics.sessionOpened()
|
||||
return try {
|
||||
if (contract.mode == GuiMode.SCREEN) {
|
||||
playerSessions.filter { it.contract.mode == GuiMode.SCREEN }.forEach(MolangUiSession::close)
|
||||
}
|
||||
check(session.state == SessionState.OPENING) { "Molang UI closed while replacing its previous screen" }
|
||||
MolangUiActionRegistry.executeOnOpen(session)
|
||||
check(session.state == SessionState.OPENING) { "Molang UI closed during on_open" }
|
||||
val hasContainer = contract.collections.isNotEmpty()
|
||||
session.notifyOpened(hasContainer)
|
||||
check(session.state == SessionState.OPENING) { "Molang UI closed while notifying the client" }
|
||||
if (hasContainer) openContainer(session, collectionSetup)
|
||||
session.activate()
|
||||
MolangUiOpenResult.Opened(session)
|
||||
} catch (error: Exception) {
|
||||
Cobblemon.LOGGER.error("Failed to open Molang UI '${request.uiId}'", error)
|
||||
session.close()
|
||||
MolangUiOpenResult.Rejected(RejectionReason.INTERNAL_ERROR, error.message ?: "Unable to open UI")
|
||||
}
|
||||
}
|
||||
|
||||
fun handleAction(sessionId: UUID, player: ServerPlayer, actionRaw: String, raw: Map<String, String>) {
|
||||
check(player.server.isSameThread)
|
||||
val session = sessions[sessionId]
|
||||
if (session == null || session.player.uuid != player.uuid || session.state != SessionState.ACTIVE) {
|
||||
MolangUiDiagnostics.actionRejected()
|
||||
return
|
||||
}
|
||||
if (!rateLimiters.getOrPut(sessionId) { MolangUiRateLimiter() }.tryAcquire()) {
|
||||
MolangUiDiagnostics.actionRejected()
|
||||
return
|
||||
}
|
||||
val action = runCatching { ActionKey(actionRaw) }.getOrNull()
|
||||
val definition = action?.let(session.contract.actions::get)
|
||||
if (action == null || definition == null || raw.keys != definition.parameters.keys || raw.size > MolangUiPacketLimits.MAX_ACTION_FIELDS) {
|
||||
MolangUiDiagnostics.actionRejected()
|
||||
return
|
||||
}
|
||||
val typed = linkedMapOf<String, ParameterValue>()
|
||||
for ((name, type) in definition.parameters) {
|
||||
val value = raw[name]
|
||||
if (value == null || value.length > MolangUiPacketLimits.MAX_VALUE_LENGTH) {
|
||||
MolangUiDiagnostics.actionRejected(); return
|
||||
}
|
||||
typed[name] = ParameterValue.parse(type, value) ?: run { MolangUiDiagnostics.actionRejected(); return }
|
||||
}
|
||||
try {
|
||||
MolangUiActionRegistry.dispatch(session, action, typed)
|
||||
} catch (error: Exception) {
|
||||
MolangUiDiagnostics.actionRejected()
|
||||
Cobblemon.LOGGER.error("Molang UI action '${action.value}' failed in session $sessionId", error)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleClosed(sessionId: UUID, player: ServerPlayer) {
|
||||
val session = sessions[sessionId] ?: return
|
||||
if (session.player.uuid == player.uuid) session.closeFromClient() else MolangUiDiagnostics.actionRejected()
|
||||
}
|
||||
|
||||
fun applyCompatibility(player: ServerPlayer, clientVersions: Map<ResourceLocation, String>) {
|
||||
val incompatible = MolangUiContractRegistry.versions().filter { (id, version) -> clientVersions[id] != version }.keys
|
||||
if (incompatible.isEmpty()) disabled.remove(player.uuid) else disabled[player.uuid] = incompatible
|
||||
MolangUiDiagnostics.compatibilityFailures(incompatible.size.toLong())
|
||||
sessions.values.filter { it.player.uuid == player.uuid && it.uiId in incompatible }.toList().forEach(MolangUiSession::close)
|
||||
}
|
||||
|
||||
fun requestCompatibility(player: ServerPlayer) = MolangUiContractRegistry.sync(player)
|
||||
|
||||
fun closeAllForPlayer(playerId: UUID) {
|
||||
sessions.values.filter { it.player.uuid == playerId }.toList().forEach(MolangUiSession::close)
|
||||
disabled.remove(playerId)
|
||||
}
|
||||
|
||||
fun closeAll() {
|
||||
sessions.values.toList().forEach(MolangUiSession::close)
|
||||
disabled.clear()
|
||||
}
|
||||
|
||||
fun getSession(sessionId: UUID): MolangUiSessionHandle? = sessions[sessionId]
|
||||
fun getSessionForPlayer(player: ServerPlayer, id: ResourceLocation): MolangUiSessionHandle? =
|
||||
sessions.values.firstOrNull { it.player.uuid == player.uuid && it.uiId == id && it.state == SessionState.ACTIVE }
|
||||
fun activeSessionCount(): Int = sessions.size
|
||||
|
||||
internal fun remove(sessionId: UUID) {
|
||||
if (sessions.remove(sessionId) != null) MolangUiDiagnostics.sessionClosed()
|
||||
rateLimiters.remove(sessionId)
|
||||
}
|
||||
|
||||
private fun resolveParameters(contract: MolangUiContract, supplied: Map<String, ParameterValue>): Map<String, ParameterValue>? {
|
||||
if (supplied.keys.any { it !in contract.parameters }) return null
|
||||
val values = linkedMapOf<String, ParameterValue>()
|
||||
for ((name, definition) in contract.parameters) {
|
||||
val suppliedValue = supplied[name]
|
||||
val value = suppliedValue ?: when (val default = definition.default) {
|
||||
is ParameterDefault.Literal -> default.value
|
||||
is ParameterDefault.Molang -> runCatching {
|
||||
val result = default.expression.asExpressionLike().resolveString(MoLangRuntime())
|
||||
ParameterValue.parse(definition.type, result)
|
||||
}.getOrNull()
|
||||
null -> null
|
||||
}
|
||||
if (value == null) {
|
||||
if (definition.required) return null else continue
|
||||
}
|
||||
if (value.type != definition.type || value.asWireValue().length > MolangUiPacketLimits.MAX_VALUE_LENGTH) return null
|
||||
values[name] = value
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
private fun createCollections(
|
||||
player: ServerPlayer,
|
||||
contract: MolangUiContract,
|
||||
blockPos: BlockPos?
|
||||
): List<ServerCollectionSetup>? {
|
||||
val setups = mutableListOf<ServerCollectionSetup>()
|
||||
for ((name, config) in contract.collections) {
|
||||
val container: Container = when (config.source) {
|
||||
CollectionSource.SESSION -> SimpleContainer(config.size)
|
||||
CollectionSource.BLOCK_ENTITY -> {
|
||||
val pos = blockPos ?: return null
|
||||
if (player.distanceToSqr(pos.x + .5, pos.y + .5, pos.z + .5) > 64.0 || !player.level().hasChunk(pos.x shr 4, pos.z shr 4)) return null
|
||||
val blockEntity = player.level().getBlockEntity(pos)
|
||||
if (blockEntity !is Container) return null
|
||||
if ((config.slots ?: (0 until config.size).toList()).any { it >= blockEntity.containerSize }) return null
|
||||
blockEntity
|
||||
}
|
||||
}
|
||||
setups += ServerCollectionSetup(name, config, container)
|
||||
}
|
||||
return setups
|
||||
}
|
||||
|
||||
private fun openContainer(session: MolangUiSession, setup: List<ServerCollectionSetup>) {
|
||||
val owned = setup.filter { it.config.source == CollectionSource.SESSION }.mapTo(mutableSetOf()) { it.name }
|
||||
val opened = session.player.openMenu(object : MenuProvider {
|
||||
override fun getDisplayName() = Component.empty()
|
||||
override fun createMenu(containerId: Int, inventory: Inventory, player: Player): AbstractContainerMenu =
|
||||
MolangContainerMenu(containerId, inventory).also { menu ->
|
||||
check(session.state == SessionState.OPENING) { "Molang UI closed before creating its container" }
|
||||
session.containerMenu = menu
|
||||
menu.initializeServer(
|
||||
setup, session.contract.shiftClickRules, owned, session.contract.closeItemsPolicy,
|
||||
owner = session.player, session = session, blockPos = session.blockPos
|
||||
)
|
||||
}
|
||||
})
|
||||
check(opened.isPresent) { "player rejected the Molang UI container menu" }
|
||||
}
|
||||
}
|
||||
|
||||
internal class MolangUiRateLimiter(private var lastNanos: Long = System.nanoTime()) {
|
||||
private var tokens = 40.0
|
||||
fun tryAcquire(now: Long = System.nanoTime()): Boolean {
|
||||
val elapsed = (now - lastNanos).coerceAtLeast(0) / 1_000_000_000.0
|
||||
tokens = minOf(40.0, tokens + elapsed * 20.0)
|
||||
lastNanos = now
|
||||
if (tokens < 1.0) return false
|
||||
tokens -= 1.0
|
||||
return true
|
||||
}
|
||||
}
|
||||
-94
@@ -1,94 +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.molangui.container
|
||||
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.core.registries.Registries
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.tags.TagKey
|
||||
import net.minecraft.world.item.ItemStack
|
||||
|
||||
data class CollectionConfig(
|
||||
val size: Int,
|
||||
val allowedItems: List<ItemFilterEntry> = emptyList(),
|
||||
val takeOnly: Boolean = false,
|
||||
val source: CollectionSource = CollectionSource.SESSION,
|
||||
val slots: List<Int>? = null,
|
||||
val layout: SlotLayout
|
||||
) {
|
||||
init {
|
||||
require(size in 1..MAX_COLLECTION_SIZE) { "collection size must be in 1..$MAX_COLLECTION_SIZE" }
|
||||
require(layout.slotCount == size) { "collection size must match its grid dimensions" }
|
||||
require(slots == null || slots.size == size) { "block collection slot mapping must match collection size" }
|
||||
require(slots == null || slots.distinct().size == slots.size) { "block collection slot mapping contains duplicates" }
|
||||
require(slots == null || slots.all { it >= 0 }) { "block collection slot indices must be nonnegative" }
|
||||
require(source == CollectionSource.BLOCK_ENTITY || slots == null) { "slot mapping is only valid for block_entity sources" }
|
||||
}
|
||||
|
||||
fun isItemAllowed(stack: ItemStack): Boolean {
|
||||
return !takeOnly && matchesFilter(stack)
|
||||
}
|
||||
|
||||
fun matchesFilter(stack: ItemStack): Boolean {
|
||||
if (allowedItems.isEmpty()) return true
|
||||
return allowedItems.any { it.matches(stack) }
|
||||
}
|
||||
|
||||
companion object { const val MAX_COLLECTION_SIZE = 256 }
|
||||
}
|
||||
|
||||
enum class CollectionSource(val id: String) {
|
||||
SESSION("session"), BLOCK_ENTITY("block_entity");
|
||||
companion object { fun fromId(id: String) = entries.firstOrNull { it.id == id } }
|
||||
}
|
||||
|
||||
data class ShiftClickRule(
|
||||
val from: List<String>,
|
||||
val to: List<String>
|
||||
) {
|
||||
companion object {
|
||||
const val PLAYER_INVENTORY = "player_inventory"
|
||||
const val PLAYER_HOTBAR = "player_hotbar"
|
||||
val PLAYER_SIDE_NAMES = listOf(PLAYER_INVENTORY, PLAYER_HOTBAR)
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ItemFilterEntry {
|
||||
fun matches(stack: ItemStack): Boolean
|
||||
|
||||
data class Item(val id: ResourceLocation) : ItemFilterEntry {
|
||||
override fun matches(stack: ItemStack): Boolean {
|
||||
if (!BuiltInRegistries.ITEM.containsKey(id)) return false
|
||||
return stack.`is`(BuiltInRegistries.ITEM.get(id))
|
||||
}
|
||||
}
|
||||
|
||||
data class Tag(val id: ResourceLocation) : ItemFilterEntry {
|
||||
override fun matches(stack: ItemStack): Boolean = stack.`is`(TagKey.create(Registries.ITEM, id))
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun parse(value: String): ItemFilterEntry? {
|
||||
val trimmed = value.trim()
|
||||
val isTag = trimmed.startsWith('#')
|
||||
val id = ResourceLocation.tryParse(if (isTag) trimmed.substring(1) else trimmed) ?: return null
|
||||
return if (isTag) Tag(id) else Item(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class CloseItemsPolicy(val id: String) {
|
||||
DROP("drop"),
|
||||
RETURN("return"),
|
||||
DESTROY("destroy");
|
||||
|
||||
companion object {
|
||||
fun fromId(id: String): CloseItemsPolicy? = entries.firstOrNull { it.id == id }
|
||||
}
|
||||
}
|
||||
-17
@@ -1,17 +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.molangui.container
|
||||
|
||||
import net.minecraft.world.Container
|
||||
|
||||
data class ServerCollectionSetup(
|
||||
val name: String,
|
||||
val config: CollectionConfig,
|
||||
val container: Container
|
||||
)
|
||||
-300
@@ -1,300 +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.molangui.container
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonMenuType
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiSession
|
||||
import com.cobblemon.mod.common.api.molangui.SessionState
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.Container
|
||||
import net.minecraft.world.SimpleContainer
|
||||
import net.minecraft.world.entity.player.Inventory
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu
|
||||
import net.minecraft.world.inventory.Slot
|
||||
import net.minecraft.world.item.ItemStack
|
||||
|
||||
private data class CollectionSlotRange(
|
||||
val name: String,
|
||||
val startIndex: Int,
|
||||
val endIndex: Int
|
||||
)
|
||||
|
||||
class MolangContainerMenu(
|
||||
containerId: Int,
|
||||
private val playerInventory: Inventory
|
||||
) : AbstractContainerMenu(CobblemonMenuType.MOLANG_UI, containerId) {
|
||||
|
||||
private val collections: MutableMap<String, Container> = mutableMapOf()
|
||||
|
||||
private val collectionRanges: MutableList<CollectionSlotRange> = mutableListOf()
|
||||
private var playerInventoryStart: Int = 0
|
||||
private var playerHotbarStart: Int = 0
|
||||
private var playerHotbarEnd: Int = 0
|
||||
|
||||
private var shiftClickRules: List<ShiftClickRule> = emptyList()
|
||||
|
||||
private val collectionConfigs: MutableMap<String, CollectionConfig> = mutableMapOf()
|
||||
private val sessionOwnedCollections: MutableSet<String> = mutableSetOf()
|
||||
private var closeItemsPolicy: CloseItemsPolicy = CloseItemsPolicy.RETURN
|
||||
private var owner: ServerPlayer? = null
|
||||
private var owningSession: MolangUiSession? = null
|
||||
private var blockPos: BlockPos? = null
|
||||
private var originalBlockContainer: Container? = null
|
||||
private var ownedItemsReleased = false
|
||||
|
||||
internal fun initializeServer(
|
||||
collectionSetup: List<ServerCollectionSetup>,
|
||||
shiftClickRules: List<ShiftClickRule> = emptyList(),
|
||||
sessionOwned: Set<String> = emptySet(),
|
||||
closeItemsPolicy: CloseItemsPolicy = CloseItemsPolicy.RETURN,
|
||||
owner: ServerPlayer,
|
||||
session: MolangUiSession,
|
||||
blockPos: BlockPos? = null
|
||||
) {
|
||||
this.shiftClickRules = shiftClickRules
|
||||
this.sessionOwnedCollections.addAll(sessionOwned)
|
||||
this.closeItemsPolicy = closeItemsPolicy
|
||||
this.owner = owner
|
||||
this.owningSession = session
|
||||
this.blockPos = blockPos
|
||||
|
||||
for (setup in collectionSetup) {
|
||||
val (name, config, container) = setup
|
||||
collections[name] = container
|
||||
if (config.source == CollectionSource.BLOCK_ENTITY) originalBlockContainer = container
|
||||
collectionConfigs[name] = config
|
||||
|
||||
val startIndex = slots.size
|
||||
addCollectionSlots(name, config, container)
|
||||
val endIndex = slots.size
|
||||
collectionRanges.add(CollectionSlotRange(name, startIndex, endIndex))
|
||||
}
|
||||
|
||||
addPlayerSlots()
|
||||
}
|
||||
|
||||
fun initializeClient(
|
||||
collectionLayouts: Map<String, SlotLayout>
|
||||
) {
|
||||
SlotLayout.validateCollections(collectionLayouts)
|
||||
for ((name, layout) in collectionLayouts) {
|
||||
val config = CollectionConfig(size = layout.slotCount, layout = layout)
|
||||
val container = SimpleContainer(layout.slotCount)
|
||||
collections[name] = container
|
||||
collectionConfigs[name] = config
|
||||
|
||||
val startIndex = slots.size
|
||||
addCollectionSlots(name, config, container)
|
||||
val endIndex = slots.size
|
||||
collectionRanges.add(CollectionSlotRange(name, startIndex, endIndex))
|
||||
}
|
||||
|
||||
addPlayerSlots()
|
||||
}
|
||||
|
||||
private fun addCollectionSlots(
|
||||
name: String,
|
||||
config: CollectionConfig,
|
||||
container: Container
|
||||
) {
|
||||
val layout = config.layout
|
||||
var containerIndex = 0
|
||||
for (row in 0 until layout.rows) {
|
||||
for (col in 0 until layout.columns) {
|
||||
val x = layout.offsetX + col * (layout.slotWidth + layout.spacingX)
|
||||
val y = layout.offsetY + row * (layout.slotHeight + layout.spacingY)
|
||||
val mappedIndex = config.slots?.getOrNull(containerIndex) ?: containerIndex
|
||||
require(mappedIndex in 0 until container.containerSize) { "collection '$name' maps outside its container" }
|
||||
addSlot(MolangSlot(
|
||||
container = container,
|
||||
index = mappedIndex,
|
||||
x = x,
|
||||
y = y,
|
||||
collectionConfig = config
|
||||
))
|
||||
containerIndex++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addPlayerSlots() {
|
||||
playerInventoryStart = slots.size
|
||||
|
||||
for (row in 0 until 3) {
|
||||
for (col in 0 until 9) {
|
||||
val inventoryIndex = 9 + row * 9 + col
|
||||
val x = 8 + col * 18
|
||||
val y = 84 + row * 18
|
||||
addSlot(Slot(playerInventory, inventoryIndex, x, y))
|
||||
}
|
||||
}
|
||||
|
||||
playerHotbarStart = slots.size
|
||||
|
||||
for (col in 0 until 9) {
|
||||
addSlot(Slot(playerInventory, col, 8 + col * 18, 142))
|
||||
}
|
||||
|
||||
playerHotbarEnd = slots.size
|
||||
}
|
||||
|
||||
override fun quickMoveStack(player: Player, index: Int): ItemStack {
|
||||
val slot = slots.getOrNull(index) ?: return ItemStack.EMPTY
|
||||
if (!slot.hasItem() || !slot.mayPickup(player)) return ItemStack.EMPTY
|
||||
|
||||
val slotItem = slot.item
|
||||
val original = slotItem.copy()
|
||||
|
||||
val clickedCollection = collectionRanges.firstOrNull { index >= it.startIndex && index < it.endIndex }
|
||||
val isPlayerInventory = index >= playerInventoryStart && index < playerInventoryEnd()
|
||||
val isHotbar = index >= playerHotbarStart && index < playerHotbarEnd
|
||||
|
||||
val sourceName = clickedCollection?.name ?: when {
|
||||
isPlayerInventory -> ShiftClickRule.PLAYER_INVENTORY
|
||||
isHotbar -> ShiftClickRule.PLAYER_HOTBAR
|
||||
else -> return ItemStack.EMPTY
|
||||
}
|
||||
val destinations = findShiftClickRule(sourceName)?.to ?: if (clickedCollection != null) {
|
||||
ShiftClickRule.PLAYER_SIDE_NAMES
|
||||
} else {
|
||||
collectionRanges.map { it.name }
|
||||
}
|
||||
if (!tryMoveToCollections(slotItem, slot, destinations)) return ItemStack.EMPTY
|
||||
|
||||
if (slotItem.isEmpty) {
|
||||
slot.setByPlayer(ItemStack.EMPTY)
|
||||
} else {
|
||||
slot.setChanged()
|
||||
}
|
||||
|
||||
if (slotItem.count == original.count) return ItemStack.EMPTY
|
||||
|
||||
slot.onTake(player, slotItem)
|
||||
return original
|
||||
}
|
||||
|
||||
override fun stillValid(player: Player): Boolean {
|
||||
val expectedOwner = owner ?: return true // client menu
|
||||
if (player.uuid != expectedOwner.uuid || owningSession?.state != SessionState.ACTIVE) return false
|
||||
val pos = blockPos ?: return true
|
||||
if (player.distanceToSqr(pos.x + .5, pos.y + .5, pos.z + .5) > 64.0) return false
|
||||
return player.level().getBlockEntity(pos) === originalBlockContainer
|
||||
}
|
||||
|
||||
override fun removed(player: Player) {
|
||||
try {
|
||||
super.removed(player)
|
||||
} finally {
|
||||
try {
|
||||
releaseOwnedItemsOnce(player)
|
||||
} finally {
|
||||
owningSession?.closeFromMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun releaseOwnedItemsOnce(player: Player) {
|
||||
if (ownedItemsReleased) return
|
||||
ownedItemsReleased = true
|
||||
for (name in sessionOwnedCollections) {
|
||||
val container = collections[name] ?: continue
|
||||
when (closeItemsPolicy) {
|
||||
CloseItemsPolicy.DROP -> clearContainer(player, container)
|
||||
CloseItemsPolicy.RETURN -> returnItemsToPlayer(player, container)
|
||||
CloseItemsPolicy.DESTROY -> container.clearContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getStack(collection: String, index: Int): ItemStack? {
|
||||
return collectionSlot(collection, index)?.item
|
||||
}
|
||||
|
||||
fun replaceSlotAtomically(
|
||||
inputCollection: String,
|
||||
inputIndex: Int,
|
||||
consumeCount: Int,
|
||||
outputCollection: String,
|
||||
outputIndex: Int,
|
||||
outputItemId: String,
|
||||
outputCount: Int
|
||||
): Boolean {
|
||||
if (consumeCount <= 0 || outputCount <= 0) return false
|
||||
val input = collectionSlot(inputCollection, inputIndex) ?: return false
|
||||
val output = collectionSlot(outputCollection, outputIndex) ?: return false
|
||||
val id = ResourceLocation.tryParse(outputItemId)?.takeIf(BuiltInRegistries.ITEM::containsKey) ?: return false
|
||||
val produced = ItemStack(BuiltInRegistries.ITEM.get(id), outputCount)
|
||||
val outputConfig = collectionConfigs[outputCollection] ?: return false
|
||||
if (!outputConfig.matchesFilter(produced)) return false
|
||||
val source = input.item
|
||||
if (source.count < consumeCount) return false
|
||||
val remaining = source.copyWithCount(source.count - consumeCount)
|
||||
val sameSlot = input.container === output.container && input.containerSlot == output.containerSlot
|
||||
val existing = if (sameSlot) remaining else output.item
|
||||
if (!existing.isEmpty && !ItemStack.isSameItemSameComponents(existing, produced)) return false
|
||||
val limit = output.getMaxStackSize(produced)
|
||||
if (existing.count > limit || outputCount > limit - existing.count) return false
|
||||
val result = if (existing.isEmpty) produced else existing.copyWithCount(existing.count + outputCount)
|
||||
if (!sameSlot) input.set(remaining)
|
||||
output.set(result)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun playerInventoryEnd() = playerHotbarStart
|
||||
|
||||
private fun findShiftClickRule(fromCollection: String): ShiftClickRule? {
|
||||
return shiftClickRules.firstOrNull { rule ->
|
||||
rule.from.contains(fromCollection)
|
||||
}
|
||||
}
|
||||
|
||||
private fun collectionSlot(name: String, index: Int): Slot? {
|
||||
val range = collectionRanges.firstOrNull { it.name == name } ?: return null
|
||||
if (index !in 0 until range.endIndex - range.startIndex) return null
|
||||
return slots[range.startIndex + index]
|
||||
}
|
||||
|
||||
private fun tryMoveToCollections(stack: ItemStack, source: Slot, toNames: List<String>): Boolean {
|
||||
val originalCount = stack.count
|
||||
for (name in toNames) {
|
||||
val indices = when (name) {
|
||||
ShiftClickRule.PLAYER_INVENTORY -> playerInventoryStart until playerHotbarStart
|
||||
ShiftClickRule.PLAYER_HOTBAR -> playerHotbarStart until playerHotbarEnd
|
||||
else -> collectionRanges.firstOrNull { it.name == name }?.let { it.startIndex until it.endIndex } ?: continue
|
||||
}
|
||||
for (merge in listOf(true, false)) {
|
||||
for (index in indices) {
|
||||
if (stack.isEmpty) return true
|
||||
val destination = slots[index]
|
||||
if (destination.container === source.container && destination.containerSlot == source.containerSlot) continue
|
||||
if (destination.hasItem() != merge || destination.item.count >= destination.getMaxStackSize(stack)) continue
|
||||
destination.safeInsert(stack)
|
||||
}
|
||||
}
|
||||
}
|
||||
return stack.count < originalCount
|
||||
}
|
||||
|
||||
private fun returnItemsToPlayer(player: Player, container: Container) {
|
||||
val inventory = player.inventory
|
||||
for (i in 0 until container.containerSize) {
|
||||
val stack = container.getItem(i)
|
||||
if (!stack.isEmpty) {
|
||||
if (!inventory.add(stack)) {
|
||||
player.drop(stack, false)
|
||||
}
|
||||
container.setItem(i, ItemStack.EMPTY)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +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.molangui.container
|
||||
|
||||
import net.minecraft.world.Container
|
||||
import net.minecraft.world.inventory.Slot
|
||||
import net.minecraft.world.item.ItemStack
|
||||
|
||||
class MolangSlot(
|
||||
container: Container,
|
||||
index: Int,
|
||||
x: Int,
|
||||
y: Int,
|
||||
private val collectionConfig: CollectionConfig
|
||||
) : Slot(container, index, x, y) {
|
||||
|
||||
override fun mayPlace(stack: ItemStack): Boolean {
|
||||
return collectionConfig.isItemAllowed(stack) && container.canPlaceItem(containerSlot, stack)
|
||||
}
|
||||
|
||||
override fun getMaxStackSize(stack: ItemStack): Int = container.getMaxStackSize(stack)
|
||||
}
|
||||
@@ -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.api.molangui.container
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiPacketLimits
|
||||
import net.minecraft.world.entity.player.Inventory
|
||||
|
||||
data class SlotLayout(
|
||||
val offsetX: Int,
|
||||
val offsetY: Int,
|
||||
val columns: Int,
|
||||
val rows: Int,
|
||||
val slotWidth: Int = 18,
|
||||
val slotHeight: Int = 18,
|
||||
val spacingX: Int = 0,
|
||||
val spacingY: Int = 0
|
||||
) {
|
||||
init {
|
||||
require(columns in 1..MAX_GRID_DIMENSION && rows in 1..MAX_GRID_DIMENSION) { "slot grid dimensions must be in 1..$MAX_GRID_DIMENSION" }
|
||||
validateAxis(offsetX, columns, slotWidth, spacingX)
|
||||
validateAxis(offsetY, rows, slotHeight, spacingY)
|
||||
}
|
||||
|
||||
val slotCount: Int get() = columns * rows
|
||||
|
||||
private fun validateAxis(offset: Int, count: Int, size: Int, spacing: Int) {
|
||||
require(size > 0 && spacing >= 0) { "slot size must be positive and spacing nonnegative" }
|
||||
val stride = size.toLong() + spacing
|
||||
val lastSlotOffset = (count - 1) * stride
|
||||
require(stride <= Int.MAX_VALUE && lastSlotOffset <= Int.MAX_VALUE && offset + lastSlotOffset + size <= Int.MAX_VALUE) {
|
||||
"slot grid coordinates exceed the integer range"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAX_GRID_DIMENSION = 16
|
||||
|
||||
fun validateCollections(layouts: Map<String, SlotLayout>) {
|
||||
require(layouts.size <= MolangUiPacketLimits.MAX_CONTRACTS) { "too many container collections" }
|
||||
layouts.keys.forEach { name ->
|
||||
MolangUiPacketLimits.requireIdentifier(name, "collection name")
|
||||
require(name !in ShiftClickRule.PLAYER_SIDE_NAMES) { "collection name '$name' is reserved for player slots" }
|
||||
}
|
||||
// Native container packets encode slot indices as signed shorts.
|
||||
require(layouts.values.sumOf { it.slotCount.toLong() } + Inventory.INVENTORY_SIZE <= Short.MAX_VALUE + 1L) {
|
||||
"container slot indices exceed the native packet range"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-89
@@ -1,89 +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.molangui.event
|
||||
|
||||
import com.bedrockk.molang.runtime.MoLangRuntime
|
||||
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.molangui.MolangUiPacketLimits
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.molangui.widget.WidgetDefinition
|
||||
import com.cobblemon.mod.common.api.molangui.widget.WidgetInstance
|
||||
import com.cobblemon.mod.common.api.molangui.widget.WidgetType
|
||||
import com.cobblemon.mod.common.net.messages.server.molangui.MolangUiActionPacket
|
||||
import com.cobblemon.mod.common.util.asExpressionLike
|
||||
import java.util.UUID
|
||||
|
||||
object ClientEventProcessor {
|
||||
|
||||
fun processEvent(
|
||||
event: WidgetEvent,
|
||||
widget: WidgetDefinition,
|
||||
runtime: MoLangRuntime,
|
||||
sessionId: UUID
|
||||
) {
|
||||
val context = QueryStruct(hashMapOf())
|
||||
event.data.forEach { (key, value) -> context.addFunction(key) { DoubleValue(value) } }
|
||||
context.addFunction("type") { StringValue(event.type.id) }
|
||||
context.addFunction("widget_id") { StringValue(event.widgetId) }
|
||||
val functions = runtime.environment.query.functions
|
||||
val previous = functions.remove("event")
|
||||
runtime.environment.query.addFunction("event") { context }
|
||||
try {
|
||||
widget.clientEvents[event.type.clientId]?.asExpressionLike()?.resolve(runtime)
|
||||
widget.events[event.type.id]?.let { serverHandler ->
|
||||
// Action arguments may depend on variables changed by the client handler.
|
||||
val parameters = serverHandler.parameters.mapValues { (_, property) -> property.resolveString(runtime) }
|
||||
require(parameters.size <= MolangUiPacketLimits.MAX_ACTION_FIELDS) { "too many action parameters" }
|
||||
parameters.forEach { (name, value) ->
|
||||
MolangUiPacketLimits.requireIdentifier(name, "action parameter")
|
||||
MolangUiPacketLimits.requireValue(value, name)
|
||||
}
|
||||
MolangUiActionPacket(sessionId, serverHandler.action.value, parameters).sendToServer()
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
Cobblemon.LOGGER.warn("Failed UI event {} on widget {}: {}", event.type.id, event.widgetId, error.message)
|
||||
} finally {
|
||||
functions.remove("event")
|
||||
if (previous != null) functions["event"] = previous
|
||||
}
|
||||
}
|
||||
|
||||
fun hitTest(root: WidgetInstance, x: Float, y: Float): WidgetInstance? {
|
||||
if (!root.visible || !root.enabled) return null
|
||||
val isScrollPanel = root.definition.type == WidgetType.SCROLL_PANEL
|
||||
if (isScrollPanel && !root.containsPoint(x, y)) return null
|
||||
|
||||
for (child in root.children.asReversed()) {
|
||||
if (!child.visible) continue
|
||||
val testY = if (isScrollPanel) y - root.scrollOffset else y
|
||||
val hit = hitTest(child, x, testY)
|
||||
if (hit != null) return hit
|
||||
}
|
||||
return if (root.containsPoint(x, y)) root else null
|
||||
}
|
||||
}
|
||||
|
||||
enum class WidgetEventType(val id: String) {
|
||||
CLICK("on_click"),
|
||||
CHANGE("on_change"),
|
||||
HOVER_ENTER("on_hover_enter"),
|
||||
HOVER_EXIT("on_hover_exit"),
|
||||
SCROLL("on_scroll"),
|
||||
KEY("on_key");
|
||||
|
||||
val clientId: String get() = "${id}_client"
|
||||
}
|
||||
|
||||
data class WidgetEvent(
|
||||
val type: WidgetEventType,
|
||||
val widgetId: String,
|
||||
val data: Map<String, Double> = emptyMap()
|
||||
)
|
||||
@@ -1,151 +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.molangui.layout
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.widget.WidgetInstance
|
||||
import com.bedrockk.molang.runtime.MoLangRuntime
|
||||
|
||||
object LayoutEngine {
|
||||
|
||||
fun layout(root: WidgetInstance, screenWidth: Float, screenHeight: Float, runtime: MoLangRuntime): Unit = root.withItemContext(runtime) {
|
||||
require(screenWidth.isFinite() && screenHeight.isFinite() && screenWidth >= 0f && screenHeight >= 0f) {
|
||||
"invalid screen dimensions"
|
||||
}
|
||||
if (!root.visible) return@withItemContext
|
||||
root.computedWidth = root.widthExpr.resolve(screenWidth, runtime)
|
||||
root.computedHeight = root.heightExpr.resolve(screenHeight, runtime)
|
||||
val anchor = root.definition.getAnchor()
|
||||
val offsetX = root.offsetXExpr.resolve(screenWidth, runtime)
|
||||
val offsetY = root.offsetYExpr.resolve(screenHeight, runtime)
|
||||
root.absoluteX = (screenWidth - root.computedWidth) * anchor.xFactor + offsetX
|
||||
root.absoluteY = (screenHeight - root.computedHeight) * anchor.yFactor + offsetY
|
||||
validateBounds(root)
|
||||
layoutChildren(root, runtime)
|
||||
}
|
||||
|
||||
private fun layoutChildren(parent: WidgetInstance, runtime: MoLangRuntime): Unit = parent.withItemContext(runtime) {
|
||||
val layout = parent.definition.getStringProperty("layout")
|
||||
val spacing = parent.definition.getProperty("spacing")?.resolveFloat(runtime) ?: 0f
|
||||
require(spacing.isFinite()) { "non-finite spacing at '${parent.definition.id}'" }
|
||||
val padding = parent.paddingProperties.map { it.resolveFloat(runtime) }
|
||||
require(padding.size == 4 && padding.all { it.isFinite() && it >= 0f }) {
|
||||
"invalid padding at '${parent.definition.id}'"
|
||||
}
|
||||
val innerWidth = (parent.computedWidth - padding[3] - padding[1]).coerceAtLeast(0f)
|
||||
val innerHeight = (parent.computedHeight - padding[0] - padding[2]).coerceAtLeast(0f)
|
||||
val innerX = parent.absoluteX + padding[3]
|
||||
val innerY = parent.absoluteY + padding[0]
|
||||
|
||||
for (child in parent.children) {
|
||||
if (!child.visible) continue
|
||||
child.computedWidth = child.withItemContext(runtime) { child.widthExpr.resolve(innerWidth, runtime) }
|
||||
child.computedHeight = child.withItemContext(runtime) { child.heightExpr.resolve(innerHeight, runtime) }
|
||||
}
|
||||
|
||||
when (layout) {
|
||||
"stack_vertical" -> layoutStackVertical(parent, innerX, innerY, innerWidth, innerHeight, spacing, runtime)
|
||||
"stack_horizontal" -> layoutStackHorizontal(parent, innerX, innerY, innerWidth, innerHeight, spacing, runtime)
|
||||
"grid" -> layoutGrid(parent, innerX, innerY, innerWidth, spacing, runtime)
|
||||
else -> layoutAnchored(parent, innerX, innerY, innerWidth, innerHeight, runtime)
|
||||
}
|
||||
|
||||
for (child in parent.children) {
|
||||
if (!child.visible) continue
|
||||
validateBounds(child)
|
||||
layoutChildren(child, runtime)
|
||||
}
|
||||
}
|
||||
|
||||
private fun layoutAnchored(parent: WidgetInstance, innerX: Float, innerY: Float, innerWidth: Float, innerHeight: Float, runtime: MoLangRuntime) {
|
||||
for (child in parent.children) {
|
||||
if (!child.visible) continue
|
||||
val anchor = child.definition.getAnchor()
|
||||
val offsetX = child.withItemContext(runtime) { child.offsetXExpr.resolve(innerWidth, runtime) }
|
||||
val offsetY = child.withItemContext(runtime) { child.offsetYExpr.resolve(innerHeight, runtime) }
|
||||
child.absoluteX = innerX + (innerWidth - child.computedWidth) * anchor.xFactor + offsetX
|
||||
child.absoluteY = innerY + (innerHeight - child.computedHeight) * anchor.yFactor + offsetY
|
||||
}
|
||||
}
|
||||
|
||||
private fun layoutStackVertical(parent: WidgetInstance, innerX: Float, innerY: Float, innerWidth: Float, innerHeight: Float, spacing: Float, runtime: MoLangRuntime) {
|
||||
val visibleChildren = parent.children.filter { it.visible }
|
||||
resolveFillSizes(visibleChildren, innerHeight, spacing, isVertical = true)
|
||||
var currentY = innerY
|
||||
for (child in visibleChildren) {
|
||||
child.absoluteX = innerX + child.withItemContext(runtime) { child.offsetXExpr.resolve(innerWidth, runtime) }
|
||||
child.absoluteY = currentY + child.withItemContext(runtime) { child.offsetYExpr.resolve(innerHeight, runtime) }
|
||||
currentY += child.computedHeight + spacing
|
||||
}
|
||||
}
|
||||
|
||||
private fun layoutStackHorizontal(parent: WidgetInstance, innerX: Float, innerY: Float, innerWidth: Float, innerHeight: Float, spacing: Float, runtime: MoLangRuntime) {
|
||||
val visibleChildren = parent.children.filter { it.visible }
|
||||
resolveFillSizes(visibleChildren, innerWidth, spacing, isVertical = false)
|
||||
var currentX = innerX
|
||||
for (child in visibleChildren) {
|
||||
child.absoluteX = currentX + child.withItemContext(runtime) { child.offsetXExpr.resolve(innerWidth, runtime) }
|
||||
child.absoluteY = innerY + child.withItemContext(runtime) { child.offsetYExpr.resolve(innerHeight, runtime) }
|
||||
currentX += child.computedWidth + spacing
|
||||
}
|
||||
}
|
||||
|
||||
private fun layoutGrid(parent: WidgetInstance, innerX: Float, innerY: Float, innerWidth: Float, spacing: Float, runtime: MoLangRuntime) {
|
||||
val columns = (parent.definition.getProperty("columns")?.resolveInt(runtime) ?: 1).coerceIn(1, 256)
|
||||
val visibleChildren = parent.children.filter { it.visible }
|
||||
val cellWidth = ((innerWidth - spacing * (columns - 1)) / columns).coerceAtLeast(0f)
|
||||
var col = 0
|
||||
var currentY = innerY
|
||||
var rowHeight = 0f
|
||||
for (child in visibleChildren) {
|
||||
if (child.widthExpr is SizeExpression.Fill) child.computedWidth = cellWidth
|
||||
child.absoluteX = innerX + col * (cellWidth + spacing)
|
||||
child.absoluteY = currentY
|
||||
rowHeight = maxOf(rowHeight, child.computedHeight)
|
||||
col++
|
||||
if (col >= columns) {
|
||||
col = 0
|
||||
currentY += rowHeight + spacing
|
||||
rowHeight = 0f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveFillSizes(children: List<WidgetInstance>, totalSpace: Float, spacing: Float, isVertical: Boolean) {
|
||||
val totalSpacing = spacing * (children.size - 1).coerceAtLeast(0)
|
||||
var usedSpace = totalSpacing
|
||||
var fillCount = 0
|
||||
for (child in children) {
|
||||
val expr = if (isVertical) child.heightExpr else child.widthExpr
|
||||
if (expr is SizeExpression.Fill) {
|
||||
fillCount++
|
||||
} else {
|
||||
usedSpace += if (isVertical) child.computedHeight else child.computedWidth
|
||||
}
|
||||
}
|
||||
if (fillCount > 0) {
|
||||
val fillSize = ((totalSpace - usedSpace) / fillCount).coerceAtLeast(0f)
|
||||
for (child in children) {
|
||||
val expr = if (isVertical) child.heightExpr else child.widthExpr
|
||||
if (expr is SizeExpression.Fill) {
|
||||
if (isVertical) child.computedHeight = fillSize else child.computedWidth = fillSize
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateBounds(widget: WidgetInstance) {
|
||||
fun validAxis(position: Float, size: Float): Boolean =
|
||||
position.isFinite() && size.isFinite() && size >= 0f && size.toDouble() <= Int.MAX_VALUE &&
|
||||
position.toDouble() >= Int.MIN_VALUE && position.toDouble() + size.toDouble() <= Int.MAX_VALUE
|
||||
|
||||
require(validAxis(widget.absoluteX, widget.computedWidth) && validAxis(widget.absoluteY, widget.computedHeight)) {
|
||||
"widget '${widget.definition.id}' has invalid or overflowing bounds"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +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.molangui.layout
|
||||
|
||||
import com.cobblemon.mod.common.api.molang.ExpressionLike
|
||||
import com.cobblemon.mod.common.util.asExpressionLike
|
||||
import com.bedrockk.molang.runtime.MoLangRuntime
|
||||
import com.cobblemon.mod.common.api.molangui.widget.WidgetExpression
|
||||
|
||||
sealed class SizeExpression {
|
||||
abstract fun resolve(parentSize: Float, runtime: MoLangRuntime): Float
|
||||
|
||||
data class Pixels(val value: Float) : SizeExpression() {
|
||||
init { require(value.isFinite()) { "pixel size must be finite" } }
|
||||
override fun resolve(parentSize: Float, runtime: MoLangRuntime) = value
|
||||
}
|
||||
|
||||
data class Percent(val fraction: Float) : SizeExpression() {
|
||||
init { require(fraction.isFinite()) { "percentage must be finite" } }
|
||||
override fun resolve(parentSize: Float, runtime: MoLangRuntime) = parentSize * fraction
|
||||
}
|
||||
|
||||
data class PercentWithOffset(val fraction: Float, val offset: Float) : SizeExpression() {
|
||||
init { require(fraction.isFinite() && offset.isFinite()) { "percentage and offset must be finite" } }
|
||||
override fun resolve(parentSize: Float, runtime: MoLangRuntime) = parentSize * fraction + offset
|
||||
}
|
||||
|
||||
data object Fill : SizeExpression() {
|
||||
override fun resolve(parentSize: Float, runtime: MoLangRuntime) = parentSize
|
||||
}
|
||||
|
||||
data class MolangSize(val expression: ExpressionLike) : SizeExpression() {
|
||||
override fun resolve(parentSize: Float, runtime: MoLangRuntime) = expression.resolveFloat(runtime)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val PERCENT_PATTERN = Regex("""^(\d+(?:\.\d+)?)%$""")
|
||||
private val PERCENT_OFFSET_PATTERN = Regex("""^(\d+(?:\.\d+)?)%\s*([+-])\s*(\d+(?:\.\d+)?)$""")
|
||||
|
||||
fun parse(value: Any): SizeExpression {
|
||||
return when (value) {
|
||||
is Number -> Pixels(value.toFloat())
|
||||
is String -> parseString(value)
|
||||
is WidgetExpression -> MolangSize(value.source.asExpressionLike())
|
||||
else -> throw IllegalArgumentException("size must be a number, percentage, fill, or explicit Molang expression")
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseString(value: String): SizeExpression {
|
||||
val trimmed = value.trim()
|
||||
return when {
|
||||
trimmed == "fill" -> Fill
|
||||
trimmed.toFloatOrNull() != null -> Pixels(trimmed.toFloat())
|
||||
PERCENT_PATTERN.matches(trimmed) -> {
|
||||
val pct = PERCENT_PATTERN.matchEntire(trimmed)!!.groupValues[1].toFloat()
|
||||
Percent(pct / 100f)
|
||||
}
|
||||
PERCENT_OFFSET_PATTERN.matches(trimmed) -> {
|
||||
val match = PERCENT_OFFSET_PATTERN.matchEntire(trimmed)!!
|
||||
val pct = match.groupValues[1].toFloat() / 100f
|
||||
val sign = if (match.groupValues[2] == "+") 1f else -1f
|
||||
val offset = match.groupValues[3].toFloat() * sign
|
||||
PercentWithOffset(pct, offset)
|
||||
}
|
||||
else -> throw IllegalArgumentException("invalid size '$value'; Molang requires an explicit expression object")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-114
@@ -1,114 +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.molangui.template
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonElement
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.packs.resources.ResourceManager
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
|
||||
|
||||
object TemplateLibraryRegistry {
|
||||
private val gson = Gson()
|
||||
private val libraries: MutableMap<ResourceLocation, Map<String, TemplateDefinition>> = mutableMapOf()
|
||||
|
||||
private const val DIRECTORY = "molang_ui/templates"
|
||||
|
||||
fun reload(manager: ResourceManager) {
|
||||
val next = mutableMapOf<ResourceLocation, Map<String, TemplateDefinition>>()
|
||||
val resources = manager.listResources(DIRECTORY) { it.path.endsWith(".json") }
|
||||
for ((location, resource) in resources) {
|
||||
try {
|
||||
val json = resource.openAsReader().use { gson.fromJson(it, JsonObject::class.java) }
|
||||
val namespace = location.namespace
|
||||
val name = location.path.removePrefix("$DIRECTORY/").removeSuffix(".json")
|
||||
val libraryId = ResourceLocation.fromNamespaceAndPath(namespace, name)
|
||||
val templates = parseTemplates(json, namespace)
|
||||
next[libraryId] = templates
|
||||
} catch (e: Exception) {
|
||||
Cobblemon.LOGGER.error("Rejected Molang UI template library '$location': ${e.message}")
|
||||
}
|
||||
}
|
||||
libraries.clear()
|
||||
libraries.putAll(next)
|
||||
}
|
||||
|
||||
fun getTemplate(qualifiedName: String): TemplateDefinition? {
|
||||
val dotIndex = qualifiedName.lastIndexOf('.')
|
||||
if (dotIndex == -1) return null
|
||||
val libraryName = qualifiedName.substring(0, dotIndex)
|
||||
val templateName = qualifiedName.substring(dotIndex + 1)
|
||||
val library = if (':' in libraryName) {
|
||||
ResourceLocation.tryParse(libraryName)?.let(libraries::get)
|
||||
} else {
|
||||
libraries.entries.singleOrNull { it.key.path == libraryName }?.value
|
||||
}
|
||||
return library?.get(templateName)
|
||||
}
|
||||
|
||||
private fun parseTemplates(json: JsonObject, namespace: String): Map<String, TemplateDefinition> {
|
||||
val result = mutableMapOf<String, TemplateDefinition>()
|
||||
val templatesObj = json.getAsJsonObject("templates") ?: return result
|
||||
for ((name, element) in templatesObj.entrySet()) {
|
||||
val templateJson = element.asJsonObject
|
||||
val inputs = mutableMapOf<String, TemplateInput>()
|
||||
templateJson.getAsJsonObject("inputs")?.let { inputsObj ->
|
||||
for ((inputName, inputEl) in inputsObj.entrySet()) {
|
||||
val inputObj = inputEl.asJsonObject
|
||||
val type = TemplateInputType.fromId(inputObj.get("type")?.asString ?: "string")
|
||||
?: throw IllegalArgumentException("unknown template input type for '$inputName'")
|
||||
val default = inputObj.get("default")
|
||||
require(default == null || type.accepts(default)) { "invalid template default at '$name.$inputName'" }
|
||||
inputs[inputName] = TemplateInput(type, default)
|
||||
}
|
||||
}
|
||||
val extends = templateJson.get("extends")?.asString
|
||||
result[name] = TemplateDefinition(
|
||||
name = name,
|
||||
namespace = namespace,
|
||||
inputs = inputs,
|
||||
extends = extends,
|
||||
body = templateJson
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
data class TemplateDefinition(
|
||||
val name: String,
|
||||
val namespace: String,
|
||||
val inputs: Map<String, TemplateInput> = emptyMap(),
|
||||
val extends: String? = null,
|
||||
val body: JsonObject
|
||||
)
|
||||
|
||||
data class TemplateInput(
|
||||
val type: TemplateInputType = TemplateInputType.STRING,
|
||||
val default: JsonElement? = null
|
||||
)
|
||||
|
||||
enum class TemplateInputType(val id: String) {
|
||||
STRING("string"), NUMBER("number"), BOOLEAN("boolean"), EXPRESSION("expression"), OBJECT("object"), ARRAY("array");
|
||||
|
||||
fun accepts(value: JsonElement): Boolean = when (this) {
|
||||
STRING -> value.isJsonPrimitive && value.asJsonPrimitive.isString
|
||||
NUMBER -> value.isJsonPrimitive && value.asJsonPrimitive.isNumber && value.asDouble.isFinite()
|
||||
BOOLEAN -> value.isJsonPrimitive && value.asJsonPrimitive.isBoolean
|
||||
EXPRESSION -> value.isJsonObject && value.asJsonObject.size() == 1 && value.asJsonObject.get("molang")?.let {
|
||||
it.isJsonPrimitive && it.asJsonPrimitive.isString
|
||||
} == true
|
||||
OBJECT -> value.isJsonObject
|
||||
ARRAY -> value.isJsonArray
|
||||
}
|
||||
|
||||
companion object { fun fromId(id: String) = entries.firstOrNull { it.id == id } }
|
||||
}
|
||||
-135
@@ -1,135 +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.molangui.template
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.widget.WidgetTree
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonPrimitive
|
||||
|
||||
object TemplateResolver {
|
||||
fun resolve(root: JsonObject, localTemplates: Map<String, TemplateDefinition>): JsonObject =
|
||||
Resolution(localTemplates).resolveNode(root, emptyList(), 1)
|
||||
|
||||
private class Resolution(private val localTemplates: Map<String, TemplateDefinition>) {
|
||||
private var nodes = 0
|
||||
private var values = 0
|
||||
|
||||
fun resolveNode(node: JsonObject, chain: List<String>, depth: Int): JsonObject {
|
||||
require(depth <= WidgetTree.MAX_DEPTH) { "template expansion exceeds maximum widget depth ${WidgetTree.MAX_DEPTH}" }
|
||||
require(++nodes <= WidgetTree.MAX_NODES) { "template expansion exceeds maximum widget count ${WidgetTree.MAX_NODES}" }
|
||||
val result = JsonObject()
|
||||
node.entrySet().filter { it.key != "controls" && it.key != "item_template" }.forEach { (key, value) ->
|
||||
result.add(key, copy(value))
|
||||
}
|
||||
node.getAsJsonArray("controls")?.let { controls ->
|
||||
val resolvedControls = JsonArray()
|
||||
controls.forEach { element ->
|
||||
val wrapper = element.takeIf(JsonElement::isJsonObject)?.asJsonObject
|
||||
?: throw IllegalArgumentException("control entries must be objects")
|
||||
require(wrapper.size() == 1) { "control entries must contain exactly one widget" }
|
||||
val (key, value) = wrapper.entrySet().single()
|
||||
val output = JsonObject()
|
||||
require(value.isJsonObject) { "widget '$key' must be an object" }
|
||||
output.add(key.substringBefore('@'), resolveWidgetEntry(key, value.asJsonObject, chain, depth + 1))
|
||||
resolvedControls.add(output)
|
||||
}
|
||||
result.add("controls", resolvedControls)
|
||||
}
|
||||
node.getAsJsonObject("item_template")?.let {
|
||||
result.add("item_template", resolveNode(it, chain, depth + 1))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun resolveWidgetEntry(
|
||||
key: String,
|
||||
overrides: JsonObject,
|
||||
chain: List<String>,
|
||||
depth: Int
|
||||
): JsonObject {
|
||||
if ('@' !in key) return resolveNode(overrides, chain, depth)
|
||||
val templateRef = key.substringAfter('@')
|
||||
require(templateRef !in chain) { "template expansion cycle: ${(chain + templateRef).joinToString(" -> ")}" }
|
||||
require(depth <= WidgetTree.MAX_DEPTH) { "template expansion exceeds maximum widget depth ${WidgetTree.MAX_DEPTH}" }
|
||||
val template = findTemplate(templateRef)
|
||||
?: throw IllegalArgumentException("missing template '$templateRef'")
|
||||
val inherited = resolveTemplateChain(templateRef, template, emptyList())
|
||||
val inputs = inherited.inputs.mapValues { (name, input) ->
|
||||
val supplied = overrides.get(name)
|
||||
val value = supplied ?: input.default
|
||||
?: throw IllegalArgumentException("missing required template input '$name' for '$templateRef'")
|
||||
require(input.type.accepts(value)) {
|
||||
"template input '$name' for '$templateRef' must be ${input.type.id}"
|
||||
}
|
||||
value
|
||||
}
|
||||
val substituted = copy(inherited.body, inputs).asJsonObject
|
||||
val nonInputs = JsonObject().also { obj ->
|
||||
overrides.entrySet().filter { it.key !in inherited.inputs }.forEach { (name, value) -> obj.add(name, value) }
|
||||
}
|
||||
return resolveNode(mergeJson(substituted, nonInputs), chain + templateRef, depth)
|
||||
}
|
||||
|
||||
private fun resolveTemplateChain(
|
||||
ref: String,
|
||||
template: TemplateDefinition,
|
||||
chain: List<String>
|
||||
): TemplateDefinition {
|
||||
require(ref !in chain) { "template inheritance cycle: ${(chain + ref).joinToString(" -> ")}" }
|
||||
require(chain.size < WidgetTree.MAX_DEPTH) { "template inheritance exceeds maximum depth ${WidgetTree.MAX_DEPTH}" }
|
||||
val nextChain = chain + ref
|
||||
val body = JsonObject().also { obj ->
|
||||
template.body.entrySet().filter { it.key != "inputs" && it.key != "extends" }
|
||||
.forEach { (name, value) -> obj.add(name, value) }
|
||||
}
|
||||
val parentRef = template.extends ?: return template.copy(body = body)
|
||||
val parent = findTemplate(parentRef)
|
||||
?: throw IllegalArgumentException("missing parent template '$parentRef' referenced by '$ref'")
|
||||
val inherited = resolveTemplateChain(parentRef, parent, nextChain)
|
||||
return template.copy(inputs = inherited.inputs + template.inputs, body = mergeJson(inherited.body, body))
|
||||
}
|
||||
|
||||
private fun findTemplate(ref: String) =
|
||||
localTemplates[ref] ?: TemplateLibraryRegistry.getTemplate(ref)
|
||||
|
||||
private fun copy(element: JsonElement, inputs: Map<String, JsonElement>? = null, depth: Int = 0): JsonElement {
|
||||
require(depth <= 128) { "template JSON exceeds maximum depth 128" }
|
||||
require(++values <= 1_048_576) { "template expansion exceeds maximum JSON work" }
|
||||
if (inputs != null && element.isJsonPrimitive && element.asJsonPrimitive.isString) {
|
||||
val text = element.asString
|
||||
if (text.startsWith('$') && text.length > 1 && text.indexOf('$', 1) == -1) {
|
||||
val supplied = inputs[text.substring(1)]
|
||||
?: throw IllegalArgumentException("unknown template input reference '$text'")
|
||||
return copy(supplied, depth = depth + 1)
|
||||
}
|
||||
return JsonPrimitive(text)
|
||||
}
|
||||
if (element.isJsonArray) return JsonArray().also { out -> element.asJsonArray.forEach { out.add(copy(it, inputs, depth + 1)) } }
|
||||
if (element.isJsonObject) return JsonObject().also { out ->
|
||||
element.asJsonObject.entrySet().forEach { (key, value) ->
|
||||
out.add(key, copy(value, inputs, depth + 1))
|
||||
}
|
||||
}
|
||||
return element.deepCopy()
|
||||
}
|
||||
|
||||
private fun mergeJson(base: JsonObject, overlay: JsonObject): JsonObject {
|
||||
val result = copy(base).asJsonObject
|
||||
overlay.entrySet().forEach { (key, value) ->
|
||||
val existing = result.get(key)
|
||||
result.add(key, if (value.isJsonObject && existing?.isJsonObject == true) {
|
||||
mergeJson(existing.asJsonObject, value.asJsonObject)
|
||||
} else copy(value))
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
-68
@@ -1,68 +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.molangui.widget
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.ActionKey
|
||||
|
||||
data class WidgetDefinition(
|
||||
val id: String,
|
||||
val type: WidgetType,
|
||||
val properties: Map<String, WidgetProperty> = emptyMap(),
|
||||
val children: List<WidgetDefinition> = emptyList(),
|
||||
val events: Map<String, ServerWidgetEvent> = emptyMap(),
|
||||
val clientEvents: Map<String, String> = emptyMap(),
|
||||
val hoverOverrides: Map<String, WidgetProperty> = emptyMap(),
|
||||
val itemTemplate: WidgetDefinition? = null
|
||||
) {
|
||||
fun getProperty(name: String): WidgetProperty? = properties[name]
|
||||
fun getStringProperty(name: String): String? = (properties[name]?.raw as? String)
|
||||
fun getAnchor(): Anchor = Anchor.fromId(getStringProperty("anchor") ?: "top_left")
|
||||
}
|
||||
|
||||
data class ServerWidgetEvent(
|
||||
val action: ActionKey,
|
||||
val parameters: Map<String, WidgetProperty> = emptyMap()
|
||||
)
|
||||
|
||||
enum class WidgetType(val id: String) {
|
||||
PANEL("panel"),
|
||||
LABEL("label"),
|
||||
IMAGE("image"),
|
||||
FILL("fill"),
|
||||
BUTTON("button"),
|
||||
LIST("list"),
|
||||
PROGRESS_BAR("progress_bar"),
|
||||
SCROLL_PANEL("scroll_panel"),
|
||||
POKEMON_RENDER("pokemon_render"),
|
||||
NUMBER_INPUT("number_input"),
|
||||
ITEM_SLOT("item_slot"),
|
||||
SLOT_GRID("slot_grid");
|
||||
|
||||
companion object {
|
||||
private val BY_ID = entries.associateBy { it.id }
|
||||
fun fromId(id: String): WidgetType? = BY_ID[id]
|
||||
}
|
||||
}
|
||||
|
||||
enum class Anchor(val id: String, val xFactor: Float, val yFactor: Float) {
|
||||
TOP_LEFT("top_left", 0f, 0f),
|
||||
TOP_CENTER("top_center", 0.5f, 0f),
|
||||
TOP_RIGHT("top_right", 1f, 0f),
|
||||
CENTER_LEFT("center_left", 0f, 0.5f),
|
||||
CENTER("center", 0.5f, 0.5f),
|
||||
CENTER_RIGHT("center_right", 1f, 0.5f),
|
||||
BOTTOM_LEFT("bottom_left", 0f, 1f),
|
||||
BOTTOM_CENTER("bottom_center", 0.5f, 1f),
|
||||
BOTTOM_RIGHT("bottom_right", 1f, 1f);
|
||||
|
||||
companion object {
|
||||
private val BY_ID = entries.associateBy { it.id }
|
||||
fun fromId(id: String): Anchor = BY_ID[id] ?: TOP_LEFT
|
||||
}
|
||||
}
|
||||
-92
@@ -1,92 +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.molangui.widget
|
||||
|
||||
import com.bedrockk.molang.runtime.MoLangRuntime
|
||||
import com.bedrockk.molang.runtime.struct.QueryStruct
|
||||
import com.bedrockk.molang.runtime.value.DoubleValue
|
||||
import com.bedrockk.molang.runtime.value.MoValue
|
||||
import com.bedrockk.molang.runtime.value.StringValue
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiPacketLimits
|
||||
import com.cobblemon.mod.common.api.molangui.layout.SizeExpression
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParser
|
||||
|
||||
internal object WidgetExpansion {
|
||||
private data class Node(val definition: WidgetDefinition, val context: QueryStruct?, val children: List<Node>)
|
||||
|
||||
fun refresh(definition: WidgetDefinition, runtime: MoLangRuntime, previous: WidgetInstance? = null): WidgetInstance {
|
||||
var count = 0
|
||||
fun plan(def: WidgetDefinition, context: QueryStruct?, depth: Int): Node = withItemContext(runtime, context) {
|
||||
require(depth <= WidgetTree.MAX_DEPTH && ++count <= WidgetTree.MAX_NODES) {
|
||||
"expanded widget tree exceeds ${WidgetTree.MAX_NODES} widgets or depth ${WidgetTree.MAX_DEPTH}"
|
||||
}
|
||||
val children = if (def.type == WidgetType.LIST) {
|
||||
val template = requireNotNull(def.itemTemplate) { "list '${def.id}' requires item_template" }
|
||||
items(def, runtime).mapIndexed { index, values ->
|
||||
val item = QueryStruct(hashMapOf())
|
||||
values.forEach { (key, value) -> item.addFunction(key) { value } }
|
||||
item.addFunction("index") { DoubleValue(index.toDouble()) }
|
||||
plan(template.copy(id = "${template.id}[$index]"), item, depth + 1)
|
||||
}
|
||||
} else def.children.map { plan(it, context, depth + 1) }
|
||||
Node(def, context, children)
|
||||
}
|
||||
val planned = plan(definition, null, 1)
|
||||
fun apply(node: Node, old: WidgetInstance?, parent: WidgetInstance?): WidgetInstance {
|
||||
val widget = old?.takeIf { it.definition == node.definition } ?: WidgetInstance(node.definition, parent).apply {
|
||||
val size = node.definition.getProperty("size")?.raw as? List<*>
|
||||
if (size?.size == 2) {
|
||||
widthExpr = SizeExpression.parse(size[0] ?: 0)
|
||||
heightExpr = SizeExpression.parse(size[1] ?: 0)
|
||||
}
|
||||
val offset = node.definition.getProperty("offset")?.raw as? List<*>
|
||||
if (offset?.size == 2) {
|
||||
offsetXExpr = SizeExpression.parse(offset[0] ?: 0)
|
||||
offsetYExpr = SizeExpression.parse(offset[1] ?: 0)
|
||||
}
|
||||
}
|
||||
widget.itemContext = node.context
|
||||
val children = node.children.mapIndexed { index, child -> apply(child, widget.children.getOrNull(index), widget) }
|
||||
widget.children.clear()
|
||||
widget.children.addAll(children)
|
||||
return widget
|
||||
}
|
||||
return apply(planned, previous, null)
|
||||
}
|
||||
|
||||
private fun items(definition: WidgetDefinition, runtime: MoLangRuntime): List<Map<String, MoValue>> =
|
||||
when (definition.getStringProperty("source_type") ?: "data") {
|
||||
"count" -> {
|
||||
val count = definition.getProperty("source")?.resolveDouble(runtime) ?: 0.0
|
||||
require(count.isFinite() && count in 0.0..WidgetTree.MAX_NODES.toDouble() && count % 1.0 == 0.0) {
|
||||
"list '${definition.id}' requires a whole count between 0 and ${WidgetTree.MAX_NODES}"
|
||||
}
|
||||
List(count.toInt()) { emptyMap() }
|
||||
}
|
||||
"data" -> {
|
||||
val source = definition.getProperty("source")?.resolveString(runtime) ?: "[]"
|
||||
require(source.length <= MolangUiPacketLimits.MAX_VALUE_LENGTH) { "list '${definition.id}' source is too large" }
|
||||
val array = JsonParser.parseString(source).asJsonArray
|
||||
require(array.size() <= WidgetTree.MAX_NODES) { "list '${definition.id}' has too many items" }
|
||||
array.map { element -> element.asJsonObject.entrySet().associate { (key, item) -> key to value(item) } }
|
||||
}
|
||||
else -> throw IllegalArgumentException("unsupported list source_type at '${definition.id}'")
|
||||
}
|
||||
|
||||
private fun value(element: JsonElement): MoValue {
|
||||
if (!element.isJsonPrimitive) return StringValue(element.toString())
|
||||
val primitive = element.asJsonPrimitive
|
||||
return when {
|
||||
primitive.isBoolean -> DoubleValue(primitive.asBoolean)
|
||||
primitive.isNumber -> DoubleValue(primitive.asDouble.also { require(it.isFinite()) { "non-finite list item value" } })
|
||||
else -> StringValue(primitive.asString)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +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.molangui.widget
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.layout.SizeExpression
|
||||
import com.bedrockk.molang.runtime.MoLangRuntime
|
||||
import com.bedrockk.molang.runtime.struct.QueryStruct
|
||||
|
||||
class WidgetInstance(
|
||||
val definition: WidgetDefinition,
|
||||
val parent: WidgetInstance? = null
|
||||
) {
|
||||
var absoluteX: Float = 0f
|
||||
var absoluteY: Float = 0f
|
||||
var computedWidth: Float = 0f
|
||||
var computedHeight: Float = 0f
|
||||
|
||||
var widthExpr: SizeExpression = SizeExpression.Pixels(0f)
|
||||
var heightExpr: SizeExpression = SizeExpression.Pixels(0f)
|
||||
var offsetXExpr: SizeExpression = SizeExpression.Pixels(0f)
|
||||
var offsetYExpr: SizeExpression = SizeExpression.Pixels(0f)
|
||||
|
||||
internal val paddingProperties: List<WidgetProperty> =
|
||||
(definition.getProperty("padding")?.raw as? List<*>)?.map {
|
||||
when (it) {
|
||||
is Number -> WidgetProperty.literal(it)
|
||||
is WidgetExpression -> WidgetProperty.molang(it.source)
|
||||
else -> throw IllegalArgumentException("invalid padding at '${definition.id}'")
|
||||
}
|
||||
} ?: List(4) { WidgetProperty.literal(0) }
|
||||
|
||||
val children: MutableList<WidgetInstance> = mutableListOf()
|
||||
|
||||
var visible: Boolean = true
|
||||
var enabled: Boolean = true
|
||||
var hovered: Boolean = false
|
||||
|
||||
var scrollOffset: Float = 0f
|
||||
var rendererState: Any? = null
|
||||
var itemContext: QueryStruct? = null
|
||||
|
||||
inline fun <T> withItemContext(runtime: MoLangRuntime, action: () -> T): T =
|
||||
withItemContext(runtime, itemContext, action)
|
||||
|
||||
fun containsPoint(x: Float, y: Float): Boolean {
|
||||
return x >= absoluteX && x <= absoluteX + computedWidth &&
|
||||
y >= absoluteY && y <= absoluteY + computedHeight
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> withItemContext(runtime: MoLangRuntime, context: QueryStruct?, action: () -> T): T {
|
||||
val functions = runtime.environment.query.functions
|
||||
val previous = functions.remove("item")
|
||||
if (context != null) runtime.environment.query.addFunction("item") { context }
|
||||
try {
|
||||
return action()
|
||||
} finally {
|
||||
functions.remove("item")
|
||||
if (previous != null) functions["item"] = previous
|
||||
}
|
||||
}
|
||||
@@ -1,63 +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.molangui.widget
|
||||
|
||||
import com.cobblemon.mod.common.api.molang.ExpressionLike
|
||||
import com.cobblemon.mod.common.util.asExpressionLike
|
||||
import com.bedrockk.molang.runtime.MoLangRuntime
|
||||
|
||||
class WidgetProperty private constructor(val raw: Any, expressionSource: String?) {
|
||||
val expression: ExpressionLike? = expressionSource?.asExpressionLike()
|
||||
|
||||
fun resolveDouble(runtime: MoLangRuntime): Double {
|
||||
return expression?.resolveDouble(runtime) ?: when (raw) {
|
||||
is Number -> raw.toDouble()
|
||||
is String -> parseNumericString(raw)
|
||||
is Boolean -> if (raw) 1.0 else 0.0
|
||||
else -> 0.0
|
||||
}
|
||||
}
|
||||
|
||||
fun resolveString(runtime: MoLangRuntime): String {
|
||||
return expression?.resolveString(runtime) ?: raw.toString()
|
||||
}
|
||||
|
||||
fun resolveBoolean(runtime: MoLangRuntime): Boolean {
|
||||
return expression?.resolveBoolean(runtime) ?: when (raw) {
|
||||
is Boolean -> raw
|
||||
is Number -> raw.toDouble() != 0.0
|
||||
is String -> raw.toBooleanStrictOrNull() ?: (raw.isNotEmpty())
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun resolveInt(runtime: MoLangRuntime): Int {
|
||||
if (expression == null && raw is String && raw.startsWith("0x", ignoreCase = true)) {
|
||||
return raw.substring(2).toLongOrNull(16)?.toInt() ?: 0
|
||||
}
|
||||
return resolveDouble(runtime).toInt()
|
||||
}
|
||||
|
||||
fun resolveFloat(runtime: MoLangRuntime): Float = resolveDouble(runtime).toFloat()
|
||||
|
||||
companion object {
|
||||
fun literal(value: Any): WidgetProperty = WidgetProperty(value, null)
|
||||
fun molang(source: String): WidgetProperty = WidgetProperty(source, source)
|
||||
|
||||
private fun parseNumericString(value: String): Double {
|
||||
value.toDoubleOrNull()?.let { return it }
|
||||
if (value.startsWith("0x", ignoreCase = true)) {
|
||||
value.substring(2).toLongOrNull(16)?.let { return it.toDouble() }
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class WidgetExpression(val source: String)
|
||||
@@ -1,195 +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.molangui.widget
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.layout.SizeExpression
|
||||
import com.cobblemon.mod.common.api.molangui.ActionKey
|
||||
import com.cobblemon.mod.common.api.molangui.requireUiString
|
||||
import com.cobblemon.mod.common.api.molangui.template.TemplateDefinition
|
||||
import com.cobblemon.mod.common.api.molangui.template.TemplateInput
|
||||
import com.cobblemon.mod.common.api.molangui.template.TemplateInputType
|
||||
import com.cobblemon.mod.common.api.molangui.template.TemplateResolver
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
|
||||
object WidgetTree {
|
||||
const val MAX_DEPTH = 32
|
||||
const val MAX_NODES = 2048
|
||||
private val UNSUPPORTED_PROPERTIES = setOf(
|
||||
"transition", "enter", "exit", "static", "on_submit", "on_submit_client", "on_slot_change", "on_slot_change_client"
|
||||
)
|
||||
private val EVENT_NAMES = setOf(
|
||||
"on_click", "on_change", "on_hover_enter", "on_hover_exit", "on_scroll", "on_key"
|
||||
)
|
||||
|
||||
fun parse(guiJson: JsonObject): WidgetDefinition {
|
||||
val localTemplates = parseLocalTemplates(guiJson)
|
||||
val rootJson = guiJson.getAsJsonObject("root")
|
||||
?: throw IllegalArgumentException("UI definition is missing 'root'")
|
||||
val resolved = TemplateResolver.resolve(rootJson, localTemplates)
|
||||
val ids = mutableSetOf<String>()
|
||||
val count = intArrayOf(0)
|
||||
return parseWidget("root", resolved, ids, 1, count)
|
||||
}
|
||||
|
||||
fun parseWidget(id: String, json: JsonObject): WidgetDefinition =
|
||||
parseWidget(id, json, mutableSetOf(), 1, intArrayOf(0))
|
||||
|
||||
private fun parseWidget(
|
||||
id: String,
|
||||
json: JsonObject,
|
||||
ids: MutableSet<String>,
|
||||
depth: Int,
|
||||
count: IntArray
|
||||
): WidgetDefinition {
|
||||
require(id.isNotBlank() && id.length <= 128) { "invalid widget id '$id'" }
|
||||
require(ids.add(id)) { "duplicate widget id '$id'" }
|
||||
require(depth <= MAX_DEPTH) { "widget tree exceeds maximum depth $MAX_DEPTH at '$id'" }
|
||||
count[0]++
|
||||
require(count[0] <= MAX_NODES) { "widget tree exceeds maximum node count $MAX_NODES" }
|
||||
|
||||
val typeName = json.get("type")?.asString ?: "panel"
|
||||
val type = WidgetType.fromId(typeName)
|
||||
?: throw IllegalArgumentException("unsupported widget renderer '$typeName' at '$id'")
|
||||
val properties = linkedMapOf<String, WidgetProperty>()
|
||||
val events = linkedMapOf<String, ServerWidgetEvent>()
|
||||
val clientEvents = linkedMapOf<String, String>()
|
||||
var hoverOverrides = emptyMap<String, WidgetProperty>()
|
||||
val reserved = setOf("type", "controls", "item_template", "hover", "inputs", "extends")
|
||||
json.entrySet().forEach { (key, value) ->
|
||||
require(key !in UNSUPPORTED_PROPERTIES) { "unsupported widget property '$id.$key'" }
|
||||
when {
|
||||
key.endsWith("_client") && key.removeSuffix("_client") in EVENT_NAMES ->
|
||||
clientEvents[key] = readExpression(value, "$id.$key")
|
||||
key in EVENT_NAMES -> {
|
||||
events[key] = parseServerEvent(value, "$id.$key")
|
||||
}
|
||||
key == "hover" && value.isJsonObject -> hoverOverrides = parsePropertyMap(value.asJsonObject, "$id.hover")
|
||||
key !in reserved -> properties[key] = parseProperty(value, "$id.$key")
|
||||
}
|
||||
}
|
||||
|
||||
val children = mutableListOf<WidgetDefinition>()
|
||||
json.getAsJsonArray("controls")?.forEach { element ->
|
||||
require(element.isJsonObject && element.asJsonObject.size() == 1) { "controls under '$id' must be single-entry objects" }
|
||||
val (childId, childValue) = element.asJsonObject.entrySet().single()
|
||||
require(childValue.isJsonObject) { "widget '$childId' must be an object" }
|
||||
children += parseWidget(childId, childValue.asJsonObject, ids, depth + 1, count)
|
||||
}
|
||||
|
||||
val itemTemplate = json.getAsJsonObject("item_template")?.let { template ->
|
||||
parseWidget("$id#item_template", template, ids, depth + 1, count)
|
||||
}
|
||||
validateGeometry(id, type, properties)
|
||||
return WidgetDefinition(
|
||||
id, type, properties.toMap(), children.toList(), events.toMap(), clientEvents.toMap(),
|
||||
hoverOverrides.toMap(), itemTemplate
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseLocalTemplates(guiJson: JsonObject): Map<String, TemplateDefinition> {
|
||||
val result = linkedMapOf<String, TemplateDefinition>()
|
||||
guiJson.getAsJsonObject("templates")?.entrySet()?.forEach { (name, element) ->
|
||||
val body = element.asJsonObject
|
||||
val inputs = linkedMapOf<String, TemplateInput>()
|
||||
body.getAsJsonObject("inputs")?.entrySet()?.forEach { (inputName, inputElement) ->
|
||||
val input = inputElement.asJsonObject
|
||||
val type = TemplateInputType.fromId(input.get("type")?.asString ?: "string")
|
||||
?: throw IllegalArgumentException("unknown template input type at '$name.$inputName'")
|
||||
val default = input.get("default")
|
||||
require(default == null || type.accepts(default)) { "invalid template default at '$name.$inputName'" }
|
||||
inputs[inputName] = TemplateInput(type, default)
|
||||
}
|
||||
result[name] = TemplateDefinition(name, "", inputs.toMap(), body.get("extends")?.asString, body)
|
||||
}
|
||||
return result.toMap()
|
||||
}
|
||||
|
||||
private fun parsePropertyMap(json: JsonObject, path: String) = json.entrySet().associate { (key, value) ->
|
||||
key to parseProperty(value, "$path.$key")
|
||||
}
|
||||
|
||||
private fun parseProperty(value: JsonElement, path: String): WidgetProperty {
|
||||
if (value.isJsonObject && value.asJsonObject.has("molang")) {
|
||||
require(value.asJsonObject.size() == 1) {
|
||||
"expression '$path' must contain only a string 'molang' field"
|
||||
}
|
||||
return WidgetProperty.molang(value.asJsonObject.get("molang").requireUiString("$path.molang"))
|
||||
}
|
||||
return WidgetProperty.literal(jsonToLiteral(value, path))
|
||||
}
|
||||
|
||||
private fun jsonToLiteral(element: JsonElement, path: String): Any = when {
|
||||
element.isJsonPrimitive -> element.asJsonPrimitive.let { when {
|
||||
it.isNumber -> it.asDouble.also { number -> require(number.isFinite()) { "non-finite number at '$path'" } }
|
||||
it.isBoolean -> it.asBoolean
|
||||
else -> it.asString
|
||||
} }
|
||||
element.isJsonArray -> element.asJsonArray.mapIndexed { index, item ->
|
||||
if (item.isJsonObject && item.asJsonObject.has("molang")) {
|
||||
require(item.asJsonObject.size() == 1) { "expression '$path[$index]' contains unknown fields" }
|
||||
WidgetExpression(item.asJsonObject.get("molang").requireUiString("$path[$index].molang"))
|
||||
} else jsonToLiteral(item, "$path[$index]")
|
||||
}.toList()
|
||||
element.isJsonObject -> element.asJsonObject.entrySet().associate { (key, item) -> key to jsonToLiteral(item, "$path.$key") }.toMap()
|
||||
else -> throw IllegalArgumentException("null is not a supported widget value at '$path'")
|
||||
}
|
||||
|
||||
private fun readExpression(element: JsonElement, path: String): String {
|
||||
require(element.isJsonObject && element.asJsonObject.size() == 1) { "client event '$path' must be an explicit molang object" }
|
||||
return element.asJsonObject.get("molang").requireUiString("$path.molang").also {
|
||||
require(!it.contains("send_action")) { "client event '$path' may not dispatch server actions" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseServerEvent(element: JsonElement, path: String): ServerWidgetEvent {
|
||||
if (element.isJsonPrimitive && element.asJsonPrimitive.isString) {
|
||||
val action = element.asString
|
||||
require(action.startsWith("action@") && action.length > 7) { "server event '$path' must declare exactly one action" }
|
||||
return ServerWidgetEvent(ActionKey(action.substringAfter("action@")))
|
||||
}
|
||||
require(element.isJsonObject) { "server event '$path' must be an object" }
|
||||
val obj = element.asJsonObject
|
||||
val action = ActionKey(obj.get("action")?.asString
|
||||
?: throw IllegalArgumentException("server event '$path' is missing action"))
|
||||
val parameters = obj.getAsJsonObject("parameters")?.entrySet()?.associate { (name, value) ->
|
||||
name to parseProperty(value, "$path.parameters.$name")
|
||||
}?.toMap() ?: emptyMap()
|
||||
require(obj.keySet().all { it == "action" || it == "parameters" }) { "server event '$path' contains unknown fields" }
|
||||
return ServerWidgetEvent(action, parameters)
|
||||
}
|
||||
|
||||
private fun validateGeometry(id: String, type: WidgetType, properties: Map<String, WidgetProperty>) {
|
||||
listOf("size", "offset", "grid_dimensions", "slot_size", "slot_spacing").forEach { key ->
|
||||
val raw = properties[key]?.raw ?: return@forEach
|
||||
require(raw is List<*> && raw.size == 2) { "'$id.$key' must contain exactly two values" }
|
||||
raw.filterIsInstance<Double>().forEach { require(it.isFinite()) { "non-finite geometry at '$id.$key'" } }
|
||||
}
|
||||
listOf("size", "offset").forEach { key ->
|
||||
(properties[key]?.raw as? List<*>)?.forEach { SizeExpression.parse(requireNotNull(it)) }
|
||||
}
|
||||
properties["padding"]?.raw?.let { padding ->
|
||||
require(padding is List<*> && padding.size == 4 && padding.all { it is Number || it is WidgetExpression }) {
|
||||
"'$id.padding' must contain four numbers or explicit Molang expressions"
|
||||
}
|
||||
padding.filterIsInstance<Number>().forEach {
|
||||
require(it.toFloat().isFinite() && it.toFloat() >= 0f) { "invalid padding at '$id'" }
|
||||
}
|
||||
}
|
||||
if (type == WidgetType.SLOT_GRID) {
|
||||
val dimensions = properties["grid_dimensions"]?.raw as? List<*>
|
||||
?: throw IllegalArgumentException("slot grid '$id' is missing grid_dimensions")
|
||||
require(dimensions.all { it is Double && it in 1.0..16.0 && it == it.toInt().toDouble() }) {
|
||||
"slot grid '$id' must have integer dimensions in 1..16"
|
||||
}
|
||||
val collectionName = properties["collection_name"]?.raw
|
||||
require(collectionName is String && collectionName.isNotBlank()) { "slot grid '$id' is missing collection_name" }
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
-7
@@ -8,8 +8,10 @@
|
||||
|
||||
package com.cobblemon.mod.common.api.orientation
|
||||
|
||||
import com.cobblemon.mod.common.util.math.QuatSpring
|
||||
import com.cobblemon.mod.common.util.math.geometry.toDegrees
|
||||
import com.cobblemon.mod.common.util.math.geometry.toRadians
|
||||
import com.cobblemon.mod.common.util.math.simpleSpringDamperQuat
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.util.Mth
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
@@ -26,6 +28,8 @@ open class OrientationController(val entity: LivingEntity) {
|
||||
val FORWARDS: Vector3f = Vector3f(0.0f, 0.0f, -1.0f)
|
||||
val UP: Vector3f = Vector3f(0.0f, 1.0f, 0.0f)
|
||||
val LEFT: Vector3f = Vector3f(-1.0f, 0.0f, 0.0f)
|
||||
private const val TICK_DT = 1f / 20f // Minecraft server tick rate
|
||||
private const val RENDER_SMOOTH_HALFLIFE = 0.1f // seconds to close ~half the gap
|
||||
}
|
||||
|
||||
open var active: Boolean = false
|
||||
@@ -33,6 +37,8 @@ open class OrientationController(val entity: LivingEntity) {
|
||||
var orientation: Matrix3f? = null
|
||||
private set
|
||||
|
||||
private val renderSpring = QuatSpring()
|
||||
|
||||
private var renderOrientationO: Matrix3f? = null
|
||||
private var renderOrientation: Matrix3f? = null
|
||||
|
||||
@@ -54,6 +60,8 @@ open class OrientationController(val entity: LivingEntity) {
|
||||
orientation = null
|
||||
renderOrientationO = null
|
||||
renderOrientation = null
|
||||
renderSpring.quat.identity()
|
||||
renderSpring.angVel.set(0f, 0f, 0f)
|
||||
active = false
|
||||
}
|
||||
|
||||
@@ -77,18 +85,26 @@ open class OrientationController(val entity: LivingEntity) {
|
||||
return oldQuat
|
||||
}
|
||||
|
||||
/** Every tick move the current render orientation towards the last known true orientation **/
|
||||
fun tick() {
|
||||
if (!active || orientation == null) return
|
||||
renderOrientationO = renderOrientation
|
||||
val current = orientation
|
||||
val renderMatrix = this.renderOrientation ?: current
|
||||
val renderQuat = Quaternionf().setFromUnnormalized(renderMatrix)
|
||||
val targetQuat = Quaternionf().setFromUnnormalized(current)
|
||||
val dampingFactor = 0.66f // Smooth interpolation to reduce jitter of orientations recieved from the server.
|
||||
renderQuat.slerp(targetQuat, dampingFactor)
|
||||
val current = orientation!!
|
||||
val targetQuat = Quaternionf().setFromUnnormalized(current)
|
||||
|
||||
if (renderOrientation == null) {
|
||||
// First tick: snap the spring to the target instead of easing in from identity
|
||||
renderSpring.quat.set(targetQuat)
|
||||
renderSpring.angVel.set(0f, 0f, 0f)
|
||||
}
|
||||
|
||||
// Use a "quat spring" to smoothly move the render orientation towards the goal orientation.
|
||||
// It takes into account the orientations angular velocities and should be much more resilient
|
||||
// to sharp changes in goal orientation or late/missed packets.
|
||||
simpleSpringDamperQuat(renderSpring, targetQuat, 0.15f, TICK_DT)
|
||||
|
||||
val newRenderOrientation = Matrix3f()
|
||||
renderQuat.get(newRenderOrientation)
|
||||
renderSpring.quat.get(newRenderOrientation)
|
||||
renderOrientation = newRenderOrientation
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ object CatchRateModifiers {
|
||||
* Used by [PokeBalls.SAFARI_BALL].
|
||||
* Checks if the target is not battling, if true boost by *1.5
|
||||
*/
|
||||
val SAFARI = WorldStateModifier { _, entity -> if (!entity.isBattling) 1.5F else 1F }
|
||||
val SAFARI = MultiplierModifier(1.5f) { _, poke -> !poke.entity?.isBattling!! }
|
||||
|
||||
/**
|
||||
* Used by [PokeBalls.PARK_BALL].
|
||||
|
||||
@@ -13,6 +13,10 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.addStandardFunctions
|
||||
import com.cobblemon.mod.common.api.molang.ObjectValue
|
||||
import com.cobblemon.mod.common.api.pokedex.entry.PokedexEntry
|
||||
import com.cobblemon.mod.common.api.pokedex.entry.PokedexForm
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.ChoiceSpeciesFeatureProvider
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeatures
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.WeightedChoiceSpeciesFeatureProvider
|
||||
import com.cobblemon.mod.common.pokedex.scanner.PokedexEntityData
|
||||
import com.cobblemon.mod.common.pokemon.Gender
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
@@ -72,6 +76,57 @@ abstract class AbstractPokedexManager {
|
||||
} ?: PokedexEntryProgress.UNREGISTERED
|
||||
}
|
||||
|
||||
fun getHighestKnowledgeFor(entry: PokedexEntry, form: PokedexForm?): PokedexEntryProgress {
|
||||
if (form == null) return getHighestKnowledgeFor(entry)
|
||||
|
||||
val speciesRecord = getSpeciesRecord(entry.speciesId) ?: return PokedexEntryProgress.UNREGISTERED
|
||||
val hasAllAspects = entry.conditionAspects.all(speciesRecord::hasAspect)
|
||||
if (!hasAllAspects) {
|
||||
return PokedexEntryProgress.UNREGISTERED
|
||||
}
|
||||
|
||||
return speciesRecord.getFormRecord(form.displayForm)?.knowledge ?: PokedexEntryProgress.UNREGISTERED
|
||||
}
|
||||
|
||||
fun getHighestKnowledgeFor(entry: PokedexEntry, form: PokedexForm?, aspects: Iterable<String>): PokedexEntryProgress {
|
||||
val formKnowledge = getHighestKnowledgeFor(entry, form)
|
||||
|
||||
if (formKnowledge == PokedexEntryProgress.UNREGISTERED) return PokedexEntryProgress.UNREGISTERED
|
||||
|
||||
val seenAspects = getSeenAspects(entry)
|
||||
|
||||
// Aspects that select one of the species' own forms are already covered by the form knowledge check above,
|
||||
// so exempt them (and every other value of the same feature, since defaults like "phony" never appear on a form directly).
|
||||
val species = PokemonSpecies.getByIdentifier(entry.speciesId)
|
||||
val formAspects = species?.forms?.flatMapTo(hashSetOf()) { it.aspects } ?: emptySet()
|
||||
val formSelectingFeatureAspects = species?.let(SpeciesFeatures::getFeaturesFor).orEmpty()
|
||||
.mapNotNull { provider ->
|
||||
val allAspects: Collection<String> = when (provider) {
|
||||
is ChoiceSpeciesFeatureProvider -> provider.getAllAspects()
|
||||
is WeightedChoiceSpeciesFeatureProvider -> provider.getAllAspects().keys
|
||||
else -> return@mapNotNull null
|
||||
}
|
||||
allAspects.takeIf { it.any { aspect -> aspect in formAspects } }
|
||||
}
|
||||
.flatten()
|
||||
.toHashSet()
|
||||
|
||||
val allSeenAspects = aspects
|
||||
.filter { aspect ->
|
||||
if (Dexes.equivalentFormsLookup[aspect] != null) return@filter false
|
||||
if (aspect in formAspects || aspect in formSelectingFeatureAspects) return@filter false
|
||||
if (aspect.uppercase() in setOf(Gender.MALE.name, Gender.FEMALE.name, Gender.GENDERLESS.name)) return@filter false
|
||||
if ("shiny".equals(aspect, ignoreCase = true)) return@filter false
|
||||
|
||||
return@filter true
|
||||
}
|
||||
.all { aspect -> seenAspects.contains(aspect) }
|
||||
|
||||
if (!allSeenAspects) return PokedexEntryProgress.UNREGISTERED
|
||||
|
||||
return formKnowledge
|
||||
}
|
||||
|
||||
fun getEncounteredForms(entry: PokedexEntry) : List<PokedexForm> {
|
||||
return getFormsWithKnowledge(entry, PokedexEntryProgress.SEEN)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ package com.cobblemon.mod.common.api.pokedex
|
||||
|
||||
import com.cobblemon.mod.common.api.data.JsonDataRegistry
|
||||
import com.cobblemon.mod.common.api.pokedex.def.PokedexDef
|
||||
import com.cobblemon.mod.common.api.pokedex.entry.PokedexEntry
|
||||
import com.cobblemon.mod.common.api.pokedex.entry.PokedexForm
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.reactive.SimpleObservable
|
||||
import com.cobblemon.mod.common.net.messages.client.data.PokedexDexSyncPacket
|
||||
import com.cobblemon.mod.common.util.adapters.CodecBackedAdapter
|
||||
@@ -40,6 +43,17 @@ object Dexes : JsonDataRegistry<PokedexDef> {
|
||||
//Maps a dex id to its PokedexDef
|
||||
val dexEntryMap = linkedMapOf<ResourceLocation, PokedexDef>()
|
||||
|
||||
val equivalentForms = mapOf(
|
||||
"galar" to listOf("region-bias-galar", "bias-galar", "galar-bias", "galarian", "galar"),
|
||||
"alola" to listOf("region-bias-alola", "bias-alola", "alola-bias", "alolan", "alola"),
|
||||
"hisui" to listOf("region-bias-hisui", "bias-hisui", "hisui-bias", "hisuian", "hisui"),
|
||||
"paldea" to listOf("region-bias-paldea", "bias-paldea", "paldea-bias", "paldean", "paldea"),
|
||||
)
|
||||
|
||||
val equivalentFormsLookup = equivalentForms.flatMap { (key, values) ->
|
||||
values.map { it to key }
|
||||
}.toMap()
|
||||
|
||||
override fun reload(data: Map<ResourceLocation, PokedexDef>) {
|
||||
dexEntryMap.clear()
|
||||
data.entries.sortedBy { it.value.sortOrder }.forEach { (id, def) -> dexEntryMap[id] = def }
|
||||
@@ -49,4 +63,17 @@ object Dexes : JsonDataRegistry<PokedexDef> {
|
||||
override fun sync(player: ServerPlayer) {
|
||||
PokedexDexSyncPacket(dexEntryMap.values).sendToPlayer(player)
|
||||
}
|
||||
|
||||
fun findDexEntryBySpeciesId(regionId: ResourceLocation, speciesId: ResourceLocation): PokedexEntry? {
|
||||
return dexEntryMap[regionId]?.getEntries()?.firstOrNull { it -> it.speciesId == speciesId }
|
||||
}
|
||||
|
||||
fun findDexEntryBySpeciesId(speciesId: ResourceLocation): PokedexEntry? {
|
||||
return findDexEntryBySpeciesId(cobblemonResource("national"), speciesId)
|
||||
}
|
||||
|
||||
fun getFormEquivalents(formName: String): List<String> {
|
||||
val key = equivalentFormsLookup[formName.lowercase()]
|
||||
return equivalentForms[key] ?: listOf(formName.lowercase())
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,7 @@ class SpeciesDexRecord {
|
||||
|
||||
fun addAspects(addedAspects: Set<String>) {
|
||||
aspects.addAll(addedAspects)
|
||||
pokedexManager.onSpeciesRecordUpdated(this)
|
||||
}
|
||||
|
||||
/** Returns true if the given Pokémon contains new information. Internal because it's only to be called from [FormDexRecord.wouldBeDifferent]. */
|
||||
|
||||
@@ -8,13 +8,20 @@
|
||||
|
||||
package com.cobblemon.mod.common.api.pokedex.entry
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.CobblemonItems
|
||||
import com.cobblemon.mod.common.api.data.JsonDataRegistry
|
||||
import com.cobblemon.mod.common.api.molang.ExpressionLike
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.reactive.SimpleObservable
|
||||
import com.cobblemon.mod.common.net.messages.client.data.DexEntrySyncPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.data.DexEvolutionDisplaySyncPacket
|
||||
import com.cobblemon.mod.common.pokemon.FormData
|
||||
import com.cobblemon.mod.common.util.adapters.ExpressionLikeAdapter
|
||||
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
@@ -38,6 +45,8 @@ object DexEntries : JsonDataRegistry<PokedexEntry> {
|
||||
|
||||
val entries = mutableMapOf<ResourceLocation, PokedexEntry>()
|
||||
|
||||
val evolutionDisplays = mutableListOf<DexEvolutionDisplay>()
|
||||
|
||||
override fun reload(data: Map<ResourceLocation, PokedexEntry>) {
|
||||
data.forEach { _, entry ->
|
||||
entries[entry.id] = entry
|
||||
@@ -50,6 +59,7 @@ object DexEntries : JsonDataRegistry<PokedexEntry> {
|
||||
}
|
||||
}
|
||||
}
|
||||
computeEvolutionDisplays()
|
||||
observable.emit(this)
|
||||
}
|
||||
|
||||
@@ -57,5 +67,58 @@ object DexEntries : JsonDataRegistry<PokedexEntry> {
|
||||
|
||||
override fun sync(player: ServerPlayer) {
|
||||
DexEntrySyncPacket(entries.values).sendToPlayer(player)
|
||||
DexEvolutionDisplaySyncPacket(evolutionDisplays).sendToPlayer(player)
|
||||
}
|
||||
}
|
||||
|
||||
fun computeEvolutionDisplays() {
|
||||
evolutionDisplays.clear()
|
||||
PokemonSpecies.species.forEach {
|
||||
try {
|
||||
evolutionDisplays.addAll(buildDisplays(it.standardForm) + it.forms.flatMap { form -> buildDisplays(form) })
|
||||
} catch (e: Exception) {
|
||||
Cobblemon.LOGGER.error("Failed to build dex evolution displays for ${it.resourceIdentifier}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun buildDisplays(form: FormData): List<DexEvolutionDisplay> {
|
||||
val result = mutableListOf<DexEvolutionDisplay>()
|
||||
(form.ownEvolutions ?: form.evolutions).forEach { evolution ->
|
||||
val requirements = mutableListOf<DexEvolutionRequirementDisplay>()
|
||||
getToolTipHeaderIcon(evolution)?.let { header ->
|
||||
requirements.add(header)
|
||||
}
|
||||
evolution.requirements.mapNotNull { resolveRequirementData(it) }.forEach { requirement ->
|
||||
requirements.add(requirement)
|
||||
}
|
||||
|
||||
listOfNotNull(evolution.result, evolution.shedder).forEach resultLoop@{ resultProperties ->
|
||||
val resultSpecies = resultProperties.species
|
||||
?.let { PokemonSpecies.getByIdentifier(it.asIdentifierDefaultingNamespace()) }
|
||||
?: return@resultLoop
|
||||
|
||||
// The result's own aspects win, otherwise carry the source form's aspects over
|
||||
// like a real evolution does (Alola-Bias Pichu -> Alola-Bias Pikachu)
|
||||
val resultForm = resultSpecies.getForm(resultProperties.aspects).takeIf { it != resultSpecies.standardForm }
|
||||
?: resultSpecies.getForm(form.aspects.toSet() + resultProperties.aspects)
|
||||
val toAspects = resultProperties.aspects + resultForm.aspects
|
||||
val resultRequirements = if (resultProperties === evolution.shedder) {
|
||||
requirements + CobblemonDexEvolutionRequirementDisplay(CobblemonItems.POKE_BALL, lang("ui.pokedex.evolution.shedder.tooltip"))
|
||||
} else {
|
||||
requirements
|
||||
}
|
||||
|
||||
result.add(DexEvolutionDisplay(
|
||||
fromSpecies = form.species.resourceIdentifier,
|
||||
fromForm = form.name,
|
||||
fromAspects = form.aspects.toSet(),
|
||||
toSpecies = resultSpecies.resourceIdentifier,
|
||||
toForm = resultForm.name,
|
||||
toAspects = toAspects,
|
||||
requirements = resultRequirements,
|
||||
))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
+466
@@ -0,0 +1,466 @@
|
||||
/*
|
||||
* 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.pokedex.entry
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonItems
|
||||
import com.cobblemon.mod.common.api.conditional.RegistryLikeCondition
|
||||
import com.cobblemon.mod.common.api.conditional.RegistryLikeIdentifierCondition
|
||||
import com.cobblemon.mod.common.api.conditional.RegistryLikeTagCondition
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.IntSpeciesFeatureProvider
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeatures
|
||||
import com.cobblemon.mod.common.api.pokemon.requirement.Requirement
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.api.text.add
|
||||
import com.cobblemon.mod.common.api.text.green
|
||||
import com.cobblemon.mod.common.api.types.ElementalTypes
|
||||
import com.cobblemon.mod.common.pokemon.evolution.variants.ItemInteractionEvolution
|
||||
import com.cobblemon.mod.common.pokemon.evolution.variants.TradeEvolution
|
||||
import com.cobblemon.mod.common.pokemon.requirements.*
|
||||
import com.cobblemon.mod.common.util.adapters.RegistryAccessResolver
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import com.cobblemon.mod.common.util.asTranslated
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.core.registries.Registries
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.network.chat.MutableComponent
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.util.ExtraCodecs
|
||||
import net.minecraft.world.item.Item
|
||||
import net.minecraft.world.item.Items
|
||||
import net.minecraft.world.level.biome.Biome
|
||||
import net.minecraft.world.level.levelgen.structure.Structure
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
data class DexEvolutionDisplay(
|
||||
val fromSpecies: ResourceLocation,
|
||||
val fromForm: String,
|
||||
val fromAspects: Set<String>,
|
||||
val toSpecies: ResourceLocation,
|
||||
val toForm: String,
|
||||
val toAspects: Set<String>,
|
||||
val requirements: List<DexEvolutionRequirementDisplay>,
|
||||
)
|
||||
|
||||
interface DexEvolutionRequirementDisplay {
|
||||
val icon: Item
|
||||
val tooltip: MutableComponent
|
||||
}
|
||||
|
||||
data class CobblemonDexEvolutionRequirementDisplay(
|
||||
override val icon: Item,
|
||||
override val tooltip: MutableComponent,
|
||||
) : DexEvolutionRequirementDisplay
|
||||
|
||||
data class CobblemonBiomeDexEvolutionRequirementDisplay(
|
||||
override val icon: Item,
|
||||
override val tooltip: MutableComponent,
|
||||
val condition: ExtraCodecs.TagOrElementLocation? = null,
|
||||
val antiCondition: ExtraCodecs.TagOrElementLocation? = null,
|
||||
) : DexEvolutionRequirementDisplay
|
||||
|
||||
fun getToolTipHeaderIcon(evolution: Evolution): DexEvolutionRequirementDisplay? {
|
||||
when (evolution) {
|
||||
is ItemInteractionEvolution -> return getToolTipHeaderIcon(evolution)
|
||||
is TradeEvolution -> return getToolTipHeaderIcon(evolution)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
fun getToolTipHeaderIcon(itemInteractionEvolution: ItemInteractionEvolution): CobblemonDexEvolutionRequirementDisplay? {
|
||||
val item = itemInteractionEvolution.requiredContext.items.orElse(null)?.firstOrNull()?.value() ?: return null
|
||||
return CobblemonDexEvolutionRequirementDisplay(item, lang("ui.pokedex.evolution.item_interact.tooltip", item.description.plainCopy().green()))
|
||||
}
|
||||
|
||||
fun getToolTipHeaderIcon(tradeEvolution: TradeEvolution): CobblemonDexEvolutionRequirementDisplay {
|
||||
val species = tradeEvolution.requiredContext.species
|
||||
val tooltip = if (species == null) {
|
||||
lang("ui.pokedex.evolution.trade.tooltip.with_any_species")
|
||||
} else {
|
||||
PokemonSpecies.getByIdentifier(species.asIdentifierDefaultingNamespace())
|
||||
?.let { lang("ui.pokedex.evolution.trade.tooltip.with_species", it.translatedName.green()) }
|
||||
?: lang("ui.pokedex.evolution.trade.tooltip.with_any_species")
|
||||
}
|
||||
return CobblemonDexEvolutionRequirementDisplay(CobblemonItems.LINK_CABLE, tooltip)
|
||||
}
|
||||
|
||||
fun resolveRequirementData(requirement: Requirement): DexEvolutionRequirementDisplay? {
|
||||
val icon = requirement.pokedexIcon?.let {
|
||||
RegistryAccessResolver.getAccess().registryOrThrow(Registries.ITEM).get(it)
|
||||
}
|
||||
|
||||
val tooltip = requirement.pokedexTooltip?.let {
|
||||
Component.translatable(it)
|
||||
}
|
||||
|
||||
if (icon != null && tooltip != null) {
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
return buildRequirementData(requirement)
|
||||
}
|
||||
|
||||
fun buildRequirementData(requirement: Requirement): DexEvolutionRequirementDisplay? {
|
||||
when (requirement) {
|
||||
is AdvancementRequirement -> return buildAdvancementRequirement(requirement)
|
||||
is BattleCriticalHitsRequirement -> return buildBattleCriticalHitsRequirement(requirement)
|
||||
is BiomeRequirement -> return buildBiomeRequirement(requirement)
|
||||
is BlocksTraveledRequirement -> return buildBlocksTraveledRequirement(requirement)
|
||||
is DamageTakenRequirement -> return buildDamageTakenRequirement(requirement)
|
||||
is DefeatRequirement -> return buildDefeatRequirement(requirement)
|
||||
is FriendshipRequirement -> return buildFriendshipRequirement(requirement)
|
||||
is HeldItemRequirement -> return buildHeldItemRequirement(requirement)
|
||||
is LevelRequirement -> return buildLevelRequirement(requirement)
|
||||
is MoonPhaseRequirement -> return buildMoonPhaseRequirement(requirement)
|
||||
is MoveSetRequirement -> return buildMoveSetRequirement(requirement)
|
||||
is MoveTypeRequirement -> return buildMoveTypeRequirement(requirement)
|
||||
is NegateRequirement -> return buildNegateRequirement(requirement)
|
||||
is OwnerHoldsItemRequirement -> return buildOwnerHoldsItemRequirement(requirement)
|
||||
is PartyMemberRequirement -> return buildPartyMemberRequirement(requirement)
|
||||
is PokemonPropertiesRequirement -> return buildPokemonPropertiesRequirement(requirement)
|
||||
is PropertyRangeRequirement -> return buildPropertyRangeRequirement(requirement)
|
||||
is RecoilRequirement -> return buildRecoilRequirement(requirement)
|
||||
is StatCompareRequirement -> return buildStatCompareRequirement(requirement)
|
||||
is StatEqualRequirement -> return buildStatEqualRequirement(requirement)
|
||||
is StructureRequirement -> return buildStructureRequirement(requirement)
|
||||
is TimeRangeRequirement -> return buildTimeRangeRequirement(requirement)
|
||||
is UseMoveRequirement -> return buildUseMoveRequirement(requirement)
|
||||
is WeatherRequirement -> return buildWeatherRequirement(requirement)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun buildAdvancementRequirement(advancementRequirement: AdvancementRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon = Items.BOOK
|
||||
val identifier = advancementRequirement.requiredAdvancement
|
||||
val advancementLanguageKey = "${identifier.namespace}.${identifier.path.substringAfterLast('/')}"
|
||||
val advancementComponent = Component.translatable("advancements.$advancementLanguageKey").green()
|
||||
val tooltip = lang("ui.pokedex.requirement.advancement.tooltip", advancementComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildBattleCriticalHitsRequirement(battleCriticalHitsRequirement: BattleCriticalHitsRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon = CobblemonItems.MEDICINAL_LEEK
|
||||
val criticalsComponent = Component.literal(battleCriticalHitsRequirement.amount.toString()).green()
|
||||
val tooltip = lang("ui.pokedex.requirement.battle_critical_hits.tooltip", criticalsComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
private fun tagDisplayName(registry: String, id: ResourceLocation): MutableComponent {
|
||||
return Component.translatableWithFallback("tag.$registry.${id.namespace}.${id.path.replace('/', '.')}", prettifyPath(id.path))
|
||||
}
|
||||
|
||||
private fun prettifyPath(path: String): String {
|
||||
return path.substringAfterLast('/').removePrefix("is_").split('_').joinToString(" ") { part -> part.replaceFirstChar(Char::titlecase) }
|
||||
}
|
||||
|
||||
fun buildNegateRequirement(negateRequirement: NegateRequirement): DexEvolutionRequirementDisplay? {
|
||||
val inner = negateRequirement.inner
|
||||
when (inner) {
|
||||
is BiomeRequirement -> buildBiomeRequirement(inner, negated = true)?.let { return it }
|
||||
is StructureRequirement -> buildStructureRequirement(inner, negated = true)?.let { return it }
|
||||
}
|
||||
val innerDisplay = resolveRequirementData(inner) ?: return null
|
||||
return CobblemonDexEvolutionRequirementDisplay(innerDisplay.icon, lang("ui.pokedex.requirement.negate.tooltip", innerDisplay.tooltip))
|
||||
}
|
||||
|
||||
fun buildBiomeRequirement(biomeRequirement: BiomeRequirement, negated: Boolean = false): DexEvolutionRequirementDisplay? {
|
||||
// Negating "in X" is "not in X"; negating both a condition and an anticondition isn't expressible as one line
|
||||
if (negated && biomeRequirement.biomeCondition != null && biomeRequirement.biomeAnticondition != null) return null
|
||||
val condition = if (negated) biomeRequirement.biomeAnticondition else biomeRequirement.biomeCondition
|
||||
val antiCondition = if (negated) biomeRequirement.biomeCondition else biomeRequirement.biomeAnticondition
|
||||
if (condition == null && antiCondition == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val icon = Items.COMPASS
|
||||
val displayName: (RegistryLikeCondition<Biome>) -> MutableComponent = {
|
||||
when (it) {
|
||||
is RegistryLikeTagCondition -> tagDisplayName("worldgen.biome", it.tag.location())
|
||||
is RegistryLikeIdentifierCondition -> Component.translatable("biome.${it.identifier.namespace}.${it.identifier.path}")
|
||||
else -> Component.literal(it.toString())
|
||||
}
|
||||
}
|
||||
|
||||
val tooltip = if (condition != null && antiCondition != null) {
|
||||
lang("ui.pokedex.requirement.biome.both.tooltip", displayName(condition).green(), displayName(antiCondition).green())
|
||||
} else if (condition != null) {
|
||||
lang("ui.pokedex.requirement.biome.tooltip", displayName(condition).green())
|
||||
} else {
|
||||
lang("ui.pokedex.requirement.biome.anticondition.tooltip", displayName(antiCondition!!).green())
|
||||
}
|
||||
|
||||
var conditionConverted: ExtraCodecs.TagOrElementLocation? = null
|
||||
if (condition != null) {
|
||||
val conditionIdentifier = if (condition is RegistryLikeIdentifierCondition) condition.identifier else (condition as RegistryLikeTagCondition).tag.location
|
||||
conditionConverted = ExtraCodecs.TagOrElementLocation(conditionIdentifier, condition is RegistryLikeTagCondition)
|
||||
}
|
||||
|
||||
var antiConditionConverted: ExtraCodecs.TagOrElementLocation? = null
|
||||
if (antiCondition != null) {
|
||||
val conditionIdentifier = if (antiCondition is RegistryLikeIdentifierCondition) antiCondition.identifier else (antiCondition as RegistryLikeTagCondition).tag.location
|
||||
antiConditionConverted = ExtraCodecs.TagOrElementLocation(conditionIdentifier, antiCondition is RegistryLikeTagCondition)
|
||||
}
|
||||
|
||||
return CobblemonBiomeDexEvolutionRequirementDisplay(icon, tooltip, conditionConverted, antiConditionConverted)
|
||||
}
|
||||
|
||||
fun buildBlocksTraveledRequirement(blocksTraveledRequirement: BlocksTraveledRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon = Items.LEATHER_BOOTS
|
||||
val blocksComponent = Component.literal(blocksTraveledRequirement.amount.toString()).green()
|
||||
val tooltip = lang("ui.pokedex.requirement.blocks_traveled.tooltip", blocksComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildDamageTakenRequirement(damageTakenRequirement: DamageTakenRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon = CobblemonItems.PUNCHING_GLOVE
|
||||
val blocksComponent = Component.literal(damageTakenRequirement.amount.toString()).green()
|
||||
val tooltip = lang("ui.pokedex.requirement.damage_taken.tooltip", blocksComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildDefeatRequirement(defeatRequirement: DefeatRequirement): DexEvolutionRequirementDisplay? {
|
||||
val description = describePokemonProperties(defeatRequirement.target) ?: return null
|
||||
val icon = CobblemonItems.PUNCHING_GLOVE
|
||||
val defeatedAmount = defeatRequirement.amount
|
||||
|
||||
val descriptionComponent = Component.literal(defeatedAmount.toString()).green().add(" ").add(description)
|
||||
val tooltip = lang("ui.pokedex.requirement.defeat.tooltip", descriptionComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildFriendshipRequirement(friendshipRequirement: FriendshipRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon = CobblemonItems.SOOTHE_BELL
|
||||
val friendshipAmountComponent = Component.literal(friendshipRequirement.amount.toString()).green()
|
||||
val tooltip = lang("ui.pokedex.requirement.friendship.tooltip", friendshipAmountComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildHeldItemRequirement(heldItemRequirement: HeldItemRequirement): DexEvolutionRequirementDisplay? {
|
||||
val items = heldItemRequirement.itemCondition.items.getOrNull() ?: return null
|
||||
val item = items.firstOrNull()?.value() ?: return null
|
||||
val itemComponent = items.unwrapKey().map { tag ->
|
||||
val id = tag.location()
|
||||
val fallback = id.path.substringAfterLast('/').split('_').joinToString(" ") { part -> part.replaceFirstChar(Char::titlecase) }
|
||||
Component.translatableWithFallback("tag.item.${id.namespace}.${id.path.replace('/', '.')}", fallback)
|
||||
}.orElseGet { item.description.plainCopy() }
|
||||
val tooltip = lang("ui.pokedex.requirement.held_item.tooltip", itemComponent.green())
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(item, tooltip)
|
||||
}
|
||||
|
||||
fun buildLevelRequirement(levelRequirement: LevelRequirement): DexEvolutionRequirementDisplay {
|
||||
val minLevel = levelRequirement.minLevel
|
||||
val maxLevel = levelRequirement.maxLevel
|
||||
|
||||
val icon = CobblemonItems.RARE_CANDY
|
||||
val tooltip =
|
||||
if (minLevel == maxLevel || maxLevel == Int.MAX_VALUE)
|
||||
lang("ui.pokedex.requirement.level.tooltip.min", Component.literal(minLevel.toString()).green())
|
||||
else
|
||||
lang("ui.pokedex.requirement.level.tooltip.between", Component.literal(minLevel.toString()).green(), Component.literal(maxLevel.toString()).green())
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildMoonPhaseRequirement(moonPhaseRequirement: MoonPhaseRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon = Items.CLOCK
|
||||
val moonPhaseComponent = moonPhaseRequirement.moonPhase.displayName().green()
|
||||
val tooltip = lang("ui.pokedex.requirement.moon_phase.tooltip", moonPhaseComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildMoveSetRequirement(moveSetRequirement: MoveSetRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon = CobblemonItems.TECHNICAL_MACHINE
|
||||
val tooltip = lang("ui.pokedex.requirement.has_move.tooltip", moveSetRequirement.move.displayName.green())
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildMoveTypeRequirement(moveTypeRequirement: MoveTypeRequirement): DexEvolutionRequirementDisplay {
|
||||
val type = ElementalTypes.get(moveTypeRequirement.type.name)
|
||||
val icon = when (type) {
|
||||
ElementalTypes.NORMAL -> CobblemonItems.NORMAL_GEM
|
||||
ElementalTypes.FIRE -> CobblemonItems.FIRE_GEM
|
||||
ElementalTypes.WATER -> CobblemonItems.WATER_GEM
|
||||
ElementalTypes.GRASS -> CobblemonItems.GRASS_GEM
|
||||
ElementalTypes.ELECTRIC -> CobblemonItems.ELECTRIC_GEM
|
||||
ElementalTypes.ICE -> CobblemonItems.ICE_GEM
|
||||
ElementalTypes.FIGHTING -> CobblemonItems.FIGHTING_GEM
|
||||
ElementalTypes.POISON -> CobblemonItems.POISON_GEM
|
||||
ElementalTypes.GROUND -> CobblemonItems.GROUND_GEM
|
||||
ElementalTypes.FLYING -> CobblemonItems.FLYING_GEM
|
||||
ElementalTypes.PSYCHIC -> CobblemonItems.PSYCHIC_GEM
|
||||
ElementalTypes.BUG -> CobblemonItems.BUG_GEM
|
||||
ElementalTypes.ROCK -> CobblemonItems.ROCK_GEM
|
||||
ElementalTypes.GHOST -> CobblemonItems.GHOST_GEM
|
||||
ElementalTypes.DRAGON -> CobblemonItems.DRAGON_GEM
|
||||
ElementalTypes.DARK -> CobblemonItems.DARK_GEM
|
||||
ElementalTypes.STEEL -> CobblemonItems.STEEL_GEM
|
||||
ElementalTypes.FAIRY -> CobblemonItems.FAIRY_GEM
|
||||
else -> CobblemonItems.NORMAL_GEM
|
||||
}
|
||||
|
||||
val tooltip = lang("ui.pokedex.requirement.has_move_type.tooltip", moveTypeRequirement.type.displayName.green())
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildOwnerHoldsItemRequirement(ownerHoldsItemRequirement: OwnerHoldsItemRequirement): DexEvolutionRequirementDisplay? {
|
||||
val item = ownerHoldsItemRequirement.itemCondition.items.orElse(null)?.firstOrNull()?.value() ?: return null
|
||||
val tooltip = lang("ui.pokedex.requirement.owner_held_item.tooltip", item.description.plainCopy().green())
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(item, tooltip)
|
||||
}
|
||||
|
||||
fun buildPartyMemberRequirement(partyMemberRequirement: PartyMemberRequirement): DexEvolutionRequirementDisplay? {
|
||||
val description = describePokemonProperties(partyMemberRequirement.target) ?: return null
|
||||
val icon = CobblemonItems.POKE_BALL
|
||||
val tooltip = if (partyMemberRequirement.contains) {
|
||||
lang("ui.pokedex.requirement.party_member.tooltip", description)
|
||||
} else {
|
||||
lang("ui.pokedex.requirement.party_member.anticondition.tooltip", description)
|
||||
}
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildPokemonPropertiesRequirement(pokemonPropertiesRequirement: PokemonPropertiesRequirement): DexEvolutionRequirementDisplay? {
|
||||
val description = describePokemonProperties(pokemonPropertiesRequirement.target) ?: return null
|
||||
val icon = Items.NAME_TAG
|
||||
val tooltip = lang("ui.pokedex.requirement.pokemon_properties.tooltip", description)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildPropertyRangeRequirement(propertyRangeRequirement: PropertyRangeRequirement): DexEvolutionRequirementDisplay? {
|
||||
val provider = SpeciesFeatures.getFeature(propertyRangeRequirement.feature) as? IntSpeciesFeatureProvider ?: return null
|
||||
val displayName = provider.display?.name?.takeIf { it.isNotBlank() }?.asTranslated()?.green() ?: return null
|
||||
val icon = provider.itemPoints.keys.firstOrNull()?.let { BuiltInRegistries.ITEM.get(it) } ?: Items.NAME_TAG
|
||||
|
||||
val range = propertyRangeRequirement.range
|
||||
val tooltip = lang(
|
||||
"ui.pokedex.requirement.property_range.tooltip",
|
||||
displayName,
|
||||
Component.literal(range.first.toString()).green(),
|
||||
Component.literal(range.last.toString()).green()
|
||||
)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildRecoilRequirement(recoilRequirement: RecoilRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon = CobblemonItems.LIFE_ORB
|
||||
val recoilComponent = Component.literal(recoilRequirement.amount.toString()).green()
|
||||
val tooltip = lang("ui.pokedex.requirement.recoil.tooltip", recoilComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildStatCompareRequirement(statCompareRequirement: StatCompareRequirement): DexEvolutionRequirementDisplay {
|
||||
val highStat = Stats.getStat(statCompareRequirement.highStat)
|
||||
val lowStat = Stats.getStat(statCompareRequirement.lowStat)
|
||||
|
||||
val icon = when (highStat) {
|
||||
Stats.HP, Stats.EVASION, Stats.ACCURACY -> CobblemonItems.HEALTH_MOCHI
|
||||
Stats.ATTACK -> CobblemonItems.MUSCLE_MOCHI
|
||||
Stats.DEFENCE -> CobblemonItems.RESIST_MOCHI
|
||||
Stats.SPECIAL_ATTACK -> CobblemonItems.GENIUS_MOCHI
|
||||
Stats.SPECIAL_DEFENCE -> CobblemonItems.CLEVER_MOCHI
|
||||
Stats.SPEED -> CobblemonItems.SWIFT_MOCHI
|
||||
}
|
||||
|
||||
val highStatComponent = highStat.displayName.plainCopy().green()
|
||||
val lowStatComponent = lowStat.displayName.plainCopy().green()
|
||||
val tooltip = lang("ui.pokedex.requirement.stat_compare.tooltip", highStatComponent, lowStatComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildStatEqualRequirement(statEqualRequirement: StatEqualRequirement): DexEvolutionRequirementDisplay {
|
||||
val statOne = Stats.getStat(statEqualRequirement.statOne)
|
||||
val statTwo = Stats.getStat(statEqualRequirement.statTwo)
|
||||
|
||||
val statOneComponent = statOne.displayName.plainCopy().green()
|
||||
val statTwoComponent = statTwo.displayName.plainCopy().green()
|
||||
|
||||
val icon = CobblemonItems.FRESH_START_MOCHI
|
||||
val tooltip = lang("ui.pokedex.requirement.stat_equal.tooltip", statOneComponent, statTwoComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildStructureRequirement(structureRequirement: StructureRequirement, negated: Boolean = false): DexEvolutionRequirementDisplay? {
|
||||
if (negated && structureRequirement.structureCondition != null && structureRequirement.structureAnticondition != null) return null
|
||||
val condition = if (negated) structureRequirement.structureAnticondition else structureRequirement.structureCondition
|
||||
val anticondition = if (negated) structureRequirement.structureCondition else structureRequirement.structureAnticondition
|
||||
if (condition == null && anticondition == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val icon = Items.FILLED_MAP
|
||||
val displayName: (RegistryLikeCondition<Structure>) -> MutableComponent = {
|
||||
when (it) {
|
||||
is RegistryLikeTagCondition -> tagDisplayName("worldgen.structure", it.tag.location())
|
||||
is RegistryLikeIdentifierCondition -> Component.literal(prettifyPath(it.identifier.path))
|
||||
else -> Component.literal(it.toString())
|
||||
}
|
||||
}
|
||||
val tooltip = if (condition != null && anticondition != null) {
|
||||
lang("ui.pokedex.requirement.structure.both.tooltip", displayName(condition).green(), displayName(anticondition).green())
|
||||
} else if (condition != null) {
|
||||
lang("ui.pokedex.requirement.structure.tooltip", displayName(condition).green())
|
||||
} else {
|
||||
lang("ui.pokedex.requirement.structure.anticondition.tooltip", displayName(anticondition!!).green())
|
||||
}
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildTimeRangeRequirement(timeRangeRequirement: TimeRangeRequirement): DexEvolutionRequirementDisplay? {
|
||||
val icon = Items.CLOCK
|
||||
val timeRangeComponent = timeRangeRequirement.range.displayName() ?: return null
|
||||
val tooltip = lang("ui.pokedex.requirement.time_range.tooltip", timeRangeComponent.green())
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildUseMoveRequirement(useMoveRequirement: UseMoveRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon = CobblemonItems.CHOICE_BAND
|
||||
val tooltip = lang("ui.pokedex.requirement.use_move.tooltip", useMoveRequirement.move.displayName.green())
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildWeatherRequirement(weatherRequirement: WeatherRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon: Item
|
||||
val tooltip: MutableComponent
|
||||
|
||||
if (weatherRequirement.isThundering == true) {
|
||||
icon = Items.LIGHTNING_ROD
|
||||
tooltip = lang("ui.pokedex.requirement.weather.tooltip.thundering")
|
||||
} else {
|
||||
icon = Items.WATER_BUCKET
|
||||
tooltip = lang("ui.pokedex.requirement.weather.tooltip.raining")
|
||||
}
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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.pokedex.entry
|
||||
|
||||
import com.cobblemon.mod.common.api.pokemon.Natures
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.IntSpeciesFeature
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.IntSpeciesFeatureProvider
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeatures
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.StringSpeciesFeature
|
||||
import com.cobblemon.mod.common.api.properties.CustomPokemonProperty
|
||||
import com.cobblemon.mod.common.api.text.add
|
||||
import com.cobblemon.mod.common.api.text.green
|
||||
import com.cobblemon.mod.common.api.types.ElementalTypes
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import com.cobblemon.mod.common.util.asTranslated
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.locale.Language
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.network.chat.MutableComponent
|
||||
import net.minecraft.world.item.Items
|
||||
|
||||
fun describePokemonProperties(properties: PokemonProperties): MutableComponent? {
|
||||
if (hasUndescribedProperty(properties)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val descriptors = mutableListOf<MutableComponent>()
|
||||
|
||||
properties.species?.let { PokemonSpecies.getByIdentifier(it.asIdentifierDefaultingNamespace()) }?.let {
|
||||
descriptors.add(it.translatedName.plainCopy().green())
|
||||
}
|
||||
properties.gender?.let {
|
||||
descriptors.add(lang("gender.${it.name.lowercase()}").green())
|
||||
}
|
||||
properties.nickname?.let {
|
||||
descriptors.add(lang("ui.pokedex.requirement.properties.nickname", it.plainCopy().green()))
|
||||
}
|
||||
properties.type?.let {
|
||||
val typeName = ElementalTypes.get(it)?.displayName?.plainCopy() ?: Component.literal(it)
|
||||
descriptors.add(lang("ui.pokedex.requirement.properties.type", typeName.green()))
|
||||
}
|
||||
properties.nature?.let {
|
||||
val natureName = Natures.getNature(it.asIdentifierDefaultingNamespace())?.displayName?.asTranslated() ?: Component.literal(it)
|
||||
descriptors.add(lang("ui.pokedex.requirement.properties.nature", natureName.green()))
|
||||
}
|
||||
properties.customProperties.forEach { customProperty ->
|
||||
describeCustomProperty(customProperty)?.let { descriptors.add(it) }
|
||||
}
|
||||
|
||||
val heldItemDescriptor = properties.heldItem?.let { heldItem ->
|
||||
val itemId = heldItem.substringBefore('[').trim().asIdentifierDefaultingNamespace()
|
||||
val item = BuiltInRegistries.ITEM.get(itemId)
|
||||
if (item == Items.AIR) null else lang("ui.pokedex.requirement.properties.held_item", item.description.plainCopy().green())
|
||||
}
|
||||
|
||||
if (descriptors.isEmpty() && heldItemDescriptor == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val mainDescriptor = when {
|
||||
descriptors.isEmpty() -> null
|
||||
descriptors.size == 1 -> descriptors[0]
|
||||
else -> {
|
||||
val allButLast = descriptors.dropLast(1).reduce { acc, next -> acc.add(", ").add(next) }
|
||||
lang("ui.pokedex.requirement.properties.and", allButLast, descriptors.last())
|
||||
}
|
||||
}
|
||||
|
||||
return when {
|
||||
mainDescriptor != null && heldItemDescriptor != null -> mainDescriptor.add(" ").add(heldItemDescriptor)
|
||||
mainDescriptor != null -> mainDescriptor
|
||||
else -> heldItemDescriptor
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasUndescribedProperty(properties: PokemonProperties): Boolean {
|
||||
return properties.form != null ||
|
||||
properties.shiny != null ||
|
||||
properties.level != null ||
|
||||
properties.friendship != null ||
|
||||
properties.fullness != null ||
|
||||
properties.pokeball != null ||
|
||||
properties.ability != null ||
|
||||
properties.status != null ||
|
||||
properties.teraType != null ||
|
||||
properties.dmaxLevel != null ||
|
||||
properties.gmaxFactor != null ||
|
||||
properties.tradeable != null ||
|
||||
properties.originalTrainerType != null ||
|
||||
properties.originalTrainer != null ||
|
||||
properties.moves != null ||
|
||||
properties.movesetBuilders != null ||
|
||||
properties.cosmeticItem != null ||
|
||||
properties.scaleModifier != null ||
|
||||
properties.isAlpha != null ||
|
||||
properties.minPerfectIVs != null ||
|
||||
properties.customProperties.any { describeCustomProperty(it) == null }
|
||||
}
|
||||
|
||||
private fun describeCustomProperty(property: CustomPokemonProperty): MutableComponent? {
|
||||
return when (property) {
|
||||
is IntSpeciesFeature -> describeIntSpeciesFeature(property)
|
||||
is StringSpeciesFeature -> describeStringSpeciesFeature(property)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun describeIntSpeciesFeature(property: IntSpeciesFeature): MutableComponent? {
|
||||
val value = Component.literal(property.value.toString())
|
||||
|
||||
val overrideKey = "ui.pokedex.requirement.properties.custom.${property.name}"
|
||||
if (Language.getInstance().has("cobblemon.$overrideKey")) {
|
||||
return lang(overrideKey, value.green())
|
||||
}
|
||||
|
||||
val displayName = (SpeciesFeatures.getFeature(property.name) as? IntSpeciesFeatureProvider)
|
||||
?.display?.name?.takeIf { it.isNotBlank() }
|
||||
?: return null
|
||||
|
||||
return lang("ui.pokedex.requirement.properties.int_feature", value.green(), displayName.asTranslated().green())
|
||||
}
|
||||
|
||||
private fun describeStringSpeciesFeature(property: StringSpeciesFeature): MutableComponent? {
|
||||
val key = "ui.pokedex.requirement.properties.custom.${property.name}"
|
||||
if (!Language.getInstance().has("cobblemon.$key")) {
|
||||
return null
|
||||
}
|
||||
|
||||
val value = PokemonSpecies.getByIdentifier(property.value.asIdentifierDefaultingNamespace())?.translatedName?.plainCopy()
|
||||
?: Component.literal(property.value)
|
||||
|
||||
return lang(key, value.green())
|
||||
}
|
||||
@@ -26,7 +26,6 @@ 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
|
||||
import com.cobblemon.mod.common.api.pokemon.effect.ShoulderEffect
|
||||
import com.cobblemon.mod.common.api.pokemon.effect.adapter.ShoulderEffectAdapter
|
||||
import com.cobblemon.mod.common.api.pokemon.egg.EggGroup
|
||||
@@ -128,7 +127,7 @@ object PokemonSpecies : JsonDataRegistry<Species> {
|
||||
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Fluid::class.java).type, FluidLikeConditionAdapter)
|
||||
.registerTypeAdapter(EggGroup::class.java, EggGroupAdapter)
|
||||
.registerTypeAdapter(MobEffect::class.java, RegistryElementAdapter<MobEffect>(BuiltInRegistries::MOB_EFFECT))
|
||||
.registerTypeAdapter(ItemPredicate::class.java, LegacyItemConditionWrapperAdapter)
|
||||
.registerTypeAdapter(ItemPredicate::class.java, LegacyItemConditionWrapperAdapter())
|
||||
.registerTypeAdapter(RidingBehaviourSettings::class.java, RidingBehaviourSettingsAdapter)
|
||||
.registerTypeAdapter(RideSoundSettingsList::class.java, RideSoundSettingsListAdapter)
|
||||
.registerTypeAdapter(ObtainableItemCondition::class.java, ObtainableItemConditionAdapter)
|
||||
|
||||
+16
-2
@@ -15,6 +15,8 @@ import com.cobblemon.mod.common.block.entity.PokeSnackBlockEntity
|
||||
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
|
||||
import com.cobblemon.mod.common.client.render.MatrixWrapper
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.ParticleStatus
|
||||
import net.minecraft.core.particles.BlockParticleOption
|
||||
import net.minecraft.core.particles.ParticleOptions
|
||||
import net.minecraft.core.particles.ParticleTypes
|
||||
@@ -40,13 +42,25 @@ sealed class ParticleData {
|
||||
val chance: Double,
|
||||
val amount: Int,
|
||||
val locatorResolver: (Map<String, MatrixWrapper>) -> List<String>
|
||||
): ParticleData()
|
||||
): ParticleData() {
|
||||
fun amount(): Int = when(Minecraft.getInstance().options.particles().get()) {
|
||||
ParticleStatus.ALL -> amount
|
||||
ParticleStatus.DECREASED -> amount / 2
|
||||
ParticleStatus.MINIMAL -> amount / 4
|
||||
}
|
||||
}
|
||||
|
||||
data class MinecraftParticle(
|
||||
val particle: ParticleOptions,
|
||||
val chance: Double,
|
||||
val amount: Int
|
||||
): ParticleData()
|
||||
): ParticleData() {
|
||||
fun amount(): Int = when(Minecraft.getInstance().options.particles().get()) {
|
||||
ParticleStatus.ALL -> amount
|
||||
ParticleStatus.DECREASED -> amount / 2
|
||||
ParticleStatus.MINIMAL -> amount / 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,8 @@ package com.cobblemon.mod.common.api.pokemon.requirement
|
||||
import com.cobblemon.mod.common.api.pokemon.evolution.ContextEvolution
|
||||
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import java.util.WeakHashMap
|
||||
|
||||
/**
|
||||
* Represents a universal requirement suitable for various checks on pokemon..
|
||||
@@ -32,4 +34,11 @@ fun interface Requirement {
|
||||
*/
|
||||
fun check(pokemon: Pokemon): Boolean
|
||||
|
||||
val pokedexIcon: ResourceLocation? get() = pokedexIconOverrides[this]
|
||||
val pokedexTooltip: String? get() = pokedexTooltipOverrides[this]
|
||||
|
||||
companion object {
|
||||
val pokedexIconOverrides = WeakHashMap<Requirement, ResourceLocation>()
|
||||
val pokedexTooltipOverrides = WeakHashMap<Requirement, String>()
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.riding.util
|
||||
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.phys.AABB
|
||||
import kotlin.math.floor
|
||||
|
||||
/**
|
||||
* Walks every block position inside [searchBox] and turns each solid block into one
|
||||
* full-cube [FieldPrimitive].
|
||||
*
|
||||
* @author Jackson
|
||||
* @since July 27th, 2026
|
||||
*/
|
||||
|
||||
fun collectPrimitivesFromBlocks(
|
||||
level: BlockGetter,
|
||||
searchBox: AABB,
|
||||
shape: PrimitiveShape,
|
||||
includeFluids: Boolean = false
|
||||
): List<FieldPrimitive> {
|
||||
val primitives = mutableListOf<FieldPrimitive>()
|
||||
|
||||
val minX = floor(searchBox.minX).toInt()
|
||||
val maxX = floor(searchBox.maxX).toInt()
|
||||
val minY = floor(searchBox.minY).toInt()
|
||||
val maxY = floor(searchBox.maxY).toInt()
|
||||
val minZ = floor(searchBox.minZ).toInt()
|
||||
val maxZ = floor(searchBox.maxZ).toInt()
|
||||
|
||||
val pos = BlockPos.MutableBlockPos()
|
||||
for (x in minX..maxX) {
|
||||
for (y in minY..maxY) {
|
||||
for (z in minZ..maxZ) {
|
||||
pos.set(x, y, z)
|
||||
val state = level.getBlockState(pos)
|
||||
|
||||
val isSolid = !state.getCollisionShape(level, pos).isEmpty
|
||||
val isFluid = includeFluids && !state.fluidState.isEmpty
|
||||
if (!isSolid && !isFluid) continue
|
||||
|
||||
primitives.add(
|
||||
FieldPrimitive(
|
||||
shape = shape,
|
||||
centerX = x + 0.5f, centerY = y + 0.5f, centerZ = z + 0.5f,
|
||||
halfWidth = 0.5f, halfHeight = 0.5f, halfDepth = 0.5f
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return primitives
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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.riding.util
|
||||
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* Helpful Functions when working with signed distance fields/functions.
|
||||
* https://iquilezles.org/articles/distfunctions/
|
||||
*
|
||||
* @author Jackson
|
||||
* @since July 27th, 2026
|
||||
*/
|
||||
|
||||
/** Which distance formula a primitive uses. */
|
||||
enum class PrimitiveShape {
|
||||
BOX,
|
||||
ROUNDED_BOX,
|
||||
ELLIPSOID
|
||||
}
|
||||
|
||||
/** Tunable constants for the field. */
|
||||
data class DistanceFieldTuning(
|
||||
val smoothingRange: Float = 0.5f,
|
||||
val roundingRadius: Float = 0.08f
|
||||
)
|
||||
|
||||
/** A single shape in the field - a center position, half-extents, and which formula to use. */
|
||||
data class FieldPrimitive(
|
||||
val shape: PrimitiveShape,
|
||||
val centerX: Float, val centerY: Float, val centerZ: Float,
|
||||
val halfWidth: Float, val halfHeight: Float, val halfDepth: Float
|
||||
)
|
||||
|
||||
/** Fast approximate 1/sqrt(x) - the Quake III trick, cheaper than a real sqrt + divide. */
|
||||
fun fastInverseSqrt(value: Float): Float {
|
||||
var x = value
|
||||
val halfX = 0.5f * x
|
||||
var bits = java.lang.Float.floatToIntBits(x)
|
||||
bits = 0x5f3759df - (bits shr 1)
|
||||
x = java.lang.Float.intBitsToFloat(bits)
|
||||
x *= (1.5f - halfX * x * x)
|
||||
return x
|
||||
}
|
||||
|
||||
/** Exact distance from a point to an axis-aligned box. Inputs are relative to the box's own center. */
|
||||
fun boxDistance(relX: Float, relY: Float, relZ: Float, halfWidth: Float, halfHeight: Float, halfDepth: Float): Float {
|
||||
val qx = abs(relX) - halfWidth
|
||||
val qy = abs(relY) - halfHeight
|
||||
val qz = abs(relZ) - halfDepth
|
||||
val ox = max(qx, 0f)
|
||||
val oy = max(qy, 0f)
|
||||
val oz = max(qz, 0f)
|
||||
val outside = sqrt(ox * ox + oy * oy + oz * oz)
|
||||
val inside = min(max(qx, max(qy, qz)), 0f)
|
||||
return outside + inside
|
||||
}
|
||||
|
||||
/** Same as boxDistance but with rounded corners/edges https://iquilezles.org/articles/distfunctions/ */
|
||||
fun roundedBoxDistance(relX: Float, relY: Float, relZ: Float, halfWidth: Float, halfHeight: Float, halfDepth: Float, roundingRadius: Float): Float =
|
||||
boxDistance(relX, relY, relZ, halfWidth - roundingRadius, halfHeight - roundingRadius, halfDepth - roundingRadius) - roundingRadius
|
||||
|
||||
/** Approximate (lower-bound) distance to an ellipsoid https://iquilezles.org/articles/ellipsoids/. */
|
||||
fun ellipsoidDistance(relX: Float, relY: Float, relZ: Float, halfWidth: Float, halfHeight: Float, halfDepth: Float): Float {
|
||||
val px = relX / halfWidth
|
||||
val py = relY / halfHeight
|
||||
val pz = relZ / halfDepth
|
||||
val inverseMagnitude = fastInverseSqrt(px * px + py * py + pz * pz)
|
||||
val trueDistance = sqrt(relX * relX + relY * relY + relZ * relZ)
|
||||
return trueDistance * (1.0f - inverseMagnitude)
|
||||
}
|
||||
|
||||
/** Distance from a point to one primitive, dispatched by its shape. */
|
||||
fun distanceToPrimitive(primitive: FieldPrimitive, sampleX: Float, sampleY: Float, sampleZ: Float, tuning: DistanceFieldTuning): Float {
|
||||
val relX = sampleX - primitive.centerX
|
||||
val relY = sampleY - primitive.centerY
|
||||
val relZ = sampleZ - primitive.centerZ
|
||||
return when (primitive.shape) {
|
||||
PrimitiveShape.BOX -> boxDistance(relX, relY, relZ, primitive.halfWidth, primitive.halfHeight, primitive.halfDepth)
|
||||
PrimitiveShape.ROUNDED_BOX -> roundedBoxDistance(relX, relY, relZ, primitive.halfWidth, primitive.halfHeight, primitive.halfDepth, tuning.roundingRadius)
|
||||
PrimitiveShape.ELLIPSOID -> ellipsoidDistance(relX, relY, relZ, primitive.halfWidth, primitive.halfHeight, primitive.halfDepth)
|
||||
}
|
||||
}
|
||||
|
||||
///** Normalized quadratic smooth-min https://iquilezles.org/articles/smin/ */
|
||||
//fun smoothMinimum(a: Float, b: Float, smoothingRange: Float): Float {
|
||||
// if (smoothingRange <= 0.001f) return min(a, b)
|
||||
// val k = smoothingRange * 4.0f
|
||||
// val h = (0.5f + 0.5f * (b - a) / k).coerceIn(0f, 1f)
|
||||
// return b + (a - b) * h - k * h * (1f - h)
|
||||
//}
|
||||
//
|
||||
///** Folds every primitive's distance together into one field value at a point. */
|
||||
//fun sampleField(primitives: List<FieldPrimitive>, sampleX: Float, sampleY: Float, sampleZ: Float, tuning: DistanceFieldTuning): Float {
|
||||
// var result = Float.POSITIVE_INFINITY
|
||||
// for (primitive in primitives) {
|
||||
// val distance = distanceToPrimitive(primitive, sampleX, sampleY, sampleZ, tuning)
|
||||
// result = if (result == Float.POSITIVE_INFINITY) distance else smoothMinimum(result, distance, tuning.smoothingRange)
|
||||
// }
|
||||
// return result
|
||||
//}
|
||||
|
||||
/** Normalized quadratic smooth-min https://iquilezles.org/articles/smin/ */
|
||||
fun smoothMinimum(a: Float, b: Float, smoothingRange: Float): Float {
|
||||
if (smoothingRange <= 0.001f) return min(a, b)
|
||||
val k = smoothingRange * 4.0f
|
||||
val h = (0.5f + 0.5f * (b - a) / k).coerceIn(0f, 1f)
|
||||
return b + (a - b) * h - k * h * (1f - h)
|
||||
}
|
||||
|
||||
/** Folds nearby primitive distances together into one field value at a point. */
|
||||
fun sampleField(primitives: List<FieldPrimitive>, sampleX: Float, sampleY: Float, sampleZ: Float, tuning: DistanceFieldTuning): Float {
|
||||
if (primitives.isEmpty()) return Float.POSITIVE_INFINITY
|
||||
|
||||
val distances = FloatArray(primitives.size) { distanceToPrimitive(primitives[it], sampleX, sampleY, sampleZ, tuning) }
|
||||
val minDistance = distances.min()
|
||||
|
||||
if (tuning.smoothingRange <= 0.001f) return minDistance
|
||||
|
||||
// Anything more than a few blend-widths past the closest primitive is cut - its
|
||||
// contribution would be negligible in a correct blend anyway, but left in, it still
|
||||
// drags the chained result down the more of them there are.
|
||||
val cutoff = tuning.smoothingRange * 3f
|
||||
|
||||
var result = Float.POSITIVE_INFINITY
|
||||
for (distance in distances) {
|
||||
if (distance - minDistance > cutoff) continue
|
||||
result = if (result == Float.POSITIVE_INFINITY) distance else smoothMinimum(result, distance, tuning.smoothingRange)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** Finite-difference gradient of the field at a point - points away from the surface, magnitude ~1 for a clean field. */
|
||||
fun estimateGradient(primitives: List<FieldPrimitive>, point: Vec3, tuning: DistanceFieldTuning, epsilon: Float = 0.05f): Vec3 {
|
||||
val px = point.x.toFloat()
|
||||
val py = point.y.toFloat()
|
||||
val pz = point.z.toFloat()
|
||||
|
||||
val dx = sampleField(primitives, px + epsilon, py, pz, tuning) - sampleField(primitives, px - epsilon, py, pz, tuning)
|
||||
val dy = sampleField(primitives, px, py + epsilon, pz, tuning) - sampleField(primitives, px, py - epsilon, pz, tuning)
|
||||
val dz = sampleField(primitives, px, py, pz + epsilon, tuning) - sampleField(primitives, px, py, pz - epsilon, tuning)
|
||||
|
||||
val gradient = Vec3(dx.toDouble(), dy.toDouble(), dz.toDouble())
|
||||
return if (gradient.lengthSqr() > 1e-8) gradient.normalize() else Vec3(0.0, 1.0, 0.0)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.riding.util
|
||||
|
||||
import net.minecraft.core.particles.DustParticleOptions
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.world.entity.Entity
|
||||
import net.minecraft.world.phys.AABB
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import org.joml.Vector3f
|
||||
|
||||
/** One shell to draw - a target field value, a color, a particle size. */
|
||||
data class DistanceFieldLayer(
|
||||
val isoValue: Float,
|
||||
val color: Vector3f,
|
||||
val particleSize: Float = 1.0f
|
||||
)
|
||||
|
||||
/**
|
||||
* Marches a grid of vertical columns through the field around [center] and spawns a
|
||||
* particle everywhere it crosses one of [layers]' target values. Server-side only.
|
||||
*/
|
||||
fun visualizeDistanceField(
|
||||
referenceEntity: Entity,
|
||||
center: Vec3,
|
||||
layers: List<DistanceFieldLayer>,
|
||||
shape: PrimitiveShape = PrimitiveShape.BOX,
|
||||
horizontalRadius: Double = 4.0,
|
||||
verticalRange: Double = 4.0,
|
||||
sampleSpacing: Double = 0.5,
|
||||
marchStep: Double = 0.1,
|
||||
tuning: DistanceFieldTuning = DistanceFieldTuning()
|
||||
) {
|
||||
val level = referenceEntity.level()
|
||||
if (level !is ServerLevel) return
|
||||
|
||||
val searchBox = AABB(
|
||||
center.x - horizontalRadius, center.y - verticalRange, center.z - horizontalRadius,
|
||||
center.x + horizontalRadius, center.y, center.z + horizontalRadius
|
||||
)
|
||||
|
||||
val primitives = collectPrimitivesFromBlocks(level, searchBox, shape, includeFluids = true)
|
||||
if (primitives.isEmpty()) return
|
||||
|
||||
var x = -horizontalRadius
|
||||
while (x <= horizontalRadius) {
|
||||
var z = -horizontalRadius
|
||||
while (z <= horizontalRadius) {
|
||||
marchColumn(level, primitives, center, x, z, verticalRange, marchStep, layers, tuning)
|
||||
z += sampleSpacing
|
||||
}
|
||||
x += sampleSpacing
|
||||
}
|
||||
}
|
||||
|
||||
private fun marchColumn(
|
||||
level: ServerLevel,
|
||||
primitives: List<FieldPrimitive>,
|
||||
center: Vec3,
|
||||
offsetX: Double,
|
||||
offsetZ: Double,
|
||||
verticalRange: Double,
|
||||
marchStep: Double,
|
||||
layers: List<DistanceFieldLayer>,
|
||||
tuning: DistanceFieldTuning
|
||||
) {
|
||||
val previousValue = FloatArray(layers.size) { Float.NaN }
|
||||
|
||||
var y = verticalRange
|
||||
while (y >= -verticalRange) {
|
||||
val worldX = (center.x + offsetX).toFloat()
|
||||
val worldY = (center.y + y).toFloat()
|
||||
val worldZ = (center.z + offsetZ).toFloat()
|
||||
val fieldValue = sampleField(primitives, worldX, worldY, worldZ, tuning)
|
||||
|
||||
for (layerIndex in layers.indices) {
|
||||
val layer = layers[layerIndex]
|
||||
val previous = previousValue[layerIndex]
|
||||
|
||||
if (!previous.isNaN() && crossedTarget(previous, fieldValue, layer.isoValue)) {
|
||||
val t = (layer.isoValue - previous) / (fieldValue - previous)
|
||||
val crossingY = (y + marchStep) - marchStep * t
|
||||
spawnLayerParticle(level, center.add(offsetX, crossingY, offsetZ), layer)
|
||||
}
|
||||
|
||||
previousValue[layerIndex] = fieldValue
|
||||
}
|
||||
|
||||
y -= marchStep
|
||||
}
|
||||
}
|
||||
|
||||
private fun crossedTarget(previous: Float, current: Float, target: Float): Boolean =
|
||||
(previous - target) * (current - target) < 0f
|
||||
|
||||
private fun spawnLayerParticle(level: ServerLevel, pos: Vec3, layer: DistanceFieldLayer) {
|
||||
val particle = DustParticleOptions(layer.color, layer.particleSize)
|
||||
level.sendParticles(particle, pos.x, pos.y, pos.z, 1, 0.0, 0.0, 0.0, 0.0)
|
||||
}
|
||||
@@ -10,6 +10,8 @@ package com.cobblemon.mod.common.api.spawning
|
||||
|
||||
import com.cobblemon.mod.common.api.spawning.TimeRange.Companion.timeRanges
|
||||
import com.cobblemon.mod.common.api.spawning.condition.SpawningCondition
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import net.minecraft.network.chat.MutableComponent
|
||||
import com.cobblemon.mod.common.util.adapters.IntRangesAdapter
|
||||
|
||||
/**
|
||||
@@ -49,4 +51,25 @@ class TimeRange : IntRanges {
|
||||
|
||||
constructor() : super()
|
||||
constructor(vararg ranges: IntRange) : super(*ranges)
|
||||
|
||||
fun displayName(): MutableComponent? {
|
||||
val key = timeRanges.entries.firstOrNull { entry -> this == entry.value }?.key ?: return null
|
||||
return lang("ui.time_range.$key")
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is TimeRange) return false
|
||||
|
||||
if (other.ranges.size != this.ranges.size) return false
|
||||
other.ranges.forEachIndexed { index, otherRange ->
|
||||
if (this.ranges[index] != otherRange) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return ranges.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
package com.cobblemon.mod.common.api.spawning.condition
|
||||
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import net.minecraft.network.chat.MutableComponent
|
||||
import net.minecraft.world.level.Level
|
||||
|
||||
/**
|
||||
@@ -42,4 +44,7 @@ enum class MoonPhase {
|
||||
fun ofWorld(world: Level): MoonPhase = VALUES[world.moonPhase]
|
||||
}
|
||||
|
||||
fun displayName(): MutableComponent {
|
||||
return lang("ui.moon_phase.${this.name.lowercase()}")
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -22,6 +22,7 @@ import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
|
||||
import com.cobblemon.mod.common.api.types.ElementalTypes
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.entity.pokemon.ai.sensors.AlphaLevelMatchingSensor
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.entity.Entity
|
||||
|
||||
@@ -76,7 +77,7 @@ open class SpawnBaitInfluence(val effects: List<SpawnBait.Effect>, val onUsed: (
|
||||
?: return super.affectWeight(detail, spawnablePosition, weight)
|
||||
|
||||
val detailSpecies = pokemonDetail.pokemon.species
|
||||
?.let { PokemonSpecies.getByName(it) }
|
||||
?.let { PokemonSpecies.getByIdentifier(it.asIdentifierDefaultingNamespace()) }
|
||||
?: return super.affectWeight(detail, spawnablePosition, weight)
|
||||
|
||||
val formData = detailSpecies.getForm(pokemonDetail.pokemon.aspects)
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.util.isBoxLoaded
|
||||
import com.cobblemon.mod.common.util.removeIf
|
||||
import com.cobblemon.mod.common.util.squeezeWithinBounds
|
||||
import com.cobblemon.mod.common.util.toBlockPos
|
||||
import com.cobblemon.mod.common.util.toVec3f
|
||||
import com.cobblemon.mod.common.util.weightedSelection
|
||||
import kotlin.math.max
|
||||
@@ -190,74 +191,61 @@ interface Spawner {
|
||||
}
|
||||
|
||||
fun isValidStartPoint(world: Level, chunk: ChunkAccess, startPos: BlockPos.MutableBlockPos): Boolean {
|
||||
val y = startPos.y
|
||||
if (!world.isLoaded(startPos) || !world.isLoaded(startPos.setY(y + 1))) {
|
||||
val abovePos = startPos.above()
|
||||
if (!world.isLoaded(startPos) || !world.isLoaded(abovePos)) {
|
||||
return false
|
||||
}
|
||||
val mid = chunk.getBlockState(startPos)
|
||||
val above = chunk.getBlockState(abovePos)
|
||||
|
||||
val mid = chunk.getBlockState(startPos.setY(y))
|
||||
val above = chunk.getBlockState(startPos.setY(y + 1))
|
||||
|
||||
// Above must be non-solid
|
||||
if (!above.isPathfindable(PathComputationType.AIR)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Position must be non-air
|
||||
if (mid.isAir) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
return above.isPathfindable(PathComputationType.AIR) && mid.isAir
|
||||
}
|
||||
|
||||
fun constrainArea(area: SpawningZoneInput): SpawningZoneInput? {
|
||||
val basePos = BlockPos.MutableBlockPos(area.baseX, area.baseY, area.baseZ)
|
||||
val originalY = area.baseY
|
||||
|
||||
val (chunkX, chunkZ) = Pair(SectionPos.blockToSectionCoord(area.baseX), SectionPos.blockToSectionCoord(area.baseZ))
|
||||
val world = area.world
|
||||
val centerPos = area.getCenter().toBlockPos()
|
||||
|
||||
val (centerChunkX, centerChunkZ) = Pair(SectionPos.blockToSectionCoord(centerPos.x), SectionPos.blockToSectionCoord(centerPos.z))
|
||||
// if the chunk isn't loaded, we don't want to go further & we don't want the getChunk function below to load/create the chunk.
|
||||
if (!area.world.areEntitiesLoaded(ChunkPos.asLong(chunkX, chunkZ))) return null
|
||||
if (!world.areEntitiesLoaded(ChunkPos.asLong(centerChunkX, centerChunkZ))) return null
|
||||
|
||||
val chunk = area.world.getChunk(chunkX, chunkZ, ChunkStatus.FULL) ?: return null
|
||||
val chunk = world.getChunk(centerChunkX, centerChunkZ, ChunkStatus.FULL) ?: return null
|
||||
|
||||
var valid = isValidStartPoint(area.world, chunk, basePos)
|
||||
val probePos = centerPos.mutable()
|
||||
var validY = if (isValidStartPoint(world, chunk, probePos)) centerPos.y else null
|
||||
|
||||
if (!valid) {
|
||||
var offset = 1
|
||||
do {
|
||||
if (isValidStartPoint(area.world, chunk, basePos.setY(originalY + offset))) {
|
||||
valid = true
|
||||
basePos.y = originalY + offset
|
||||
if (validY == null) {
|
||||
for (offset in 1..Cobblemon.config.maxVerticalCorrectionBlocks) {
|
||||
if (isValidStartPoint(world, chunk, probePos.set(centerPos.x, centerPos.y + offset, centerPos.z))) {
|
||||
validY = centerPos.y + offset
|
||||
break
|
||||
} else if (isValidStartPoint(area.world, chunk, basePos.setY(originalY - offset))) {
|
||||
valid = true
|
||||
basePos.y = originalY - offset
|
||||
} else if (isValidStartPoint(world, chunk, probePos.set(centerPos.x, centerPos.y - offset, centerPos.z))) {
|
||||
validY = centerPos.y - offset
|
||||
break
|
||||
}
|
||||
offset++
|
||||
} while (offset <= Cobblemon.config.maxVerticalCorrectionBlocks)
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
val min = area.world.squeezeWithinBounds(basePos)
|
||||
val max = area.world.squeezeWithinBounds(basePos.move(area.length, area.height, area.width))
|
||||
if (area.world.isLoaded(min) && area.world.isLoaded(max) &&
|
||||
min.x < max.x && min.y < max.y && min.z < max.z
|
||||
) {
|
||||
return SpawningZoneInput(
|
||||
cause = area.cause,
|
||||
world = area.world,
|
||||
baseX = min.x,
|
||||
baseY = min.y,
|
||||
baseZ = min.z,
|
||||
length = max.x - min.x,
|
||||
height = max.y - min.y,
|
||||
width = max.z - min.z
|
||||
)
|
||||
}
|
||||
}
|
||||
val targetY = validY ?: return null
|
||||
val newBaseY = (targetY - (area.height / 2)).coerceIn(world.minBuildHeight, world.maxBuildHeight)
|
||||
val minPos = world.squeezeWithinBounds(BlockPos(area.baseX, newBaseY, area.baseZ))
|
||||
val maxPos = world.squeezeWithinBounds(BlockPos(area.baseX + area.length, newBaseY + area.height, area.baseZ + area.width))
|
||||
|
||||
val finalBox = AABB(
|
||||
minPos.x.toDouble(), minPos.y.toDouble(), minPos.z.toDouble(),
|
||||
maxPos.x.toDouble(), maxPos.y.toDouble(), maxPos.z.toDouble()
|
||||
)
|
||||
if (minPos.x < maxPos.x && minPos.y < maxPos.y && minPos.z < maxPos.z && world.isBoxLoaded(finalBox)) {
|
||||
return SpawningZoneInput(
|
||||
cause = area.cause,
|
||||
world = world,
|
||||
baseX = minPos.x,
|
||||
baseY = minPos.y,
|
||||
baseZ = minPos.z,
|
||||
length = maxPos.x - minPos.x,
|
||||
height = maxPos.y - minPos.y,
|
||||
width = maxPos.z - minPos.z
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ class ElementalType(
|
||||
val resourceLocation: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Cobblemon.MODID, "ui/types.png"),
|
||||
val showdownId: String = ShowdownIdentifiable.REGEX.replace(name.lowercase(), ""),
|
||||
val typeGem: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Cobblemon.MODID, showdownId + "_gem"),
|
||||
val order: Int = 1000
|
||||
) : ShowdownIdentifiable {
|
||||
|
||||
override fun showdownId(): String {
|
||||
|
||||
@@ -25,7 +25,8 @@ object ElementalTypes {
|
||||
hue = 0xE8E8DA,
|
||||
textureXMultiplier = 0,
|
||||
primaryColor = 0x8FA689,
|
||||
secondaryColor = 0xFFFBE2
|
||||
secondaryColor = 0xFFFBE2,
|
||||
order = 0
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -35,7 +36,8 @@ object ElementalTypes {
|
||||
hue = 0xFF6E21,
|
||||
textureXMultiplier = 1,
|
||||
primaryColor = 0xF92D2D,
|
||||
secondaryColor = 0xF99B1D
|
||||
secondaryColor = 0xF99B1D,
|
||||
order = 1
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -45,7 +47,8 @@ object ElementalTypes {
|
||||
hue = 0x3FA5FF,
|
||||
textureXMultiplier = 2,
|
||||
primaryColor = 0x377AF6,
|
||||
secondaryColor = 0x50DDF9
|
||||
secondaryColor = 0x50DDF9,
|
||||
order = 2
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -55,7 +58,8 @@ object ElementalTypes {
|
||||
hue = 0x62D14F,
|
||||
textureXMultiplier = 3,
|
||||
primaryColor = 0x0EAE95,
|
||||
secondaryColor = 0xC6F554
|
||||
secondaryColor = 0xC6F554,
|
||||
order = 3
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -65,7 +69,8 @@ object ElementalTypes {
|
||||
hue = 0xFFD314,
|
||||
textureXMultiplier = 4,
|
||||
primaryColor = 0xF7AB19,
|
||||
secondaryColor = 0xFFF257
|
||||
secondaryColor = 0xFFF257,
|
||||
order = 4
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -75,7 +80,8 @@ object ElementalTypes {
|
||||
hue = 0x54F2F2,
|
||||
textureXMultiplier = 5,
|
||||
primaryColor = 0x41BAED,
|
||||
secondaryColor = 0x91F4F9
|
||||
secondaryColor = 0x91F4F9,
|
||||
order = 5
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -85,7 +91,8 @@ object ElementalTypes {
|
||||
hue = 0xEF565D,
|
||||
textureXMultiplier = 6,
|
||||
primaryColor = 0xC8356B,
|
||||
secondaryColor = 0xF36A3F
|
||||
secondaryColor = 0xF36A3F,
|
||||
order = 6
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -95,7 +102,8 @@ object ElementalTypes {
|
||||
hue = 0xD651FF,
|
||||
textureXMultiplier = 7,
|
||||
primaryColor = 0x7B41DD,
|
||||
secondaryColor = 0xE568F4
|
||||
secondaryColor = 0xE568F4,
|
||||
order = 7
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -105,7 +113,8 @@ object ElementalTypes {
|
||||
hue = 0xF4A453,
|
||||
textureXMultiplier = 8,
|
||||
primaryColor = 0xE08835,
|
||||
secondaryColor = 0xFFD057
|
||||
secondaryColor = 0xFFD057,
|
||||
order = 8
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -115,7 +124,8 @@ object ElementalTypes {
|
||||
hue = 0xB8B2FF,
|
||||
textureXMultiplier = 9,
|
||||
primaryColor = 0x6083F6,
|
||||
secondaryColor = 0xD5CCFA
|
||||
secondaryColor = 0xD5CCFA,
|
||||
order = 9
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -125,7 +135,8 @@ object ElementalTypes {
|
||||
hue = 0xFF5E9E,
|
||||
textureXMultiplier = 10,
|
||||
primaryColor = 0xE553CE,
|
||||
secondaryColor = 0xF69DD5
|
||||
secondaryColor = 0xF69DD5,
|
||||
order = 10
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -135,7 +146,8 @@ object ElementalTypes {
|
||||
hue = 0xD3D319,
|
||||
textureXMultiplier = 11,
|
||||
primaryColor = 0x9FBE1B,
|
||||
secondaryColor = 0xE9FB5B
|
||||
secondaryColor = 0xE9FB5B,
|
||||
order = 11
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -145,7 +157,8 @@ object ElementalTypes {
|
||||
hue = 0xB7A16E,
|
||||
textureXMultiplier = 12,
|
||||
primaryColor = 0x9F814A,
|
||||
secondaryColor = 0xEECB7A
|
||||
secondaryColor = 0xEECB7A,
|
||||
order = 12
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -155,7 +168,8 @@ object ElementalTypes {
|
||||
hue = 0x9C80F7,
|
||||
textureXMultiplier = 13,
|
||||
primaryColor = 0x4F57DD,
|
||||
secondaryColor = 0xC182F1
|
||||
secondaryColor = 0xC182F1,
|
||||
order = 13
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -165,7 +179,8 @@ object ElementalTypes {
|
||||
hue = 0x7580FF,
|
||||
textureXMultiplier = 14,
|
||||
primaryColor = 0x4E68E2,
|
||||
secondaryColor = 0x47C7F7
|
||||
secondaryColor = 0x47C7F7,
|
||||
order = 14
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -175,7 +190,8 @@ object ElementalTypes {
|
||||
hue = 0x587DA0,
|
||||
textureXMultiplier = 15,
|
||||
primaryColor = 0x465B99,
|
||||
secondaryColor = 0x689EBF
|
||||
secondaryColor = 0x689EBF,
|
||||
order = 15
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -185,7 +201,8 @@ object ElementalTypes {
|
||||
hue = 0xABD1F4,
|
||||
textureXMultiplier = 16,
|
||||
primaryColor = 0x628CB9,
|
||||
secondaryColor = 0xD6F7F3
|
||||
secondaryColor = 0xD6F7F3,
|
||||
order = 16
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@@ -195,18 +212,21 @@ object ElementalTypes {
|
||||
hue = 0xFF7FE5,
|
||||
textureXMultiplier = 17,
|
||||
primaryColor = 0xE2678C,
|
||||
secondaryColor = 0xF5A7B7
|
||||
secondaryColor = 0xF5A7B7,
|
||||
order = 17
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun register(name: String, displayName: MutableComponent, hue: Int, textureXMultiplier: Int, primaryColor: Int, secondaryColor: Int): ElementalType {
|
||||
@JvmOverloads
|
||||
fun register(name: String, displayName: MutableComponent, hue: Int, textureXMultiplier: Int, primaryColor: Int, secondaryColor: Int, order: Int = 1000): ElementalType {
|
||||
return ElementalType(
|
||||
name = name,
|
||||
displayName = displayName,
|
||||
hue = hue,
|
||||
textureXMultiplier = textureXMultiplier,
|
||||
primaryColor = primaryColor,
|
||||
secondaryColor = secondaryColor
|
||||
secondaryColor = secondaryColor,
|
||||
order = order
|
||||
).also {
|
||||
allTypes.add(it)
|
||||
}
|
||||
|
||||
@@ -1,97 +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.vending
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.data.JsonDataRegistry
|
||||
import com.cobblemon.mod.common.api.molangui.ActionKey
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiActionContext
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiActionHandler
|
||||
import com.cobblemon.mod.common.api.molangui.ParameterValue
|
||||
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.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.server.packs.PackType
|
||||
|
||||
data class CatalogEntry(val item: ResourceLocation, val price: Int)
|
||||
|
||||
data class VendingMachineCatalog(val entries: List<CatalogEntry> = emptyList()) {
|
||||
|
||||
val pageCount: Int get() = if (entries.isEmpty()) 1 else (entries.size + PAGE_SIZE - 1) / PAGE_SIZE
|
||||
|
||||
fun clampPage(page: Int): Int = page.coerceIn(0, pageCount - 1)
|
||||
|
||||
fun pageParameters(page: Int): Map<String, ParameterValue> {
|
||||
val safePage = clampPage(page)
|
||||
val parameters = linkedMapOf<String, ParameterValue>(
|
||||
"page" to ParameterValue.NumberValue(safePage.toDouble()),
|
||||
"page_count" to ParameterValue.NumberValue(pageCount.toDouble()),
|
||||
"page_text" to ParameterValue.StringValue("${safePage + 1}/$pageCount")
|
||||
)
|
||||
for (row in 0 until PAGE_SIZE) {
|
||||
val entry = entries.getOrNull(safePage * PAGE_SIZE + row)
|
||||
parameters["row${row}_item"] = ParameterValue.StringValue(entry?.item?.toString() ?: "")
|
||||
parameters["row${row}_price"] = ParameterValue.StringValue(entry?.price?.toString() ?: "")
|
||||
}
|
||||
return parameters
|
||||
}
|
||||
|
||||
companion object { const val PAGE_SIZE = 3 }
|
||||
}
|
||||
|
||||
object VendingMachinePageHandler : MolangUiActionHandler {
|
||||
val ACTION = ActionKey("vending_page")
|
||||
|
||||
override fun handle(context: MolangUiActionContext) {
|
||||
val requested = (context.parameters["page"] as? ParameterValue.NumberValue)?.value ?: return
|
||||
val catalog = VendingMachineCatalogs.catalog(VendingMachineCatalogs.DEFAULT)
|
||||
context.session.updateParameters(catalog.pageParameters(requested.toInt()))
|
||||
}
|
||||
}
|
||||
|
||||
object VendingMachineCatalogs : JsonDataRegistry<VendingMachineCatalog> {
|
||||
override val id = cobblemonResource("vending_machine")
|
||||
override val type = PackType.SERVER_DATA
|
||||
override val observable = SimpleObservable<VendingMachineCatalogs>()
|
||||
override val typeToken: TypeToken<VendingMachineCatalog> = TypeToken.get(VendingMachineCatalog::class.java)
|
||||
override val resourcePath = "vending_machine"
|
||||
override val gson = GsonBuilder()
|
||||
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
|
||||
.create()
|
||||
|
||||
val DEFAULT = cobblemonResource("default")
|
||||
|
||||
private val catalogs = mutableMapOf<ResourceLocation, VendingMachineCatalog>()
|
||||
|
||||
fun catalog(id: ResourceLocation): VendingMachineCatalog = catalogs[id] ?: VendingMachineCatalog()
|
||||
|
||||
override fun reload(data: Map<ResourceLocation, VendingMachineCatalog>) {
|
||||
catalogs.clear()
|
||||
data.forEach { (id, catalog) -> catalogs[id] = validate(id, catalog) }
|
||||
Cobblemon.LOGGER.info("Loaded ${catalogs.size} vending machine catalogs")
|
||||
observable.emit(this)
|
||||
}
|
||||
|
||||
override fun sync(player: ServerPlayer) = Unit
|
||||
|
||||
private fun validate(id: ResourceLocation, catalog: VendingMachineCatalog): VendingMachineCatalog {
|
||||
val kept = catalog.entries.filter { entry ->
|
||||
val known = BuiltInRegistries.ITEM.containsKey(entry.item)
|
||||
if (!known) Cobblemon.LOGGER.error("Vending catalog '$id' drops unknown item '${entry.item}'")
|
||||
else if (entry.price <= 0) Cobblemon.LOGGER.error("Vending catalog '$id' drops '${entry.item}' with price ${entry.price}")
|
||||
known && entry.price > 0
|
||||
}
|
||||
return VendingMachineCatalog(kept)
|
||||
}
|
||||
}
|
||||
@@ -202,6 +202,7 @@ object BattleRegistry {
|
||||
): BattleStartResult {
|
||||
val battle = PokemonBattle(battleFormat, side1, side2)
|
||||
val start: () -> Unit = {
|
||||
battle.playerUUIDs.forEach { stopSpectating(it) }
|
||||
battleMap[battle.battleId] = battle
|
||||
startShowdown(battle)
|
||||
}
|
||||
@@ -224,6 +225,11 @@ object BattleRegistry {
|
||||
ShowdownService.service.endBattle(battle)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun stopSpectating(playerId: UUID) {
|
||||
battleMap.values.forEach { it.spectators.remove(playerId) }
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getBattle(id: UUID): PokemonBattle? {
|
||||
return battleMap[id]
|
||||
|
||||
@@ -83,7 +83,8 @@ open class BattlePokemon(
|
||||
"max_health" to Function { DoubleValue(maxHealth.toDouble()) },
|
||||
"ivs" to Function { effectedPokemon.ivs.struct },
|
||||
"nature" to Function { StringValue(effectedPokemon.nature.name.toString()) },
|
||||
"moveset" to Function { effectedPokemon.moveSet.toStruct() }
|
||||
"moveset" to Function { effectedPokemon.moveSet.toStruct() },
|
||||
"has_faced_opponents" to Function { DoubleValue(facedOpponents.isNotEmpty()) }
|
||||
))
|
||||
|
||||
val uuid: UUID
|
||||
|
||||
@@ -48,7 +48,8 @@ class DecorativeItemBlock(settings: Properties, val type: DecorativeItemBlockTyp
|
||||
NEVERMELT,
|
||||
SCOPE,
|
||||
WIDE,
|
||||
ZOOM;
|
||||
ZOOM,
|
||||
IZER;
|
||||
|
||||
override fun getSerializedName(): String = this.name.lowercase()
|
||||
|
||||
@@ -76,6 +77,7 @@ class DecorativeItemBlock(settings: Properties, val type: DecorativeItemBlockTyp
|
||||
val SCOPE_HITBOX = Shapes.box(0.625, 0.0, 0.4375, 0.9375, 0.125, 0.75)
|
||||
val WIDE_HITBOX = Shapes.box(0.625, 0.0, 0.125, 0.9375, 0.0625, 0.625)
|
||||
val ZOOM_HITBOX = Shapes.box(0.125, 0.0, 0.1875, 0.375, 0.1875, 0.5)
|
||||
val IZER_HITBOX = Shapes.box(0.25, 0.0, 0.25, 0.75, 0.375, 0.75)
|
||||
|
||||
fun getShape(type: DecorativeItemBlockType): VoxelShape {
|
||||
return when (type) {
|
||||
@@ -87,6 +89,7 @@ class DecorativeItemBlock(settings: Properties, val type: DecorativeItemBlockTyp
|
||||
DecorativeItemBlockType.SCOPE -> SCOPE_HITBOX
|
||||
DecorativeItemBlockType.WIDE -> WIDE_HITBOX
|
||||
DecorativeItemBlockType.ZOOM -> ZOOM_HITBOX
|
||||
DecorativeItemBlockType.IZER -> IZER_HITBOX
|
||||
DecorativeItemBlockType.FLATTENED_10x4 -> SMOOSHED4_HITBOX
|
||||
DecorativeItemBlockType.FLATTENED_8x4 -> FLAT4_HITBOX
|
||||
DecorativeItemBlockType.FLATTENED_8x6 -> FLAT6_HITBOX
|
||||
|
||||
@@ -103,15 +103,15 @@ class MintBlock(private val mintType: MintType, settings: Properties) : CropBloc
|
||||
GREEN -> CobblemonItems.GREEN_MINT_SEEDS
|
||||
WHITE -> CobblemonItems.WHITE_MINT_SEEDS
|
||||
}
|
||||
|
||||
fun getLeaf(): MintLeafItem = when (this) {
|
||||
RED -> CobblemonItems.RED_MINT_LEAF
|
||||
BLUE -> CobblemonItems.BLUE_MINT_LEAF
|
||||
CYAN -> CobblemonItems.CYAN_MINT_LEAF
|
||||
PINK -> CobblemonItems.PINK_MINT_LEAF
|
||||
GREEN -> CobblemonItems.GREEN_MINT_LEAF
|
||||
WHITE -> CobblemonItems.WHITE_MINT_LEAF
|
||||
}
|
||||
// These don't seem to get called to anywhere besides the direct item, and since I'm trying to do funny stuff there, just commenting this out
|
||||
// fun getLeaf(): MintLeafItem = when (this) {
|
||||
// RED -> CobblemonItems.RED_MINT_LEAF
|
||||
// BLUE -> CobblemonItems.BLUE_MINT_LEAF
|
||||
// CYAN -> CobblemonItems.CYAN_MINT_LEAF
|
||||
// PINK -> CobblemonItems.PINK_MINT_LEAF
|
||||
// GREEN -> CobblemonItems.GREEN_MINT_LEAF
|
||||
// WHITE -> CobblemonItems.WHITE_MINT_LEAF
|
||||
// }
|
||||
|
||||
fun getCropBlock(): MintBlock = when (this) {
|
||||
RED -> CobblemonBlocks.RED_MINT
|
||||
|
||||
@@ -34,7 +34,9 @@ import net.minecraft.world.item.alchemy.Potions
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.level.Level
|
||||
import net.minecraft.world.level.LevelAccessor
|
||||
import net.minecraft.world.level.LevelReader
|
||||
import net.minecraft.world.level.block.Block
|
||||
import net.minecraft.world.level.block.BonemealableBlock
|
||||
import net.minecraft.world.level.block.DispenserBlock
|
||||
import net.minecraft.world.level.block.LeavesBlock
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
@@ -52,7 +54,7 @@ import net.minecraft.world.phys.shapes.EntityCollisionContext
|
||||
import net.minecraft.world.phys.shapes.Shapes
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
|
||||
class SaccharineLeafBlock(settings: Properties) : LeavesBlock(settings) {
|
||||
class SaccharineLeafBlock(settings: Properties) : LeavesBlock(settings), BonemealableBlock {
|
||||
companion object {
|
||||
val AGE: IntegerProperty = BlockStateProperties.AGE_2
|
||||
val DISTANCE: IntegerProperty = BlockStateProperties.DISTANCE
|
||||
@@ -181,6 +183,15 @@ class SaccharineLeafBlock(settings: Properties) : LeavesBlock(settings) {
|
||||
super.randomTick(state, world, pos, random)
|
||||
}
|
||||
|
||||
override fun isValidBonemealTarget(level: LevelReader, pos: BlockPos, state: BlockState): Boolean =
|
||||
state.getValue(AGE) < MAX_AGE && !state.getValue(BlockStateProperties.WATERLOGGED)
|
||||
|
||||
override fun isBonemealSuccess(level: Level, random: RandomSource, pos: BlockPos, state: BlockState) = true
|
||||
|
||||
override fun performBonemeal(level: ServerLevel, random: RandomSource, pos: BlockPos, state: BlockState) {
|
||||
level.setBlock(pos, changeAge(state, 1), UPDATE_CLIENTS)
|
||||
}
|
||||
|
||||
override fun placeLiquid(level: LevelAccessor, pos: BlockPos, state: BlockState, fluidState: FluidState): Boolean {
|
||||
if (!state.getValue(BlockStateProperties.WATERLOGGED) && fluidState.type === Fluids.WATER) {
|
||||
val hasHoney = state.getValue(AGE) > 0
|
||||
|
||||
@@ -45,7 +45,9 @@ class StackableItemBlock(settings: Properties, val type: StackableItemBlockType)
|
||||
IRON_BALL,
|
||||
THROAT_SPRAY,
|
||||
UPGRADE,
|
||||
DICE;
|
||||
DICE,
|
||||
GLASS_BOTTLE,
|
||||
EXTENDER;
|
||||
|
||||
override fun getSerializedName(): String = this.name.lowercase()
|
||||
|
||||
@@ -133,6 +135,18 @@ class StackableItemBlock(settings: Properties, val type: StackableItemBlockType)
|
||||
Shapes.box(0.125, 0.0, 0.625, 0.5625, 0.1875, 0.875),
|
||||
Shapes.box(0.125, 0.0, 0.4375, 0.5625, 0.1875, 0.875),
|
||||
Shapes.box(0.125, 0.0, 0.4375, 0.5625, 0.375, 0.875)
|
||||
),
|
||||
StackableItemBlockType.GLASS_BOTTLE to listOf(
|
||||
Shapes.box(0.375, 0.0, 0.375, 0.625, 0.375, 0.625),
|
||||
Shapes.box(0.1875, 0.0, 0.1875, 0.8125, 0.375, 0.6875),
|
||||
Shapes.box(0.125, 0.0, 0.125, 0.8125, 0.375, 0.875),
|
||||
Shapes.box(0.125, 0.0, 0.125, 0.875, 0.375, 0.875)
|
||||
),
|
||||
StackableItemBlockType.EXTENDER to listOf(
|
||||
Shapes.box(0.25, 0.0, 0.375, 0.625, 0.5625, 0.625),
|
||||
Shapes.box(0.125, 0.0, 0.25, 0.875, 0.5625, 0.75),
|
||||
Shapes.box(0.0625, 0.0, 0.0625, 0.875, 0.5625, 0.875),
|
||||
Shapes.box(0.0625, 0.0, 0.0625, 0.9375, 0.5625, 0.9375)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.util.RandomSource
|
||||
import net.minecraft.util.StringRepresentable
|
||||
import net.minecraft.world.item.BlockItem
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.component.BlockItemStateProperties
|
||||
@@ -33,8 +34,8 @@ import net.minecraft.world.level.block.Mirror
|
||||
import net.minecraft.world.level.block.Rotation
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.StateDefinition
|
||||
import net.minecraft.world.level.block.state.properties.BooleanProperty
|
||||
import net.minecraft.world.level.block.state.properties.DirectionProperty
|
||||
import net.minecraft.world.level.block.state.properties.EnumProperty
|
||||
import net.minecraft.world.level.block.state.properties.IntegerProperty
|
||||
import net.minecraft.world.level.material.PushReaction
|
||||
import net.minecraft.world.level.storage.loot.LootParams
|
||||
@@ -61,22 +62,27 @@ class TypeGemClusterBlock(
|
||||
}
|
||||
val gemToClusterMap: MutableMap<Block, TypeGemClusterBlock> = mutableMapOf()
|
||||
|
||||
val SHOULD_GROW: BooleanProperty = BooleanProperty.create("should_grow")
|
||||
val FACING: DirectionProperty = DirectionalBlock.FACING
|
||||
val STAGE: IntegerProperty = IntegerProperty.create("stage", 0, 3)
|
||||
val STUNTED: BooleanProperty = BooleanProperty.create("stunted")
|
||||
val GROWTH_TYPE: EnumProperty<GrowthType> = EnumProperty.create("growth_type", GrowthType::class.java)
|
||||
|
||||
fun clusterFromGemBlock(gemBlock: Block): TypeGemClusterBlock? {
|
||||
return gemToClusterMap[gemBlock]
|
||||
}
|
||||
}
|
||||
|
||||
enum class GrowthType(private val label: String, val chance: Float): StringRepresentable {
|
||||
DECORATION("none", 0.25f),
|
||||
FULL("full", 0.5f),
|
||||
STUNTED("stunted", 0.25f);
|
||||
override fun getSerializedName(): String = label
|
||||
}
|
||||
|
||||
init {
|
||||
registerDefaultState(stateDefinition.any()
|
||||
.setValue(FACING, Direction.UP)
|
||||
.setValue(SHOULD_GROW, true)
|
||||
.setValue(STAGE, 0)
|
||||
.setValue(STUNTED, false)
|
||||
.setValue(GROWTH_TYPE, GrowthType.FULL)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -89,15 +95,19 @@ class TypeGemClusterBlock(
|
||||
|
||||
return defaultBlockState()
|
||||
.setValue(FACING, facing)
|
||||
.setValue(SHOULD_GROW, shouldGrow)
|
||||
.setValue(STAGE, 0)
|
||||
.setValue(STUNTED, false)
|
||||
.setValue(GROWTH_TYPE, if(shouldGrow) GrowthType.FULL else GrowthType.DECORATION)
|
||||
}
|
||||
|
||||
public override fun isRandomlyTicking(state: BlockState): Boolean = state.getValue(SHOULD_GROW)
|
||||
public override fun isRandomlyTicking(state: BlockState): Boolean {
|
||||
// if growth type is either stunted or full, we need to tick
|
||||
return state.getValue(GROWTH_TYPE) != GrowthType.DECORATION
|
||||
}
|
||||
|
||||
override fun randomTick(state: BlockState, level: ServerLevel, pos: BlockPos, random: RandomSource) {
|
||||
/*
|
||||
advanceGrowth(state, level, pos, random)
|
||||
*/
|
||||
}
|
||||
|
||||
fun advanceGrowth(state: BlockState, level: LevelAccessor, pos: BlockPos, random: RandomSource) {
|
||||
@@ -111,15 +121,8 @@ class TypeGemClusterBlock(
|
||||
return
|
||||
}
|
||||
|
||||
// At STAGE_3
|
||||
val isStunted = state.getValue(STUNTED)
|
||||
val facing = state.getValue(FACING)
|
||||
|
||||
if (isStunted) {
|
||||
level.setBlock(pos, state.setValue(SHOULD_GROW, false), UPDATE_ALL)
|
||||
return
|
||||
}
|
||||
|
||||
// Check neighboring gem blocks
|
||||
val hasConflict = Direction.entries
|
||||
.filter { it != facing.opposite }
|
||||
@@ -132,61 +135,36 @@ class TypeGemClusterBlock(
|
||||
if (hasConflict) {
|
||||
level.setBlock(
|
||||
pos,
|
||||
state.setValue(STUNTED, true).setValue(SHOULD_GROW, false),
|
||||
state.setValue(GROWTH_TYPE, GrowthType.STUNTED),
|
||||
UPDATE_ALL
|
||||
)
|
||||
} else {
|
||||
}
|
||||
var nextState = nextStage.defaultBlockState()
|
||||
if (nextState.hasProperty(FACING)) {
|
||||
nextState = nextState.setValue(FACING, facing)
|
||||
}
|
||||
level.setBlock(pos, nextState, UPDATE_ALL)
|
||||
attachDecorativeClusters(level, pos, random)
|
||||
}
|
||||
if (isGem(nextState)) {
|
||||
TypeGemCoreBlock.addClustersToBlock(state, pos, level, random)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isGem(state: BlockState): Boolean {
|
||||
return state.`is`(CobblemonBlockTags.TYPE_GEM_BLOCKS)
|
||||
}
|
||||
|
||||
private fun attachDecorativeClusters(level: LevelAccessor, gemPos: BlockPos, random: RandomSource) {
|
||||
val openDirections = Direction.entries.filter { direction ->
|
||||
level.getBlockState(gemPos.relative(direction)).isAir
|
||||
}.toMutableList()
|
||||
|
||||
if (openDirections.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val minClusters = maxOf(1, (openDirections.size * 0.75f).toInt())
|
||||
val clusterCount = random.nextInt(minClusters, openDirections.size + 1)
|
||||
|
||||
repeat(clusterCount) {
|
||||
if (openDirections.isEmpty()) return@repeat
|
||||
|
||||
val index = random.nextInt(openDirections.size)
|
||||
val direction = openDirections.removeAt(index)
|
||||
val clusterPos = gemPos.relative(direction)
|
||||
val stage = random.nextInt(0, 4)
|
||||
|
||||
val clusterState = defaultBlockState()
|
||||
.setValue(FACING, direction)
|
||||
.setValue(STAGE, stage)
|
||||
.setValue(SHOULD_GROW, false) // so we can reduce lag... hopefully
|
||||
.setValue(STUNTED, false)
|
||||
|
||||
level.setBlock(clusterPos, clusterState, UPDATE_ALL)
|
||||
}
|
||||
}
|
||||
|
||||
fun gemClusterCanGrow(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
|
||||
if (!state.getValue(SHOULD_GROW)) return false
|
||||
if (state.getValue(GROWTH_TYPE) == GrowthType.DECORATION) return false
|
||||
if (state.getValue(GROWTH_TYPE) == GrowthType.STUNTED
|
||||
&& state.getValue(STAGE) == 3) return false
|
||||
|
||||
val supportPos = pos.relative(state.getValue(FACING).opposite)
|
||||
val supportBlock = level.getBlockState(supportPos).block
|
||||
return CobblemonBlocks.typeGemBlocks().containsValue(supportBlock) || (supportBlock == CobblemonBlocks.TYPE_GEM_CORE) // Cannot grow if not on any gem blocks
|
||||
}
|
||||
|
||||
|
||||
override fun getDrops(state: BlockState, params: LootParams.Builder): List<ItemStack> {
|
||||
val drops = super.getDrops(state, params)
|
||||
|
||||
@@ -200,8 +178,7 @@ class TypeGemClusterBlock(
|
||||
DataComponents.BLOCK_STATE,
|
||||
BlockItemStateProperties.EMPTY
|
||||
.with(STAGE, state.getValue(STAGE))
|
||||
.with(SHOULD_GROW, false)
|
||||
.with(STUNTED, false)
|
||||
.with(GROWTH_TYPE, GrowthType.DECORATION)
|
||||
)
|
||||
stack.set(
|
||||
DataComponents.CUSTOM_MODEL_DATA,
|
||||
@@ -272,7 +249,7 @@ class TypeGemClusterBlock(
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
super.createBlockStateDefinition(builder)
|
||||
builder.add(FACING, SHOULD_GROW, STAGE, STUNTED)
|
||||
builder.add(FACING, STAGE, GROWTH_TYPE)
|
||||
}
|
||||
|
||||
override fun getCloneItemStack(level: LevelReader, pos: BlockPos, state: BlockState): ItemStack {
|
||||
@@ -283,8 +260,7 @@ class TypeGemClusterBlock(
|
||||
DataComponents.BLOCK_STATE,
|
||||
BlockItemStateProperties.EMPTY
|
||||
.with(STAGE, state.getValue(STAGE))
|
||||
.with(SHOULD_GROW, false)
|
||||
.with(STUNTED, false)
|
||||
.with(GROWTH_TYPE, GrowthType.DECORATION)
|
||||
)
|
||||
stack.set(
|
||||
DataComponents.CUSTOM_MODEL_DATA,
|
||||
|
||||
@@ -11,8 +11,10 @@ package com.cobblemon.mod.common.block
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.CobblemonBlocks
|
||||
import com.cobblemon.mod.common.api.tags.CobblemonBlockTags
|
||||
import com.cobblemon.mod.common.block.TypeGemClusterBlock.Companion.SHOULD_GROW
|
||||
import com.cobblemon.mod.common.block.TypeGemClusterBlock.Companion.STUNTED
|
||||
import com.cobblemon.mod.common.block.TypeGemClusterBlock.Companion.FACING
|
||||
import com.cobblemon.mod.common.block.TypeGemClusterBlock.Companion.GROWTH_TYPE
|
||||
import com.cobblemon.mod.common.block.TypeGemClusterBlock.Companion.STAGE
|
||||
import com.cobblemon.mod.common.block.TypeGemClusterBlock.GrowthType
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
@@ -21,8 +23,8 @@ import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.util.RandomSource
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.level.LevelAccessor
|
||||
import net.minecraft.world.level.WorldGenLevel
|
||||
import net.minecraft.world.level.block.Block
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import net.minecraft.world.level.block.DirectionalBlock
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import java.util.*
|
||||
@@ -30,13 +32,6 @@ import java.util.*
|
||||
class TypeGemCoreBlock(properties: Properties) : Block(properties) {
|
||||
|
||||
companion object {
|
||||
const val MAX_CONNECTED_GEMS = 7
|
||||
const val MIN_DISTANCE_BETWEEN_GEMS = 1
|
||||
const val CONTINUATION_CHANCE = 0.8f
|
||||
|
||||
// Upper safety check to prevent infinite loops
|
||||
const val MAX_FORCE_GROW_ATTEMPTS = 64
|
||||
|
||||
val BLOCK_TO_CLUSTER: Map<ResourceLocation, Block> by lazy {
|
||||
mapOf(
|
||||
ResourceLocation.parse("cobblemon:normal_gem_block") to CobblemonBlocks.TYPE_GEM_CLUSTER_NORMAL,
|
||||
@@ -60,28 +55,6 @@ class TypeGemCoreBlock(properties: Properties) : Block(properties) {
|
||||
)
|
||||
}
|
||||
|
||||
fun setStuntState(level: LevelAccessor, gemBlocks: List<Pair<BlockState, BlockPos>>, stunted: Boolean) {
|
||||
for ((_, gemPos) in gemBlocks) {
|
||||
for (dir in Direction.entries) {
|
||||
val neighborPos = gemPos.relative(dir)
|
||||
val neighborState = level.getBlockState(neighborPos)
|
||||
|
||||
if (neighborState.block is TypeGemClusterBlock) {
|
||||
if (neighborState.hasProperty(STUNTED) && neighborState.getValue(STUNTED) != stunted) {
|
||||
|
||||
var updatedState = neighborState.setValue(STUNTED, stunted)
|
||||
|
||||
if (updatedState.hasProperty(SHOULD_GROW)) {
|
||||
updatedState = updatedState.setValue(SHOULD_GROW, !stunted)
|
||||
}
|
||||
|
||||
level.setBlock(neighborPos, updatedState, 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getConnectedGemBlocks(level: BlockGetter, pos: BlockPos): List<Pair<BlockState, BlockPos>> {
|
||||
val connectedGems = mutableListOf<Pair<BlockState, BlockPos>>()
|
||||
val visited = mutableSetOf<BlockPos>()
|
||||
@@ -107,119 +80,159 @@ class TypeGemCoreBlock(properties: Properties) : Block(properties) {
|
||||
}
|
||||
return connectedGems
|
||||
}
|
||||
|
||||
fun addClustersToBlock(state: BlockState, pos: BlockPos, level: LevelAccessor, random: RandomSource) {
|
||||
Direction.entries.forEach {
|
||||
if (random.nextFloat() < Cobblemon.config.typeGemGrowthChance) {
|
||||
val faceState = level.getBlockState(pos.relative(it))
|
||||
val randomGrowthType = decideClusterGrowthType(level, pos)
|
||||
val newState = BLOCK_TO_CLUSTER[BuiltInRegistries.BLOCK.getKey(state.block)]?.defaultBlockState()
|
||||
?.setValue(DirectionalBlock.FACING, it)
|
||||
?.setValue(GROWTH_TYPE, randomGrowthType)
|
||||
?: return
|
||||
|
||||
if (faceState.`is`(Blocks.AIR)) {
|
||||
level.setBlock(pos.relative(it), newState, 3)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun decideClusterGrowthType(level: LevelAccessor, pos: BlockPos, random: RandomSource = RandomSource.create()): GrowthType {
|
||||
val growthTypes = if (getConnectedGemBlocks(level, pos).size > Cobblemon.config.typeGemMaxConnected) {
|
||||
listOf(GrowthType.DECORATION, GrowthType.STUNTED)
|
||||
} else {
|
||||
GrowthType.entries
|
||||
}
|
||||
val totalChance = growthTypes.sumOf { it.chance.toDouble() }
|
||||
var roll = random.nextDouble() * totalChance
|
||||
|
||||
for (type in growthTypes) {
|
||||
roll -= type.chance.toDouble()
|
||||
if (roll <= 0.0) {
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
return GrowthType.DECORATION
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConnectedClusters(level: LevelAccessor, pos: BlockPos): List<Pair<BlockState, BlockPos>> {
|
||||
val gemBlocks = getConnectedGemBlocks(level, pos)
|
||||
val clusters = mutableListOf<Pair<BlockState, BlockPos>>()
|
||||
val visited = mutableSetOf<BlockPos>()
|
||||
|
||||
for ((gemState, gemPos) in gemBlocks) {
|
||||
val expectedCluster = BLOCK_TO_CLUSTER[BuiltInRegistries.BLOCK.getKey(gemState.block)] ?: continue
|
||||
|
||||
for (direction in Direction.entries) {
|
||||
val clusterPos = gemPos.relative(direction)
|
||||
if (!visited.add(clusterPos)) continue
|
||||
|
||||
val clusterState = level.getBlockState(clusterPos)
|
||||
if (clusterState.block == expectedCluster) {
|
||||
clusters += clusterState to clusterPos
|
||||
}
|
||||
}
|
||||
}
|
||||
return clusters
|
||||
}
|
||||
|
||||
override fun randomTick(state: BlockState, level: ServerLevel, pos: BlockPos, random: RandomSource) {
|
||||
grow(level, pos, random)
|
||||
}
|
||||
// should we tick at all?
|
||||
if (random.nextFloat() >= Cobblemon.config.typeGemTickChance) {
|
||||
return
|
||||
}
|
||||
val gemCount = getConnectedGemBlocks(level, pos).size
|
||||
|
||||
fun forceGrow(level: WorldGenLevel, pos: BlockPos, random: RandomSource, percentage: Float) {
|
||||
val desiredLength = MAX_CONNECTED_GEMS * percentage
|
||||
|
||||
repeat(MAX_FORCE_GROW_ATTEMPTS) {
|
||||
val (grown, clusterSize) = grow(level, pos, random, true)
|
||||
if (!grown || clusterSize >= desiredLength) return
|
||||
// if growth has stopped fully but min growth hasn't been reached, randomly set a stopped bud to GROWTH_TYPE full
|
||||
val clusters = getConnectedClusters(level, pos)
|
||||
val fullClusterCount = clusters.count { (clusterState, _) ->
|
||||
clusterState.getValue(GROWTH_TYPE) == GrowthType.FULL
|
||||
}
|
||||
|
||||
Cobblemon.LOGGER.warn(
|
||||
"Forced type gem growth at {} stopped after {} attempts without reaching its target size; keeping the partial formation.",
|
||||
pos,
|
||||
MAX_FORCE_GROW_ATTEMPTS
|
||||
)
|
||||
}
|
||||
|
||||
private fun grow(
|
||||
level: WorldGenLevel,
|
||||
pos: BlockPos,
|
||||
random: RandomSource,
|
||||
forced: Boolean = false
|
||||
): Pair<Boolean, Int> {
|
||||
val connectedGems = getConnectedGemBlocks(level, pos) // maybe we can store this and access it for better performance? check for changes in list rather than checking each time grow is called? idk
|
||||
val gemCount = connectedGems.size
|
||||
val overLimit = gemCount >= MAX_CONNECTED_GEMS
|
||||
|
||||
if (overLimit) {
|
||||
// if there is no open air pockets to grow any new clusters then exit early
|
||||
if (!hasBreathingRoom(level, connectedGems)) {
|
||||
return Pair(false, gemCount)
|
||||
}
|
||||
// max type gem block count met, tell clusters to grow but not turn into blocks
|
||||
setStuntState(level, connectedGems, true)
|
||||
//return Pair(false, gemCount)
|
||||
} else {
|
||||
// make sure connected clusters know they can grow and turn into blocks
|
||||
setStuntState(level, connectedGems, false)
|
||||
}
|
||||
|
||||
val javaRandom = java.util.Random(random.nextLong())
|
||||
|
||||
for ((gemState, gemPos) in connectedGems.shuffled(javaRandom)) {
|
||||
val registryKey = BuiltInRegistries.BLOCK.getKey(gemState.block)
|
||||
val clusterBlock = BLOCK_TO_CLUSTER[registryKey] ?: continue // If there is no block to grow, skip the block
|
||||
|
||||
for (dir in Direction.entries.shuffled(javaRandom)) {
|
||||
val targetPos = gemPos.relative(dir)
|
||||
val targetState = level.getBlockState(targetPos)
|
||||
|
||||
if (!targetState.isAir) continue
|
||||
if (!isPositionValidForGrowth(level, targetPos)) continue
|
||||
|
||||
// Place gem cluster
|
||||
val placeState = clusterBlock.defaultBlockState()
|
||||
.setValue(DirectionalBlock.FACING, dir)
|
||||
.setValue(TypeGemClusterBlock.STAGE, 0)
|
||||
.setValue(SHOULD_GROW, true)
|
||||
|
||||
level.setBlock(targetPos, placeState, UPDATE_ALL)
|
||||
|
||||
// During world gen, the region silently discards writes into neighboring chunks that are already fully generated.
|
||||
// Through ImposterProtoChunk with writes set to disabled. Checks if the block is still air after the write and skips if so as it can't be filled.
|
||||
// Preventing infinite loops.
|
||||
if (!level.getBlockState(targetPos).`is`(clusterBlock)) continue
|
||||
|
||||
if (forced) {
|
||||
forceAdvanceClusterGrowth(level, targetPos, random)
|
||||
}
|
||||
|
||||
val updatedGemCount = if (forced) getConnectedGemBlocks(level, pos).size else connectedGems.size
|
||||
return Pair(true, updatedGemCount)
|
||||
if (fullClusterCount == 0) {
|
||||
if (gemCount < Cobblemon.config.typeGemMinGrowth && clusters.isNotEmpty()) {
|
||||
println("Force set a cluster to FULL at size $gemCount")
|
||||
val randomCluster = clusters.random()
|
||||
level.setBlockAndUpdate(randomCluster.second, randomCluster.first.setValue(GROWTH_TYPE, GrowthType.FULL))
|
||||
}
|
||||
}
|
||||
|
||||
// println("[TypeGemCoreBlock] No valid spot found for gem placement.")
|
||||
return Pair(false, connectedGems.size)
|
||||
|
||||
expand(level, pos, random)
|
||||
}
|
||||
|
||||
private fun forceAdvanceClusterGrowth(level: WorldGenLevel, clusterPos: BlockPos, random: RandomSource) {
|
||||
repeat(5) {
|
||||
val clusterState = level.getBlockState(clusterPos)
|
||||
val clusterBlock = clusterState.block as? TypeGemClusterBlock ?: return
|
||||
clusterBlock.advanceGrowth(clusterState, level, clusterPos, random)
|
||||
fun expand(level: LevelAccessor, pos: BlockPos, random: RandomSource, forceGrow: Boolean = false) {
|
||||
getConnectedGemBlocks(level, pos).forEach {
|
||||
val state = it.first
|
||||
val pos = it.second
|
||||
addClustersToBlock(state, pos, level, random)
|
||||
growClusters(pos, level, random, forceGrow)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasBreathingRoom(level: WorldGenLevel, gems: List<Pair<BlockState, BlockPos>>): Boolean {
|
||||
for ((_, gemPos) in gems) {
|
||||
for (dir in Direction.entries) {
|
||||
if (level.getBlockState(gemPos.relative(dir)).isAir) {
|
||||
return true
|
||||
}
|
||||
private fun growClusters(pos: BlockPos, level: LevelAccessor, random: RandomSource, forceGrow: Boolean) {
|
||||
Direction.entries.forEach { direction ->
|
||||
val relativePos = pos.relative(direction)
|
||||
val relativeState = level.getBlockState(relativePos)
|
||||
val clusterBlock = relativeState.block as? TypeGemClusterBlock ?: return@forEach
|
||||
|
||||
if ((relativeState.getValue(DirectionalBlock.FACING) == direction &&
|
||||
random.nextFloat() < Cobblemon.config.typeGemGrowthChance) || forceGrow
|
||||
) {
|
||||
clusterBlock.advanceGrowth(relativeState, level, relativePos, random)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun isPositionValidForGrowth(level: WorldGenLevel, pos: BlockPos): Boolean {
|
||||
return Direction.entries.none { dir ->
|
||||
val nearby = pos.relative(dir)
|
||||
isGem(level.getBlockState(nearby)) &&
|
||||
pos.distManhattan(nearby) < MIN_DISTANCE_BETWEEN_GEMS
|
||||
private fun addInitialClusters(pos: BlockPos, level: ServerLevel, clusterType: TypeGemClusterBlock) {
|
||||
Direction.entries.forEach {
|
||||
val faceState = level.getBlockState(pos.relative(it))
|
||||
|
||||
if (faceState.`is`(Blocks.AIR)) {
|
||||
level.setBlockAndUpdate(pos.relative(it), clusterType.defaultBlockState().setValue(
|
||||
DirectionalBlock.FACING, it)
|
||||
.setValue(GROWTH_TYPE, GrowthType.FULL)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isGem(state: BlockState): Boolean {
|
||||
return state.`is`(CobblemonBlockTags.TYPE_GEM_BLOCKS)
|
||||
}
|
||||
|
||||
override fun isRandomlyTicking(state: BlockState): Boolean = true
|
||||
}
|
||||
|
||||
private fun attachDecorativeClusters(level: LevelAccessor, gemPos: BlockPos, random: RandomSource) {
|
||||
val openDirections = Direction.entries.filter { direction ->
|
||||
level.getBlockState(gemPos.relative(direction)).isAir
|
||||
}.toMutableList()
|
||||
|
||||
if (openDirections.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val gemState = level.getBlockState(gemPos)
|
||||
val clusterBlock = BLOCK_TO_CLUSTER[BuiltInRegistries.BLOCK.getKey(gemState.block)] ?: return
|
||||
|
||||
val minClusters = maxOf(1, (openDirections.size * 0.75f).toInt())
|
||||
val clusterCount = random.nextInt(minClusters, openDirections.size + 1)
|
||||
|
||||
repeat(clusterCount) {
|
||||
if (openDirections.isEmpty()) return@repeat
|
||||
|
||||
val index = random.nextInt(openDirections.size)
|
||||
val direction = openDirections.removeAt(index)
|
||||
val clusterPos = gemPos.relative(direction)
|
||||
val stage = random.nextInt(0, 4)
|
||||
|
||||
val clusterState = clusterBlock.defaultBlockState()
|
||||
.setValue(FACING, direction)
|
||||
.setValue(STAGE, stage)
|
||||
.setValue(GROWTH_TYPE, GrowthType.DECORATION)
|
||||
|
||||
level.setBlock(clusterPos, clusterState, UPDATE_ALL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,61 +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.block
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiOpenRequest
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiService
|
||||
import com.cobblemon.mod.common.api.vending.VendingMachineCatalogs
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.InteractionResult
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.item.context.BlockPlaceContext
|
||||
import net.minecraft.world.level.Level
|
||||
import net.minecraft.world.level.block.Block
|
||||
import net.minecraft.world.level.block.Rotation
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.StateDefinition
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_FACING
|
||||
import net.minecraft.world.phys.BlockHitResult
|
||||
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
class VendingMachineBlock(settings: Properties) : Block(settings) {
|
||||
init {
|
||||
registerDefaultState(stateDefinition.any().setValue(HORIZONTAL_FACING, Direction.NORTH))
|
||||
}
|
||||
|
||||
override fun getStateForPlacement(context: BlockPlaceContext): BlockState =
|
||||
defaultBlockState().setValue(HORIZONTAL_FACING, context.horizontalDirection.opposite)
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
builder.add(HORIZONTAL_FACING)
|
||||
}
|
||||
|
||||
override fun rotate(state: BlockState, rotation: Rotation): BlockState =
|
||||
state.setValue(HORIZONTAL_FACING, rotation.rotate(state.getValue(HORIZONTAL_FACING)))
|
||||
|
||||
override fun useWithoutItem(
|
||||
state: BlockState,
|
||||
world: Level,
|
||||
pos: BlockPos,
|
||||
player: Player,
|
||||
hitResult: BlockHitResult
|
||||
): InteractionResult {
|
||||
if (player !is ServerPlayer) return InteractionResult.SUCCESS
|
||||
val catalog = VendingMachineCatalogs.catalog(VendingMachineCatalogs.DEFAULT)
|
||||
MolangUiService.open(player, MolangUiOpenRequest(UI_ID, catalog.pageParameters(0), pos))
|
||||
return InteractionResult.SUCCESS
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val UI_ID = cobblemonResource("vending_machine")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.block.decor
|
||||
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.world.item.context.BlockPlaceContext
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.level.block.Block
|
||||
import net.minecraft.world.level.block.SimpleWaterloggedBlock
|
||||
import net.minecraft.world.level.block.TallFlowerBlock
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.StateDefinition
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED
|
||||
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf
|
||||
import net.minecraft.world.level.material.FluidState
|
||||
import net.minecraft.world.level.material.Fluids
|
||||
import net.minecraft.world.phys.shapes.CollisionContext
|
||||
import net.minecraft.world.phys.shapes.Shapes
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
|
||||
|
||||
class LobailaFlowerBlock(properties: Properties?) : TallFlowerBlock(properties), SimpleWaterloggedBlock {
|
||||
|
||||
init {
|
||||
registerDefaultState(stateDefinition.any()
|
||||
.setValue(HALF, DoubleBlockHalf.LOWER)
|
||||
.setValue(WATERLOGGED, false))
|
||||
}
|
||||
|
||||
override fun getFluidState(blockState: BlockState): FluidState? {
|
||||
return if (blockState.getValue(WATERLOGGED)) Fluids.WATER.getSource(false) else super.getFluidState(blockState);
|
||||
}
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
builder.add(HALF, WATERLOGGED)
|
||||
}
|
||||
|
||||
override fun getStateForPlacement(context: BlockPlaceContext): BlockState? {
|
||||
val fluidState = context.level.getFluidState(context.clickedPos)
|
||||
return this.defaultBlockState()
|
||||
.setValue(
|
||||
WATERLOGGED,
|
||||
fluidState.type === Fluids.WATER
|
||||
)
|
||||
}
|
||||
|
||||
override fun getShape(state: BlockState, world: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape = AABB
|
||||
|
||||
companion object {
|
||||
private val AABB = Shapes.box(0.125, 0.0, 0.125, 0.875, 1.0, 0.875)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.block.decor
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonBlockEntities
|
||||
import com.cobblemon.mod.common.block.entity.MistleafFlowerBlockEntity
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.level.Level
|
||||
import net.minecraft.world.level.LevelReader
|
||||
import net.minecraft.world.level.block.BaseEntityBlock
|
||||
import net.minecraft.world.level.block.Block
|
||||
import net.minecraft.world.level.block.EntityBlock
|
||||
import net.minecraft.world.level.block.SoundType
|
||||
import net.minecraft.world.level.block.TallFlowerBlock
|
||||
import net.minecraft.world.level.block.entity.BlockEntity
|
||||
import net.minecraft.world.level.block.entity.BlockEntityTicker
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.StateDefinition
|
||||
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf
|
||||
import net.minecraft.world.level.block.state.properties.IntegerProperty
|
||||
import net.minecraft.world.level.material.MapColor
|
||||
import net.minecraft.world.level.material.PushReaction
|
||||
import net.minecraft.world.phys.shapes.CollisionContext
|
||||
import net.minecraft.world.phys.shapes.Shapes
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
|
||||
class MistleafFlowerBlock(lightLevel: (BlockState) -> Int) :
|
||||
EntityBlock,
|
||||
TallFlowerBlock(
|
||||
Properties.of()
|
||||
.mapColor(MapColor.PLANT)
|
||||
.noCollission()
|
||||
.instabreak()
|
||||
.sound(SoundType.GRASS)
|
||||
.offsetType(OffsetType.XZ)
|
||||
.ignitedByLava()
|
||||
.pushReaction(PushReaction.DESTROY)
|
||||
.lightLevel(lightLevel)
|
||||
) {
|
||||
|
||||
init {
|
||||
registerDefaultState(stateDefinition.any()
|
||||
.setValue(HALF, DoubleBlockHalf.LOWER)
|
||||
.setValue(GLOWING, 0))
|
||||
}
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
builder.add(HALF, GLOWING)
|
||||
}
|
||||
override fun isValidBonemealTarget(level: LevelReader, pos: BlockPos, state: BlockState): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getShape(state: BlockState, world: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape = AABB
|
||||
|
||||
override fun newBlockEntity(pos: BlockPos, state: BlockState): BlockEntity? {
|
||||
return MistleafFlowerBlockEntity(pos, state)
|
||||
}
|
||||
|
||||
override fun <T : BlockEntity?> getTicker(
|
||||
level: Level,
|
||||
state: BlockState,
|
||||
blockEntityType: BlockEntityType<T?>
|
||||
): BlockEntityTicker<T?>? {
|
||||
return if (level.isClientSide) null else BaseEntityBlock.createTickerHelper(blockEntityType as BlockEntityType<*>,
|
||||
CobblemonBlockEntities.MISTLEAF_FLOWER, MistleafFlowerBlockEntity::serverTick) as BlockEntityTicker<T?>?
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val AABB = Shapes.box(0.125, 0.0, 0.125, 0.875, 1.0, 0.875)
|
||||
val GLOWING = IntegerProperty.create("glowing", 0, 8)
|
||||
}
|
||||
|
||||
override fun hasAnalogOutputSignal(state: BlockState): Boolean { return true }
|
||||
override fun getAnalogOutputSignal(state: BlockState, level: Level, pos: BlockPos): Int {
|
||||
return state.getValue(GLOWING) as Int
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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.block.decor
|
||||
|
||||
import com.mojang.serialization.MapCodec
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.util.RandomSource
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.context.BlockPlaceContext
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.level.Level
|
||||
import net.minecraft.world.level.LevelAccessor
|
||||
import net.minecraft.world.level.LevelReader
|
||||
import net.minecraft.world.level.block.Block
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import net.minecraft.world.level.block.BonemealableBlock
|
||||
import net.minecraft.world.level.block.DirectionalBlock
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.StateDefinition
|
||||
import net.minecraft.world.phys.shapes.CollisionContext
|
||||
import net.minecraft.world.phys.shapes.Shapes
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
|
||||
class PauvilleaFlowerBlock(properties: Properties?) : DirectionalBlock(properties), BonemealableBlock {
|
||||
init {
|
||||
registerDefaultState(stateDefinition.any()
|
||||
.setValue(FACING, Direction.DOWN))
|
||||
}
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
builder.add(FACING)
|
||||
}
|
||||
|
||||
override fun getStateForPlacement(ctx: BlockPlaceContext): BlockState? {
|
||||
var blockState = defaultBlockState()
|
||||
val worldView = ctx.level
|
||||
val blockPos = ctx.clickedPos
|
||||
blockState = blockState.setValue(FACING, ctx.clickedFace) as BlockState
|
||||
if (blockState.canSurvive(worldView, blockPos)) {
|
||||
return blockState
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun updateShape(state: BlockState, direction: Direction, neighborState: BlockState, level: LevelAccessor, pos: BlockPos, neighborPos: BlockPos): BlockState {
|
||||
val facing = state.getValue(FACING)
|
||||
if (direction == facing.opposite && !canSurvive(state, level, pos)) {
|
||||
return Blocks.AIR.defaultBlockState()
|
||||
}
|
||||
return super.updateShape(state, direction, neighborState, level, pos, neighborPos)
|
||||
}
|
||||
|
||||
override fun canSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
|
||||
val direction = state.getValue(FACING) as Direction
|
||||
val blockState = level.getBlockState(pos.relative(direction.opposite))
|
||||
return blockState.isCollisionShapeFullBlock(level, pos) or blockState.canOcclude()
|
||||
}
|
||||
|
||||
override fun codec(): MapCodec<PauvilleaFlowerBlock> = CODEC
|
||||
|
||||
override fun getShape(state: BlockState, blockGetter: BlockGetter, pos: BlockPos, collisionContext: CollisionContext): VoxelShape {
|
||||
return when (state.getValue(FACING)) {
|
||||
Direction.DOWN -> CEILING
|
||||
Direction.NORTH -> WALL_NORTH
|
||||
Direction.EAST -> WALL_EAST
|
||||
Direction.SOUTH -> WALL_SOUTH
|
||||
Direction.WEST -> WALL_WEST
|
||||
Direction.UP -> FLOOR
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CODEC: MapCodec<PauvilleaFlowerBlock> = RecordCodecBuilder.mapCodec {
|
||||
it.group(propertiesCodec()).apply(it, ::PauvilleaFlowerBlock)
|
||||
}
|
||||
val FLOOR = Shapes.box(0.075, 0.0, 0.075, 0.925, 0.4375, 0.925)
|
||||
val WALL_NORTH = Shapes.box(0.0, 0.0, 0.8125, 1.0, 1.0, 1.0)
|
||||
val WALL_EAST = Shapes.box(0.0, 0.0, 0.0, 0.1875, 1.0, 1.0)
|
||||
val WALL_SOUTH = Shapes.box(0.0, 0.0, 0.0, 1.0, 1.0, 0.1875)
|
||||
val WALL_WEST = Shapes.box(0.8125, 0.0, 0.0, 1.0, 1.0, 1.0)
|
||||
val CEILING = Shapes.box(0.075, 0.0, 0.075, 0.925, 1.0, 0.925)
|
||||
}
|
||||
|
||||
override fun isValidBonemealTarget(level: LevelReader?, pos: BlockPos?, state: BlockState?): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun isBonemealSuccess(level: Level?, random: RandomSource?, pos: BlockPos?, state: BlockState?): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun performBonemeal(level: ServerLevel, random: RandomSource?, pos: BlockPos, state: BlockState?) {
|
||||
popResource(level, pos, ItemStack(this))
|
||||
}
|
||||
}
|
||||
@@ -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.block.decor
|
||||
|
||||
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.level.block.PinkPetalsBlock
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.phys.shapes.CollisionContext
|
||||
import net.minecraft.world.phys.shapes.Shapes
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
|
||||
|
||||
class PomohanaFlowerBlock(properties: Properties?) : PinkPetalsBlock(properties) {
|
||||
|
||||
override fun getShape(state: BlockState, world: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape = AABB
|
||||
|
||||
companion object {
|
||||
private val AABB = Shapes.box(0.125, 0.0, 0.125, 0.875, 1.0, 0.875)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.block.decor
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonBlockEntities
|
||||
import com.cobblemon.mod.common.CobblemonBlocks
|
||||
import com.cobblemon.mod.common.block.entity.TerrainPlantBlockEntity
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.util.RandomSource
|
||||
import net.minecraft.world.entity.Entity
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.level.Level
|
||||
import net.minecraft.world.level.LevelReader
|
||||
import net.minecraft.world.level.block.BaseEntityBlock
|
||||
import net.minecraft.world.level.block.Block
|
||||
import net.minecraft.world.level.block.EntityBlock
|
||||
import net.minecraft.world.level.block.TallFlowerBlock
|
||||
import net.minecraft.world.level.block.entity.BlockEntity
|
||||
import net.minecraft.world.level.block.entity.BlockEntityTicker
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.StateDefinition
|
||||
import net.minecraft.world.level.block.state.properties.BooleanProperty
|
||||
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf
|
||||
import net.minecraft.world.phys.shapes.CollisionContext
|
||||
import net.minecraft.world.phys.shapes.Shapes
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
|
||||
class TerrainPlantBlock(properties: Properties) : TallFlowerBlock(properties), EntityBlock {
|
||||
|
||||
init {
|
||||
registerDefaultState(stateDefinition.any()
|
||||
.setValue(HALF, DoubleBlockHalf.LOWER)
|
||||
.setValue(GLOWING, false))
|
||||
}
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
builder.add(HALF, GLOWING)
|
||||
}
|
||||
override fun isValidBonemealTarget(level: LevelReader, pos: BlockPos, state: BlockState): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun tick(state: BlockState, level: ServerLevel, pos: BlockPos, random: RandomSource) {
|
||||
if (state.`is`(CobblemonBlocks.LUCIBELL)) { this.checkInside(level, pos, state) }
|
||||
}
|
||||
|
||||
private fun checkInside(level: Level, pos: BlockPos, state: BlockState) {
|
||||
// Only lower will handle this
|
||||
val upperState = level.getBlockState(pos.above()).takeIf { it.`is`(CobblemonBlocks.LUCIBELL) && it.getValue(HALF) == DoubleBlockHalf.UPPER }
|
||||
|
||||
val entityInside = level.getEntities(null, state.getShape(level, pos).bounds().move(pos)).isNotEmpty()
|
||||
|| upperState?.let { level.getEntities(null, it.getShape(level, pos.above()).bounds().move(pos.above())).isNotEmpty() } ?: false
|
||||
|
||||
val glowingState = state.getValue(GLOWING)
|
||||
if (glowingState != entityInside) {
|
||||
level.setBlock(pos, state.setValue(GLOWING, entityInside), UPDATE_CLIENTS)
|
||||
upperState?.let { level.setBlock(pos.above(), it.setValue(GLOWING, entityInside), UPDATE_CLIENTS) }
|
||||
}
|
||||
if (entityInside) level.scheduleTick(pos, this, 30) // tick occasionally to check if player inside
|
||||
}
|
||||
|
||||
override fun entityInside(state: BlockState, level: Level, pos: BlockPos, entity: Entity) {
|
||||
// Lucibell lower block will be our 'driver'
|
||||
if (!level.isClientSide && !state.getValue(GLOWING) && state.`is`(CobblemonBlocks.LUCIBELL)) {
|
||||
val halfState = state.getValue(HALF)
|
||||
if (halfState == DoubleBlockHalf.UPPER) {
|
||||
val lowerState = level.getBlockState(pos.below())
|
||||
.takeIf { it.`is`(CobblemonBlocks.LUCIBELL) && it.getValue(HALF) == DoubleBlockHalf.LOWER }
|
||||
?: return
|
||||
(lowerState.block as TerrainPlantBlock).checkInside(level, pos.below(), lowerState)
|
||||
} else {
|
||||
checkInside(level, pos, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getShape(state: BlockState, world: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape = AABB
|
||||
|
||||
override fun newBlockEntity(pos: BlockPos, state: BlockState): BlockEntity {
|
||||
return TerrainPlantBlockEntity(pos, state)
|
||||
}
|
||||
|
||||
override fun <T : BlockEntity> getTicker(
|
||||
level: Level,
|
||||
state: BlockState,
|
||||
blockEntityType: BlockEntityType<T?>
|
||||
): BlockEntityTicker<T?>? {
|
||||
return if (level.isClientSide) null else BaseEntityBlock.createTickerHelper(blockEntityType as BlockEntityType<*>,
|
||||
CobblemonBlockEntities.TERRAIN_PLANT, TerrainPlantBlockEntity::serverTick) as BlockEntityTicker<T?>?
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val AABB = Shapes.box(0.125, 0.0, 0.125, 0.875, 1.0, 0.875)
|
||||
val GLOWING = BooleanProperty.create("glowing")
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* 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.block.decor
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonBlocks
|
||||
import com.cobblemon.mod.common.CobblemonItems
|
||||
import com.cobblemon.mod.common.CobblemonSounds
|
||||
import com.mojang.serialization.MapCodec
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.util.RandomSource
|
||||
import net.minecraft.util.StringRepresentable
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.item.Item
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.level.*
|
||||
import net.minecraft.world.level.block.*
|
||||
import net.minecraft.world.level.block.entity.BlockEntity
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.StateDefinition
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties
|
||||
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf
|
||||
import net.minecraft.world.level.block.state.properties.EnumProperty
|
||||
import net.minecraft.world.level.block.state.properties.IntegerProperty
|
||||
import net.minecraft.world.level.material.Fluids
|
||||
import net.minecraft.world.phys.shapes.CollisionContext
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
class TerrainSeedBlock(private val terrainSeedType: TerrainSeedType, settings: Properties) : CropBlock(settings) {
|
||||
companion object {
|
||||
val CODEC: MapCodec<TerrainSeedBlock> = RecordCodecBuilder.mapCodec { it.group(
|
||||
TerrainSeedType.CODEC.fieldOf("terrainSeedType").forGetter(TerrainSeedBlock::terrainSeedType),
|
||||
propertiesCodec()
|
||||
).apply(it, ::TerrainSeedBlock) }
|
||||
const val MATURE_AGE = 4
|
||||
val AGE: IntegerProperty = IntegerProperty.create("age", 0, MATURE_AGE)
|
||||
val HALF: EnumProperty<DoubleBlockHalf> = BlockStateProperties.DOUBLE_BLOCK_HALF
|
||||
|
||||
val AGE_TO_SHAPE = arrayOf(
|
||||
box(2.0, 0.0, 2.0, 14.0, 6.0, 14.0), // Stage 0
|
||||
box(2.0, 0.0, 2.0, 14.0, 16.0, 14.0), // Stage 1
|
||||
box(2.0, 0.0, 2.0, 14.0, 16.0, 14.0), // Stage 2
|
||||
box(2.0, 0.0, 2.0, 14.0, 16.0, 14.0), // Stage 3
|
||||
box(2.0, 0.0, 2.0, 14.0, 16.0, 14.0), // Stage 4
|
||||
)
|
||||
|
||||
val AGE_TO_SHAPE_TOP = arrayOf(
|
||||
box(2.0, 0.0, 2.0, 14.0, 1.0, 14.0), // Stage 0
|
||||
box(2.0, 0.0, 2.0, 14.0, 1.0, 14.0), // Stage 1
|
||||
box(2.0, 0.0, 2.0, 14.0, 8.0, 14.0), // Stage 2
|
||||
box(2.0, 0.0, 2.0, 14.0, 16.0, 14.0), // Stage 3
|
||||
box(2.0, 0.0, 2.0, 14.0, 16.0, 14.0), // Stage 4
|
||||
)
|
||||
}
|
||||
|
||||
override fun codec(): MapCodec<out CropBlock> {
|
||||
return CODEC
|
||||
}
|
||||
|
||||
init {
|
||||
this.registerDefaultState(
|
||||
this.stateDefinition.any()
|
||||
.setValue(AGE, 0)
|
||||
.setValue(HALF, DoubleBlockHalf.LOWER)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getAgeProperty(): IntegerProperty = AGE
|
||||
|
||||
override fun getMaxAge(): Int = MATURE_AGE
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
builder.add(AGE, HALF)
|
||||
}
|
||||
|
||||
override fun getBaseSeedId(): ItemLike = this.terrainSeedType.getSeed()
|
||||
|
||||
override fun getShape(state: BlockState, world: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape =
|
||||
if (state.getValue(HALF) == DoubleBlockHalf.UPPER) AGE_TO_SHAPE_TOP[getAge(state)] else AGE_TO_SHAPE[this.getAge(state)]
|
||||
|
||||
override fun randomTick(state: BlockState, world: ServerLevel, pos: BlockPos, random: RandomSource) {
|
||||
if (state.getValue(HALF) == DoubleBlockHalf.UPPER || !canGrow(world, pos, state)) return
|
||||
|
||||
if (hasSufficientLight(world, pos) && random.nextInt(32) == 0) {
|
||||
grow(world, pos, state, 1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun canGrow(world: LevelReader, pos: BlockPos, state: BlockState): Boolean {
|
||||
return !this.isMaxAge(state) && canSurvive(state, world, pos) && !(!canGrowInto(world, pos.above()) && state.getValue(AGE) >= 4)
|
||||
}
|
||||
|
||||
private fun canGrowInto(world: LevelReader, pos: BlockPos): Boolean {
|
||||
val blockState = world.getBlockState(pos)
|
||||
return blockState.isAir || blockState.`is`(this.terrainSeedType.getCropBlock())
|
||||
}
|
||||
|
||||
override fun updateShape(state: BlockState, direction: Direction, neighborState: BlockState, world: LevelAccessor, pos: BlockPos, neighborPos: BlockPos): BlockState {
|
||||
return if (state.getValue(AGE) >= 2) {
|
||||
super.updateShape(state, direction, neighborState, world, pos, neighborPos)
|
||||
} else {
|
||||
if (state.canSurvive(world, pos)) state else Blocks.AIR.defaultBlockState()
|
||||
}
|
||||
}
|
||||
|
||||
override fun isValidBonemealTarget(level: LevelReader, pos: BlockPos, state: BlockState): Boolean {
|
||||
val posAndState = getLowerHalf(level, pos, state)
|
||||
return if (posAndState == null) false else this.canGrow(
|
||||
level,
|
||||
posAndState.first,
|
||||
posAndState.second
|
||||
)
|
||||
}
|
||||
|
||||
override fun performBonemeal(level: ServerLevel, random: RandomSource, pos: BlockPos, state: BlockState) {
|
||||
val posAndState = getLowerHalf(level, pos, state)
|
||||
if (posAndState != null) {
|
||||
grow(level, posAndState.first, posAndState.second, getBonemealAgeIncrease(level))
|
||||
}
|
||||
}
|
||||
|
||||
fun grow(world: Level, pos: BlockPos, state: BlockState, increment: Int) {
|
||||
if (!canGrow(world, pos, state)) return
|
||||
|
||||
val newAge = (this.getAge(state) + increment).coerceAtMost(this.maxAge)
|
||||
world.setBlock(pos, state.setValue(this.ageProperty, newAge), UPDATE_CLIENTS)
|
||||
|
||||
val abovePos = pos.above()
|
||||
val stateAbove = world.getBlockState(abovePos)
|
||||
// Set top half depending on age
|
||||
if (newAge >= (2)) {
|
||||
if (stateAbove.isAir) {
|
||||
world.setBlock(
|
||||
abovePos,
|
||||
this.defaultBlockState().setValue(AGE, newAge).setValue(HALF, DoubleBlockHalf.UPPER),
|
||||
UPDATE_CLIENTS
|
||||
)
|
||||
}
|
||||
else if (stateAbove.hasProperty(HALF) && stateAbove.getValue(HALF) == DoubleBlockHalf.UPPER) {
|
||||
world.setBlock(
|
||||
abovePos,
|
||||
stateAbove.setValue(AGE, newAge),
|
||||
UPDATE_CLIENTS
|
||||
)
|
||||
}
|
||||
}
|
||||
// Swaps blocks to the actual Flower when fully grown
|
||||
if (newAge == this.maxAge) {
|
||||
val nextStateLower = this.terrainSeedType.getFlowerBlock().defaultBlockState().setValue(HALF, DoubleBlockHalf.LOWER)
|
||||
val nextStateUpper = this.terrainSeedType.getFlowerBlock().defaultBlockState().setValue(HALF, DoubleBlockHalf.UPPER)
|
||||
world.setBlock(abovePos, nextStateUpper, UPDATE_CLIENTS)
|
||||
world.setBlock(pos, nextStateLower, UPDATE_CLIENTS)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getBonemealAgeIncrease(level: Level): Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
override fun canSurvive(state: BlockState, world: LevelReader, pos: BlockPos): Boolean {
|
||||
return when (state.getValue(HALF)) {
|
||||
DoubleBlockHalf.LOWER -> {
|
||||
val floorBlock = pos.below()
|
||||
val floor = world.getBlockState(floorBlock)
|
||||
|
||||
floor.`is`(Blocks.FARMLAND)
|
||||
}
|
||||
DoubleBlockHalf.UPPER -> {
|
||||
getLowerHalf(world, pos, state) != null
|
||||
// Upper part survives only if the lower part is valid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLowerHalf(level: LevelReader, pos: BlockPos, state: BlockState): Pair<BlockPos, BlockState>? {
|
||||
if (isLower(state)) {
|
||||
return Pair(pos, state)
|
||||
} else {
|
||||
val blockPos = pos.below()
|
||||
val blockState = level.getBlockState(blockPos)
|
||||
return if (isLower(blockState)) Pair(
|
||||
blockPos,
|
||||
blockState
|
||||
) else null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isLower(state: BlockState): Boolean =
|
||||
state.`is`(this.terrainSeedType.getCropBlock()) && state.getValue(HALF) == DoubleBlockHalf.LOWER
|
||||
|
||||
override fun getSoundType(state: BlockState): SoundType {
|
||||
return CobblemonSounds.HEARTY_GRAINS_SOUNDS
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
enum class TerrainSeedType : StringRepresentable {
|
||||
ELECTRIC,
|
||||
PSYCHIC,
|
||||
MISTY,
|
||||
GRASSY;
|
||||
fun getSeed(): Item = when (this) {
|
||||
ELECTRIC -> CobblemonItems.ELECTRIC_SEED
|
||||
PSYCHIC -> CobblemonItems.PSYCHIC_SEED
|
||||
MISTY -> CobblemonItems.MISTY_SEED
|
||||
GRASSY -> CobblemonItems.GRASSY_SEED
|
||||
}
|
||||
fun getCropBlock(): TerrainSeedBlock = when (this) {
|
||||
ELECTRIC -> CobblemonBlocks.ELECTRIC_SEED
|
||||
PSYCHIC -> CobblemonBlocks.PSYCHIC_SEED
|
||||
MISTY -> CobblemonBlocks.MISTY_SEED
|
||||
GRASSY -> CobblemonBlocks.GRASSY_SEED
|
||||
}
|
||||
fun getFlowerBlock(): Block = when (this) {
|
||||
ELECTRIC -> CobblemonBlocks.VOLTACONIA
|
||||
PSYCHIC -> CobblemonBlocks.LUCIBELL
|
||||
MISTY -> CobblemonBlocks.MISTLEAF
|
||||
GRASSY -> CobblemonBlocks.VERDROOT
|
||||
}
|
||||
|
||||
override fun getSerializedName(): String = this.name.lowercase()
|
||||
|
||||
companion object {
|
||||
val CODEC = StringRepresentable.fromEnum(::values)
|
||||
}
|
||||
}
|
||||
|
||||
override fun playerWillDestroy(level: Level, pos: BlockPos, state: BlockState, player: Player): BlockState {
|
||||
if (!level.isClientSide) {
|
||||
if (player.isCreative()) {
|
||||
preventDropFromBottomPart(level, pos, state, player)
|
||||
} else {
|
||||
dropResources(state, level, pos, null as BlockEntity?, player, player.getMainHandItem())
|
||||
}
|
||||
}
|
||||
|
||||
return super.playerWillDestroy(level, pos, state, player)
|
||||
}
|
||||
|
||||
override fun playerDestroy(
|
||||
level: Level,
|
||||
player: Player,
|
||||
pos: BlockPos,
|
||||
state: BlockState,
|
||||
blockEntity: BlockEntity?,
|
||||
tool: ItemStack
|
||||
) {
|
||||
super.playerDestroy(level, player, pos, Blocks.AIR.defaultBlockState(), blockEntity, tool)
|
||||
}
|
||||
|
||||
fun preventDropFromBottomPart(level: Level, pos: BlockPos, state: BlockState, player: Player?) {
|
||||
val doubleblockhalf = state.getValue<DoubleBlockHalf?>(DoublePlantBlock.HALF) as DoubleBlockHalf
|
||||
if (doubleblockhalf == DoubleBlockHalf.UPPER) {
|
||||
val blockpos = pos.below()
|
||||
val blockstate = level.getBlockState(blockpos)
|
||||
if (blockstate.`is`(state.getBlock()) && blockstate.getValue<DoubleBlockHalf?>(DoublePlantBlock.HALF) == DoubleBlockHalf.LOWER) {
|
||||
val blockstate1 = if (blockstate.getFluidState()
|
||||
.`is`(Fluids.WATER)
|
||||
) Blocks.WATER.defaultBlockState() else Blocks.AIR.defaultBlockState()
|
||||
level.setBlock(blockpos, blockstate1, 35)
|
||||
level.levelEvent(player, 2001, blockpos, getId(blockstate))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user