Compare commits

..
Author SHA1 Message Date
Plastered_Crab 071f05337c Scale tab in Implementation GUI moves test wool in X direction based on hitbox size of pokemon to prevent overlap 2026-03-02 15:47:59 -08:00
Plastered_Crab e9d274fbfd Scale tab in Implementation GUI renders test wool block nicely 2026-03-02 15:23:45 -08:00
Plastered_Crab 5709527bf2 Merge branch 'main' into ta-daaa 2026-03-02 13:17:58 -08:00
Polymeta e6d2430405 Merge branch 'BWG-tag-updates' into 'main'
Fixes to BWG compat.

See merge request cable-mc/cobblemon!2172
2026-03-02 09:42:03 +00:00
Veraxiel 6c425a43c6 Fixes to BWG compat. 2026-03-01 22:37:30 -05:00
Polymeta a505bad554 [skip-ci] tidy up changelog 2026-02-27 10:56:57 +00:00
Polymeta e27e5a1d92 Merge branch 'ai-switch-fix' into 'main'
Stop the constant switching from AI NPC Trainers

See merge request cable-mc/cobblemon!2128
2026-02-27 10:55:00 +00:00
Hiroku 5e0357101d Merge branch 'feat/fishing-spawner-factory' into 'main'
PokeSnack and Fishing Spawner factories

See merge request cable-mc/cobblemon!2136
2026-02-27 02:11:53 +00:00
Andy 5420f7419f re-run licenseFormat 2026-02-26 19:14:03 -05:00
Andy 43ed2d3c7d local backmerge 2026-02-26 19:12:17 -05:00
Andy 93500f59ed Remove changelog lines from backmerge and run licenseFormat 2026-02-26 19:10:45 -05:00
Polymeta 08912fd53f Merge branch '#1945-brewing-stand-crash' into 'main'
#1945 Brewing Stand Shift Click Crash NeoForge 1.7.3 [1.21.1]

See merge request cable-mc/cobblemon!2163
2026-02-27 00:08:36 +00:00
Cyrus 92c8fc0afc #1945 Brewing Stand Shift Click Crash NeoForge 1.7.3 [1.21.1] 2026-02-27 00:08:36 +00:00
Polymeta fae6bdb782 Merge branch 'main' into 'main'
Move BYG compat to Biomes We've Gone and Nether Descent

See merge request cable-mc/cobblemon!2149
2026-02-26 22:52:33 +00:00
J.T. McQuigg bba0100542 Move BYG compat to Biomes We've Gone and Nether Descent 2026-02-26 22:52:33 +00:00
Andy f9b66de742 Add fishing spawner factory to register influences to 2026-02-26 15:54:09 +00:00
Andy 3d0f1c826a Merge branch 'main' into feat/fishing-spawner-factory
update with cobblemon head
2026-02-26 10:52:48 -05:00
Andy 2a59add285 Merge branch cobblemon:main into main 2026-02-26 15:50:16 +00:00
Polymeta 0b83128aa0 Merge branch 'main' into 'main'
Fixes brushing for neoforge

Closes #1939

See merge request cable-mc/cobblemon!2154
2026-02-26 15:43:03 +00:00
Polymeta e2b04ff770 Merge branch 'ufo-fixes-and-improvements' into 'main'
UFO Fixes and Improvements

See merge request cable-mc/cobblemon!2156
2026-02-26 15:29:38 +00:00
Jackson Bowes 4dabe50d43 UFO Fixes and Improvements 2026-02-26 15:29:37 +00:00
Hugin Juliansson Zachariasen 3dd0795b88 Merge branch cobblemon:main into main 2026-02-26 14:14:56 +00:00
Polymeta 70f1a8207f fix a apricorn bonemeal crash
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2026-02-26 12:17:32 +00:00
Polymeta 3ac18cce6e Merge branch 'main' into 'main'
Berry Growth Performance Optimization

See merge request cable-mc/cobblemon!2142
2026-02-26 12:15:32 +00:00
Polymeta aa0a0672d0 Merge branch 'show-my-pokemon-exp-gain' into 'main'
Party Overlay EXP display and evoltion unlocked Sound

See merge request cable-mc/cobblemon!2140
2026-02-26 11:59:48 +00:00
Gertimoshka ec2958400b Party Overlay EXP display and evoltion unlocked Sound 2026-02-26 11:59:48 +00:00
Polymeta fcb6bea78b Merge branch 'selecting-starters-in-style' into 'main'
The Cooler Starter Selection GUI

See merge request cable-mc/cobblemon!2164
2026-02-26 11:58:31 +00:00
Gertimoshka 5a76a39b77 The Cooler Starter Selection GUI 2026-02-26 11:58:31 +00:00
Jackson Bowes b5a4acfb7a Merge branch 'weight-influenced-collision' into 'main'
Weight influenced collision

See merge request cable-mc/cobblemon!2158
2026-02-26 01:49:56 -06:00
Jackson Bowes 1248230e67 Merge branch 'pasture-wander' into 'main'
No more pastured mons despawning

See merge request cable-mc/cobblemon!2155
2026-02-26 01:47:17 -06:00
Jackson Bowes 096db6cdcd No more pastured mons despawning 2026-02-26 01:47:16 -06:00
Jackson Bowes 72e416b9c7 Merge branch 'sub-drifting-fix' into 'main'
Submarine bug fixes

See merge request cable-mc/cobblemon!2157
2026-02-26 01:44:58 -06:00
Polymeta 9e8e80965b Merge branch 'fix/missing-botanypots-entries' into 'main'
Updated Botany Pots integration

See merge request cable-mc/cobblemon!2166
2026-02-26 07:41:03 +00:00
Gitoido 2325b436b1 Updated Botany Pots integration 2026-02-26 07:41:02 +00:00
Jackson Bowes 57421a2884 Merge branch 'scrolling_textures' into 'main'
Scrolling Texture Layer

See merge request cable-mc/cobblemon!2168
2026-02-25 21:54:47 -06:00
jackowes 4fdd3eba8e - changelog entry 2026-02-25 20:24:29 -06:00
jackowes e6efe800c0 - Clean up mixin
- add changelog entry
2026-02-25 19:45:52 -06:00
Polymeta 95aa99e18c Merge branch 'fix/particle-disapearing' into 'main'
fix: adapting the boundingBox to the visual if it's a beam it should have a...

See merge request cable-mc/cobblemon!2167
2026-02-26 01:16:09 +00:00
Terrainwax 270484e745 fix: adapting the boundingBox to the visual if it's a beam it should have a... 2026-02-26 01:16:09 +00:00
Polymeta 719addf633 Merge branch 'molang-or-dishonor' into 'main'
Added `has_chosen_starter` & `get_starter_uuid` as available Molang functions

See merge request cable-mc/cobblemon!2159
2026-02-25 23:05:54 +00:00
Polymeta 31e386871f Merge branch 'little-dose-of-molang' into 'main'
Enchanting Molang

See merge request cable-mc/cobblemon!2162
2026-02-25 23:00:15 +00:00
Tyzillion bfd7e596d0 changelog 2026-02-25 17:52:39 -05:00
Tyzillion 643545d54c Added the ability to set a layer in the resolver as a scrolling texture. 2026-02-25 16:41:50 -05:00
Tyzillion fcb017f980 New Molang functions to check if an item is enchanted or has a certain enchantment. 2026-02-23 08:48:51 -05:00
Plastered_Crab bc9f28811d Implementation Tab Animation option improvements 2026-02-19 13:43:41 -08:00
Plastered_Crab ff9eb60cd3 Implementation Tab Animation option improvements 2026-02-19 12:39:39 -08:00
Spicy dd52da7c42 oops 2026-02-18 20:57:23 -06:00
Spicy ff45def0e8 Added has_chosen_starter & get_starter_uuid as available Molang functions 2026-02-18 20:42:40 -06:00
Plastered_Crab de0b53e529 Implementation tabs updated. Walk Speed tab blocks rendering 2026-02-18 16:15:05 -08:00
jackowes f078584e56 - Add weight influenced collision 2026-02-18 15:24:43 -06:00
jackowes a11382b5a1 - Braking now works on Submarine (whoops).
- Strafing now has the same friction as forward movement (funny whoops).
- Submarine's surfacewater animations now play.
- Submarine camera no longer freaks out when looking up or down on the surface.
2026-02-17 20:28:55 -06:00
Plastered_Crab 56ecbfe3c0 Move tab testing 2026-02-16 15:45:35 -08:00
Plastered_Crab 62f03c536d Polishing up of the General Tab 2026-02-16 12:02:54 -08:00
Plastered_Crab 2fa03b6013 Adjust position of elements 2026-02-16 08:40:53 -08:00
Plastered_Crab 59af4053a1 Held Item Item Slot Searching 2026-02-15 22:09:33 -08:00
Plastered_Crab 1aba8c52b6 Held Item Item Slot Renders now 2026-02-15 21:11:23 -08:00
Plastered_Crab 6a386937c8 Held Item Item Slot 2026-02-15 20:01:58 -08:00
Plastered_Crab 00132f6428 Cosmetic Item Slot 2026-02-15 19:10:55 -08:00
Plastered_Crab 7fe222422b Cosmetic Item filtering 2026-02-15 18:37:11 -08:00
Plastered_Crab 812f389231 Cosmetic Item fixes 2026-02-15 18:08:17 -08:00
Paul d1b8094539 Validate party positions of traded Pokémon upon trading 2026-02-16 11:23:24 +10:00
Paul b8d79af911 Migrate to a more unique tag name for data and config on scriptable entities, closes #1869 2026-02-16 10:57:11 +10:00
Plastered_Crab 24acddf750 Aspects Tab adjustments 2026-02-15 16:22:51 -08:00
Plastered_Crab 1d696c58be Abra kadabra 2026-02-15 10:59:27 -08:00
Paul ef4fbecad6 Merge remote-tracking branch 'origin/1.8.0' into 1.8.0 2026-02-14 14:30:41 +10:00
Paul 96075ed34a Merge remote-tracking branch 'origin/main' into 1.8.0 2026-02-14 14:30:11 +10:00
Hugin Juliansson Zachariasen 155b4b2182 Added changelog doc 2026-02-13 17:50:46 +00:00
Faering666 690ed845b2 Fixes brushing for neoforge 2026-02-13 15:55:51 +01:00
Hiroku a7198fd324 Merge branch 'fossil_crash' into 'main'
Adjust fossil creation

Closes #1801

See merge request cable-mc/cobblemon!2150
2026-02-08 01:16:45 +00:00
aeroeng15 bc69ce91f3 Changelog 2026-02-07 16:00:00 -07:00
aeroeng15 1de1d8cdc3 Adjust ordering of party modification 2026-02-07 15:26:36 -07:00
Konoha 1f8171dd94 Experimental Hanging fix 2026-02-03 13:51:41 -05:00
Hiroku 087bf75500 Merge branch 'party-selection-toast' into 'main'
Party Selection Tutorial Toasts

See merge request cable-mc/cobblemon!2148
2026-02-02 05:58:58 +00:00
Ceejay bac435ba7b Party Selection Tutorial Toasts 2026-02-02 05:58:58 +00:00
Hiroku 74a6106eb6 Merge branch 'snack-stack-fix' into 'main'
Fixes odd stacking behavior with the poke_bait item.

See merge request cable-mc/cobblemon!2143
2026-02-01 03:29:20 +00:00
Neovitalism 5ec993ad3c Update CHANGELOG.md 2026-01-31 21:13:10 -06:00
Paul 73a03d90da Resnapshot 2026-01-31 13:22:11 +10:00
Paul e6fda6137e Teeny tiny changelog [skip-ci] 2026-01-31 10:47:13 +10:00
Paul afae39acc8 Missed a bit of changelog [skip-ci] 2026-01-31 09:21:40 +10:00
Paul 37264aff0f Changelog stuff and no snapshot 2026-01-31 09:17:43 +10:00
Paul 2b60e6de77 Merge remote-tracking branch 'origin/lang-updates' 2026-01-31 09:12:27 +10:00
Polymeta 3c2bf0e2b3 Merge branch 'cobblemon-structure' into '1.8.0'
Slippy can Cook now!

See merge request cable-mc/cobblemon!2144
2026-01-30 09:45:31 +00:00
Aram ac695b1092 Slippy can Cook now! 2026-01-30 09:45:31 +00:00
Hiroku 4f29e7457c Merge branch 'feature/riding-scrape' into 'main'
scrape riding data and fix script

See merge request cable-mc/cobblemon!2141
2026-01-30 09:43:44 +00:00
Polymeta 04a6ab31f4 rescrape data
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2026-01-30 09:26:22 +00:00
Hiroku a2cdc847dd Merge branch 'packet-sync-error-fix' into 'main'
Fixed packet sync issue with pokemon interactions, added "chance" requirement type

Closes #1933 and #1938

See merge request cable-mc/cobblemon!2147
2026-01-30 03:04:14 +00:00
阿鱼 0bca678f56 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (9061 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/zh_Hans/
2026-01-30 02:59:44 +00:00
Paul 067c3607aa Tweak /pctake to check the PC's box count and use that for constraints 2026-01-30 12:20:17 +10:00
Hiroku 823c1f6444 Merge branch 'main' into 'main'
Fixes PcTake not respecting defaultBoxCount

See merge request cable-mc/cobblemon!2146
2026-01-30 02:18:38 +00:00
Paul bfc00635cd Fixed packet sync issue with pokemon interactions, added "chance" requirement type 2026-01-30 12:16:01 +10:00
Hiroku b2e5b85ac5 Merge branch 'Custom-Ball-Resources-updated' into 'main'
updated ball texture locations missing from last update

See merge request cable-mc/cobblemon!2145
2026-01-30 00:24:49 +00:00
阿鱼 2c27476954 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.7% (9034 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/zh_Hans/
2026-01-29 13:40:10 +00:00
Auristite aab78d84fc Fixes PcTake not respecting defaultBoxCount
Command is also missing lang option for "cobblemon.command.pctake.removed" (Removing Pokémon from self)
2026-01-29 04:46:25 +00:00
Zack the Ghast cb632c2a65 Translated using Weblate (Portuguese (Portugal))
Currently translated at 67.9% (6154 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/pt_PT/
2026-01-29 01:28:38 +00:00
Dragonwhisper92 8d219f1338 updated ball texture locations 2026-01-28 23:45:08 +00:00
Zack the Ghast 1e6a4ed984 Translated using Weblate (Portuguese (Portugal))
Currently translated at 67.7% (6143 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/pt_PT/
2026-01-28 21:10:25 +00:00
geumu 64240c31fc Translated using Weblate (Korean)
Currently translated at 100.0% (9061 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/ko/
2026-01-28 17:35:35 +00:00
Zack the Ghast d2222c3858 Translated using Weblate (Portuguese (Portugal))
Currently translated at 67.6% (6129 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/pt_PT/
2026-01-28 14:44:02 +00:00
geumu c7f31c3e14 Translated using Weblate (Korean)
Currently translated at 100.0% (9061 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/ko/
2026-01-28 14:44:01 +00:00
Zack the Ghast 740e846b2d Translated using Weblate (Portuguese (Portugal))
Currently translated at 67.5% (6125 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/pt_PT/
2026-01-28 08:26:35 +00:00
Neovitalism 246d00aa3f Fixes odd stacking behavior with the poke_bait item. 2026-01-27 21:45:05 -06:00
Zack the Ghast 06f9d60595 Translated using Weblate (Portuguese (Portugal))
Currently translated at 67.5% (6123 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/pt_PT/
2026-01-27 04:12:54 +00:00
Konoha b3ed47fbca Adjust thresholds and scoring 2026-01-26 15:00:50 -05:00
Loopiwc 3a6ad70b76 fix: Use scheduleTick in randomTick to avoid tick scheduler NPE
- Schedule growth to next tick instead of direct setBlock in randomTick
- Add hasScheduledTick check to prevent duplicate scheduling
- Add IS_ROOTED guard in tick() handler
2026-01-27 03:35:11 +08:00
Loopiwc 8120270bf8 perf: Replace BlockEntityTicker with Random Tick + time compensation for Berry growth
- Remove per-tick BlockEntityTicker to reduce server load
- Add processTick() method with gameTime-based compensation
- Track lastTickTime in NBT for accurate growth calculation
- Berry growth timing remains accurate via elapsed time batching
2026-01-26 16:30:43 +08:00
Zack the Ghast d54439ce14 Translated using Weblate (Portuguese (Portugal))
Currently translated at 67.3% (6103 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/pt_PT/
2026-01-26 02:31:37 +00:00
Teggy 32ee4bb72a Translated using Weblate (Spanish)
Currently translated at 84.3% (7644 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/es/
2026-01-25 13:02:37 +00:00
Polymeta e7d2a46cc0 Bumping snapshot version 2026-01-25 06:57:31 +00:00
Yasito 3ee94ec076 Translated using Weblate (French)
Currently translated at 100.0% (9061 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-25 03:06:53 +00:00
Yasito 589293abc4 Translated using Weblate (French (Canada))
Currently translated at 100.0% (9061 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/fr_CA/
2026-01-25 03:06:49 +00:00
Paul 279865cc7c Resnaps your shot 2026-01-25 11:53:29 +10:00
Paul b597b149c3 Snapnot 2026-01-25 10:18:32 +10:00
Paul b7f592f2a8 Relocate mongodb packages 2026-01-25 10:18:02 +10:00
Paul 70e17c22e7 Merge remote-tracking branch 'origin/lang-updates' 2026-01-25 10:17:49 +10:00
Yasito 51b18ca0f6 Translated using Weblate (French)
Currently translated at 99.6% (9027 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-25 00:16:58 +00:00
Yasito a83f243a3b Translated using Weblate (French (Canada))
Currently translated at 99.9% (9059 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/fr_CA/
2026-01-25 00:16:55 +00:00
Weblate f5b088c708 Update translation files
Updated by "Remove blank strings" add-on in Weblate.

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/
2026-01-25 00:14:04 +00:00
Yasito dc454cbb68 Translated using Weblate (French)
Currently translated at 99.5% (9022 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-25 00:13:56 +00:00
Yasito 3b99e38da2 Translated using Weblate (French (Canada))
Currently translated at 99.9% (9057 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/fr_CA/
2026-01-25 00:13:54 +00:00
Paul 483af57964 Merge remote-tracking branch 'origin/lang-updates' 2026-01-25 10:12:48 +10:00
Dmytro Bunnua a941bdddf3 Translated using Weblate (Ukrainian)
Currently translated at 76.9% (6971 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/uk/
2026-01-24 18:24:12 +00:00
Teggy 246ba4a93b Translated using Weblate (Spanish)
Currently translated at 83.9% (7611 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/es/
2026-01-24 18:24:09 +00:00
Paul 3347c084ed More changelog [skip-ci] 2026-01-24 22:42:58 +10:00
Paul 8175cee454 More translations 2026-01-24 22:11:15 +10:00
Paul 37d54053a5 Merge remote-tracking branch 'origin/lang-updates' 2026-01-24 22:10:10 +10:00
Dmytro Bunnua b8846bd74c Translated using Weblate (Ukrainian)
Currently translated at 76.8% (6966 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/uk/
2026-01-24 11:47:21 +00:00
Paul 1716e135c4 Changelog [skip-ci] 2026-01-24 21:45:14 +10:00
Paul 0ac804cfcb Cleanup 2026-01-24 21:20:50 +10:00
Hiroku aecc132814 Merge branch 'pokeballUIResourcePathChange' into 'main'
ball_icon resource location changes

See merge request cable-mc/cobblemon!2113
2026-01-24 11:13:26 +00:00
Hiroku 8b47876dff Merge branch 'Add-Plaques-to-Village-Pokecenters' into 'main'
Adding Plaques to Village Pokecenters

See merge request cable-mc/cobblemon!2139
2026-01-24 09:27:56 +00:00
Slippy ffc108953a Adding Plaques to Village Pokecenters 2026-01-24 01:51:33 -06:00
Paul 0a106174fe Changelog cleanup [skip-ci] 2026-01-24 15:59:27 +10:00
Paul 90210bee3c Fixed cottonee cries being inconsistent due to not having appropriate pose type coverage 2026-01-24 15:13:08 +10:00
Paul 8603cd4ad0 Merge remote-tracking branch 'origin/main' into Expression-stuffs 2026-01-24 13:56:14 +10:00
Hiroku 7b2c6827c2 Merge branch 'Expression-stuffs' into 'main'
Expression stuffs

See merge request cable-mc/cobblemon!2138
2026-01-24 03:55:02 +00:00
Paul 902b4a6d21 Fixed croagunk and toxicroak playing cry animations without the sound 2026-01-24 13:54:41 +10:00
Hiroku a847cb5926 Merge branch 'fix-plain-aprijuice-prefix' into 'main'
Fix Aprijuice 'Plain' prefix

See merge request cable-mc/cobblemon!2137
2026-01-24 03:46:05 +00:00
Paul 050c6f5adc Merge remote-tracking branch 'origin/Expression-stuffs' into Expression-stuffs 2026-01-24 13:41:23 +10:00
Paul fb95fb3cf7 Various cry corrections from implementation 2026-01-24 13:41:11 +10:00
Carmendar d507367411 Bird stamina changes. 2026-01-23 21:30:43 -06:00
Paul faa19cebcb Fixed perma zero stamina on horses when landing from bird 2026-01-24 13:16:38 +10:00
Paul aced374367 Fixed birds falling out of the sky and not gliding all chill like 2026-01-24 12:40:04 +10:00
Paul d424a6f1d3 Fixed stamina stat flowing to crazy negative from bird sometimes 2026-01-24 11:58:25 +10:00
Carmendar 71bb1c32de stamina expression changes for Bird and Hover. Changes to Mantine riding. 2026-01-23 18:56:17 -06:00
Carmendar f726fcedb9 Merge branch 'main' into implement_pokemon 2026-01-23 18:09:48 -06:00
Weblate 77344ccc2f Update translation files
Updated by "Remove blank strings" add-on in Weblate.

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/
2026-01-23 17:29:44 +00:00
Dmytro Bunnua 90bc16be90 Translated using Weblate (Ukrainian)
Currently translated at 76.8% (6960 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/uk/
2026-01-23 17:29:37 +00:00
Gustavo Puerari d1b743a01f Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.3% (8908 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/pt_BR/
2026-01-23 17:29:33 +00:00
Teggy aed651c0a0 Translated using Weblate (Spanish)
Currently translated at 83.9% (7608 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/es/
2026-01-23 17:29:28 +00:00
Faintful 4c477e1675 Add changelog 2026-01-23 17:36:05 +01:00
Faintful d1cdc6789c Handle case for LOW quality Aprijuice in getName() for proper 'Plain' prefix 2026-01-23 17:33:04 +01:00
Andy 70fe1682b0 remove whitespace in pokesnack block entity 2026-01-23 10:49:24 -05:00
Andy 9058bde326 remove whitespace from changelog 2026-01-23 10:41:52 -05:00
Andy bf425efd40 Merge branch 'feat/snack-spawner-factory' into 'main'
Added PokeSnackSpawnerFactory so influences can be registered to pokesnacks

See merge request ppVon/cobblemon!1
2026-01-23 15:39:21 +00:00
geumu cd3127b983 Translated using Weblate (Korean)
Currently translated at 100.0% (9061 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/ko/
2026-01-23 13:22:11 +00:00
Watuneni iteite 68f5eff048 Translated using Weblate (Japanese)
Currently translated at 100.0% (9061 of 9061 strings)

Translation: Cobblemon/Mod
Translate-URL: https://lang.cobblemon.com/projects/cobblemon/mod/ja/
2026-01-23 13:22:06 +00:00
Andy cebff87656 update changelog 2026-01-23 03:49:09 -05:00
Andy d24fb13877 Add fishing spawner factory to register influences to 2026-01-23 03:47:34 -05:00
Paul c3d2918a2b Fixed a crash when joining a server and a player is riding nearby 2026-01-23 17:49:57 +10:00
Andy 1d82df464a Added PokeSnackSpawnerFactory so influences can be registered to pokesnacks 2026-01-23 02:19:02 -05:00
Paul 0432525c06 Throh resize 2026-01-23 17:04:45 +10:00
Paul a60f27e282 Fixed implemented flag missing from the new 1.7.2 pokemon, subtle size changes 2026-01-23 17:04:14 +10:00
Test 5f0182cb7e fix type asset 2026-01-23 01:48:49 -05:00
Paul 4a3ae9407b Translation changelog [skip-ci] 2026-01-23 16:44:32 +10:00
Paul 766c2838d2 Merge remote-tracking branch 'origin/lang-updates' 2026-01-23 14:34:19 +10:00
Hiroku 4cb95589b4 Merge branch 'more-fixes' into 'main'
More fixes

See merge request cable-mc/cobblemon!2134
2026-01-23 04:30:49 +00:00
Hiroku a8d3c2dd52 Merge branch '1.7.2-spawns' into 'main'
Added spawns. Fixed spawns. Updated tags.

Closes #1896

See merge request cable-mc/cobblemon!2131
2026-01-23 04:13:07 +00:00
Test cb5d7b6ad7 rm unused 2026-01-22 23:05:38 -05:00
Test c7dcc64595 sync repo assets 2026-01-22 23:00:10 -05:00
Paul 6573e08da7 Fix for packet handling to ensure all packets are handled on main and without extra delays on Fabric, closes #1903 2026-01-23 13:37:36 +10:00
Paul cd695ade4a Apparently I put more changelog in than was meant to be there 2026-01-23 13:22:37 +10:00
Paul 55cb413cfc Merge remote-tracking branch 'origin/main' into 1.7.2-spawns 2026-01-23 13:11:50 +10:00
Paul 4f1bf7fcbb Changelog for spawns [skip-ci] 2026-01-23 13:11:42 +10:00
Test 154ee80ead fix some block issues 2026-01-22 21:32:51 -05:00
Test 1c9f9bb198 Merge branch 'main' of https://gitlab.com/cable-mc/cobblemon 2026-01-22 20:38:08 -05:00
Test 6836343f11 ride summary 2026-01-22 20:37:46 -05:00
RedGenesectNinja aa0d1cd943 Updated changelog 2026-01-22 14:44:46 -06:00
Hiroku 798f1da8b3 Merge branch 'main' into 'main'
Issue 1926: Make SpawnBaitInfluence apply all effects correctly

See merge request cable-mc/cobblemon!2130
2026-01-22 13:44:44 +00:00
Andy d98efb52b6 Add changelog line and extend author tag 2026-01-22 08:38:13 -05:00
Polymeta f450d9d2d6 Merge branch 'fix-bucket-reuse' into '1.8.0'
Fixed multi-spawns in a single pass using the same bucket for all of them

See merge request cable-mc/cobblemon!2104
2026-01-22 13:21:39 +00:00
Kenji_64 2f27a03f08 Alolan Raichu can look around in the menu now (yay) 2026-01-22 13:36:19 +01:00
Kenji_64 e33025b1fa Fixed the weird eye clipping on Dragapult (fr this time) 2026-01-22 13:36:00 +01:00
Kenji_64 ade2481f42 Treecko line has their cries back 2026-01-22 13:35:39 +01:00
Paul 0752ab3cac Fixed woopsies in changelog 2026-01-22 22:23:01 +10:00
Hiroku 576e2301ce Merge branch 'open-actor-classes' into 'main'
Mark the actor classes as open so they can be extended.

See merge request cable-mc/cobblemon!2129
2026-01-22 12:19:06 +00:00
Polymeta a568d45d75 [skip-ci] tidy up changelog take two 2026-01-22 09:58:38 +00:00
Hiroku bc19a7e4c1 Merge branch 'decor-split' into 'main'
Decor split

See merge request cable-mc/cobblemon!2127
2026-01-22 09:42:35 +00:00
Polymeta 616e438f17 [skip-ci] tidy up changelog 2026-01-22 09:40:47 +00:00
Polymeta 9bb11c46d5 Merge branch 'implement_pokemon' into 'main'
Implement pokemon

Closes #1921 and #1800

See merge request cable-mc/cobblemon!2063
2026-01-22 09:28:25 +00:00
Carmendar 5f470bb703 Honchkrow and Mantine camera fixes. 2026-01-21 23:25:53 -06:00
Andy 276f13cea5 1926: Re-add affectWeight comment 2026-01-22 00:08:34 -05:00
Andy ac069687f1 1926: Changed SpawnBaitInfluence to apply all bait effects properly in affectWeight 2026-01-22 00:07:14 -05:00
Deltric f248c22eed Fix initialPos being able to change and changelog 2026-01-21 22:54:53 -06:00
RedGenesectNinja 65a5687f92 Fixed Dusknoir's stomach mouth lips glowing while it sleeps 2026-01-21 22:27:11 -06:00
Deltric ea50fa534b Mark the actor classes as open so they can be extended. 2026-01-21 21:35:03 -06:00
RedGenesectNinja 4bd3c4af58 Updated changelog 2026-01-21 21:20:16 -06:00
RedGenesectNinja 283d80f7ed Implemented Croagunk and Toxicroak's animations 2026-01-21 21:19:22 -06:00
Veraxiel 0238ca80f0 Added spawns. Fixed spawns. Updated tags. 2026-01-21 18:53:40 -05:00
RedGenesectNinja d24c5fe874 Fixed a mistake I made with file placement 2026-01-21 15:32:14 -06:00
RedGenesectNinja e65e21f2ff Fixed Chimecho's cry not playing a sound 2026-01-21 15:21:02 -06:00
Carmendar e1d417718f Added Flygon jump and Mantine ride anims with temp ride stats. 2026-01-21 02:25:05 -06:00
Polymeta d4308b0a4f Merge branch 'fix-battle-memory-leak' into 'main'
Fixed memory leak from battles not being cleaned up after ending.

See merge request cable-mc/cobblemon!2075
2026-01-21 08:09:44 +00:00
Konoha 2a1d1e4474 fix: AI constant switching 2026-01-20 21:09:30 -05:00
Test bc0d530484 loisence 2026-01-20 19:19:53 -05:00
Jerzykv 56650418d2 Implemented Throh and Sawk 2026-01-20 20:32:21 +01:00
Jerzykv 0543b84d34 The battle idle should probably play the battle idle animation 2026-01-20 19:57:45 +01:00
Jerzykv 1f50687a59 Re-added Honchkrow's driving license 2026-01-20 19:53:25 +01:00
Polymeta ed04faf0d8 Merge branch 'dispenser-fix' into 'main'
Resolved Dispenser Behaviors not working

See merge request cable-mc/cobblemon!2118
2026-01-20 14:46:55 +00:00
Aram 1ed020f3e0 Resolved Dispenser Behaviors not working 2026-01-20 14:46:54 +00:00
Polymeta 7e385d8c94 Merge branch 'default-offest' into 'main'
[FEAT] Add `default_seat` option to seat offset posers

See merge request cable-mc/cobblemon!1999
2026-01-20 14:46:44 +00:00
Polymeta ee509b5438 Merge branch 'molang-inventory-and-move-utility' into 'main'
molang additions: Move struct accessor and player inventory utils

See merge request cable-mc/cobblemon!2103
2026-01-20 14:46:34 +00:00
Jerzykv ac9394131c Implemented Litleo and Pyroar - Changelog 2026-01-20 12:01:09 +01:00
Jerzykv d058a306a7 Implemented Litleo and Pyroar 2026-01-20 11:58:48 +01:00
Jerzykv b9be0c3fcb Swirlix and Slurpuff cry fixes 2026-01-20 10:14:16 +01:00
Jerzykv b59b46568a Swirlix and Slurpuff poser changes 2026-01-20 10:14:11 +01:00
Jerzykv 408cbc8c49 Slurpuff and Aromatisse scale changes 2026-01-20 10:13:55 +01:00
Test 1537816132 log of change 2026-01-20 00:56:36 -05:00
Test 1705639950 loot 2026-01-20 00:50:04 -05:00
Test ce32541dd6 Merge branch 'main' of https://gitlab.com/cable-mc/cobblemon into decor-split 2026-01-19 23:45:28 -05:00
Test 3ffea4f4dd split items 2026-01-19 23:44:20 -05:00
Hiroku fe35a63769 Merge branch 'complex-movesets' into '1.8.0'
Complex movesets, parties, and pools

See merge request cable-mc/cobblemon!2086
2026-01-19 04:09:56 +00:00
Hiroku f942d7c34a Merge branch 'misc-additions-fixes' into 'main'
Couple of misc API additions and bug fixes.

See merge request cable-mc/cobblemon!2125
2026-01-19 02:16:09 +00:00
Deltric 0f1ada4624 Pass in pnx to the event 2026-01-18 18:52:27 -06:00
Deltric b4ee4629bf Couple of misc API additions and bug fixes. 2026-01-18 17:57:28 -06:00
Paul d0b64e2d82 Respect the BYPASSES_INVULNERABILITY damage tag 2026-01-18 22:12:25 +10:00
Jerzykv a8ce3c2175 Fixed Wishiwashi's School Form, Fixes #1921 2026-01-18 12:39:16 +01:00
Jerzykv 80ae127379 Implemented Spritzee and Aromatisse. Smells good. 2026-01-18 12:36:48 +01:00
Jerzykv bd3a9fb3af Implemented Swirlix and Slurpuff. How sweet! 2026-01-18 11:28:15 +01:00
Instrafe d0eaccb81b Fixed Ferroseed and Ferrothorn's posers not identifying a root bone (woops) 2026-01-17 12:12:08 -05:00
Paul 1ed2042283 Fixed /calculateseatpositions expecting no underscore when actually we always use an underscore 2026-01-17 22:32:57 +10:00
Deltric 0b531d967e Merge remote-tracking branch 'origin/fix-battle-memory-leak' into fix-battle-memory-leak
# Conflicts:
#	common/src/main/resources/data/cobblemon/showdown.zip
2026-01-17 03:42:38 -06:00
Deltric b0d1aec9ab Update showdown 2026-01-17 03:42:21 -06:00
Deltric f4d47098b5 Fixed memory leak from battles not being cleaned up after ending. 2026-01-17 03:42:08 -06:00
Instrafe 1989b8aa92 Implemented remodels for Ferroseed and Ferrothorn 2026-01-16 17:06:45 -05:00
Hiroku 948ca61ef3 Merge branch 'fix-super-potion-ingredients-tag' into 'main'
Fix super potion ingredients tag

See merge request cable-mc/cobblemon!2121
2026-01-16 08:27:19 +00:00
Hiroku e60aca30ba Merge branch 'patch-2' into 'main'
Fix Syrupy Apple missing from evolution items tag

See merge request cable-mc/cobblemon!2120
2026-01-16 08:26:22 +00:00
Hiroku fb5055c39e Merge branch 'form_crash' into 'main'
Initialize FormData when updating

Closes #1916

See merge request cable-mc/cobblemon!2122
2026-01-16 08:20:23 +00:00
Paul dfb5650121 Merge remote-tracking branch 'origin/main' 2026-01-16 18:17:01 +10:00
Paul 8a0cbf28fe Fixed battles not ending fully when a switch is in progress, closes #1917 2026-01-16 18:16:11 +10:00
aeroeng15 a6b9060d57 Changelog 2026-01-15 20:54:54 -07:00
aeroeng15 8e57995e44 Make It Proper 2026-01-15 20:51:59 -07:00
aeroeng15 1d8257808c Initialize rather than set; seems to fix the issue 2026-01-15 20:42:14 -07:00
Carmendar e290fe9c75 Modified Hover expressions and buffs/nerfs for hover mounts. 2026-01-15 21:23:34 -06:00
Jerzykv 06a50cfeeb Changelog for the cottons 2026-01-15 11:12:17 +01:00
Jerzykv 00888c8ad5 Implemented Cottonee and Whimsicott remodels 2026-01-15 11:11:44 +01:00
Faintful e3582ccfb4 Add changelog 2026-01-14 19:49:11 +01:00
Faintful 4c6822b1c6 Merge remote-tracking branch 'upstream/main' into fix-super-potion-ingredients-tag 2026-01-14 19:46:54 +01:00
Faintful 2e6a4c7341 Change Hondew for Aguav 2026-01-14 19:28:49 +01:00
Micky 8edb5e1013 Fix Syrupy Apple missing from evolution items tag 2026-01-14 18:25:06 +00:00
Polymeta 6fb139cbb9 Merge branch 'pokemon-interactions-fixes' into 'main'
Pokémon Interaction fixes

See merge request cable-mc/cobblemon!2117
2026-01-14 00:03:33 +00:00
lion c81c95142b Pokémon Interaction fixes 2026-01-14 00:03:32 +00:00
Hiroku afde822860 Edit CHANGELOG.md [skip-ci] 2026-01-12 10:50:47 +00:00
Hiroku 474c386535 Merge branch 'main' into 'main'
Fix Fresh Start Mochi EV Reset

See merge request cable-mc/cobblemon!2115
2026-01-12 10:48:40 +00:00
RedGenesectNinja 3963e83717 Implemented Ducklett and Swanna's cries with placeholder animations 2026-01-12 01:04:46 -06:00
Konoha 7893e365da yolo: drop item on give_item if inventory is full & sneak in a fix for active_dialogue 2026-01-11 18:50:52 -05:00
ToothCollector 71b9ae8dee add fix to CHANGELOG.md 2026-01-11 21:31:22 +02:00
ToothCollector b87b47d62a Merge branch 'main' of https://gitlab.com/cable-mc/cobblemon 2026-01-11 21:28:04 +02:00
Hiroku 2bb85718d7 Merge branch 'main' into 'main'
Fix Adorn compatibility

See merge request cable-mc/cobblemon!2112
2026-01-11 12:21:25 +00:00
Tate Frost ee187d7b89 Delete file + update Adorn resource pack metadata
- Delete config file that was committed in error
- Update Adorn resource pack metadata to reflect new wood type
2026-01-10 22:30:27 -06:00
Hiroku b6a7051b2c Fixed galarian weezing blink crash, closes #1800 2026-01-11 04:28:37 +00:00
Tate Frost 3532cac278 Update CHANGELOG.md 2026-01-10 22:18:01 -06:00
Hiroku 1447ab25b4 Merge branch 'CombeeWorldGenFix' into 'main'
CombeeWorldGenFix

See merge request cable-mc/cobblemon!2071
2026-01-11 02:55:56 +00:00
Hiroku 9a06ca78c6 Merge branch 'fix-mongo-driver' into 'main'
Fix MongoDB ClassNotFoundException by including drivers at runtime

See merge request cable-mc/cobblemon!2098
2026-01-11 02:47:10 +00:00
Hiroku f6ba70634b Merge branch 'double-battle-gimmicks' into 'main'
Fix double battle gimmicks not being processed

See merge request cable-mc/cobblemon!2094
2026-01-11 02:46:36 +00:00
Hiroku 148fd04a93 Merge branch 'molang-evaluate_expression' into 'main'
molang addition: exception handling for run_molang

See merge request cable-mc/cobblemon!2102
2026-01-11 02:12:07 +00:00
Hiroku 17f60623ee Merge branch 'effect-id-regex-update' into 'main'
Custom messages for datapack moves and abilities

See merge request cable-mc/cobblemon!2106
2026-01-11 02:08:42 +00:00
Hiroku c5ece27c11 Merge branch '1861-galarien_mrmime_mount' into 'main'
Fix Galarian Mr. Mime incorrectly being rideable.

Closes #1861

See merge request cable-mc/cobblemon!2107
2026-01-11 02:02:14 +00:00
Hiroku 296ccd1a76 Merge branch 'main' into 'main'
Fix pasture block incorrect hit-box when facing north and south.

See merge request cable-mc/cobblemon!2109
2026-01-11 02:01:44 +00:00
ToothCollector 82a4f71f24 fix(FreshStartMochiItem.kt): Set all Pokemon's EVs to zero with the appropriate method. 2026-01-10 17:10:15 +02:00
Jerzykv 2cb1cf2f83 tiny fix on Togekiss 2026-01-10 13:36:40 +01:00
阿鱼 f13a4e0e24 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/zh_Hans/
2026-01-08 18:45:37 +00:00
Dragonwhisper92 17e1bbd6a1 oops changelog extra '-' removed 2026-01-07 20:00:26 +00:00
Dragonwhisper92 c36ff0a0fe ball_icon resource location changes
Changed the resource location of pokeball icons in the gui to use the balls namespace instead of cobblemon, allowing mods to properly use their own name space for these textures
2026-01-07 19:57:36 +00:00
Tate Frost 08b58e2f78 Fix Adorn compatibility
- Fixed Apricorn block model rotation for counters/sinks/cupboards/drawers to match that of most recent version of Adorn
- Added Saccharine wood + Adorn compatibility
2026-01-07 02:04:52 -06:00
Merv 62d2b8db2d Edit CHANGELOG.md 2026-01-06 18:47:45 +00:00
Merv 3869f7ed93 Merge branch cobblemon:main into main 2026-01-05 23:09:08 +00:00
Jerzykv 8dbb350230 minor fix to apricorn textures 2026-01-05 21:56:02 +01:00
Dragonwhisper92 c9c44872d6 Updated Changelog. 2026-01-05 19:35:05 +00:00
Dragonwhisper92 cae53efedc Updated ActivateInstruction.kt to have an optional message parameter. 2026-01-05 19:14:40 +00:00
Jerzykv 52f9ffb1f6 fixed Politoed's drop again. thanks scraper 2026-01-05 12:23:07 +01:00
Polymeta 5fac24cee8 Merge branch 'Molang-SimpleDateFormat-Fix' into 'main'
Fixed Molang functions:

See merge request cable-mc/cobblemon!2105
2026-01-05 11:22:45 +00:00
Polymeta 1954dfec80 Merge branch '1784-riding_stats_order' into 'main'
Fix riding stat boosts to always render in the same order

Closes #1784

See merge request cable-mc/cobblemon!2108
2026-01-05 11:20:29 +00:00
Landon 2eea2b64c2 Merge branch 'bag-clause' into 'main'
Bag Clause (Preventing Item use in Battles)

See merge request cable-mc/cobblemon!1849
2026-01-05 08:12:36 +00:00
GDWR a127a93be8 Sort boosts when rendering to remove the jumping of stats between items
The sort uses the key. As `RidingStat` it sorts them by the enum value.
2026-01-04 20:27:42 +00:00
GDWR 9b32f85817 Set an empty riding definition for Galarian Mr. Mime to avoid inheriting the default definition 2026-01-04 17:28:02 +00:00
Dragonwhisper92 ec139d2677 Updated Effect.kt to use a custom REGEX entry 2026-01-04 16:46:44 +00:00
Landon Wills 4327be0cf9 Omit bag clause from getting sent to showdown 2026-01-04 02:22:32 -05:00
Landon Wills c25c832b31 Remove import 2026-01-04 01:21:20 -05:00
Landon Wills eabaabc535 Move bag clause check to bag items instead of pokemon entity 2026-01-04 01:19:26 -05:00
Dragonwhisper92 9acf97f5a0 Updated CHANGELOG.md 2026-01-04 00:18:24 +00:00
Dragonwhisper92 24fc212c76 Updated CHANGELOG.md 2026-01-04 00:15:32 +00:00
Hiroku 04f88bdd63 Merge branch 'form-change-move-sanitization-fix' into 'main'
Fix the improper sanitization of moves on mons with compounding properties

Closes #1853 and #1846

See merge request cable-mc/cobblemon!2085
2026-01-04 00:05:26 +00:00
Dragonwhisper92 a4eb58aa57 Fixed molang functions:
- date_local_time
- date_of
- date_is_after
too use the correct SimpleDateFormat of "dd/MM/yyyy"
2026-01-03 23:40:12 +00:00
Lucas 030195e3ff Translated using Weblate (French)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-03 08:52:44 +00:00
Yasito d21a52ae68 Translated using Weblate (French)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-03 08:52:43 +00:00
Yasito 3c1f1dbacb Translated using Weblate (French (Canada))
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr_CA/
2026-01-03 08:52:42 +00:00
Merv 6704d4e5ee Maybe fix pasture block incorrect hitbox 2026-01-03 02:05:03 +00:00
Lucas 787c4851ff Translated using Weblate (French)
Currently translated at 99.9% (9021 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 21:02:21 +00:00
Yasito bf6e7fafd3 Translated using Weblate (French)
Currently translated at 99.9% (9021 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 21:02:20 +00:00
Lucas ea6df764c0 Translated using Weblate (French)
Currently translated at 99.9% (9020 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 21:01:41 +00:00
Yasito f242de272d Translated using Weblate (French)
Currently translated at 99.9% (9020 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 21:01:41 +00:00
Lucas 5232e2e9bc Translated using Weblate (French)
Currently translated at 99.9% (9019 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 21:01:09 +00:00
Yasito ca35898243 Translated using Weblate (French)
Currently translated at 99.9% (9019 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 21:01:08 +00:00
Lucas 869457d235 Translated using Weblate (French)
Currently translated at 99.9% (9018 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 21:00:52 +00:00
Yasito 6736c8160b Translated using Weblate (French)
Currently translated at 99.9% (9018 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 21:00:51 +00:00
Lucas 243c50550d Translated using Weblate (French)
Currently translated at 99.9% (9017 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 21:00:37 +00:00
Yasito fe7d3ef23a Translated using Weblate (French)
Currently translated at 99.9% (9017 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 21:00:36 +00:00
Lucas 57ad77fc39 Translated using Weblate (French)
Currently translated at 99.9% (9016 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 21:00:10 +00:00
Lucas d70e7bd1b3 Translated using Weblate (French)
Currently translated at 99.9% (9015 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:59:34 +00:00
Yasito 254cc931ea Translated using Weblate (French)
Currently translated at 99.9% (9015 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:59:34 +00:00
Lucas 01fe5cffe8 Translated using Weblate (French)
Currently translated at 99.9% (9014 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:59:19 +00:00
Yasito 3e18995a37 Translated using Weblate (French)
Currently translated at 99.9% (9014 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:59:18 +00:00
Yasito ff161c9c9b Translated using Weblate (French)
Currently translated at 99.9% (9013 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:58:36 +00:00
Lucas 6b7b4f326d Translated using Weblate (French)
Currently translated at 99.9% (9013 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:58:35 +00:00
Lucas f50f7dec29 Translated using Weblate (French)
Currently translated at 99.8% (9012 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:58:17 +00:00
Yasito a5def20c58 Translated using Weblate (French)
Currently translated at 99.8% (9012 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:58:16 +00:00
Yasito b23204f9c9 Translated using Weblate (French)
Currently translated at 99.8% (9011 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:57:51 +00:00
Lucas 4972de0aaa Translated using Weblate (French)
Currently translated at 99.8% (9011 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:57:50 +00:00
Lucas 58a1f3d7b4 Translated using Weblate (French)
Currently translated at 99.8% (9010 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:57:33 +00:00
Yasito f516edb858 Translated using Weblate (French)
Currently translated at 99.8% (9010 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:57:32 +00:00
Lucas 161aa952fa Translated using Weblate (French)
Currently translated at 99.8% (9008 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:55:43 +00:00
Yasito 3b26a13ad6 Translated using Weblate (French)
Currently translated at 99.8% (9008 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:55:43 +00:00
Lucas 014189217a Translated using Weblate (French)
Currently translated at 99.8% (9007 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:55:29 +00:00
Yasito 978debb40c Translated using Weblate (French)
Currently translated at 99.8% (9007 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:55:28 +00:00
Lucas 5be707811e Translated using Weblate (French)
Currently translated at 99.8% (9006 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:55:13 +00:00
Yasito ac44a98f8a Translated using Weblate (French)
Currently translated at 99.8% (9006 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:55:12 +00:00
Lucas 03ecc2ae8d Translated using Weblate (French)
Currently translated at 99.8% (9005 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:54:13 +00:00
Yasito 1518379107 Translated using Weblate (French)
Currently translated at 99.8% (9005 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:54:12 +00:00
Yasito a58c8c59e1 Translated using Weblate (French)
Currently translated at 99.8% (9004 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:53:58 +00:00
Lucas 37959b5048 Translated using Weblate (French)
Currently translated at 99.8% (9004 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:53:58 +00:00
Yasito cbcc9fc6f1 Translated using Weblate (French (Canada))
Currently translated at 99.7% (9002 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr_CA/
2026-01-02 20:53:57 +00:00
Yasito d236aaa7da Translated using Weblate (French)
Currently translated at 99.7% (9000 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:50:35 +00:00
Lucas a2cd5a671c Translated using Weblate (French)
Currently translated at 99.7% (9000 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:50:35 +00:00
Lucas 1d6a45bf4d Translated using Weblate (French)
Currently translated at 99.7% (8998 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:49:59 +00:00
Yasito 5bfe703ed7 Translated using Weblate (French)
Currently translated at 99.7% (8998 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:49:58 +00:00
Yasito fc8485388e Translated using Weblate (French)
Currently translated at 99.7% (8997 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:45:38 +00:00
Lucas a5593b3131 Translated using Weblate (French)
Currently translated at 99.7% (8997 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:45:38 +00:00
Yasito 9541bf3267 Translated using Weblate (French (Canada))
Currently translated at 99.6% (8991 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr_CA/
2026-01-02 20:45:37 +00:00
Lucas 1037fe0a0e Translated using Weblate (French)
Currently translated at 99.6% (8989 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:38:19 +00:00
Yasito a6eea3198a Translated using Weblate (French)
Currently translated at 99.6% (8989 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:38:19 +00:00
Yasito b28e2313a8 Translated using Weblate (French (Canada))
Currently translated at 99.6% (8988 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr_CA/
2026-01-02 20:38:19 +00:00
Lucas 0f0b4e6c86 Translated using Weblate (French)
Currently translated at 99.6% (8988 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:37:45 +00:00
Yasito e033467054 Translated using Weblate (French)
Currently translated at 99.6% (8988 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:37:45 +00:00
Yasito 0f092bf6bf Translated using Weblate (French (Canada))
Currently translated at 99.6% (8987 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr_CA/
2026-01-02 20:37:44 +00:00
Lucas 66ba762360 Translated using Weblate (French)
Currently translated at 99.6% (8987 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:37:01 +00:00
Yasito c7c11838a0 Translated using Weblate (French)
Currently translated at 99.6% (8987 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2026-01-02 20:37:01 +00:00
Yasito 301d80fabe Translated using Weblate (French (Canada))
Currently translated at 99.6% (8986 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr_CA/
2026-01-02 20:37:00 +00:00
Paul 95d238ada0 Fixed multi-spawns in a single pass using the same bucket for all of them 2026-01-02 22:38:46 +10:00
Hiroku c88087de29 Merge branch 'main' into 'main'
Small improvements to the Battle AI

See merge request cable-mc/cobblemon!2101
2026-01-02 00:38:26 +00:00
Konoha fade5502d9 molang additions: Move struct accessor and player inventory utils 2026-01-01 15:31:40 -05:00
Konoha 86bb802140 adjustment: exception handling to run_molang 2026-01-01 11:44:29 -05:00
GGavino1 6b644b384d Updated CHANGELOG.md to include AI changes 2026-01-01 15:00:15 +00:00
Konoha c370af8914 feature: molang function for safe evaluation of booleans 2025-12-31 22:23:50 -05:00
GGavino1 1cd6d2bfc7 Edit StrongBattleAI.kt 2025-12-31 23:15:52 +00:00
Test fc720916fb wall block properties 2025-12-31 00:24:39 -05:00
Jerzykv ce55035408 changelog 2025-12-30 14:46:38 +01:00
Jerzykv d4665ddaeb implemented Falinks remodel 2025-12-30 14:44:46 +01:00
Jerzykv 4375c6f667 fixed Revavroom's hitbox. Who implemented this guy (it was me) 2025-12-30 14:43:24 +01:00
Konoha 1edeae2a60 fix: let's actually fix 2025-12-27 09:48:49 -05:00
mkh 409f1056b9 fix: Add MongoDB driver dependency to Fabric/NeoForge build configuration and shade it into the mod jar to resolve ClassNotFoundException. 2025-12-26 17:31:46 +08:00
Landon Wills d8c470db3d Oops, forgot a file 2025-12-23 01:28:50 -05:00
Landon Wills c642c4b84b Update changelog 2025-12-23 01:22:01 -05:00
Landon Wills 0b80f7e547 Make the gimmick buttons work with the back button 2025-12-23 01:18:35 -05:00
Landon Wills d543f45607 Fix double battle gimmicks not being processed, prevent people from trying to use the same gimmick multiple times on the same turn 2025-12-23 00:43:31 -05:00
Test e5699c1cdc plaques, policies 2025-12-22 01:34:10 -05:00
RedGenesectNinja f4a7c6e873 Implemented the remodels for Chingling and Chimecho 2025-12-21 22:40:17 -06:00
Konoha 2b4fdfd219 changelog: sanitization fix 2025-12-21 15:40:17 -05:00
geumu 3bef1dad4a Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-20 19:34:20 +00:00
geumu da50ee59cf Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-20 14:40:56 +00:00
Hiroku 1a3b655d3f Merge branch 'the-tiniest-of-tiny-changes' into 'main'
Edit pokemon_model.json

See merge request cable-mc/cobblemon!2091
2025-12-20 12:41:36 +00:00
dhi_holo 1f5f278970 Edit pokemon_model.json 2025-12-20 21:25:12 +10:00
Hiroku 0b4eb5239a Merge branch 'in-flight-dismounting-config' into 'main'
Add enable in-flight dismounting config

See merge request cable-mc/cobblemon!2079
2025-12-20 06:01:12 +00:00
lion 118e8f6205 Add enable in-flight dismounting config 2025-12-20 06:01:12 +00:00
Hiroku cf126c5906 Merge branch 'molang-is-love-molang-is-life' into 'main'
Fixed the `run_script` Molang function to not fail if the environment's context is null.

See merge request cable-mc/cobblemon!2090
2025-12-20 01:59:22 +00:00
Spicy 645cf61911 Fixed the run_script Molang function to not fail if the environment's context is null. 2025-12-19 19:48:27 -06:00
arvid6 6e8985d97e Translated using Weblate (Spanish)
Currently translated at 84.2% (7599 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/es/
2025-12-19 08:15:16 +00:00
geumu 19c4c0d50d Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-19 08:15:15 +00:00
阿鱼 b23110950e Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/zh_Hans/
2025-12-19 08:15:14 +00:00
Watuneni iteite 60c213c44b Translated using Weblate (Japanese)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-12-19 08:15:13 +00:00
Hiroku 87d5ede3dd Merge branch 'RestoreCombeeLeavesPathing' into 'main'
RestoreCombeeLeavesPathing

See merge request cable-mc/cobblemon!2046
2025-12-19 07:36:50 +00:00
Hiroku 203e5d89cd Merge branch 'label-consistency' into 'main'
Who are we if not consistent?

See merge request cable-mc/cobblemon!1985
2025-12-19 07:36:21 +00:00
Konoha ce611cc61c cleanup: fix redundancies in form change sanitization 2025-12-18 21:17:48 -05:00
geumu 3df8dea71b Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-18 17:09:19 +00:00
geumu 3cca50b7cc Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-18 07:49:53 +00:00
geumu 1ff5622694 Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-18 06:01:40 +00:00
JazzMcNade f6ad7ff865 Fix changelog 2025-12-17 21:08:46 -06:00
JazzMcNade d69627a9c8 Merge branch 'main' into CombeeWorldGenFix 2025-12-17 20:05:09 -06:00
Alexis Salgueiro 411f388bd3 Translated using Weblate (French)
Currently translated at 99.5% (8984 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2025-12-18 00:21:32 +00:00
geumu 5b8d35604a Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-18 00:21:30 +00:00
geumu 619d002bc4 Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-17 07:19:25 +00:00
geumu 7049656f64 Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-16 06:33:05 +00:00
geumu e6e7c2982d Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-15 12:47:01 +00:00
Paul aafdb7688a Added more molang expressions to a bunch of composed pool properties, fixed some bugs 2025-12-15 14:22:59 +10:00
Paul 73b53f9702 Added egg move selector 2025-12-15 12:07:06 +10:00
Paul 33317a57c7 more doco changes [skip-ci] 2025-12-14 20:33:34 +10:00
Paul 72d8f26cdb Change silly default example weight to the actual default weight 2025-12-14 20:27:30 +10:00
Paul aea1afe145 Rearrange doco 2025-12-14 20:26:21 +10:00
Paul 0fec45b7ea Added npcAspects condition for party pool entries 2025-12-14 20:24:52 +10:00
Paul af98b823aa Documentation 2025-12-14 20:13:04 +10:00
Paul 07d9da2ca9 Added last_suitable_offensive move selector, added alpha moveset builder 2025-12-14 17:29:47 +10:00
Paul c2c86b2ef1 Stack all the cobblemon:move_weights.json from any datapacks onto one big map, like tags 2025-12-14 17:05:58 +10:00
JazzMcNade 54e8763470 Fixed a bug in which Combees could become stuck inside of leaves 2025-12-13 21:17:59 -06:00
JazzMcNade db34687df0 Merge branch 'main' into RestoreCombeeLeavesPathing 2025-12-13 21:11:57 -06:00
Konoha 7f008d85ff fix: form_change move updates no longer sanitize the sets of compounding property mons 2025-12-13 18:37:37 -05:00
Polymeta 06ebf51078 Merge branch 'Wii-Break-It-Wii-Fix-It' into 'main'
Boat Tags and Mint Leaves

Closes #1855

See merge request cable-mc/cobblemon!2082
2025-12-13 22:14:32 +00:00
Polymeta 39c5bcd73c Merge branch 'even-more-seasonings' into 'main'
Adding Missing Effects for Seasonings

See merge request cable-mc/cobblemon!1991
2025-12-13 22:13:24 +00:00
Ceejay 8f5f158b60 Adding Missing Effects for Seasonings 2025-12-13 22:13:24 +00:00
dhi_holo 3bc1e35125 Update CHANGELOG.md 2025-12-13 18:42:50 +10:00
dhi_holo 460bedddae Boat Tags
Resolves #1855
2025-12-13 18:12:24 +10:00
dhi_holo 8b64c315f3 Merge branch 'main' into Wii-Break-It-Wii-Fix-It 2025-12-13 17:51:15 +10:00
dhi_holo 7c824a2b72 OVERSIGHTS 2025-12-13 17:50:57 +10:00
Paul f197ac5077 Removed a bunch of duplicates from is_held_item because it looks whack. 2025-12-12 16:24:30 +10:00
Hiroku e0be669482 Merge branch 'patch-1' into 'main'
Fix Soothe Bell not tagged as `cobblemon:held/is_held_item`

See merge request cable-mc/cobblemon!2081
2025-12-12 06:17:17 +00:00
Watuneni iteite 82686b0511 Translated using Weblate (Japanese)
Currently translated at 99.4% (8976 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-12-12 03:20:38 +00:00
JPAKx4 f4eca30163 Fix merge issues 2025-12-11 10:40:53 -06:00
arvid6 2814183012 Translated using Weblate (Spanish)
Currently translated at 84.2% (7597 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/es/
2025-12-11 16:38:11 +00:00
JPAKx4 bbc3c2a8d6 Merge branch 'main' into default-offest
# Conflicts:
#	common/src/main/kotlin/com/cobblemon/mod/common/client/render/camera/MountedCameraRenderer.kt
2025-12-11 10:27:50 -06:00
geumu 7eeb7feeae Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-11 06:27:41 +00:00
geumu b2f850c1c8 Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-10 15:57:39 +00:00
Micky 08ea21fff4 Fix Soothe Bell not tagged as cobblemon:held/is_held_item 2025-12-10 13:27:23 +00:00
JazzMcNade 9dac604a93 Merge branch 'main' into RestoreCombeeLeavesPathing 2025-12-10 00:20:49 -06:00
Nico Merenq 23db346070 Translated using Weblate (Russian)
Currently translated at 77.7% (7012 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ru/
2025-12-09 19:49:13 +00:00
geumu d3b7d739bb Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-09 19:49:11 +00:00
Polymeta 28c0f8ab7d Merge branch 'item-interactions-sync' into 'main'
Item interactions sync

See merge request cable-mc/cobblemon!2068
2025-12-08 16:08:48 +00:00
lion fcbe0db568 Item interactions sync 2025-12-08 16:08:48 +00:00
Polymeta 83cefe476e Merge branch 'partner-mark' into 'main'
Add Partner Mark

See merge request cable-mc/cobblemon!2034
2025-12-08 15:57:21 +00:00
Guilherme Pereira 690ccbd260 refactor check riding statistics method for server player mixin 2025-12-08 12:06:20 -03:00
Polymeta 63846d53fb Merge branch 'main' into 'main'
Fixed shiny Pokémon not respecting the silent flag on spawn.

See merge request cable-mc/cobblemon!2064
2025-12-08 13:07:25 +00:00
Polymeta 86cb865742 fixed mochi items being consumed twice
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-12-08 13:01:14 +00:00
Polymeta 1a0c9b6a82 Merge branch 'malformed-jsons-no-more' into 'main'
Fixes a crash caused by malformed riding prespectives json file when trying to ride a pokemon

See merge request cable-mc/cobblemon!2077
2025-12-08 12:59:22 +00:00
Polymeta 985eb103fe Merge branch 'sterr/ride-sound-changes' into 'main'
New ride sound stuff

See merge request cable-mc/cobblemon!2065
2025-12-08 11:44:22 +00:00
Polymeta aec76a78da Merge branch 'restore_adorn_texturing' into 'main'
Restore adorn texturing

Closes #1833

See merge request cable-mc/cobblemon!2078
2025-12-08 11:36:41 +00:00
geumu eb6c134f4b Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-08 10:48:16 +00:00
Paul 22733e7117 Added more advanced moveset and party construction for NPCs 2025-12-08 19:31:48 +10:00
Jacob Hartmann 17f3edf2be Changelog 2025-12-07 20:35:11 -07:00
Jacob Hartmann 9c5bfda20f Updating texture paths 2025-12-07 20:34:09 -07:00
geumu aa0b8d88e8 Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-07 12:09:09 +00:00
Gertimoshka 6d545fc1ca Fixes a crash caused by malformed riding prespectives json file when trying to ride a pokemon 2025-12-07 11:47:04 +02:00
Polymeta 9a8ad27122 Merge branch 'fix-spectator-crash' into 'main'
fix spectator crash

Closes #1847

See merge request cable-mc/cobblemon!2076
2025-12-07 08:39:18 +00:00
500-internal-server-error 9f1dbc9ff0 add notes to changelog 2025-12-07 07:58:55 +07:00
Polymeta 99942748b0 Merge branch 'entropydragon--spawn-bait-regional-forms' into 'main'
fix: use regional form data for spawn bait influence

See merge request cable-mc/cobblemon!2074
2025-12-07 00:49:48 +00:00
jasoncramer 64c34230bf chore: add to changelog 2025-12-06 19:28:41 -05:00
500-internal-server-error 36881b70be fix spectator crash 2025-12-06 20:51:48 +07:00
Deltric 3cf9425247 Fixed memory leak from battles not being cleaned up after ending. 2025-12-06 01:35:53 -06:00
jasoncramer 9b052478f6 fix: use regional form data for spawn bait influence 2025-12-05 14:23:05 -05:00
Hiroku 339f8339f0 Merge branch 'RestorePokemonItemPickup' into 'main'
Restore Pokemon item pickup

See merge request cable-mc/cobblemon!2073
2025-12-05 08:19:41 +00:00
Hiroku db9a802c3f Merge branch 'fix-spawner-crash' into 'main'
Prevents outside spawn influences from crashing the logical server.

See merge request cable-mc/cobblemon!2072
2025-12-05 07:45:29 +00:00
Carmendar b5578e72bd Merge branch 'main' into implement_pokemon 2025-12-05 01:30:51 -06:00
JazzMcNade e5666e7a82 Restore Pokemon item pickup. Remove unnecessary check in shouldBeSaved() 2025-12-05 01:30:20 -06:00
Carmendar 3aea0909a4 Merge branch 'main' into implement_pokemon 2025-12-05 01:27:25 -06:00
Teggy 5adef3336e Translated using Weblate (Spanish)
Currently translated at 84.2% (7597 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/es/
2025-12-05 06:23:53 +00:00
geumu 1a96b418bc Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-05 06:23:51 +00:00
geumu e373e86b60 Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-04 15:33:38 +00:00
geumu 6f3035184c Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-04 09:30:13 +00:00
Jerzykv 7ae8b1957c fixed Arcanine's texture having a line 2025-12-04 10:27:42 +01:00
Neovitalism a588213323 Update CHANGELOG.md 2025-12-04 03:05:03 -06:00
Neovitalism 56b2ba08ad Prevents outside influences from crashing the logical server.
Before when it was completed exceptionally w/ the pokesnack spawner, it would crash the server.
2025-12-04 03:03:57 -06:00
geumu 824b9718ee Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-04 08:07:32 +00:00
JazzMcNade 8540a73885 Fix for error occurring while populating bee nests with pokemon on world gen 2025-12-04 01:38:56 -06:00
Khvrram 7e6ca7be5e Merge branch cobblemon:main into main 2025-12-04 01:59:17 +00:00
geumu ef43f6c087 Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-03 21:23:55 +00:00
geumu c8a70caf79 Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-03 16:30:22 +00:00
Neovitalism 25ce784a60 Hopefully fixes changelog
Idk why gitlab didn't detect that when merging main
2025-12-03 10:17:48 -06:00
Neovitalism 5358cccb62 Merge branch 'main' into label-consistency 2025-12-03 10:09:41 -06:00
geumu ba11758e1f Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-03 12:53:16 +00:00
geumu f0412d6b1d Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-03 10:05:08 +00:00
Polymeta c43b3bbb4d Merge branch 'fix-can-drop-held-item' into 'main'
Fix can drop held item function

See merge request cable-mc/cobblemon!2069
2025-12-03 08:17:38 +00:00
lion d826aeeb07 Fix can drop held item function 2025-12-03 08:17:35 +00:00
Polymeta f90c6967ce Merge branch 'FixHoneyedLeavesInteraction' into 'main'
FixHoneyedLeavesInteraction

Closes #1832

See merge request cable-mc/cobblemon!2067
2025-12-03 08:15:26 +00:00
Jerzykv f5f87ce1a3 fixed Dewgong swimming in battle 2025-12-03 06:57:40 +01:00
Jerzykv f28b269734 fixed Camerupt jumping in battle 2025-12-03 06:57:31 +01:00
Jerzykv d545efd843 adjusted Tinkaton & Tinkatuff profile scaling 2025-12-03 06:57:07 +01:00
JazzMcNade b9cf18adbd Changelog 2025-12-02 20:16:56 -06:00
JazzMcNade 30650e1f9f Code simplication 2025-12-02 20:12:37 -06:00
JazzMcNade eaa558a7c8 Check for creative on using empty bottle 2025-12-02 00:36:53 -06:00
JazzMcNade 0dc725e513 Can only receive honey at max age. Adds result to hand if possible. Checks for creative and server/client 2025-12-02 00:28:23 -06:00
Zack the Ghast f8682c2123 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.2% (8866 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/pt_BR/
2025-12-02 05:47:12 +00:00
geumu f62e31a1eb Translated using Weblate (Korean)
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-02 05:46:50 +00:00
Watuneni iteite ea623b08a6 Translated using Weblate (Japanese)
Currently translated at 99.4% (8971 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-12-02 05:46:48 +00:00
Sterrezo a575c6bc3d Update CHANGELOG.md 2025-12-01 15:47:34 +01:00
Sterrezo 9c102a7850 Merge branch 'main' into sterr/ride-sound-changes 2025-12-01 15:45:40 +01:00
Sterrezo 22a1eaab19 Volume balancing 2025-12-01 15:35:22 +01:00
Sterrezo 0118ace5fc Implemented stereo flight loops 2025-12-01 15:09:37 +01:00
Sterrezo d4a93abbf7 Added support for submerged ride sounds 2025-12-01 14:27:55 +01:00
geumu ce3d5a413c Translated using Weblate (Korean)
Currently translated at 95.8% (8650 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-01 13:03:30 +00:00
Sterrezo d0ee3f3e9d Added an option for ride sounds to not play for the passengers 2025-12-01 13:56:56 +01:00
JazzMcNade 698fa9dff9 Merge branch 'main' into RestoreCombeeLeavesPathing 2025-12-01 00:46:50 -06:00
geumu d23fd6e23c Translated using Weblate (Korean)
Currently translated at 84.1% (7595 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-12-01 05:33:07 +00:00
Khvrram 46ae53f565 Merge branch cobblemon:main into main 2025-12-01 04:25:52 +00:00
Hiroku 7d4e5892ba Merge branch 'main' into 'main'
Orientation after crash fix

See merge request cable-mc/cobblemon!2061
2025-12-01 00:23:40 +00:00
Khvrram f9de415020 Orientation after crash fix 2025-12-01 00:23:39 +00:00
geumu 59395a254d Translated using Weblate (Korean)
Currently translated at 81.2% (7328 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-11-30 20:59:27 +00:00
geumu 5d7c6c8d6f Translated using Weblate (Korean)
Currently translated at 80.6% (7276 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-11-30 08:10:30 +00:00
Khurram Valiyev 4d123b6e4b Fixed shiny Pokémon not respecting the silent flag on spawn. 2025-11-29 22:50:52 -05:00
Polymeta 54dc2e7f6f tidy up changelog [skip-ci] 2025-11-29 12:57:24 +00:00
Polymeta 1d60ba093f Merge branch 'convention-tags-not-being-consistent-grrrrrrrr' into 'main'
Why

See merge request cable-mc/cobblemon!2060
2025-11-29 11:53:50 +00:00
Hiroku 0fef74dce7 Edit OneToOneFileStoreAdapter.kt 2025-11-29 09:46:27 +00:00
violet 6529293d21 Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-29 02:44:47 -05:00
violet a1fce624ea something was forgotten in the changelog... 2025-11-29 02:44:34 -05:00
Hiroku b08647bb32 Revert "Merge branch 'main' into 'main'"
This reverts merge request !2062
2025-11-29 04:26:57 +00:00
Hiroku de9e89f040 Merge branch 'main' into 'main'
Editar en_us.json

See merge request cable-mc/cobblemon!2062
2025-11-29 04:25:10 +00:00
Ezequiel Neri Ferreira 84a29ebc2a Editar en_us.json 2025-11-29 04:22:49 +00:00
Paul 37b13ae959 Unsnaps your shot 2025-11-29 12:44:40 +10:00
dhi_holo 1cacbe8306 Why
It's called Shears, that's the item name, why does NeoForge use a Shear tag instead
Atleast Fabric has that tag as well so it's not a hard fix
2025-11-29 12:30:45 +10:00
Paul 12d224d15a Reinforce switchForm for crashes 2025-11-29 11:25:58 +10:00
Paul a57eee3afc Changelog updates [skip-ci] 2025-11-29 10:46:19 +10:00
geumu b7773f7471 Translated using Weblate (Korean)
Currently translated at 79.1% (7144 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-11-28 23:27:54 +00:00
Hiroku 57595c999c Merge branch 'remove-snapshot' into 'main'
Remove snapshot for release

See merge request cable-mc/cobblemon!2059
2025-11-28 23:24:59 +00:00
Landon Wills 83615b7eb3 Prepare release 2025-11-28 17:54:24 -05:00
Polymeta c53c544a8b Merge branch 'implement_pokemon' into 'main'
Last minute changes

See merge request cable-mc/cobblemon!2058
2025-11-28 22:23:03 +00:00
Kenji_64 c7c272c79c Had to fix Dragapult (again) 2025-11-28 21:04:50 +01:00
Kenji_64 c759161367 Added cries to Eelektrik and Eelektross 2025-11-28 21:01:27 +01:00
Landon 19b7b4804f Merge branch 'add-defend-owner-task' into 'main'
Make Pokémon only attack mobs targeting the player

See merge request cable-mc/cobblemon!2051
2025-11-28 18:30:05 +00:00
Landon 3a005cc813 Merge branch '1.7.1-spawns' into 'main'
Updated Dratini line spawns.

See merge request cable-mc/cobblemon!2055
2025-11-28 18:28:24 +00:00
Landon a8e16efa48 Merge branch 'jet-handling' into 'main'
Increase pitch rotation of Jet

See merge request cable-mc/cobblemon!2054
2025-11-28 18:08:20 +00:00
Veraxiel c3ddfce0a7 Updated Dratini spawn. 2025-11-28 12:38:53 -05:00
Landon Wills 9500f7a1f3 adjust wording 2025-11-28 12:21:44 -05:00
Landon Wills 83a8d92e3f Add changelog 2025-11-28 12:21:08 -05:00
Landon Wills e105c5d4d7 Remove multipliers, adjust expression 2025-11-28 12:17:42 -05:00
Landon Wills 3d816f57e2 Increase pitch rotation of Jet 2025-11-28 12:13:55 -05:00
Landon 1e6dcfba9b Merge branch 'change-freelook-default' into 'main'
Change free look binding default to Left Alt

See merge request cable-mc/cobblemon!2053
2025-11-28 16:30:35 +00:00
Polymeta e59d755d32 Merge branch 'implement_pokemon' into 'main'
Various implementation fixes

See merge request cable-mc/cobblemon!2050
2025-11-28 15:38:40 +00:00
geumu f927e47bfe Translated using Weblate (Korean)
Currently translated at 77.9% (7030 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-11-28 15:25:08 +00:00
Watuneni iteite 48a37b2018 Translated using Weblate (Japanese)
Currently translated at 99.2% (8953 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-11-28 15:25:07 +00:00
Guilherme Pereira c155b5575d change free look binding default to left alt 2025-11-28 09:35:13 -03:00
Jerzykv 9c6b8b231a oops 2 2025-11-28 13:30:16 +01:00
Jerzykv 92b404dac4 oops 2025-11-28 13:27:19 +01:00
Jerzykv 8a1c536d68 Fixed Furfrou's pink and magenta trims being swapped 2025-11-28 12:56:07 +01:00
Jerzykv 04719d22a5 Fixed a tiny issue with Ceruledge's poser 2025-11-28 12:55:54 +01:00
Jerzykv dacc312756 Implemented Tinkatink line remodels 2025-11-28 12:55:26 +01:00
Polymeta ee4a3d46ba Merge branch '1.7.1-spawns' into 'main'
Updated some spawns.

See merge request cable-mc/cobblemon!2052
2025-11-28 09:31:16 +00:00
violet 80b11f1c3d Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-28 01:20:00 -05:00
Carmendar ddcf4eea7b Heracross and Mudsdale jump fix. 2025-11-28 00:16:50 -06:00
violet 6a0681e4c6 fixed cyclizar's textures 2025-11-27 23:37:39 -05:00
Veraxiel 8ebb378da7 Updated some spawns. 2025-11-27 23:31:32 -05:00
violet e92a878074 fixed boltund's model 2025-11-27 23:30:22 -05:00
Paul 0b97b41c08 Merge remote-tracking branch 'origin/main' 2025-11-28 12:27:10 +10:00
Paul 20fe732cfc Potentially minor perf improvement on following walk targets that are unreachable 2025-11-28 12:26:26 +10:00
Carmendar 0575aa29d9 Doubled high end land mount stamina. 2025-11-27 18:43:20 -06:00
Guilherme Pereira 29b560d244 Merge branch 'main' of gitlab.com:cable-mc/cobblemon into partner-mark 2025-11-27 18:21:19 -03:00
Guilherme Pereira dcc98b4a54 rename mob to living entity 2025-11-27 18:19:49 -03:00
Guilherme Pereira d3ce455391 prevents defend owner task from targeting players 2025-11-27 18:10:59 -03:00
Kenji_64 85c6b633d8 Fixed Dragapult's eyes clippings 2025-11-27 22:03:48 +01:00
Guilherme Pereira 956b599060 update CHANGELOG.md 2025-11-27 17:51:11 -03:00
Jerzykv f88552f99b Gholdengo no longer dances in air. what a shame (fixed flying battle pose issue) 2025-11-27 21:46:35 +01:00
Carmendar e2b559f3eb Merge branch 'main' into implement_pokemon 2025-11-27 14:42:31 -06:00
Carmendar 39816d33b6 Rufflet eepy cry and battle pose fix 2025-11-27 14:38:39 -06:00
Landon 0cf2a23fd4 Merge branch 'cobble-land-fixes' into 'main'
Land adjustments for a smoother riding experience. And bug fixes to land rides

See merge request cable-mc/cobblemon!2039
2025-11-27 19:31:24 +00:00
Guilherme Pereira c4d8eae47e add defend owner task once again 2025-11-27 12:16:54 -03:00
Polymeta 2a5a292386 fix saccharine hanging signs sometimes dropping oak wood signs
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-11-27 11:27:58 +00:00
阿鱼 f6b2da6ed6 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (9022 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/zh_Hans/
2025-11-27 11:09:47 +00:00
Polymeta e8a09f5c48 Merge branch 'json-fixing' into 'main'
JSON Fixes

Closes #1819

See merge request cable-mc/cobblemon!2048
2025-11-27 10:42:07 +00:00
dhi_holo e82193cdfd Update interact_with_pokemon.json
Removed accidental flag
2025-11-27 19:56:33 +10:00
dhi_holo 87e7cdebf6 Fabric Tags that aren't true c tags are the bane of my existence
Fixed the concrete tags to behave properly, changelog
2025-11-27 15:25:53 +10:00
dhi_holo 9ca84d852f Update interact_with_pokemon.json
Should close #1819
2025-11-27 14:22:06 +10:00
Carmendar 8194f93d13 Merge branch 'main' into implement_pokemon 2025-11-26 20:54:41 -06:00
Carmendar c158ec33fa Braviary sleep cry fix 2025-11-26 20:53:46 -06:00
Carmendar 1e27c7494e Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-26 20:52:27 -06:00
Carmendar 54a1f8d932 Braviary battle pose fix 2025-11-26 20:51:45 -06:00
Watuneni iteite 275779b20e Translated using Weblate (Japanese)
Currently translated at 99.2% (8953 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-11-26 22:24:14 +00:00
Polymeta 635f29e1ac fix glass bottle not being returned when using honey on saccharine leaves
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-11-26 22:08:12 +00:00
Polymeta db2001b60a Merge branch 'attempted-jvm-fix' into 'main'
Fixed JVM crash from campfire pot

Closes #1795

See merge request cable-mc/cobblemon!2040
2025-11-26 21:40:30 +00:00
Hiroku b5098a2ad0 Fixed JVM crash from campfire pot 2025-11-26 21:40:29 +00:00
阿鱼 2102f8ff88 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.6% (8991 of 9022 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/zh_Hans/
2025-11-26 15:42:52 +00:00
Kenji_64 d2af0c9332 Fixed Swampert line's missing recoils crashing the game 2025-11-26 15:40:40 +01:00
Polymeta 8c939ee276 Merge branch 'lets-wind-down' into 'main'
Fixes wind charges recalling pastured pokémon

Closes #1805

See merge request cable-mc/cobblemon!2042
2025-11-26 13:27:12 +00:00
Neovitalism 645b8993c8 Fixes wind charges recalling pastured pokémon 2025-11-26 13:27:12 +00:00
Polymeta d96799cac7 Merge branch 'feature/neoforge-recipe-book' into 'main'
split recipebooktype and category across fabric and neoforge so we can leverage neoforge APIs

Closes #1789 and #1787

See merge request cable-mc/cobblemon!2044
2025-11-26 12:44:06 +00:00
Polymeta b822b626c4 split recipebooktype and category across fabric and neoforge so we can leverage neoforge APIs 2025-11-26 12:44:05 +00:00
Polymeta 4d9b182fb3 fix hearty grains crashing if a block is in the way
Closes #1818

Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-11-26 11:59:25 +00:00
Polymeta 88d155eaf4 [skip-ci] more changelog
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-11-26 11:33:57 +00:00
Paul af96469b7a changelog [skip-ci] 2025-11-26 21:17:15 +10:00
Paul 65f1e7a3be Merge remote-tracking branch 'weblate/lang-updates' 2025-11-26 21:14:31 +10:00
Kenji_64 379a566d71 Fixed Ditto being silent 2025-11-26 11:52:43 +01:00
Kenji_64 f30ff739ed Fixed Metagross' ground_idle not looping properly (again) >:( 2025-11-26 11:36:23 +01:00
阿鱼 16cd2de02f Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.4% (8973 of 9019 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/zh_Hans/
2025-11-26 10:03:51 +00:00
Polymeta 22a44bd7e9 Merge branch 'ImprovedPastureBees' into 'main'
Improved Pasture Combees and More

See merge request cable-mc/cobblemon!2043
2025-11-26 09:36:28 +00:00
Polymeta d37bd4ef03 Merge branch 'OmnipathNavigationCrashFix' into 'main'
OmnipathNavigationCrashFix

See merge request cable-mc/cobblemon!2045
2025-11-26 09:34:03 +00:00
Hiroku a3d9a10f2e Merge branch 'wallpaper' into 'main'
Remove auto generated screen glow due to rendering issues

See merge request cable-mc/cobblemon!2047
2025-11-26 08:48:00 +00:00
Test 7801e3feee changelog clarification [skip-ci] 2025-11-26 04:56:10 +00:00
Test 4a54cb4674 remove auto generated screen glow 2025-11-26 04:38:14 +00:00
JazzMcNade a8949cf1f9 changelog 2025-11-25 21:49:38 -06:00
JazzMcNade 24fe1c125c changelog 2025-11-25 21:47:45 -06:00
JazzMcNade 61db6334f9 Restores Combee pathing through leaves 2025-11-25 21:37:57 -06:00
JazzMcNade 023160f87a Removed a really silly cast 2025-11-25 21:32:17 -06:00
RedGenesectNinja c45e34648c Fixed Toedscool and Toedscruel's hitboxes being too small 2025-11-25 18:53:12 -06:00
Polymeta 17fbf7c6f0 defensive pokemon casts as NPCs use this too
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-11-25 16:53:26 +00:00
Polymeta ec86b7d4bf fix tag reference
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-11-25 15:18:09 +00:00
阿鱼 7561eb116f Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.3% (8960 of 9019 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/zh_Hans/
2025-11-25 14:20:09 +00:00
Polymeta 6467b97a23 Merge branch 'implement_pokemon' into 'main'
marked furfrou as implemented

See merge request cable-mc/cobblemon!2041
2025-11-25 12:16:02 +00:00
JazzMcNade 6d5909a1e3 Added rain clause, do not attempt to enter a full hive, changelog 2025-11-25 02:17:28 -06:00
JazzMcNade 2bb0d43ca8 Fix wandering for pastured bees, fix hoverpos not being cleared 2025-11-25 00:51:04 -06:00
Watuneni iteite 1fb3f0ebdc Translated using Weblate (Japanese)
Currently translated at 99.2% (8954 of 9019 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-11-25 06:46:07 +00:00
jackowes d56586ecac - Fix walking animation issues 2025-11-24 21:39:36 -06:00
violet fb2154e2c3 Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-24 22:06:34 -05:00
violet 3b14d19fdb marked furfrou as implemented 2025-11-24 22:06:22 -05:00
Carmendar 6840c2fdef Merge branch 'main' into implement_pokemon 2025-11-24 17:28:35 -06:00
阿鱼 8fc2031806 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.2% (8954 of 9019 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/zh_Hans/
2025-11-24 21:23:59 +00:00
jackowes 9df8be33e9 - revert a bad change 2025-11-24 14:51:33 -06:00
Alexis Salgueiro 4795e2e1bc Translated using Weblate (French)
Currently translated at 99.6% (8984 of 9019 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2025-11-24 11:22:45 +00:00
Carmendar c563627b05 Shiny female Combee is now actually shiny. 2025-11-24 02:31:25 -06:00
violet 73ebf7a789 tiny typo fix 2025-11-24 03:18:38 -05:00
Landon a1b3d3d2ca Merge branch 'lets-be-friends-again' into 'main'
Move friendship logic to spawn action

See merge request cable-mc/cobblemon!2037
2025-11-24 06:50:24 +00:00
lion 8baacbcf90 Move friendship logic to spawn action 2025-11-24 06:50:23 +00:00
Hiroku f2e357b749 Merge branch 'pc-crash-fix' into 'main'
Fix PC crash with VulkanMod

Closes #1793

See merge request cable-mc/cobblemon!2038
2025-11-24 04:28:32 +00:00
jackowes ff2afd546e changelog 2025-11-23 21:50:30 -06:00
Test b95900b29a Fix PC crash with VulkanMod 2025-11-23 22:39:28 -05:00
jackowes b8b7756a45 - Add double tap to sprint on land mounts
- Adjust step up in air and on ground to reflect vanilla behaviour
- Fix inAir check so pose does not change back to walking or running while in air
2025-11-23 21:29:30 -06:00
jackowes 3d6146f551 - Add double tap to sprint on land mounts.
- Adjust step up in air and on ground to reflect vanilla behaviour
2025-11-23 21:15:41 -06:00
Hiroku 03585fafb0 Merge branch 'HoneyIForgotTheNectar' into 'main'
Don't Forget the Nectar

See merge request cable-mc/cobblemon!2031
2025-11-24 01:38:14 +00:00
jackowes 31d81db243 Merge branch 'main' into cobble-land-fixes 2025-11-23 18:19:51 -06:00
jackowes 4c36db8685 - Fix ridden pokemon land collision and weird step up issues. Horse is smoother overall now.
- Change the turnrate debuff to be squareroot instead of linear.
- Accel stat does not affect land ridden mounts walks.
- Add vanilla sprinting fov to pokemon land mounts
2025-11-23 18:19:13 -06:00
washashu0905 e3d0252d75 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 94.4% (8519 of 9019 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/zh_Hant/
2025-11-24 00:10:57 +00:00
阿鱼 dbe8e57341 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.2% (8950 of 9019 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/zh_Hans/
2025-11-24 00:10:56 +00:00
Zack the Ghast 6f37fd69cd Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.4% (8791 of 9019 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/pt_BR/
2025-11-24 00:10:55 +00:00
JazzMcNade b191f4e9fb Check hivecooldown memory registration 2025-11-23 15:42:56 -06:00
Hiroku a5a7cc20ba Merge branch 'eating-cough-drops' into 'main'
Throat Spray Recipe

See merge request cable-mc/cobblemon!2036
2025-11-23 11:48:00 +00:00
Polymeta 02b6c46082 [skip-ci] 1.7 tag doc files
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-11-23 11:39:39 +00:00
dhi_holo 881a30cca4 Merge branch 'main' into eating-cough-drops 2025-11-23 20:33:20 +10:00
dhi_holo d01011f4d0 Throat Spray Recipe
This either was lost at some point or never existed in the mod, I'm not sure which. It's here now!
2025-11-23 20:32:21 +10:00
Landon 309ed379c7 Merge branch 'AAAAAAAAAAAAAAAAAAAAA' into 'main'
unfudge camera maybe please

See merge request cable-mc/cobblemon!2030
2025-11-23 10:30:50 +00:00
Landon Wills edbf667f45 Add changelog 2025-11-23 04:44:02 -05:00
Landon Wills b6bc766adf Merge branch 'main' into AAAAAAAAAAAAAAAAAAAAA 2025-11-23 04:42:52 -05:00
Test 1ecf556cf2 Merge branch 'nom' into 'main'
Small Fixes

See merge request cable-mc/cobblemon!2035
2025-11-23 03:46:33 +00:00
dhi_holo 6d32073604 Small Fixes
- Ponigiri can no longer be eaten at full hunger
- Medicinal Brew's Campfire Pot recipe now correctly displays that it can be made using an empty glass bottle or a filled bottle
- Added trades for Saccharine Saplings, Hearty Grains, Chipped Pot, and Masterpiece Teacup to the Wandering Trader
- Reduced cost of the Vivichoke Seed trade with the Wandering Trader
2025-11-23 12:45:18 +10:00
Guilherme Pereira 7cedfb9047 update CHANGELOG.md 2025-11-22 22:02:14 -03:00
Guilherme Pereira a7ab2e37c2 Merge branch 'main' of gitlab.com:cable-mc/cobblemon into partner-mark 2025-11-22 21:57:49 -03:00
Guilherme Pereira 4723f91500 add riding stats 2025-11-22 21:57:25 -03:00
Hiroku aa0f5d8a05 Merge branch 'vanilla-ride-fix-and-misc-ride-fixes' into 'main'
Fix vanilla rides suffocating players and misc cobble riding fixes

See merge request cable-mc/cobblemon!2033
2025-11-22 23:50:31 +00:00
jackowes 6202c14215 - changelog update 2025-11-22 16:42:20 -06:00
Guilherme Pereira e23aa6b066 add partner mark molang callback 2025-11-22 19:39:21 -03:00
Hiroku b0790341c7 Merge branch 'some-fixes' into 'main'
Some fixes

See merge request cable-mc/cobblemon!2032
2025-11-22 22:36:17 +00:00
jackowes 4f0fa6af4a - Fix horse player suffocation on vanilla mounts
- stop passengers from hearing the shiny noise of a ridden shiny pokemon.
- Convert the riding freelook button to a configurable keybinding

(cherry picked from commit 6432db9f7d)
2025-11-22 16:24:25 -06:00
JazzMcNade 4e24e09924 Safer memory reading in hive occupant mixins 2025-11-22 16:19:30 -06:00
jackowes 48676067b7 Revert "- Fix horse player suffocation on vanilla mounts"
This reverts commit 6432db9f7d.
2025-11-22 16:16:17 -06:00
jackowes 6432db9f7d - Fix horse player suffocation on vanilla mounts
- stop passengers from hearing the shiny noise of a ridden shiny pokemon.
- Convert the riding freelook button to a configurable keybinding
2025-11-22 16:13:47 -06:00
Test 2cb28fb384 rm unused imports 2025-11-22 12:57:59 -05:00
JazzMcNade 3b59148151 changelog again 2025-11-22 11:43:44 -06:00
JazzMcNade d6198631ba New Changelog 2025-11-22 11:40:03 -06:00
geumu 47daea6195 Translated using Weblate (Korean)
Currently translated at 75.6% (6826 of 9019 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-11-22 17:37:36 +00:00
Watuneni iteite aca66a4bb0 Translated using Weblate (Japanese)
Currently translated at 99.2% (8954 of 9019 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-11-22 17:37:35 +00:00
Test e9fe1774c0 fix poke snack sound issue, prevent ride controls from showing to passengers 2025-11-22 12:36:31 -05:00
JazzMcNade bb8b8a77c9 Fixed bees forgetting that they had nectar upon entering a hive 2025-11-22 11:07:37 -06:00
Paul c692b6b5c1 Reset changelog [skip-ci] 2025-11-22 23:09:37 +10:00
geumu 68c6d5d9b2 Translated using Weblate (Korean)
Currently translated at 75.6% (6825 of 9019 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-11-22 07:33:14 +00:00
Landon Wills 221547aabf unfuck camera maybe please 2025-11-21 22:39:19 -05:00
Watuneni iteite f6b3591d80 Translated using Weblate (Japanese)
Currently translated at 99.2% (8954 of 9019 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-11-22 03:06:51 +00:00
geumu f9878d6137 Translated using Weblate (Korean)
Currently translated at 75.2% (6740 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-11-22 01:45:14 +00:00
Watuneni iteite 94da3b8d21 Translated using Weblate (Japanese)
Currently translated at 100.0% (8962 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-11-22 01:45:14 +00:00
Nick 3a2ff9de75 1.7.1 SNAPSHOT in prep for next development cycle 2025-11-21 17:06:08 -08:00
Paul b249896316 more changelog [skip-ci] 2025-11-22 09:37:51 +10:00
Test 1cb6979b90 More changelog cleanup [skip-ci] 2025-11-21 20:56:30 +00:00
Polymeta e983d2c1db Merge branch 'implement_pokemon' into 'main'
marked togedemaru as implemented

See merge request cable-mc/cobblemon!2029
2025-11-21 20:16:07 +00:00
violet 9ee0cf941b Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-21 14:29:35 -05:00
violet 8436c93317 marked togedemaru as implemented 2025-11-21 14:29:19 -05:00
Paul 7f0329acb7 Loads of changelog restructuring, separated old versions out into their own files [skip-ci] 2025-11-21 23:49:00 +10:00
Polymeta 83489b7230 nothing to see here
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-11-21 13:09:27 +00:00
Landon 081a25f04b Merge branch 'does-anyone-even-read-these' into 'main'
Fix mutation of vector after changing from Vector3f to Vec3

See merge request cable-mc/cobblemon!2028
2025-11-21 12:42:41 +00:00
Landon Wills 38bdfae0db Fix mutation of vector after changing from Vector3f to Vec3 2025-11-21 06:57:06 -05:00
Landon bd3a9e30f1 Merge branch 'fix-npc-behavior-json' into 'main'
Fix NPC Behavior Json key

See merge request cable-mc/cobblemon!2026
2025-11-21 11:02:11 +00:00
Hiroku 7ce968f467 Merge branch 'implement_pokemon' into 'main'
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...

See merge request cable-mc/cobblemon!2027
2025-11-21 10:33:34 +00:00
Hiroku 9ac8cc56f0 Merge branch 'main' into 'implement_pokemon'
# Conflicts:
#   common/src/main/resources/data/cobblemon/species/generation5/patrat.json
#   common/src/main/resources/data/cobblemon/species/generation9/tadbulb.json
2025-11-21 09:45:22 +00:00
Polymeta 98159e1e0e Merge branch 'smeargle-tail-reset' into 'main'
Fixed smeargle tail changing colour between relogs

See merge request cable-mc/cobblemon!2024
2025-11-21 09:44:00 +00:00
violet 025b5aae74 edited treecko line, braixen, and delphox's files 2025-11-21 04:27:03 -05:00
Deltric 2fddb21585 Fixed NPC behavior key fix, yes that's right behavior not behaviour @Hiroku. 2025-11-21 01:32:54 -06:00
violet 6b75976236 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2025-11-21 02:17:36 -05:00
Paul fdf419ec8f Rescraped for willFlee scraper bug, restoring flee behaviour for hundreds of pokes 2025-11-21 17:12:20 +10:00
Paul d2ceb2d58a Fixed wooper and whiscash forms being fished by all rods 2025-11-21 17:11:11 +10:00
Hiroku 6d8311a66c Merge branch '1.7-spawns-update' into 'main'
Updated spawns for 1/100 chance form Pokémon.

See merge request cable-mc/cobblemon!2025
2025-11-21 07:07:00 +00:00
Veraxiel 18a3d1c22c Updated spawns for 1/100 chance form Pokémon. 2025-11-21 01:54:50 -05:00
Hiroku b382a1b2db Merge branch '1.7-tag-cleanup' into 'main'
1.7 tag cleanup

See merge request cable-mc/cobblemon!2017
2025-11-21 06:29:22 +00:00
Paul dd17fa138e Fixed smeargle tail changing colour between relogs 2025-11-21 16:27:56 +10:00
Test 09dd623ea7 Amend dolphin controls overlay 2025-11-21 06:24:56 +00:00
Landon 8c01bb831f Merge branch 'path-faster-and-less-and-faster' into 'main'
Following the owner should not be so aggressive. Path cooldown, and consider...

See merge request cable-mc/cobblemon!2019
2025-11-21 06:09:10 +00:00
Paul 2cb4372496 Removed cry from slowpoke tail'ing 2025-11-21 15:04:43 +10:00
Hiroku 7597b2d98e Merge branch 'fated-faint' into 'main'
Crafting Fix

See merge request cable-mc/cobblemon!2023
2025-11-21 04:54:05 +00:00
dhi_holo 5ac2729a77 Crafting Fix
EV Feathers now unlock in recipe book, and Mochi cannot be crafted in a table, now requires campfire pot cooking
2025-11-21 14:46:13 +10:00
Hiroku bec1fca7a5 Merge branch 'brewer-only' into 'main'
Quick Brewing Only-ification

See merge request cable-mc/cobblemon!2022
2025-11-21 04:19:04 +00:00
dhi_holo 32d145f3cc Quick Brewing Only-ification
Removed 8 recipes that were meant to be removed, added translation for a newer tag that was missed over
2025-11-21 13:34:52 +10:00
Paul a5f1a14e21 Merge remote-tracking branch 'weblate/lang-updates'
# Conflicts:
#	common/src/main/resources/assets/cobblemon/lang/pt_br.json
2025-11-21 11:43:28 +10:00
Landon 57784b8620 Merge branch 'camera-farlands' into 'main'
Switch to using Doubles when positioning camera.

See merge request cable-mc/cobblemon!2021
2025-11-20 23:28:26 +00:00
JPAKx4 a29b3342a1 Switch to using Doubles when positioning camera. 2025-11-20 16:41:13 -06:00
Polymeta 64d6560caf Merge branch 'cobble-jail-here-i-come' into 'main'
Small fixes

See merge request cable-mc/cobblemon!2018
2025-11-20 22:10:32 +00:00
Polymeta 2e314e48fb Merge branch 'Car-fixes' into 'main'
Car fixes

See merge request cable-mc/cobblemon!2016
2025-11-20 21:19:50 +00:00
Nick e8d1e3766b Merge branch 'fix-pc-deserialisation' into 'main'
Fix PC JSON loading

See merge request cable-mc/cobblemon!2020
2025-11-20 12:09:54 -08:00
Landon Wills 012682268e Add safe check for nbt 2025-11-20 14:22:32 -05:00
Landon Wills 38428e4ea9 Fix PC JSON loading 2025-11-20 14:19:17 -05:00
Paul 567132bc0c Even more aggressive optimizations for the nodemaker 2025-11-20 23:07:59 +10:00
Paul e7209e4000 More aggressive optimizations for the nodemaker 2025-11-20 22:40:19 +10:00
Landon Wills 273b520c0b Shave iterations off of node type checks 2025-11-20 02:13:27 -05:00
Guilherme Pereira 0f02313a14 prevent pokemon from being busy when capturing player gets killed 2025-11-20 03:13:55 -03:00
Guilherme Pereira 3249e4bbb1 prevent pokemon from doing damage when being captured 2025-11-20 03:13:15 -03:00
geumu 36aca65003 Translated using Weblate (Korean)
Currently translated at 67.0% (6010 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-11-20 05:48:24 +00:00
Watuneni iteite c6635213a2 Translated using Weblate (Japanese)
Currently translated at 99.2% (8897 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-11-20 05:48:23 +00:00
JazzMcNade bf6fab3936 Only check for nearby danger nodes if the hitbox is small 2025-11-19 23:18:11 -06:00
Watuneni iteite b76158d1d7 Translated using Weblate (Japanese)
Currently translated at 99.2% (8896 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-11-20 02:32:39 +00:00
geumu 916ec97fce Translated using Weblate (Korean)
Currently translated at 66.6% (5972 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-11-20 02:32:36 +00:00
Veraxiel 36b38d36cb Updated biome tags. 2025-11-19 21:00:27 -05:00
Veraxiel f1163977e1 Updated biome and seasoning tags. Updated Sinister Tea recipe filter. 2025-11-19 20:59:42 -05:00
Carmendar a7dd6d52c8 Charizard and Mudsdale Look tweaks. 2025-11-19 19:29:19 -06:00
Watuneni iteite 6898804e83 Translated using Weblate (Japanese)
Currently translated at 96.9% (8686 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-11-20 00:40:19 +00:00
Carmendar 2331603b27 Tyrantrum sleep t-pose fix. 2025-11-19 18:30:38 -06:00
Carmendar c8e9cbbe59 Furfrou cry fix 2025-11-19 18:30:11 -06:00
Carmendar d374635648 Aerodactyl Battle Cry and Texture fix 2025-11-19 18:29:39 -06:00
Polymeta 803d3be325 recipe and tag tweak
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-11-19 23:59:55 +00:00
Polymeta 50bacbe25e Merge branch 'fix_medicinal_brew_from_cooking_pot_recipe' into 'main'
Revert Medicinal Brew recipe back to water bottle as opposed to empty bottle.

See merge request cable-mc/cobblemon!2015
2025-11-19 23:55:28 +00:00
Faintful 94ef9830b7 Revert Medicinal Brew recipe back to water bottle as opposed to empty bottle. 2025-11-19 21:17:22 +01:00
Weblate 87502f36c4 Update translation files
Updated by "Remove blank strings" add-on in Weblate.

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/
2025-11-19 16:43:18 +00:00
Nico Merenq bed511d403 Translated using Weblate (Russian)
Currently translated at 78.3% (7019 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ru/
2025-11-19 16:43:14 +00:00
阿鱼 4170b00e31 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (8962 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/zh_Hans/
2025-11-19 16:43:13 +00:00
geumu 3cc90daaf0 Translated using Weblate (Korean)
Currently translated at 66.2% (5937 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-11-19 16:43:12 +00:00
Polymeta b0ecd24f42 Merge branch 'aprijuice-advancement-fix' into 'main'
Fixed max ride boosts advancement not working properly

See merge request cable-mc/cobblemon!2014
2025-11-19 14:10:10 +00:00
Polymeta ade5603331 Edit max_ride_stats.json 2025-11-19 13:25:03 +00:00
Paul 34bcefb09f Fixed max ride boosts advancement not working properly 2025-11-19 21:41:14 +10:00
Hiroku dccc9dfbcb Merge branch 'sterr/sound-fixes' into 'main'
Audio fixes stuff

See merge request cable-mc/cobblemon!2013
2025-11-19 11:29:40 +00:00
Weblate 9a1d6461d9 Update translation files
Updated by "Remove blank strings" add-on in Weblate.

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/
2025-11-19 11:12:29 +00:00
Watuneni iteite 2dc76ffe3e Translated using Weblate (Japanese)
Currently translated at 96.4% (8646 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-11-19 11:12:25 +00:00
geumu 87225d8981 Translated using Weblate (Korean)
Currently translated at 65.6% (5886 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-11-19 11:12:24 +00:00
Sterrezo 4bf4101fd1 Audio fixes stuff 2025-11-19 11:39:24 +01:00
Paul 58c6ea0269 Following the owner should not be so aggressive. Path cooldown, and consider hitbox when deciding if it needs to start walking closer 2025-11-19 19:16:42 +10:00
Paul 034bd2eec6 Only use strict walking pathing when the hitbox is small 2025-11-19 19:16:02 +10:00
Polymeta a24be04d68 Merge branch 'ihatebugtypes1.7' into 'main'
sacrificing Bugsy for the sake of the release

Closes #1771 and #1768

See merge request cable-mc/cobblemon!2000
2025-11-19 07:32:27 +00:00
Segfault Guy 0d878d57b7 Merge remote-tracking branch 'origin/main' into ihatebugtypes1.7 2025-11-19 00:55:42 -05:00
Paul 08880e2294 Fixed instance of check for diagonal validation on non pokemon 2025-11-19 12:52:20 +10:00
Paul 1c45654930 Update the changed-default root variables on 1.7 installation 2025-11-19 12:33:36 +10:00
Hiroku dae3acb5a0 Merge branch 'i-dont-know-what-to-call-this-branch' into 'main'
Small fixes

See merge request cable-mc/cobblemon!2007
2025-11-19 02:30:20 +00:00
Hiroku 7bb6f8a12e Merge branch 'damage-formula' into 'main'
Add attack to damage formula

See merge request cable-mc/cobblemon!2011
2025-11-19 02:18:25 +00:00
lion b25f3ca3f6 Add attack to damage formula 2025-11-19 02:18:25 +00:00
Hiroku 610b762188 Merge branch 'nut-rition' into 'main'
Food Tweaks

See merge request cable-mc/cobblemon!2010
2025-11-19 02:00:41 +00:00
Hiroku c4c954c898 Merge branch 'Meta&Jet-fixes' into 'main'
Metagross fix and Jet handling tweak.

See merge request cable-mc/cobblemon!2009
2025-11-19 01:49:19 +00:00
Segfault Guy 10b70a0e3f Merge remote-tracking branch 'origin/main' into ihatebugtypes1.7 2025-11-18 20:20:46 -05:00
Segfault Guy af437f9d3e Merge branch 'ihatebugtypes1.7' of https://gitlab.com/cable-mc/cobblemon into ihatebugtypes1.7 2025-11-18 20:14:21 -05:00
Segfault Guy ca5a96e979 Fix berry chomp sfx not playing for unrestricted berries when full. 2025-11-18 20:11:02 -05:00
dhi_holo a2936ebcff Berry Reorganisation
Reordered berries as Vera requested
2025-11-19 11:10:15 +10:00
geumu c3d7537b3e Translated using Weblate (Korean)
Currently translated at 65.0% (5832 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-11-19 00:43:57 +00:00
Carmendar 81e5d830c6 Metagross walks once more & Jet handling expression tweak. 2025-11-18 18:38:02 -06:00
dhi_holo 4b09714ed5 Merge branch 'main' into nut-rition 2025-11-19 10:27:33 +10:00
Polymeta fa4a4c7e51 Merge branch 'farming-simulator-2025' into 'main'
Allow villagers to plant hearty rice

See merge request cable-mc/cobblemon!2008
2025-11-19 00:25:29 +00:00
dhi_holo 493b8bbecf Seasoning Tweaks
values are now what they should be, hopefully
2025-11-19 10:23:56 +10:00
Guilherme Pereira 9b519daff1 allow villagers to plant hearty rice 2025-11-18 21:02:10 -03:00
Guilherme Pereira a5ba3e7084 buff roots 2025-11-18 19:23:38 -03:00
Guilherme Pereira 6d2d146543 sync combat behaviour 2025-11-18 19:17:13 -03:00
Landon 22d93cb92c Merge branch 'navigator-performance' into 'main'
Performance optimizations for pathfinding

See merge request cable-mc/cobblemon!2006
2025-11-18 18:41:03 +00:00
geumu 31c55a9b6f Translated using Weblate (Korean)
Currently translated at 65.0% (5832 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-11-18 17:39:25 +00:00
Paul 5281dbd81c Performance optimizations for pathfinding 2025-11-19 03:02:52 +10:00
Segfault Guy 8b863070fe addressing MR comments 2025-11-18 11:20:07 -05:00
Paul da1064772d Fixed rattata cry crash 2025-11-19 01:59:00 +10:00
Landon 8ca1957a60 Merge branch 'ai-scrape' into 'main'
Scraped AI and Behaviors

See merge request cable-mc/cobblemon!1994
2025-11-18 12:51:11 +00:00
lion cbab2b7e5c Scraped AI and Behaviors 2025-11-18 12:51:10 +00:00
Landon c47bd604d5 Merge branch 'wrong-land-shark' into 'main'
Prevent sharpedo from trying to swim on land

See merge request cable-mc/cobblemon!2003
2025-11-18 12:21:59 +00:00
Polymeta 63776ca7a0 added missing food colours and remove debug annotation
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-11-18 11:56:32 +00:00
Polymeta c66c7170f6 Merge branch 'juice-tweaks' into 'main'
Fixed apricorn stat changes only applying when points are already assigned to that stat, adjusted flavour floor

See merge request cable-mc/cobblemon!2004
2025-11-18 10:00:03 +00:00
Weblate e9e4158666 Update translation files
Updated by "Remove blank strings" add-on in Weblate.

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/
2025-11-18 09:44:12 +00:00
Watuneni iteite b15917898f Translated using Weblate (Japanese)
Currently translated at 95.7% (8582 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-11-18 09:44:07 +00:00
Teggy ef3179bfa2 Translated using Weblate (Spanish)
Currently translated at 84.8% (7602 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/es/
2025-11-18 09:44:06 +00:00
阿鱼 f0afb8b3d9 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.9% (8959 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/zh_Hans/
2025-11-18 09:44:05 +00:00
geumu 17e58b0ce5 Translated using Weblate (Korean)
Currently translated at 63.7% (5714 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-11-18 09:44:04 +00:00
Paul fb0bfb2d19 Merge remote-tracking branch 'origin/main' 2025-11-18 19:42:15 +10:00
Landon 0d87de157a Merge branch 'implement_pokemon' into 'main'
Implemplemenation

See merge request cable-mc/cobblemon!2002
2025-11-18 09:35:33 +00:00
Paul 4189da0cc7 Merge remote-tracking branch 'origin/main' 2025-11-18 19:33:17 +10:00
Paul 662ccdc268 Removed unrefined sinister tea seasonings 2025-11-18 19:29:54 +10:00
Hiroku 6779995680 Merge branch 'evo_particles_fix' into 'main'
Claire Obscure Particles: Expedition 33

See merge request cable-mc/cobblemon!1997
2025-11-18 09:26:28 +00:00
Landon c4b6cc7c00 Merge branch 'slowpoke-dex-fix' into 'main'
Fixed some dex display issues and fixed slowpoke appearance in dex

See merge request cable-mc/cobblemon!2001
2025-11-18 09:24:09 +00:00
Paul 07ef404518 Fixed apricorn stat changes only applying when points are already assigned to that stat, adjusted flavour floor 2025-11-18 19:13:04 +10:00
Landon Wills 4c8a8351ac Prevent sharpedo from trying to swim on land 2025-11-18 04:06:57 -05:00
Carmendar 17f5ed03d0 poser/species tweaks. 2025-11-18 02:46:11 -06:00
Paul a3d3c987bc Fixed some dex display issues and fixed slowpoke appearance in dex 2025-11-18 18:34:46 +10:00
Carmendar 6db0567f03 Merge branch 'main' into implement_pokemon 2025-11-18 02:02:32 -06:00
Segfault Guy 3e89499588 Fix SocketShowdownService and custom showdown data registration. 2025-11-18 02:09:39 -05:00
Segfault Guy 7982034964 Fix missing optional ability activation langs. 2025-11-18 02:02:19 -05:00
JPAKx4 8f462aab40 Add default_seat option to seat offset posers, and remove shouldRotate as it's not needed for positioning the camera. 2025-11-18 00:56:40 -06:00
Jackson Bowes ac3628d907 Merge branch 'ufo-turning-and-head-rot-fix' into 'main'
Fix various bugs with riding.

See merge request cable-mc/cobblemon!1998
2025-11-18 00:32:11 -06:00
Segfault Guy 4a1e0020f7 remove redundant bag item 2025-11-18 01:10:12 -05:00
Segfault Guy 3c1486e45b Implement volatile status curing berries and patch fullness conflict with restoration berries. 2025-11-18 00:36:11 -05:00
jackowes 4563770835 - fix nonJumping horses from falling through to Tpose when trying to jump. The in_air flag checks for the player.jumping flag so its true for a moment. Block this from being true when horse has no jump. 2025-11-17 23:00:13 -06:00
dhi_holo c49eed3aaf Initial value tweaks 2025-11-18 14:59:28 +10:00
jackowes cc80f69c47 - Fix jet bug/mishap
- Fixed ride sounds not working in multiplayer
- Made the velocity threshold lower for ridesounds so that they can be heard in more 'normal' situations.
- Revert player head rotation changes for rollables.
2025-11-17 22:56:30 -06:00
Landon 128366f9c2 Merge branch '1.7-spawns-update' into 'main'
Updated spawns for Marill, Bidoof, Rufflet, and Tadbulb lines.

See merge request cable-mc/cobblemon!1996
2025-11-18 04:43:51 +00:00
Tyzillion 1d158ae12c Evolution obscure particles fixed. 2025-11-17 23:10:58 -05:00
Landon ddd63ac841 Merge branch 'Aprijuice-Recipe-Change' into 'main'
Energy Root added to Aprijuice recipes & slight update to compost values.

See merge request cable-mc/cobblemon!1995
2025-11-18 03:57:18 +00:00
Veraxiel 118a682451 Updated spawns for Marill, Bidoof, Rufflet, and Tadbulb lines. 2025-11-17 22:26:13 -05:00
Landon d202efc0a9 Merge branch 'validate-op-permissions-riding' into 'main'
Add is OP player check to riding stats debug GUI

See merge request cable-mc/cobblemon!1988
2025-11-18 03:16:00 +00:00
Carmendar 81a1314493 Energy Root added to Aprijuice recipes & slight update to compost values. 2025-11-17 21:08:16 -06:00
Guilherme Pereira d20673d9bf fix pokemon not being able to enter boats 2025-11-17 21:32:30 -03:00
FrankTheFarmer 4634dc7539 Fixed both stunfisks using portrait pose when in water 2025-11-18 09:56:52 +10:00
FrankTheFarmer c2db648d25 Tweaked Rayqwayqway's poser 2025-11-18 08:59:33 +10:00
FrankTheFarmer 76f1ace6b7 Fixed scovillain's cry interpolation 2025-11-18 08:51:15 +10:00
Jackson Bowes a316329359 Merge branch 'very-minor-molang-change' into 'main'
Fix boats and truthy values

See merge request cable-mc/cobblemon!1993
2025-11-17 16:29:54 -06:00
violet 4aa1b804bc Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-17 17:25:32 -05:00
violet 233d27413d poser tweaks to tynamo, ribombee, and cutiefly 2025-11-17 17:25:24 -05:00
FrankTheFarmer 05621ccb51 Merge branch 'main' into implement_pokemon 2025-11-18 08:13:24 +10:00
jackowes ea59981b58 - fix ufo momentum by rotating the rideVelocity vector by the induced ride yaw. 2025-11-17 15:22:41 -06:00
JPAKx4 960460ec08 Fix Boats being 'in_air' in water and vice versa. 2025-11-17 13:58:06 -06:00
JPAKx4 8d587c5287 Fix unintendid behaviour, shouldn't actually change anything currently though. 2025-11-17 13:35:19 -06:00
Guilherme Pereira e0b79c9bab change is op check to use cobblemon permissions validator 2025-11-17 09:36:56 -03:00
Polymeta 6206330b76 Merge branch 'getting-statistical' into 'main'
Getting statistical and minor Advancement fixes

See merge request cable-mc/cobblemon!1760
2025-11-17 09:30:28 +00:00
FrankTheFarmer 8e7a2a0356 Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-17 19:29:51 +10:00
FrankTheFarmer 7657cf4f73 Fixed Dragapult spinning rapidly after every physical attack 2025-11-17 19:28:59 +10:00
Polymeta 7b137fbe14 Merge branch 'Saccharine-Tree-Biome-List-Update' into 'main'
Saccharine tree biome list update

See merge request cable-mc/cobblemon!1990
2025-11-17 09:21:35 +00:00
Neovitalism 46d278ca55 key \:) 2025-11-17 03:18:02 -06:00
Landon 4d472d0b36 Merge branch 'molang-json-interaction' into 'main'
Molang json interaction

See merge request cable-mc/cobblemon!1934
2025-11-17 09:10:40 +00:00
Neovitalism 339038956d Support for deserialization from any key
...I think.
2025-11-17 03:09:21 -06:00
Slippy fd442211d3 Change Saccharine Tree Rarity 2025-11-17 03:07:58 -06:00
Landon 144f857a3b Merge branch 'fix-blocks-traveled-requirement' into 'main'
Fix Blocks Traveled requirement

See merge request cable-mc/cobblemon!1979
2025-11-17 08:20:35 +00:00
lion 80131e57f8 Fix Blocks Traveled requirement 2025-11-17 08:20:35 +00:00
Slippy 30d89a2704 Make Saccharine Trees a little less common 2025-11-17 02:04:51 -06:00
Landon 5e147545b7 Merge branch 'advancing-advancements' into 'main'
Advancing Advancements for Cobblemon 1.7.0

See merge request cable-mc/cobblemon!1974
2025-11-17 07:59:41 +00:00
Landon e962079cda Merge branch 'remove-poke-puff' into 'main'
Disable Poké Puffs temporarily

See merge request cable-mc/cobblemon!1989
2025-11-17 07:30:41 +00:00
lion 6f934cfbd4 Disable Poké Puffs temporarily 2025-11-17 07:30:41 +00:00
MoeBoy76 47ecef81a6 fix changelog errors 2025-11-17 18:02:04 +11:00
MoeBoy76 16762ae933 Merge branch 'main' of https://gitlab.com/cable-mc/cobblemon into getting-statistical 2025-11-17 17:56:53 +11:00
MoeBoy76 ba47f6a833 remove species check from catch trigger 2025-11-17 17:56:26 +11:00
dhi_holo 522ffefd0a Stone fix and Z-fix
All Evolution Stone blocks (besides Nether Fire Stone Ore) now need Iron to mine, and Display Cases now look better in more contexts
2025-11-17 16:04:15 +10:00
dhi_holo e17ca360df Merge branch 'main' into advancing-advancements 2025-11-17 15:46:04 +10:00
Carmendar a559737106 Merge branch 'main' into implement_pokemon 2025-11-16 23:27:00 -06:00
Landon e18d5837af Merge branch 'riding-molang-electric-bogaloo' into 'main'
Switch asMoLangValue from the states to the behaviour to allow for more dynamic options.

See merge request cable-mc/cobblemon!1986
2025-11-17 05:15:10 +00:00
Landon 65ae67d9ac Merge branch 'disable-nvb' into 'main'
Disable third_person_view_bobbing Config.

See merge request cable-mc/cobblemon!1987
2025-11-17 05:15:08 +00:00
Paul 05c77a6156 Hitbox improvements for melee attack, also don't randomly attack piglins or slimes or creepers 2025-11-17 15:11:50 +10:00
Paul 8ae6792785 Fixed fossil substitutes not working, fixed aspects not working in fossil tank rendering, fixed slowpoke tail aspects being added to everything 2025-11-17 11:21:23 +10:00
Guilherme Pereira 5f67fc167a add is op player check to riding stats debug gui 2025-11-16 20:16:08 -03:00
FrankTheFarmer 8dd62269ed Updated delphox animations 2025-11-17 08:03:29 +10:00
JPAKx4 0eee6e8634 Merge remote-tracking branch 'origin/riding-molang-electric-bogaloo' into riding-molang-electric-bogaloo
# Conflicts:
#	common/src/main/kotlin/com/cobblemon/mod/common/api/riding/behaviour/types/air/BirdBehaviour.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/api/riding/behaviour/types/air/HoverBehaviour.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/api/riding/behaviour/types/air/JetBehaviour.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/api/riding/behaviour/types/air/RocketBehaviour.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/api/riding/behaviour/types/land/HorseBehaviour.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/api/riding/behaviour/types/land/MinekartBehaviour.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/api/riding/behaviour/types/land/VehicleBehaviour.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/api/riding/behaviour/types/liquid/BoatBehaviour.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/api/riding/behaviour/types/liquid/BurstBehaviour.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/api/riding/behaviour/types/liquid/DolphinBehaviour.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/api/riding/behaviour/types/liquid/SubmarineBehaviour.kt
2025-11-16 15:10:45 -06:00
JPAKx4 a16a7c31fe Switch asMoLangValue from the states to the behaviour to allow for more dynamic options. 2025-11-16 15:10:07 -06:00
JPAKx4 a4254df0ca Disable third_person_view_bobbing Config. 2025-11-16 12:02:25 -06:00
JPAKx4 21df52daf7 Switch asMoLangValue from the states to the behaviour to allow for more dynamic options. 2025-11-16 11:49:08 -06:00
Landon 5e81e26c69 Merge branch 'molang-riding' into 'main'
Updates to riding to work better in Molang

See merge request cable-mc/cobblemon!1984
2025-11-16 15:33:38 +00:00
Neovitalism 116a93d8cc Update CHANGELOG.md 2025-11-16 04:19:32 -06:00
Neovitalism acebc35d3f Who are we if not consistent?
Before, this property was called `PokemonFlagProperty`, had the key `tag`, and checked pokémon's labels. It was also in a package by itself.

Now, it's moved down into the properties package, renamed to LabelProperty, and the key "label" was added, keeping "tag" as a secondary for those that are using it currently.
2025-11-16 04:10:45 -06:00
Hiroku d103629fd2 Merge branch '1.7-spawns-update' into 'main'
1.7 spawns update

See merge request cable-mc/cobblemon!1983
2025-11-16 09:27:54 +00:00
JPAKx4 6e2a0cd3b9 Switch to using ObjectValues for states to support riding styles with multiple properties. Added a few useful properties that can now be accessed through get_riding_state(name). 2025-11-16 07:12:59 +00:00
FrankTheFarmer 77490783e4 Swampert model tweak to fix its look animation 2025-11-16 15:51:10 +10:00
FrankTheFarmer ed28c70c5a Tweaked Meganium's profile data to look better in the dex 2025-11-16 15:50:40 +10:00
FrankTheFarmer 3f71584a6e Clamped Shelmet's look range to stop clipping with the shell. 2025-11-16 14:55:31 +10:00
FrankTheFarmer 4f09e56510 Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-16 14:54:36 +10:00
FrankTheFarmer b534183a50 Clamped Sandaconda's look range and stopped it from using the donut to look 2025-11-16 14:54:27 +10:00
Carmendar 56ea6c7161 Merge branch 'main' into implement_pokemon 2025-11-15 22:48:16 -06:00
Veraxiel 37f6010381 Edit to Popplio line spawns. 2025-11-15 23:44:38 -05:00
Hiroku 798dd64f84 Merge branch 'RestoreSheepShearing' into 'main'
Restore Sheep Shearing

See merge request cable-mc/cobblemon!1982
2025-11-16 03:43:58 +00:00
Paul c1acf8494c Slowpoke tails are snipped a bit less short 2025-11-16 13:15:44 +10:00
Veraxiel d4601326a7 Removed Water and Lava block tags. Updated spawns using those tags to use the blocks directly. 2025-11-15 22:09:50 -05:00
JazzMcNade a037af6948 Allow herding sheep to eat grass, fix typo 2025-11-15 20:55:58 -06:00
FrankTheFarmer 976c94cb6b Puppy and Icecream changelog 2025-11-16 12:48:28 +10:00
FrankTheFarmer 422154e4c7 Tweaked Eevee's shoulder positioning 2025-11-16 12:46:38 +10:00
JazzMcNade 517ac94b8d Restore sheep shearing and eating grass to restore wool 2025-11-15 20:26:31 -06:00
Paul b5ec2b28fd Merge remote-tracking branch 'origin/main' 2025-11-16 12:14:50 +10:00
Paul f45a4b784b Fixed percentSum being exceeded during multi-pass spawns, closes #1778 2025-11-16 12:14:35 +10:00
Hiroku 91dda046d8 Merge branch 'revert-transition-logic' into 'main'
Revert transition logic so land mounts can be consistently mounted

See merge request cable-mc/cobblemon!1981
2025-11-16 01:47:59 +00:00
FrankTheFarmer 71fabbd245 Added placeholder shoulder animations to Alcremie 2025-11-16 11:45:07 +10:00
Carmendar fa0f29b085 Merge branch 'main' into implement_pokemon 2025-11-15 18:57:34 -06:00
Landon Wills 49577d2518 Revert transition logic so land mounts can be consistently mounted 2025-11-15 19:29:36 -05:00
FrankTheFarmer 12de6750de Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-16 10:19:01 +10:00
FrankTheFarmer 4e1b9785a0 Stupid fish assets updated 🔵🔴⚪ 2025-11-16 10:18:53 +10:00
RedGenesectNinja 2f2de32493 Implemented the updated textures for Slowpoke and Galarian Slowpoke 2025-11-15 18:09:39 -06:00
FrankTheFarmer a38b68ce23 Added translucent culling to flygon's resolver to fix goggle z fighting 2025-11-16 09:49:09 +10:00
FrankTheFarmer 7d605f8bf9 Updated flygon's animation to fix the static legs in the ground idle 2025-11-16 09:48:46 +10:00
FrankTheFarmer 75134f41e3 Fixed flygon's look animation to use the proper head_ai bone 2025-11-16 09:48:20 +10:00
Landon 2bad8ec673 Merge branch 'implement_pokemon' into 'main'
Implement pokemon

See merge request cable-mc/cobblemon!1945
2025-11-15 22:44:48 +00:00
Carmendar 1524e9c476 smol species fixes. 2025-11-15 15:55:14 -06:00
Carmendar c8ad0cd7e7 Merge branch 'main' into implement_pokemon
# Conflicts:
#	common/src/main/resources/data/cobblemon/species/generation1/aerodactyl.json
#	common/src/main/resources/data/cobblemon/species/generation1/arcanine.json
#	common/src/main/resources/data/cobblemon/species/generation1/blastoise.json
#	common/src/main/resources/data/cobblemon/species/generation1/charizard.json
#	common/src/main/resources/data/cobblemon/species/generation1/dewgong.json
#	common/src/main/resources/data/cobblemon/species/generation1/dragonite.json
#	common/src/main/resources/data/cobblemon/species/generation1/gyarados.json
#	common/src/main/resources/data/cobblemon/species/generation1/lapras.json
#	common/src/main/resources/data/cobblemon/species/generation1/mrmime.json
#	common/src/main/resources/data/cobblemon/species/generation1/nidoking.json
#	common/src/main/resources/data/cobblemon/species/generation1/parasect.json
#	common/src/main/resources/data/cobblemon/species/generation1/rhydon.json
#	common/src/main/resources/data/cobblemon/species/generation1/rhyhorn.json
#	common/src/main/resources/data/cobblemon/species/generation1/seaking.json
#	common/src/main/resources/data/cobblemon/species/generation1/tauros.json
#	common/src/main/resources/data/cobblemon/species/generation1/venusaur.json
#	common/src/main/resources/data/cobblemon/species/generation2/ariados.json
#	common/src/main/resources/data/cobblemon/species/generation2/forretress.json
#	common/src/main/resources/data/cobblemon/species/generation2/girafarig.json
#	common/src/main/resources/data/cobblemon/species/generation2/heracross.json
#	common/src/main/resources/data/cobblemon/species/generation2/piloswine.json
#	common/src/main/resources/data/cobblemon/species/generation2/ursaring.json
#	common/src/main/resources/data/cobblemon/species/generation3/altaria.json
#	common/src/main/resources/data/cobblemon/species/generation3/camerupt.json
#	common/src/main/resources/data/cobblemon/species/generation3/claydol.json
#	common/src/main/resources/data/cobblemon/species/generation3/flygon.json
#	common/src/main/resources/data/cobblemon/species/generation3/latias.json
#	common/src/main/resources/data/cobblemon/species/generation3/latios.json
#	common/src/main/resources/data/cobblemon/species/generation3/metagross.json
#	common/src/main/resources/data/cobblemon/species/generation3/relicanth.json
#	common/src/main/resources/data/cobblemon/species/generation3/salamence.json
#	common/src/main/resources/data/cobblemon/species/generation3/sharpedo.json
#	common/src/main/resources/data/cobblemon/species/generation3/slaking.json
#	common/src/main/resources/data/cobblemon/species/generation3/tropius.json
#	common/src/main/resources/data/cobblemon/species/generation3/wailmer.json
#	common/src/main/resources/data/cobblemon/species/generation3/wailord.json
#	common/src/main/resources/data/cobblemon/species/generation4/bastiodon.json
#	common/src/main/resources/data/cobblemon/species/generation4/bronzong.json
#	common/src/main/resources/data/cobblemon/species/generation4/drifblim.json
#	common/src/main/resources/data/cobblemon/species/generation4/dusknoir.json
#	common/src/main/resources/data/cobblemon/species/generation4/garchomp.json
#	common/src/main/resources/data/cobblemon/species/generation4/gastrodon.json
#	common/src/main/resources/data/cobblemon/species/generation4/lickilicky.json
#	common/src/main/resources/data/cobblemon/species/generation4/magnezone.json
#	common/src/main/resources/data/cobblemon/species/generation4/mamoswine.json
#	common/src/main/resources/data/cobblemon/species/generation4/rhyperior.json
#	common/src/main/resources/data/cobblemon/species/generation4/staraptor.json
#	common/src/main/resources/data/cobblemon/species/generation4/togekiss.json
#	common/src/main/resources/data/cobblemon/species/generation5/bouffalant.json
#	common/src/main/resources/data/cobblemon/species/generation5/braviary.json
#	common/src/main/resources/data/cobblemon/species/generation5/crustle.json
#	common/src/main/resources/data/cobblemon/species/generation5/darmanitan.json
#	common/src/main/resources/data/cobblemon/species/generation5/golurk.json
#	common/src/main/resources/data/cobblemon/species/generation5/hydreigon.json
#	common/src/main/resources/data/cobblemon/species/generation5/klinklang.json
#	common/src/main/resources/data/cobblemon/species/generation5/serperior.json
#	common/src/main/resources/data/cobblemon/species/generation5/volcarona.json
#	common/src/main/resources/data/cobblemon/species/generation5/zebstrika.json
#	common/src/main/resources/data/cobblemon/species/generation6/gogoat.json
#	common/src/main/resources/data/cobblemon/species/generation6/noivern.json
#	common/src/main/resources/data/cobblemon/species/generation6/skiddo.json
#	common/src/main/resources/data/cobblemon/species/generation6/tyrantrum.json
#	common/src/main/resources/data/cobblemon/species/generation7/dhelmise.json
#	common/src/main/resources/data/cobblemon/species/generation7/mudsdale.json
#	common/src/main/resources/data/cobblemon/species/generation8/corviknight.json
#	common/src/main/resources/data/cobblemon/species/generation8/dragapult.json
#	common/src/main/resources/data/cobblemon/species/generation8a/sneasler.json
#	common/src/main/resources/data/cobblemon/species/generation8a/ursaluna.json
#	common/src/main/resources/data/cobblemon/species/generation8a/wyrdeer.json
#	common/src/main/resources/data/cobblemon/species/generation9/cyclizar.json
#	common/src/main/resources/data/cobblemon/species/generation9/dondozo.json
#	common/src/main/resources/data/cobblemon/species/generation9/dudunsparce.json
#	common/src/main/resources/data/cobblemon/species/generation9/espathra.json
#	common/src/main/resources/data/cobblemon/species/generation9/farigiraf.json
#	common/src/main/resources/data/cobblemon/species/generation9/kilowattrel.json
#	common/src/main/resources/data/cobblemon/species/generation9/orthworm.json
#	common/src/main/resources/data/cobblemon/species/generation9/revavroom.json
2025-11-15 15:53:14 -06:00
Kenji_64 33a4e31b60 Fixed Wartortle's shiny shell not being green 2025-11-15 19:51:14 +01:00
Nico Merenq 250724ef1f Translated using Weblate (Russian)
Currently translated at 77.9% (6988 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ru/
2025-11-15 15:05:37 +00:00
Patch Notes Guy 3e1837543d Translated using Weblate (Ukrainian)
Currently translated at 77.5% (6948 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/uk/
2025-11-15 10:45:37 +00:00
Carmendar 1526c2f037 Merge branch 'main' into implement_pokemon 2025-11-15 04:30:45 -06:00
Carmendar 408cf8c5ca One last poser tweak for the night. 2025-11-15 04:29:59 -06:00
violet ceb70dbf2d Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-15 04:26:14 -05:00
Carmendar 6d46992ad7 More poser tweaks. 2025-11-15 03:25:53 -06:00
violet 84242194ba fixed drampa look 2025-11-15 04:25:52 -05:00
violet 529e4bef46 fixed espurr line posers 2025-11-15 04:07:10 -05:00
violet aa1658bb5d fixed alolan muk's portrait 2025-11-15 03:55:16 -05:00
violet 068df2b138 added eelektross and eelektrik placeholder animations + fixed look animations 2025-11-15 03:47:56 -05:00
Carmendar 3499758203 Darmanitan Zen poser fix. 2025-11-15 00:29:54 -06:00
RedGenesectNinja 7d97df3407 Fixed Shiny Spring, Summer and Winter Sawsbuck using the wrong models 2025-11-15 00:17:06 -06:00
Nico Merenq 1add7ea4cb Translated using Weblate (Russian)
Currently translated at 77.1% (6916 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ru/
2025-11-15 06:14:04 +00:00
Felipe Bermudes Ormeño e497cd445a Translated using Weblate (Spanish (Mexico))
Currently translated at 26.7% (2394 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/es_MX/
2025-11-15 06:14:02 +00:00
Carmendar 22b0e0612a Vanillite line shiny textures for real. 2025-11-14 23:57:48 -06:00
Carmendar e52c63d565 Metagross jump stat adjustment. 2025-11-14 23:52:45 -06:00
Carmendar fe7f72cb3d Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-14 23:51:50 -06:00
Carmendar 8187905987 Adjusted walk speed for Gogoat. 2025-11-14 23:51:04 -06:00
RedGenesectNinja 11b3043e6c Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-14 22:47:24 -06:00
RedGenesectNinja 8d871ac949 Fixed Dragonair's shiny not working 2025-11-14 22:47:04 -06:00
Carmendar 8f7a523404 stat tweaks. 2025-11-14 22:40:22 -06:00
Carmendar bb98992635 Surface mount tweaks. 2025-11-14 20:02:34 -06:00
Landon Wills d3984843e2 Run scraper 2025-11-14 19:32:31 -05:00
Carmendar 7a14c51df2 Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-14 18:05:45 -06:00
Carmendar a7b023203f Merge branch 'main' into implement_pokemon 2025-11-14 18:04:18 -06:00
Nico Merenq e382e89e06 Translated using Weblate (Russian)
Currently translated at 76.6% (6868 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ru/
2025-11-14 17:21:23 +00:00
Gustavo Puerari faf64a6092 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.2% (8804 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/pt_BR/
2025-11-14 17:21:21 +00:00
MoeBoy76 2b64174cfe fix input order which causes logic bug 2025-11-15 00:56:58 +11:00
MoeBoy76 918282893c Merge branch 'main' of https://gitlab.com/cable-mc/cobblemon into getting-statistical 2025-11-15 00:50:37 +11:00
violet dbbca02619 centiskorch texture tweaks 2025-11-14 05:52:46 -05:00
violet c50d5841d8 Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-14 05:51:05 -05:00
violet 5489cfb4b0 fixed bellibolt's look anims 2025-11-14 05:39:07 -05:00
violet e421de547f SLOWPOKE TAILS NOW DROP WHEN YOU KILL THEM 2025-11-14 05:30:24 -05:00
geumu aa8d0a9d48 Translated using Weblate (Korean)
Currently translated at 61.5% (5513 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ko/
2025-11-14 10:22:59 +00:00
violet 075dcdb9e9 BRAND NEW COBBLEMON EXCLUSIVE FURFROU FORMS PROPERLY IMPLEMENTED 2025-11-14 04:53:22 -05:00
Carmendar 2ebe8d6434 breach jump for Dragapult 2025-11-14 02:53:18 -06:00
Carmendar d7c77f5f3f a few ride stats adjusted ahead of scraper 2025-11-14 02:50:10 -06:00
Zack the Ghast 7a8dbbb4ce Translated using Weblate (Portuguese (Portugal))
Currently translated at 67.6% (6059 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/pt_PT/
2025-11-14 08:26:53 +00:00
Watuneni iteite b8e1abc80b Translated using Weblate (Japanese)
Currently translated at 94.6% (8484 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-11-14 08:26:52 +00:00
Yasito 12df9d29f5 Translated using Weblate (French)
Currently translated at 100.0% (8962 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2025-11-14 08:26:51 +00:00
Yasito 2df0542631 Translated using Weblate (French (Canada))
Currently translated at 100.0% (8962 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr_CA/
2025-11-14 08:26:50 +00:00
dhi_holo e22b79a47c Translated using Weblate (English (en_PT))
Currently translated at 28.4% (2550 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/en_PT/
2025-11-14 08:26:49 +00:00
Carmendar 572682a23a Merge branch 'main' into implement_pokemon 2025-11-14 01:36:27 -06:00
Carmendar 6241865f95 Grounded again 2025-11-14 01:34:02 -06:00
dhi_holo 67dbda3113 Final Checks
Fixed the icons being outdated formattings, we are good to gold
2025-11-14 17:33:33 +10:00
Carmendar c50d3183e6 Submarine expression tweak 2025-11-14 01:22:35 -06:00
dhi_holo 9eae394ff5 Merge branch 'advancing-advancements' of https://gitlab.com/cable-mc/cobblemon into advancing-advancements 2025-11-14 17:15:14 +10:00
MoeBoy76 76b640984c changelog
[skip-ci]
2025-11-14 18:13:40 +11:00
dhi_holo 72d142a781 Merge branch 'main' into advancing-advancements 2025-11-14 17:08:21 +10:00
MoeBoy76 af1df22e4d ride stat trigger logic finalisation, added getBaseRideStat method to check the minimum value of a stat and updated the advancement 2025-11-14 18:07:15 +11:00
dhi_holo 66787bfea4 Merge branch 'main' into advancing-advancements 2025-11-14 15:03:10 +10:00
dhi_holo c113e68488 Moomoo Mistake
Fixed lang string for moomoo milk tooltip
2025-11-14 15:03:05 +10:00
Carmendar 8284b04426 Merge branch 'main' into implement_pokemon
# Conflicts:
#	common/src/main/resources/data/cobblemon/species/generation1/rattata.json
#	common/src/main/resources/data/cobblemon/species/generation2/unown.json
#	common/src/main/resources/data/cobblemon/species/generation3/latias.json
#	common/src/main/resources/data/cobblemon/species/generation3/latios.json
#	common/src/main/resources/data/cobblemon/species/generation3/shuppet.json
#	common/src/main/resources/data/cobblemon/species/generation5/hydreigon.json
#	common/src/main/resources/data/cobblemon/species/generation5/volcarona.json
#	common/src/main/resources/data/cobblemon/species/generation9/wiglett.json
2025-11-13 18:16:41 -06:00
Weblate 0c4bbef855 Update translation files
Updated by "Remove blank strings" add-on in Weblate.

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/
2025-11-13 13:42:34 +00:00
Nico Merenq 6fb34b5ee3 Translated using Weblate (Russian)
Currently translated at 76.0% (6819 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ru/
2025-11-13 13:42:29 +00:00
dhi_holo f345b19d57 Translated using Weblate (English (en_PT))
Currently translated at 26.7% (2399 of 8962 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/en_PT/
2025-11-13 13:42:29 +00:00
dhi_holo f1bdaad97d Merge branch 'main' into advancing-advancements 2025-11-13 19:29:15 +10:00
dhi_holo f725e5fcba Improve max_ride_stats description 2025-11-13 19:28:40 +10:00
violet 2237e1eeca added missing emissive textures for magmar, magmortar, torracat, incineroar, sizzlipede, & centiskorch 2025-11-13 04:24:36 -05:00
dhi_holo bb54f8414e Merge branch 'main' into advancing-advancements 2025-11-13 18:17:07 +10:00
Carmendar 99c7d39705 Grounded. 2025-11-13 01:42:08 -06:00
Carmendar bdd27078b5 Merge branch 'main' into implement_pokemon 2025-11-13 00:33:06 -06:00
dhi_holo 7c3fdbc921 Merge branch 'main' into advancing-advancements 2025-11-13 14:06:59 +10:00
RedGenesectNinja 9596f08957 Fixed Dusknoir's shiny not working and Updated Bronzong's textures 2025-11-12 21:12:00 -06:00
MoeBoy76 d9433960cd fml, forgot the license 2025-11-13 13:09:02 +11:00
MoeBoy76 89b6fd56b6 Merge remote-tracking branch 'origin/getting-statistical' into getting-statistical 2025-11-13 12:52:58 +11:00
MoeBoy76 10c441e3cd worked out rod casting, updated changelog 2025-11-13 12:52:36 +11:00
MoeBoy76 461b22efa8 Merge branch 'main' of https://gitlab.com/cable-mc/cobblemon into getting-statistical
# Conflicts:
#	common/src/main/resources/assets/cobblemon/lang/en_us.json
2025-11-13 12:44:17 +11:00
Carmendar 57a19909aa Merge branch 'main' into implement_pokemon 2025-11-12 19:32:00 -06:00
MoeBoy76 dc1473919e riding trigger added, added empty_bait check to reel in trigger 2025-11-13 12:24:16 +11:00
Carmendar ee20986ee0 Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-12 19:23:57 -06:00
RedGenesectNinja 436aa5ea75 Updated textures for a handful of Pokemon 2025-11-12 19:14:19 -06:00
Carmendar e49f761417 Poser fixes and cries added. 2025-11-12 19:09:38 -06:00
Carmendar 95e3ab213a anim fixes for Scorbunny and Sneasler 2025-11-12 18:48:14 -06:00
Carmendar 0c33de61b9 Species file updates for Tauros, Salamence, Latias, Latios and Staraptor. 2025-11-12 03:05:12 -06:00
Carmendar 91a3fb5031 Removed Kilowattrel mounted run. 2025-11-12 03:04:32 -06:00
Carmendar 3a46b2c218 Updated look posing for Tropius, Garchomp and Pidove. 2025-11-12 03:03:28 -06:00
Carmendar 2f631ddc64 Ursaring texture fix 2025-11-12 03:02:39 -06:00
Carmendar 2a2ef6ff7d Removed mount 2025-11-12 02:42:32 -06:00
Carmendar d3d94a3614 Adjusted Dragonite flying pose when mounted. 2025-11-12 02:42:03 -06:00
Carmendar f34cc3072b Cutiefly Shoulder poses. 2025-11-12 02:41:12 -06:00
Carmendar 68bff6a40b Jet Handling expression tweak. 2025-11-12 00:45:26 -06:00
Segfault Guy d1a99608a5 Fix SwapInstruction not updating party positions. 2025-11-11 22:10:50 -05:00
dhi_holo 2a6381d610 Eggant Is Here Now 2025-11-12 10:47:12 +10:00
dhi_holo 93dba4f3b8 Merge branch 'main' into advancing-advancements 2025-11-12 10:43:55 +10:00
Carmendar a7e7c2e814 Merge branch 'main' into implement_pokemon 2025-11-11 17:52:15 -06:00
MoeBoy76 6bf62b3f9d change debug message to log as error 2025-11-11 11:05:38 +00:00
Carmendar 00be470281 Alolan Rattata fix. 2025-11-11 02:58:28 -06:00
Carmendar 57afbe79c2 Orthworm and Honedge line texture fixes. 2025-11-11 00:49:30 -06:00
Carmendar d5f336ce73 Merge branch 'main' into implement_pokemon 2025-11-11 00:48:17 -06:00
violet b79a0b1b6f various animation fixes 2025-11-10 03:17:58 -05:00
violet 57721d1696 Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-09 22:51:33 -05:00
violet b0711528d6 tiny tweaks :3 2025-11-09 22:51:19 -05:00
violet 7e968fcff9 Murdered Shulker Forretress 2025-11-09 22:50:41 -05:00
Carmendar e399130fda forgor one last change 2025-11-09 18:45:12 -06:00
Carmendar 0083e54f6a Galar Weezing crash fix 2025-11-09 18:27:38 -06:00
Carmendar 9512252b72 ^ 2025-11-09 14:24:41 -06:00
Carmendar 73028d4c2a Gave Dondozo basic land & fixed pivot issues with Bramblin. 2025-11-09 14:20:58 -06:00
Carmendar 2f5910c436 Merge branch 'main' into implement_pokemon 2025-11-09 03:09:26 -06:00
Carmendar d1197cbc0b Updated Kangaskhan poser & added held item condition. 2025-11-09 02:30:09 -06:00
Carmendar 2ccf699a86 Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-09 01:37:47 -06:00
Carmendar d767420814 Wiglett lost floating privileges. 2025-11-09 01:37:08 -06:00
Carmendar 77100300b1 Fixed Luxray feet textures. 2025-11-09 01:35:05 -06:00
Carmendar 5c80fc03e2 Toxicroak remodel 2025-11-09 01:33:55 -06:00
Carmendar aa39bb84fe Fixed Lanturn t-posing on land. 2025-11-09 00:50:19 -06:00
violet a767b3d0fe Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-09 01:28:31 -05:00
violet 3dbcbc34d0 ICE CREAM 2025-11-09 01:28:17 -05:00
violet 0bdadb3794 altered posers for solosis line 2025-11-09 01:20:50 -05:00
dhi_holo 05b5782cd5 Merge branch 'main' into advancing-advancements 2025-11-09 12:54:24 +10:00
dhi_holo 0c220a8990 Berry Colour Tags
Small sorting thing to make it easier to find specific berry colours for cooking/crafting/smelting
2025-11-09 12:53:58 +10:00
dhi_holo da787bf9bf Merge branch 'main' into advancing-advancements 2025-11-07 16:15:29 +10:00
Paul 57a839d000 Removed some test stuff I forgor about 2025-11-06 22:14:08 +10:00
Paul e77e49c8b1 Minor cleanup 2025-11-06 22:13:03 +10:00
Paul ce6d142bd4 Merge remote-tracking branch 'origin/main' into molang-json-interaction 2025-11-06 22:12:17 +10:00
MoeBoy76 f2db63164c general riding and PokéRod cast/reel stats 2025-11-05 16:05:52 +11:00
MoeBoy76 604b4d39ea Merge branch 'main' of https://gitlab.com/cable-mc/cobblemon into getting-statistical 2025-11-05 14:26:27 +11:00
Paul 5d3741f2e5 Added JSON file interactions for molang and function arguments for q.run_script 2025-11-02 11:23:50 +10:00
dhi_holo 5e21ac94dc Merge branch 'main' into advancing-advancements 2025-10-25 17:13:13 +10:00
dhi_holo 1d055253d2 Merge branch 'main' into advancing-advancements 2025-10-10 16:00:30 +10:00
dhi_holo d56c5520f9 Merge branch 'main' into advancing-advancements 2025-10-04 17:26:10 +10:00
dhi_holo 89bc893f06 gamerule.healersHealPC 2025-10-04 17:25:50 +10:00
dhi_holo acc8d066e5 stOre 2025-10-03 19:17:00 +10:00
dhi_holo 6608268117 Update cobblemon.json
fixed typo
2025-10-03 14:03:35 +10:00
dhi_holo bdecab25a6 Merge branch 'main' into advancing-advancements 2025-10-03 13:38:45 +10:00
dhi_holo 81ed97a68b Update cobblemon.json
Removed removed item, added new items
2025-09-30 11:03:26 +10:00
dhi_holo 1e051deb9e Update CHANGELOG.md 2025-09-29 18:06:37 +10:00
dhi_holo 38c54e255a Chromatic Upgrades
- Removed Vivichoke advancement
- Reworded Galar Fossil advancement
- Renamed Master Rod advancement, made hidden by default
- Name tweak for IV Candy advancement
- Description update for regional delicacy advancement
- Renamed Poke Bait advancement
- Description update to ride stats
- Removed Aprijuice advancement
- Combined interaction advancements
- Added Saccharine advancement
- Moved Poke Snack advancement
- Updated some icons for advancements
2025-09-29 15:34:14 +10:00
dhi_holo e7ad38d1df Interactions and Clean Up
Added more Interaction based advancements, moved some stuff around, cleaned up visuals and description for max ride stats
2025-09-28 23:13:59 +10:00
dhi_holo 67c0abea8f Better Brush Desc 2025-09-28 21:15:54 +10:00
dhi_holo 85c1543df1 More tweaks
Brushing Pokemon advancement now works swell, Aprijuice advancement good, started work on the concept for Max Ride Stats, still need to actually get a trigger for it though
2025-09-28 17:10:15 +10:00
dhi_holo f38a1f83e2 More
Started on Bait and Brush advancements, renamed A Luring Aroma
2025-09-28 00:36:02 +10:00
dhi_holo 28785a01f6 Update revive.json
Reduced output to 1
2025-09-26 20:04:08 +10:00
dhi_holo 01c4260763 Merge branch 'main' into advancing-advancements 2025-09-25 20:05:09 +10:00
dhi_holo 150b3eb178 Snack 2025-09-25 20:05:07 +10:00
dhi_holo 5ab52d37f2 Fix Typo 2025-09-25 19:50:26 +10:00
dhi_holo ce191fd4c7 3 New, Minor Reorg
New: Cooking Regional Delicacies, Cooking EV Mochi, Cooking Poké Snack
Cleaned Up: Any Plant no longer announces to chat, Battle Win is on root, Use Candy is on Place Campfire Pot (also moves IV Candy to that tree instead)
2025-09-24 19:34:20 +10:00
MoeBoy76 9a7e349101 Merge branch 'main' of https://gitlab.com/cable-mc/cobblemon into getting-statistical 2025-09-23 09:33:46 +10:00
MoeBoy76 07a7d2a51a Statistics correctly register and if something goes wrong a more helpful crash should be thrown. 2025-09-23 09:33:28 +10:00
MoeBoy76 d2b192cb07 stats still don't register 2025-09-22 13:36:54 +10:00
MoeBoy76 d8e3e93a87 Merge branch 'main' of https://gitlab.com/cable-mc/cobblemon into getting-statistical 2025-09-22 10:06:52 +10:00
MoeBoy76 af12df38ed stats should work for NF now 2025-09-22 10:05:03 +10:00
Konoha aee89f1c92 Preliminary Bag Clause implementation 2025-09-09 18:15:37 -04:00
MoeBoy76 7fb8268fae Lang tweaks 2025-08-04 10:38:54 +10:00
MoeBoy76 8b49eb1526 Stats work and Advancement fixes to match wiki 2025-08-04 10:19:25 +10:00
MoeBoy76 c6cdcf83a9 Merge branch 'main' of https://gitlab.com/cable-mc/cobblemon into getting-statistical
# Conflicts:
#	common/src/main/resources/assets/cobblemon/lang/en_us.json
2025-08-04 08:26:06 +10:00
MoeBoy76 4bf82083b4 Merge branch 'main' of https://gitlab.com/cable-mc/cobblemon into getting-statistical 2025-04-28 09:02:44 +10:00
MoeBoy76 6579fd8ebe more stats work, they load now and have lang 2025-04-23 11:02:12 +10:00
MoeBoy76 4d62ef771a Merge branch 'main' of https://gitlab.com/cable-mc/cobblemon into getting-statistical 2025-04-23 10:11:33 +10:00
MoeBoy76 7784a3a20c initial stats work 2025-04-22 09:49:47 +10:00
127 changed files with 8162 additions and 2501 deletions

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
# Changelog
## [1.8.0 (MONTH Xth, 2026)](#1-8-0)
### Additions
- Added a new `cobblemon:structure`, which is a feature without any restrictions allowing the Builders to place certain structures in much more specific manner.
### Changes
### Fixes
- Fixed rare Pokémon being far too likely to spawn multiple times in a single spawn cycle. The dev told me that the exact bug is too complicated to explain.
### Developer
- Tweaked the spawning selector interface to take a bucket function rather than a bucket directly.
### Molang & Datapacks
### Particles
### Localization
- Updated translations for:
+16
View File
@@ -0,0 +1,16 @@
### Additions
### Changes
- Wild Pokémon now reliably have at least one offensive move, if they can have offensive moves at that level.
### Fixes
### Developer
### Molang & Datapacks
- Added `moveset_builders` datapack folder for different styles of movesets.
- Added `party_pools` datapack folder for defining groups of Pokémon for the purposes of complex NPC party definitions.
- Added `party_compositions` datapack folder for arranging parties on NPCs that rely on party pools.
- Added `composed_pool` NPC party provider type which combines a party pool with a party composition to create more complex NPC parties.
### Particles
-7
View File
@@ -2,7 +2,6 @@
## [1.7.4 (MONTH Xth, 2026)](#1-7-4)
### Additions
- Added config options to award experience to fainted Pokémon for opponents they helped defeat (`awardExperienceToFaintedPokemon`) and to still award experience for defeated enemies even after battle loss/forfeit (`awardExperienceOnBattleLoss`)
- Party Overlay Exp Gained Animation will now display the amount of EXP the Pokémon gained
- Added a toast to notify players of how they can scroll through their party. Did you know you can hold R and scroll instead of using arrow keys??!
- Added new layer property that makes a layer scroll in a direction. "scrolling": {"speedU": 0.1, "speedV": 0.1}
@@ -38,21 +37,15 @@
- [Various UFO Ride Improvements](https://gitlab.com/cable-mc/cobblemon/-/merge_requests/2156)
- Fixed NeoForge brewing stand unexpectedly crashing users when shift clicking items within it's menu
- Fixed NPCs switching pokemon very often during battles
- Fixed Campfire pot causing crashes under certain circumstances
- Fixed Type Gems not being consumed upon use
- Fixed Ancient Pokeball's to their respective modifiers
- Fixed a Server warning when recalling Pokemon
### Developer
- Added `PokeSnackSpawnerFactory` which allows influence registration on PokeSnack encounters
- Added `FishingSpawnerFactory` which allows influence registration on fishing encounters
- Added `BattleFleeAttemptEvent` fired from `PokemonBattle.checkFleeAttempt`, allowing developers to intercept and control flee attempts
### Molang & Datapacks
- q.item.is_enchanted()
- q.item.has_enchantment(minecraft:sharpness, 3)
- Added `has_chosen_starter` & `get_starter_uuid` as available Molang functions
- Added `marks`, `has_mark`, & `remove_marks` as available Molang functions for pokemon
### Localization
- Updated translations for:
+1 -1
View File
@@ -55,7 +55,7 @@ dependencies {
// modCompileOnly(libs.flywheelFabric)
// Showdown
modCompileOnly(libs.graal.core)
modCompileOnly(libs.graal)
// Data Storage
modCompileOnly(libs.bundles.mongo)
@@ -101,7 +101,7 @@ public abstract class ServerEntityMixin {
cobblemon$broadcast(new ClientboundUpdateDriverInputPacket(driverInput, entity.getId()));
}
public void cobblemon$broadcast(NetworkPacket<?> packet) {
private void cobblemon$broadcast(NetworkPacket<?> packet) {
if (!(entity.level() instanceof ServerLevel level)) return;
ChunkMap chunkMap = level.getChunkSource().chunkMap;
Int2ObjectMap<?> entityMap = ((ChunkMapAccessor) chunkMap).getEntityMap();
@@ -28,8 +28,6 @@ object CobblemonActivities {
@JvmField
val POKEMON_HERD = register(Activity("pokemon_herd"))
@JvmField
val POKEMON_BOIDS = register(Activity("pokemon_boid"))
@JvmField
val POKEMON_POLLINATION = register(Activity("pokemon_pollination"))
@JvmStatic
@@ -74,6 +74,8 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val NPC_EDITOR = create("npc_editor", CobblemonItem(Item.Properties().stacksTo(1)))
@JvmField
val POKEEDIT_WAND = create("pokeedit_wand", PokeEditWandItem())
@JvmField
val pokeBalls = mutableListOf<PokeBallItem>()
@@ -21,7 +21,6 @@ import net.minecraft.core.UUIDUtil
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import java.util.*
import net.minecraft.world.entity.player.Player
object CobblemonMemories {
val memories = mutableMapOf<String, MemoryModuleType<*>>()
@@ -93,9 +92,6 @@ object CobblemonMemories {
val DISABLE_WALK_TO_WANTED_ITEM = register<Boolean>("disable_walk_to_wanted_item")
@JvmField
val TIME_TRYING_TO_REACH_WANTED_ITEM = register<Int>("time_trying_to_wanted_item")
/** Is the nearest player holding a diamond for debugging purposes? */
@JvmField
val DIAMOND_DEBUGGER = register<Player>("diamond_debugger")
@JvmStatic
fun <U> register(id: String, codec: Codec<U>): MemoryModuleType<U> {
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.moves.MoveSelector
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
import com.cobblemon.mod.common.util.adapters.MovesetBuilderAdapter
import com.cobblemon.mod.common.util.adapters.StringToObjectAdapter
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
object CobblemonMovesetBuilders : JsonDataRegistry<MovesetBuilder> {
override val gson: Gson = GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(MovesetBuilder::class.java, MovesetBuilderAdapter)
.registerTypeAdapter(MoveSelector::class.java, StringToObjectAdapter(MoveSelector.selectors))
.create()
override val typeToken = TypeToken.get(MovesetBuilder::class.java)
override val resourcePath = "moveset_builders"
override val id: ResourceLocation = cobblemonResource("moveset_builders")
override val type = PackType.SERVER_DATA
override val observable = SimpleObservable<CobblemonMovesetBuilders>()
val movesetBuilders = mutableMapOf<ResourceLocation, MovesetBuilder>()
fun getOrThrow(id: ResourceLocation): MovesetBuilder {
return movesetBuilders[id]
?: throw IllegalArgumentException("Unknown MovesetBuilder id: $id")
}
override fun sync(player: ServerPlayer) { /* These don't sync. But what if they did? Nah probably shouldn't sync. */ }
override fun reload(data: Map<ResourceLocation, MovesetBuilder>) {
movesetBuilders.clear()
data.forEach { (id, value) -> value.id = id }
movesetBuilders.putAll(data)
observable.emit(this)
}
}
@@ -23,7 +23,6 @@ import com.cobblemon.mod.common.client.net.data.CobblemonMechanicsSyncHandler
import com.cobblemon.mod.common.client.net.data.DataRegistrySyncPacketHandler
import com.cobblemon.mod.common.client.net.data.RideSettingsSyncHandler
import com.cobblemon.mod.common.client.net.debug.OpenRidingStatsDebugGUIHandler
import com.cobblemon.mod.common.client.net.debug.S2CUpdateObsAvoidHandler
import com.cobblemon.mod.common.client.net.dialogue.DialogueClosedHandler
import com.cobblemon.mod.common.client.net.dialogue.DialogueOpenedHandler
import com.cobblemon.mod.common.client.net.effect.PokeSnackBlockParticlesHandler
@@ -81,7 +80,6 @@ import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyMoveCallba
import com.cobblemon.mod.common.net.messages.client.cooking.SeasoningRegistrySyncPacket
import com.cobblemon.mod.common.net.messages.client.cooking.ToggleCookingPotLidPacket
import com.cobblemon.mod.common.net.messages.client.data.*
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
import com.cobblemon.mod.common.net.messages.client.debug.RequestOpenRidingStatsDebugGUIPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueClosedPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueOpenedPacket
@@ -172,6 +170,7 @@ import com.cobblemon.mod.common.net.messages.server.pasture.UnpastureAllPokemonP
import com.cobblemon.mod.common.net.messages.server.pasture.UnpasturePokemonPacket
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.FinishScanningPacket
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.StartScanningPacket
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditActionPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.interact.InteractPokemonPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.ServerboundUpdateRidingStatePacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetActiveMarkPacket
@@ -229,6 +228,7 @@ import com.cobblemon.mod.common.net.serverhandling.pasture.UnpastureAllPokemonHa
import com.cobblemon.mod.common.net.serverhandling.pasture.UnpasturePokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.FinishScanningHandler
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.StartScanningHandler
import com.cobblemon.mod.common.net.serverhandling.pokeedit.PokeEditActionHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.interact.InteractPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.ServerboundUpdateRidingStateHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetActiveMarkHandler
@@ -255,11 +255,7 @@ import com.cobblemon.mod.common.net.serverhandling.trade.OfferTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.PerformTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.UpdateTradeOfferHandler
import com.cobblemon.mod.common.util.server
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.Entity
import com.cobblemon.mod.common.mixin.accessor.ChunkMapAccessor
import com.cobblemon.mod.common.mixin.accessor.TrackedEntityAccessor
/**
* Registers Cobblemon network packets.
@@ -282,19 +278,6 @@ object CobblemonNetwork {
fun sendToAllPlayers(packet: NetworkPacket<*>) = sendPacketToPlayers(server()!!.playerList.players, packet)
@JvmStatic
fun sendPacketToPlayers(players: Iterable<ServerPlayer>, packet: NetworkPacket<*>) = players.forEach { sendPacketToPlayer(it, packet) }
fun sendPacketToTrackingPlayers(entity: Entity, packet: NetworkPacket<*>) {
val level = entity.level() as? ServerLevel ?: return
val chunkMap = level.chunkSource.chunkMap
val entityTracker = (chunkMap as ChunkMapAccessor).entityMap[entity.id]
if (entityTracker is TrackedEntityAccessor) {
entityTracker.seenBy.forEach { connection ->
sendPacketToPlayer(connection.player, packet)
}
}
}
val s2cPayloads = generateS2CPacketInfoList()
val c2sPayloads = generateC2SPacketInfoList()
@@ -502,7 +485,6 @@ object CobblemonNetwork {
// Debug / cheats
list.add(PacketRegisterInfo(CalculateSeatPositionsPacket.ID, CalculateSeatPositionsPacket::decode, CalculateSeatPositionsHandler))
list.add(PacketRegisterInfo(ClientboundUpdateObsAvoidPacket.ID, ClientboundUpdateObsAvoidPacket::decode, S2CUpdateObsAvoidHandler))
// Riding
list.add(PacketRegisterInfo(ClientboundUpdateRidingStatePacket.ID, ClientboundUpdateRidingStatePacket::decode, ClientboundUpdateRidingStateHandler))
@@ -581,6 +563,9 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(StartScanningPacket.ID, StartScanningPacket::decode, StartScanningHandler))
list.add(PacketRegisterInfo(FinishScanningPacket.ID, FinishScanningPacket::decode, FinishScanningHandler))
// PokeEdit actions
list.add(PacketRegisterInfo(PokeEditActionPacket.ID, PokeEditActionPacket::decode, PokeEditActionHandler))
// Pasture
list.add(PacketRegisterInfo(PasturePokemonPacket.ID, PasturePokemonPacket::decode, PasturePokemonHandler))
list.add(PacketRegisterInfo(UnpasturePokemonPacket.ID, UnpasturePokemonPacket::decode, UnpasturePokemonHandler))
@@ -639,4 +624,4 @@ object CobblemonNetwork {
fun sendPacketToPlayer(player: ServerPlayer, packet: NetworkPacket<*>) {
Cobblemon.implementation.networkManager.sendPacketToPlayer(player, packet)
}
}
}
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.npc.PartyComposition
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
object CobblemonPartyCompositions : JsonDataRegistry<PartyComposition> {
override val id: ResourceLocation = cobblemonResource("party_compositions")
override val type = PackType.SERVER_DATA
override val typeToken: TypeToken<PartyComposition> = TypeToken.get(PartyComposition::class.java)
override val resourcePath: String = "party_compositions"
override val observable = SimpleObservable<CobblemonPartyCompositions>()
override val gson: Gson = GsonBuilder()
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.setPrettyPrinting()
.create()
val partyCompositions = mutableMapOf<ResourceLocation, PartyComposition>()
override fun sync(player: ServerPlayer) {
// probably worth syncing which exist, yeah. the deeper details probably not. I'll get to this eventually.
}
override fun reload(data: Map<ResourceLocation, PartyComposition>) {
data.entries.forEach { it.value.id = it.key }
partyCompositions.clear()
partyCompositions.putAll(data)
observable.emit(this)
Cobblemon.LOGGER.info("Loaded ${data.size} party compositions.")
}
}
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.npc.PartyPool
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.ScriptableIntRange
import com.cobblemon.mod.common.util.adapters.ExpressionLikeAdapter
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
import com.cobblemon.mod.common.util.adapters.IntRangeAdapter
import com.cobblemon.mod.common.util.adapters.MovesetBuilderReferenceAdapter
import com.cobblemon.mod.common.util.adapters.PokemonPropertiesAdapter
import com.cobblemon.mod.common.util.adapters.ScriptableIntRangeAdapter
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
object CobblemonPartyPools : JsonDataRegistry<PartyPool> {
override val id: ResourceLocation = cobblemonResource("party_pools")
override val type = PackType.SERVER_DATA
override val typeToken: TypeToken<PartyPool> = TypeToken.get(PartyPool::class.java)
override val resourcePath: String = "party_pools"
override val observable = SimpleObservable<CobblemonPartyPools>()
override val gson: Gson = GsonBuilder()
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(PokemonProperties::class.java, PokemonPropertiesAdapter(saveLong = false))
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
.registerTypeAdapter(ScriptableIntRange::class.java, ScriptableIntRangeAdapter)
.registerTypeAdapter(MovesetBuilder::class.java, MovesetBuilderReferenceAdapter)
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
.setPrettyPrinting()
.create()
val partyPools = mutableMapOf<ResourceLocation, PartyPool>()
override fun sync(player: ServerPlayer) {
// probably worth syncing. Knowing the details might help exploit the server? lot of effort for little though.
}
override fun reload(data: Map<ResourceLocation, PartyPool>) {
data.entries.forEach { it.value.id = it.key }
partyPools.clear()
partyPools.putAll(data)
observable.emit(this)
}
}
@@ -49,9 +49,6 @@ object CobblemonSensors {
@JvmField
val NEARBY_SWEET_BERRY_BUSH = register("nearby_sweet_berry_bush", ::SweetBerryBushSensor)
@JvmField
val PLAYER_DEBUGGING = register("player_debugging", ::PlayerDebuggingSensor)
@JvmField
val POKEMON_NEARBY_WANTED_ITEM = register("pokemon_nearby_wanted_item", ::PokemonItemSensor)
@@ -1,34 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai.config.task
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.CobblemonSensors
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
import com.cobblemon.mod.common.api.ai.WrapperLivingEntityTask
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.tasks.WakeUpTask
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.BehaviorControl
class BoidsTaskConfig : SingleTaskConfig {
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) = emptyList<MoLangConfigVariable>()
override fun createTask(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): BehaviorControl<in LivingEntity>? {
if (entity !is PokemonEntity) {
return null
}
behaviourConfigurationContext.addMemories(CobblemonMemories.POKEMON_DROWSY, CobblemonMemories.POKEMON_SLEEPING)
behaviourConfigurationContext.addSensors(CobblemonSensors.POKEMON_DROWSY)
return WrapperLivingEntityTask(WakeUpTask.create(), PokemonEntity::class.java)
}
}
@@ -1,44 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai.config.task
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.BoidsMoveControl
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.BehaviorControl
import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder
import net.minecraft.world.entity.ai.behavior.declarative.Trigger
class SwitchFromBoidTaskConfig : SingleTaskConfig {
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) = emptyList<MoLangConfigVariable>()
override fun createTask(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): BehaviorControl<in LivingEntity>? {
behaviourConfigurationContext.addMemories(CobblemonMemories.DIAMOND_DEBUGGER)
return BehaviorBuilder.create {
it.group(
it.absent(CobblemonMemories.DIAMOND_DEBUGGER)
).apply(it) { _ ->
Trigger { world, entity, time ->
if(entity !is PokemonEntity) return@Trigger false
if (entity.moveControl !is PokemonMoveControl) {
entity.moveControl = PokemonMoveControl(entity)
}
entity.brain.updateActivityFromSchedule(world.dayTime, world.gameTime)
return@Trigger true
}
}
}
}
}
@@ -1,62 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai.config.task
import com.cobblemon.mod.common.CobblemonActivities
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.CobblemonSensors
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.BoidsMoveControl
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormEntityParticlePacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.BehaviorControl
import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder
import net.minecraft.world.entity.ai.behavior.declarative.Trigger
import net.minecraft.world.entity.player.Player
class SwitchToBoidTaskConfig : SingleTaskConfig {
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) = emptyList<MoLangConfigVariable>()
override fun createTask(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): BehaviorControl<in LivingEntity>? {
behaviourConfigurationContext.addMemories(CobblemonMemories.DIAMOND_DEBUGGER)
behaviourConfigurationContext.addSensors(CobblemonSensors.PLAYER_DEBUGGING)
return BehaviorBuilder.create {
it.group(
it.present(CobblemonMemories.DIAMOND_DEBUGGER)
).apply(it) { _ ->
Trigger { world, entity, time ->
if(entity !is PokemonEntity) return@Trigger false
val player = entity.brain.getMemory(CobblemonMemories.DIAMOND_DEBUGGER).orElse(null)
if (entity.moveControl !is BoidsMoveControl && player is LivingEntity) {
entity.moveControl = BoidsMoveControl(entity, player)
} else {
return@Trigger false
}
entity.brain.setActiveActivityIfPossible(CobblemonActivities.POKEMON_BOIDS)
// SpawnSnowstormEntityParticlePacket(
// cobblemonResource("evo_particles"),
// entity.id,
// listOf("shiny_particles", "middle")).sendToPlayersAround(
// entity.x,
// entity.y,
// entity.z,
// 64.0,
// entity.level().dimension()
// )
return@Trigger true
}
}
}
}
}
@@ -124,8 +124,6 @@ interface TaskConfig {
cobblemonResource("target_entity") to TargetEntityTaskConfig::class.java,
cobblemonResource("memory_aspect") to MemoryAspectTaskConfig::class.java,
cobblemonResource("activity_change") to ActivityChangeTaskConfig::class.java,
cobblemonResource("switch_from_boid") to SwitchFromBoidTaskConfig::class.java,
cobblemonResource("switch_to_boid") to SwitchToBoidTaskConfig::class.java,
)
}
@@ -22,21 +22,18 @@ import com.cobblemon.mod.common.api.battles.model.actor.EntityBackedBattleActor
import com.cobblemon.mod.common.api.battles.model.actor.FleeableBattleActor
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.battles.BattleFledEvent
import com.cobblemon.mod.common.api.events.battles.BattleFleeAttemptEvent
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.pokemon.stats.BattleEvSource
import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.white
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.battles.ActiveBattlePokemon
import com.cobblemon.mod.common.battles.BattleCaptureAction
import com.cobblemon.mod.common.battles.BattleFormat
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.BattleSide
import com.cobblemon.mod.common.battles.FleeAttemptActionResponse
import com.cobblemon.mod.common.battles.ForfeitActionResponse
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.battles.dispatch.BattleDispatch
@@ -51,9 +48,7 @@ import com.cobblemon.mod.common.entity.npc.NPCBattleActor
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.battle.BattleEndPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMakeChoicePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMessagePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleQueueRequestPacket
import com.cobblemon.mod.common.pokemon.evolution.progress.DefeatEvolutionProgress
import com.cobblemon.mod.common.pokemon.evolution.progress.LastBattleCriticalHitsEvolutionProgress
import com.cobblemon.mod.common.pokemon.requirements.DefeatRequirement
@@ -142,9 +137,6 @@ open class PokemonBattle(
val captureActions = mutableListOf<BattleCaptureAction>()
val majorBattleActions = hashMapOf<UUID, BattleMessage>()
/** Monotonically increasing counter to track faint ordering within this battle. */
var faintCounter = 0
val minorBattleActions = hashMapOf<UUID, BattleMessage>()
val contextManager = ContextManager()
@@ -252,23 +244,12 @@ open class PokemonBattle(
fun end() {
ended = true
val awardToFainted = Cobblemon.config.awardExperienceToFaintedPokemon
val awardOnLoss = Cobblemon.config.awardExperienceOnBattleLoss
this.actors.forEach { actor ->
val faintedPokemons = actor.pokemonList.filter { it.health <= 0 }
actor.getSide().getOppositeSide().actors.forEach { opponent ->
val opponentPokemons = if (awardToFainted) opponent.pokemonList else opponent.pokemonList.filter { it.health > 0 }
val opponentNonFaintedPokemons = opponent.pokemonList.filter { it.health > 0 }
faintedPokemons.forEach { faintedPokemon ->
for (opponentPokemon in opponentPokemons) {
// Faint-order check: if awarding XP to fainted pokemon, only award for enemies that fainted before this pokemon did
if (awardToFainted) {
val enemyFaintedAt = faintedPokemon.faintedAt ?: continue
val opponentFaintedAt = opponentPokemon.faintedAt
if (opponentFaintedAt != null && opponentFaintedAt <= enemyFaintedAt) {
continue
}
}
for (opponentPokemon in opponentNonFaintedPokemons) {
val facedFainted = opponentPokemon.facedOpponents.contains(faintedPokemon)
val pokemon = opponentPokemon.effectedPokemon
if (facedFainted) {
@@ -289,8 +270,7 @@ open class PokemonBattle(
else -> continue
}
val experience = Cobblemon.experienceCalculator.calculate(opponentPokemon, faintedPokemon, multiplier)
val enemyTeamWiped = actor.pokemonList.all { it.health <= 0 }
if (experience > 0 && (enemyTeamWiped || awardOnLoss)) {
if (experience > 0 && actor.pokemonList.all { it.health <= 0 }) {
opponent.awardExperience(opponentPokemon, experience)
}
Cobblemon.evYieldCalculator.calculate(opponentPokemon, faintedPokemon).forEach { (stat, amount) ->
@@ -525,7 +505,6 @@ open class PokemonBattle(
fun checkForInputDispatch() {
if (checkForfeit()) return // ignore actors that are still choosing, their choices don't matter anymore
if (checkFleeAttempt()) return
val readyToInput = (actors.any { !it.mustChoose && it.responses.isNotEmpty() } && actors.none { it.mustChoose })
if (readyToInput && captureActions.isEmpty()) {
actors.filter { it.responses.isNotEmpty() }.forEach { it.writeShowdownResponse() }
@@ -533,20 +512,6 @@ open class PokemonBattle(
}
}
private fun checkFleeAttempt(): Boolean {
val runner = actors.find { it.responses.any { it is FleeAttemptActionResponse } } as? PlayerBattleActor ?: return false
CobblemonEvents.BATTLE_FLEE_ATTEMPT.post(BattleFleeAttemptEvent(this, runner))
runner.responses.clear()
runner.mustChoose = true
runner.request?.let { runner.sendUpdate(BattleQueueRequestPacket(it)) }
runner.sendUpdate(BattleMakeChoicePacket())
runner.entity?.sendSystemMessage(battleLang("run_prompt").white())
return true
}
/** Forces Showdown to end the battle when a [BattleActor] chooses to forfeit. */
private fun checkForfeit(): Boolean {
val forfeit = actors.find { it.responses.any { it is ForfeitActionResponse } }
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.api.events
import com.cobblemon.mod.common.api.events.battles.BattleFaintedEvent
import com.cobblemon.mod.common.api.events.battles.BattleFledEvent
import com.cobblemon.mod.common.api.events.battles.BattleFleeAttemptEvent
import com.cobblemon.mod.common.api.events.battles.BattleStartedEvent
import com.cobblemon.mod.common.api.events.battles.BattleVictoryEvent
import com.cobblemon.mod.common.api.events.battles.instruction.FormeChangeEvent
@@ -111,9 +110,6 @@ object CobblemonEvents {
@JvmField
val BATTLE_STARTED_POST = EventObservable<BattleStartedEvent.Post>()
@JvmField
val BATTLE_FLEE_ATTEMPT = EventObservable<BattleFleeAttemptEvent>()
@JvmField
val BATTLE_FLED = EventObservable<BattleFledEvent>()
@@ -1,34 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.battles
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.util.asArrayValue
/**
* Event fired when a [PokemonBattle] is fled by a [PlayerBattleActor].
*
* @author saultsc
* @since March 31th 2026
*/
data class BattleFleeAttemptEvent(
override val battle: PokemonBattle,
val player: PlayerBattleActor
) : BattleEvent {
val context = mutableMapOf<String, MoValue>(
"battle" to battle.struct,
"players" to battle.actors.filter { it.type == ActorType.PLAYER }.asArrayValue { it.struct },
"npcs" to battle.actors.filter { it.type == ActorType.NPC }.asArrayValue { it.struct },
"wild_pokemon" to battle.actors.filter { it.type == ActorType.WILD }.asArrayValue { it.struct }
)
}
@@ -1842,30 +1842,6 @@ object MoLangFunctions {
map.put("remove_cosmetic_item") { _ ->
pokemon.removeCosmeticItem()
}
map.put("marks") { _ -> pokemon.marks.asArrayValue { StringValue(it.identifier.toString()) } }
map.put("has_mark") { params ->
var hasMark = false
val identifier = params.getString(0).asIdentifierDefaultingNamespace()
val mark = Marks.getByIdentifier(identifier)
if (mark != null) {
hasMark = pokemon.marks.contains(mark)
}
DoubleValue(hasMark)
}
map.put("remove_marks") { params ->
var removedMark = false
for (param in params.params) {
val identifier = param.asString().asIdentifierDefaultingNamespace()
val mark = Marks.getByIdentifier(identifier)
if (mark != null) {
pokemon.exchangeMark(mark, false)
removedMark = true
}
}
DoubleValue(removedMark)
}
map.put("add_marks") { params ->
var appliedMark = false
for (param in params.params) {
@@ -1965,8 +1941,7 @@ object MoLangFunctions {
return@put DoubleValue(if (pokemon.addRideBoost(stat, value)) 1.0 else 0.0)
}
map.put("initialize_moveset") { params ->
val preferLatest = params.getBooleanOrNull(0) ?: true
pokemon.initializeMoveset(preferLatest)
pokemon.initializeMovesetFromDefault()
return@put DoubleValue.ONE
}
map.put("validate_moveset") { params ->
@@ -2059,13 +2034,9 @@ object MoLangFunctions {
}
}
map.put("is_sprinting") {
if (pokemonEntity.hasControllingPassenger()) { // If its being ridden then the riding state sets the animation
pokemonEntity.ifRidingAvailableSupply(DoubleValue.ZERO) { behaviour, settings, state ->
val moValue = behaviour.asMoLangValue(settings, state, pokemonEntity)
return@ifRidingAvailableSupply moValue.functions.get("sprinting")?.apply(MoParams.EMPTY) as? MoValue ?: DoubleValue.ZERO
}
} else {
pokemonEntity.isPokemonSprinting
pokemonEntity.ifRidingAvailableSupply(DoubleValue.ZERO) { behaviour, settings, state ->
val moValue = behaviour.asMoLangValue(settings, state, pokemonEntity)
return@ifRidingAvailableSupply moValue.functions.get("sprinting")?.apply(MoParams.EMPTY) as? MoValue ?: DoubleValue.ZERO
}
}
map.put("is_drifting") {
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.moves
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.resources.ResourceLocation
/**
* This is all you need tbh. A moveset builder that picks moves from up to 4 slots of move selectors.
*
* @author Hiroku
* @since December 5th, 2025
*/
class DefaultMovesetBuilder : MovesetBuilder {
companion object {
val TYPE = cobblemonResource("default")
}
override val type: ResourceLocation = TYPE
override var id: ResourceLocation = cobblemonResource("temp") // replaced on load.
val slot1: List<MoveSelector> = listOf()
val slot2: List<MoveSelector> = listOf()
val slot3: List<MoveSelector> = listOf()
val slot4: List<MoveSelector> = listOf()
override fun build(form: FormData, level: Int): MoveSet {
val chosenMoves = mutableSetOf<MoveTemplate>()
tryAddMoveFromSlot(form, level, slot1, chosenMoves)
tryAddMoveFromSlot(form, level, slot2, chosenMoves)
tryAddMoveFromSlot(form, level, slot3, chosenMoves)
tryAddMoveFromSlot(form, level, slot4, chosenMoves)
if (chosenMoves.isEmpty()) {
chosenMoves.add(Moves.getExceptional())
}
val moveset = MoveSet()
chosenMoves.map(MoveTemplate::create).forEach(moveset::add)
return moveset
}
fun tryAddMoveFromSlot(form: FormData, level: Int, selectors: List<MoveSelector>, chosenMoves: MutableSet<MoveTemplate>) {
for (selector in selectors) {
val move = selector(form, form.moves, level, chosenMoves)
if (move != null) {
chosenMoves.add(move)
return
}
}
}
}
@@ -0,0 +1,148 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.moves
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.pokemon.moves.Learnset
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.util.weightedSelection
/**
* A function that selects a move from a given [Learnset] for a particular [FormData] at a certain level,
* taking into account any moves that have already been chosen for the moveset.
*
* @author Hiroku
* @since December 5th, 2025
*/
fun interface MoveSelector {
operator fun invoke(form: FormData, learnset: Learnset, level: Int, chosenMoves: Set<MoveTemplate>): MoveTemplate?
companion object {
val NONE = MoveSelector { _, _, _, _ -> null }
val LAST_LEVELUP = MoveSelector { form, learnset, level, chosenMoves ->
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
levels.forEach {
val moves = (learnset.levelUpMoves[it] ?: return@forEach) - chosenMoves
if (moves.isNotEmpty()) {
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
}
return@MoveSelector null
}
val LAST_OFFENSIVE = MoveSelector { form, learnset, level, chosenMoves ->
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
levels.forEach {
val moves = (learnset.levelUpMoves[it]?.filter { it.damageCategory != DamageCategories.STATUS } ?: return@forEach) - chosenMoves
if (moves.isNotEmpty()) {
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
}
return@MoveSelector null
}
val LAST_SUITABLE_OFFENSIVE = MoveSelector { form, learnset, level, chosenMoves ->
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
val suitableCategories = if (form.baseStats[Stats.ATTACK]!! > form.baseStats[Stats.SPECIAL_ATTACK]!!) {
setOf(DamageCategories.PHYSICAL)
} else if (form.baseStats[Stats.SPECIAL_ATTACK]!! > form.baseStats[Stats.ATTACK]!!) {
setOf(DamageCategories.SPECIAL)
} else {
setOf(DamageCategories.PHYSICAL, DamageCategories.SPECIAL)
}
levels.forEach {
val moves = (learnset.levelUpMoves[it]?.filter { it.damageCategory in suitableCategories } ?: return@forEach) - chosenMoves
if (moves.isNotEmpty()) {
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
}
return@MoveSelector null
}
val LAST_STATUS = MoveSelector { form, learnset, level, chosenMoves ->
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
levels.forEach {
val moves = (learnset.levelUpMoves[it]?.filter { it.damageCategory == DamageCategories.STATUS } ?: return@forEach) - chosenMoves
if (moves.isNotEmpty()) {
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
}
return@MoveSelector null
}
val STAB = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val stabMoves = moves.filter { it.elementalType in form.types && it.damageCategory != DamageCategories.STATUS }
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
}
val STAB_PHYSICAL = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val stabMoves = moves.filter { it.elementalType in form.types && it.damageCategory == DamageCategories.PHYSICAL }
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
}
val STAB_SPECIAL = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val stabMoves = moves.filter { it.elementalType in form.types && it.damageCategory == DamageCategories.SPECIAL }
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
}
val PHYSICAL = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val physicalMoves = moves.filter { it.damageCategory == DamageCategories.PHYSICAL }
return@MoveSelector physicalMoves.weightedSelection { it.getSelectionWeight(form) }
}
val SPECIAL = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val specialMoves = moves.filter { it.damageCategory == DamageCategories.SPECIAL }
return@MoveSelector specialMoves.weightedSelection { it.getSelectionWeight(form) }
}
val OFFENSIVE = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val offensiveMoves = moves.filter { it.damageCategory != DamageCategories.STATUS }
return@MoveSelector offensiveMoves.weightedSelection { it.getSelectionWeight(form) }
}
val RANDOM_LEVELUP = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
val STATUS = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val statusMoves = moves.filter { it.damageCategory == DamageCategories.STATUS }
return@MoveSelector statusMoves.weightedSelection { it.getSelectionWeight(form) }
}
val TM = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.tmMoves - chosenMoves - learnset.getLevelUpMovesUpTo(level)
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
val STAB_TM = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.tmMoves - chosenMoves - learnset.getLevelUpMovesUpTo(level)
val stabMoves = moves.filter { it.elementalType in form.types }
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
}
val EGG = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.eggMoves - chosenMoves - learnset.getLevelUpMovesUpTo(level)
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
val selectors = mutableMapOf(
"none" to NONE,
"last_levelup" to LAST_LEVELUP,
"last_offensive" to LAST_OFFENSIVE,
"last_suitable_offensive" to LAST_SUITABLE_OFFENSIVE,
"last_status" to LAST_STATUS,
"levelup" to RANDOM_LEVELUP,
"stab" to STAB,
"stab_physical" to STAB_PHYSICAL,
"stab_special" to STAB_SPECIAL,
"physical" to PHYSICAL,
"special" to SPECIAL,
"offensive" to OFFENSIVE,
"status" to STATUS,
"tm" to TM,
"stab_tm" to STAB_TM,
"egg" to EGG
)
}
}
@@ -12,12 +12,12 @@ import com.bedrockk.molang.runtime.struct.MoStruct
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.api.moves.animations.ActionEffectTimeline
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.moves.categories.DamageCategory
import com.cobblemon.mod.common.api.types.ElementalType
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.battles.MoveTarget
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.util.codec.CodecUtils
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.lang
@@ -53,7 +53,8 @@ open class MoveTemplate(
val pp: Int,
val priority: Int,
val critRatio: Double,
val effectChances: Array<Double>
val effectChances: Array<Double>,
val weight: Float
) {
val struct: MoStruct by lazy {
QueryStruct(hashMapOf())
@@ -69,9 +70,9 @@ open class MoveTemplate(
.addFunction("max_pp") { DoubleValue(maxPp) }
.addFunction("priority") { DoubleValue(priority) }
.addFunction("crit_ratio") { DoubleValue(critRatio) }
.addFunction("weight") { DoubleValue(weight.toDouble()) }
}
val displayName: MutableComponent
get() = lang("move.$name")
val description: MutableComponent
@@ -89,7 +90,8 @@ open class MoveTemplate(
pp = 5,
priority = 0,
critRatio = 0.0,
effectChances = emptyArray()
effectChances = emptyArray(),
weight = 0F,
)
companion object {
@@ -160,4 +162,12 @@ open class MoveTemplate(
}
return this.elementalType
}
fun getSelectionWeight(form: FormData): Float {
return if (this in form.signatureMoves) {
weight * MovesetBuilder.signatureMoveWeightMultiplier
} else {
weight
}
}
}
@@ -10,15 +10,15 @@ package com.cobblemon.mod.common.api.moves
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.data.DataRegistry
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.battles.MoveTarget
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.net.messages.client.data.MovesRegistrySyncPacket
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.fromJson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import net.minecraft.resources.ResourceLocation
@@ -46,6 +46,14 @@ object Moves : DataRegistry {
this.idMapping.clear()
this.moveScripts.clear()
val weights = mutableMapOf<String, Number>()
manager.getResourceStack(cobblemonResource("move_weights.json")).forEach {
it.open().use {
val map = GsonBuilder().create().fromJson<Map<String, Number>>(it.reader())
weights.putAll(map)
}
}
ShowdownService.service.resetRegistryData("move")
manager.listResources("moves") { it.path.endsWith(".js") }.forEach { (identifier, resource) ->
resource.open().use { stream ->
@@ -75,6 +83,7 @@ object Moves : DataRegistry {
val priority = jsMove.get("priority").asInt
val critRatio = jsMove.get("critRatio")?.asDouble ?: 1.0
val effectChances = arrayListOf<Double>()
val weight = weights[id]?.toFloat() ?: 50F
val secondariesMember = jsMove.get("secondaries")
val secondaryMember = jsMove.get("secondary")
if (secondariesMember != null && secondariesMember is JsonArray) {
@@ -92,7 +101,20 @@ object Moves : DataRegistry {
effectChances += secondaryMember.get("chance").asDouble
}
}
val move = MoveTemplate(id, num, elementalType, damageCategory, power, target, accuracy, pp, priority, critRatio, effectChances.toTypedArray())
val move = MoveTemplate(
name = id,
num = num,
elementalType = elementalType,
damageCategory = damageCategory,
power = power,
target = target,
accuracy = accuracy,
pp = pp,
priority = priority,
critRatio = critRatio,
effectChances = effectChances.toTypedArray(),
weight = weight
)
this.register(move)
} catch (e: Exception) {
Cobblemon.LOGGER.error("Caught exception trying to resolve the move '{}'", id, e)
@@ -113,7 +135,7 @@ object Moves : DataRegistry {
@JvmStatic
fun getByNameOrDummy(name: String) = allMoves[name.lowercase()] ?: MoveTemplate.dummy(name.lowercase())
@JvmStatic
fun getExceptional() = getByName("tackle") ?: allMoves.values.random()
fun getExceptional() = getByName("tackle") ?: allMoves.values.first()
@JvmStatic
fun count() = allMoves.size
@JvmStatic
@@ -129,5 +151,4 @@ object Moves : DataRegistry {
this.allMoves[move.name] = move
this.idMapping[move.num] = move
}
}
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.moves
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.resources.ResourceLocation
/**
* Builders a moveset for a particular [FormData] and level. Probably only really need [DefaultMovesetBuilder] though.
*
* @author Hiroku
* @since December 5th, 2025
*/
interface MovesetBuilder {
/** The ID of this builder. It's autofilled from the registry loader. */
var id: ResourceLocation
/** The subtype designation for this moveset builder. Generally unused once read from JSON. */
val type: ResourceLocation
/** Builds a brand new moveset for the [form] and [level]. */
fun build(form: FormData, level: Int): MoveSet
companion object {
/** Weight multiplier for moves that are in the form's signatureMoves list. Makes them way more likely to be chosen. */
var signatureMoveWeightMultiplier = 5F
/** Subtypes of MovesetBuilder. I personally doubt you'll need any other subtypes, but hey. */
val movesetBuilderTypes = mutableMapOf<ResourceLocation, Class<out MovesetBuilder>>(
cobblemonResource("default") to DefaultMovesetBuilder::class.java
)
}
}
@@ -8,12 +8,14 @@
package com.cobblemon.mod.common.api.npc
import com.cobblemon.mod.common.api.npc.partyproviders.ComposedPoolPartyProvider
import com.cobblemon.mod.common.api.npc.partyproviders.PoolPartyProvider
import com.cobblemon.mod.common.api.npc.partyproviders.ScriptPartyProvider
import com.cobblemon.mod.common.api.npc.partyproviders.SimplePartyProvider
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.google.gson.JsonElement
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
/**
@@ -25,14 +27,15 @@ import net.minecraft.server.level.ServerPlayer
*/
interface NPCPartyProvider {
companion object {
val types = mutableMapOf<String, (String) -> NPCPartyProvider>(
SimplePartyProvider.TYPE to { SimplePartyProvider() },
PoolPartyProvider.TYPE to { PoolPartyProvider() },
ScriptPartyProvider.TYPE to { ScriptPartyProvider() }
val types = mutableMapOf(
SimplePartyProvider.TYPE to ::SimplePartyProvider,
PoolPartyProvider.TYPE to ::PoolPartyProvider,
ScriptPartyProvider.TYPE to ::ScriptPartyProvider,
ComposedPoolPartyProvider.TYPE to ::ComposedPoolPartyProvider
)
}
val type: String
val type: ResourceLocation
val isStatic: Boolean
fun provide(npc: NPCEntity, level: Int, players: List<ServerPlayer> = emptyList()): NPCPartyStore
// Why did I opt for manual JSON loading??? I must have had a reason but I can't remember. Maybe for S2C? Use a codec doofus
@@ -0,0 +1,68 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.npc
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.cobblemonResource
import kotlin.random.Random
/**
* A kind of definition for how to compose a party of Pokémon given a [PartyPool].
*
* @author Hiroku
* @since December 7th, 2025
*/
class PartyComposition {
var id = cobblemonResource("dummy_composition")
// val displayName = "cobblemon.party_composition.$id" // dunno if i will use this yet
val scrambleOrder: Boolean = false
val slot1 = listOf<String>()
val slot2 = listOf<String>()
val slot3 = listOf<String>()
val slot4 = listOf<String>()
val slot5 = listOf<String>()
val slot6 = listOf<String>()
fun compose(pool: PartyPool, level: Int, aspects: Set<String>, desiredPokemonCount: Int, runtime: MoLangRuntime, random: Random = Random.Default): List<Pokemon> {
val chosenEntries = mutableListOf<PartyPool.PoolEntry>()
val availableEntries = pool.entries.filter { level in it.npcLevels.resolve(runtime) && (it.npcAspects.isEmpty() || it.npcAspects.all(aspects::contains)) }
for (labelSet in listOf(slot1, slot2, slot3, slot4, slot5, slot6)) {
pool.tryChoosingEntry(labelSet, runtime, random, availableEntries, chosenEntries)
if (chosenEntries.size >= desiredPokemonCount) {
break
}
}
val finalEntries = if (scrambleOrder) {
chosenEntries.shuffled(random)
} else {
chosenEntries
}
val pokemon = finalEntries.map { entry ->
val levelVariation = entry.levelVariation.resolve(runtime)
val lvlVariation = if (levelVariation.first == levelVariation.last) {
levelVariation.first
} else {
levelVariation.random(random)
}
val finalLevel = level + lvlVariation
val properties = entry.pokemon.copy()
properties.level = properties.level ?: finalLevel
val pokemon = properties.create()
entry.movesetBuilders.randomOrNull()?.let(pokemon::initializeMovesetFrom)
pokemon
}
return pokemon
}
}
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.npc
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.util.ScriptableIntRange
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.toProperties
import com.cobblemon.mod.common.util.weightedSelection
import kotlin.random.Random
/**
* A pool of Pokémon to choose from, typically for generating a party for an NPC. The entries
* of the pool work on a weight and label system which is used as part of a [PartyComposition].
*
* @author Hiroku
* @since December 7th, 2025
*/
class PartyPool {
var id = cobblemonResource("dummy")
val displayName = "cobblemon.party_pool.$id"
val entries = mutableListOf<PoolEntry>()
class PoolEntry {
val pokemon: PokemonProperties = "pikachu".toProperties()
val labels: List<String> = emptyList()
val npcAspects: List<String> = emptyList()
val required: List<String> = emptyList()
val excluded: List<String> = emptyList()
val maxSelectableTimes: ExpressionLike = "6".asExpressionLike()
val levelVariation: ScriptableIntRange = ScriptableIntRange(0, 0)
val npcLevels: ScriptableIntRange = ScriptableIntRange(1, 100)
val movesetBuilders: List<MovesetBuilder> = emptyList()
val weight: ExpressionLike = "50".asExpressionLike()
}
/**
* Goes through each of the labels in order, trying to find a valid choice. Once it finds one, it skips the later
* labels and adds the value to [chosenEntries] and returns. If none of the labels could find a valid entry, nothing is added.
*/
fun tryChoosingEntry(
labels: List<String>,
runtime: MoLangRuntime,
random: Random,
availableEntries: List<PoolEntry>,
chosenEntries: MutableList<PoolEntry>,
) {
for (label in labels) {
val entries = availableEntries
.filter { label in it.labels || label.lowercase() == "any" }
.filter { entry ->
// Check required labels
entry.required.all { it in labels } &&
// Check excluded labels
entry.excluded.none { it in labels } &&
// Check max selectable times
chosenEntries.count { it == entry } < entry.maxSelectableTimes.resolveFloat(runtime)
}
if (entries.isNotEmpty()) {
val selectedEntry = entries.weightedSelection(random) { it.weight.resolveFloat(runtime) } ?: return
chosenEntries.add(selectedEntry)
return
}
}
}
}
@@ -0,0 +1,94 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.npc.partyproviders
import com.bedrockk.molang.Expression
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.CobblemonPartyCompositions
import com.cobblemon.mod.common.CobblemonPartyPools
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.npc.NPCPartyProvider
import com.cobblemon.mod.common.api.npc.PartyComposition
import com.cobblemon.mod.common.api.npc.PartyPool
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.util.asArrayValue
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.resolveInt
import com.cobblemon.mod.common.util.withQueryValue
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import kotlin.random.Random
import kotlin.ranges.random
import net.minecraft.server.level.ServerPlayer
/**
* An [NPCPartyProvider] which uses a [PartyComposition] and [PartyPool] to generate a party.
*
* There are controls for minimum and maximum Pokémon count, as well as whether to use a fixed random seed for the NPC.
*
* @author Hiroku
* @since December 8th, 2025
*/
class ComposedPoolPartyProvider : NPCPartyProvider {
companion object {
val TYPE = cobblemonResource("composed_pool")
}
override val type = TYPE
override var isStatic: Boolean = true
var useFixedRandom: Boolean = false
var minPokemon: Expression = "1".asExpression()
var maxPokemon: Expression = "6".asExpression()
lateinit var pool: PartyPool
lateinit var composition: PartyComposition
override fun loadFromJSON(json: JsonElement) {
json as JsonObject
isStatic = json.getAsJsonPrimitive("isStatic")?.asBoolean ?: true
useFixedRandom = json.getAsJsonPrimitive("useFixedRandom")?.asBoolean ?: false
pool = CobblemonPartyPools.partyPools[json.get("pool").asString.asIdentifierDefaultingNamespace()]
?: throw IllegalArgumentException("Unknown PartyPool id: ${json.get("pool").asString}")
composition = CobblemonPartyCompositions.partyCompositions[json.get("composition").asString.asIdentifierDefaultingNamespace()]
?: throw IllegalArgumentException("Unknown PartyComposition id: ${json.get("composition").asString}")
minPokemon = json.getAsJsonPrimitive("minPokemon").asString?.asExpression() ?: "1".asExpression()
maxPokemon = json.getAsJsonPrimitive("maxPokemon").asString?.asExpression() ?: "6".asExpression()
}
override fun provide(npc: NPCEntity, level: Int, players: List<ServerPlayer>): NPCPartyStore {
val runtime = MoLangRuntime().setup().withQueryValue("npc", npc.struct)
val random = if (useFixedRandom) Random(npc.uuid.hashCode()) else Random.Default
runtime.withQueryValue("level", DoubleValue(level))
runtime.withQueryValue("players", players.asArrayValue { it.asMoLangValue() })
if (players.size == 1) {
// This is for the convenience, most cases will be pvn one player
runtime.withQueryValue("player", players.first().asMoLangValue())
}
val minPokemon = runtime.resolveInt(this.minPokemon)
val maxPokemon = runtime.resolveInt(this.maxPokemon)
val desiredPokemonCount = (minPokemon..maxPokemon).random(random)
val pokemonList = composition.compose(
pool = pool,
level = level,
aspects = npc.aspects,
desiredPokemonCount = desiredPokemonCount,
runtime = runtime,
random = random
)
val partyStore = NPCPartyStore(npc)
pokemonList.forEach(partyStore::add)
return partyStore
}
}
@@ -48,7 +48,7 @@ import kotlin.random.Random
*/
class PoolPartyProvider : NPCPartyProvider {
companion object {
const val TYPE = "pool"
val TYPE = cobblemonResource("pool")
}
@Transient
@@ -150,10 +150,10 @@ class PoolPartyProvider : NPCPartyProvider {
val instance = selected.pokemon.copy().also { it.level = it.level ?: dictatedLevel ?: randomLevel }.create()
party.add(instance)
}
if (party.none()) {
LOGGER.error("${npc} has no Pokemon on Party")
}
if (party.none()) {
LOGGER.error("$npc had no Pokémon produced by their party provider")
}
}
override fun provide(npc: NPCEntity, level: Int, players: List<ServerPlayer>): NPCPartyStore {
@@ -28,7 +28,7 @@ import net.minecraft.server.level.ServerPlayer
class ScriptPartyProvider : NPCPartyProvider {
companion object {
const val TYPE = "script"
val TYPE = cobblemonResource("script")
}
override val type = TYPE
@@ -16,6 +16,7 @@ import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.util.asArrayValue
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.getBooleanOrNull
import com.cobblemon.mod.common.util.toProperties
import com.google.gson.JsonElement
@@ -29,7 +30,7 @@ import net.minecraft.server.level.ServerPlayer
*/
class SimplePartyProvider : NPCPartyProvider {
companion object {
const val TYPE = "simple"
val TYPE = cobblemonResource("simple")
}
@Transient
@@ -289,11 +289,11 @@ object PokeBalls : JsonDataRegistry<PokeBall> {
createDefault("ancient_great_ball", MultiplierModifier(1.5F), ancient = true)
createDefault("ancient_ultra_ball", MultiplierModifier(2F), ancient = true)
createDefault("ancient_heavy_ball", throwPower = 0.75f, ancient = true)
createDefault("ancient_leaden_ball", MultiplierModifier(1.5F), throwPower = 0.75f, ancient = true)
createDefault("ancient_gigaton_ball", MultiplierModifier(2F), throwPower = 0.75f, ancient = true)
createDefault("ancient_leaden_ball", throwPower = 0.75f, ancient = true)
createDefault("ancient_gigaton_ball", throwPower = 0.75f, ancient = true)
createDefault("ancient_feather_ball", throwPower = 2.5f, ancient = true)
createDefault("ancient_wing_ball", MultiplierModifier(1.5F), throwPower = 2.5f, ancient = true)
createDefault("ancient_jet_ball", MultiplierModifier(2F), throwPower = 2.5f, ancient = true)
createDefault("ancient_wing_ball", throwPower = 2.5f, ancient = true)
createDefault("ancient_jet_ball", throwPower = 2.5f, ancient = true)
createDefault("ancient_origin_ball", GuaranteedModifier(), ancient = true)
// Luxury ball effect, low priority as it must be triggered before soothe bell as of gen 4
CobblemonEvents.FRIENDSHIP_UPDATED.subscribe(priority = Priority.LOW) { event ->
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.api.pokemon
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.config
import com.cobblemon.mod.common.CobblemonMovesetBuilders
import com.cobblemon.mod.common.api.abilities.Abilities
import com.cobblemon.mod.common.api.abilities.Ability
import com.cobblemon.mod.common.api.events.CobblemonEvents
@@ -148,6 +149,13 @@ open class PokemonProperties {
props.originalTrainerType = OriginalTrainerType.entries.toList().parsePropertyOfCollection(keyPairs, listOf("originaltrainertype", "ottype"), labelsOptional = true) { it.name.lowercase() }
props.originalTrainer = parsePlayerProperty(keyPairs, listOf("originaltrainer", "ot"))
props.moves = parseString(keyPairs, listOf("moves"))?.split(",")
props.movesetBuilders = parseString(keyPairs, listOf("moveset_builders", "movesetbuilders", "moveset_builder", "movesetbuilder"))?.split(",")?.mapNotNull {
try {
it.asIdentifierDefaultingNamespace()
} catch (_: ResourceLocationException) {
null
}
}
props.heldItem = parseString(keyPairs, listOf("helditem", "held_item"))
props.minPerfectIVs = parseIntProperty(keyPairs, listOf("min_perfect_ivs"))?.coerceIn(0, Stats.PERMANENT.size)
props.scaleModifier = parseFloatProperty(keyPairs, listOf("scale_modifier"))
@@ -356,6 +364,7 @@ open class PokemonProperties {
var originalTrainerType: OriginalTrainerType? = null
var originalTrainer: String? = null // Original Trainer by Username or UUID
var moves: List<String>? = null
var movesetBuilders: List<ResourceLocation>? = null
var heldItem: String? = null
var cosmeticItem: String? = null
var scaleModifier: Float? = null
@@ -462,7 +471,7 @@ open class PokemonProperties {
}
moves?.let { moves ->
if (pokemon.moveSet.getMoves().isEmpty()) {
pokemon.initializeMoveset(preferLatest = true)
pokemon.initializeMovesetFromDefault()
}
val moveTemplates = moves.mapNotNull(Moves::getByName).shuffled()
val replaceableIndices = (0..3).filterNot { pokemon.moveSet[it]?.template?.let(moveTemplates::contains) == true }.shuffled()
@@ -475,6 +484,13 @@ open class PokemonProperties {
pokemon.moveSet.update()
}
}
movesetBuilders?.let { movesetBuilders ->
val builders = movesetBuilders.mapNotNull { CobblemonMovesetBuilders.movesetBuilders[it] }
if (builders.isNotEmpty()) {
val movesetBuilder = builders.random()
pokemon.initializeMovesetFrom(movesetBuilder)
}
}
heldItem?.let { itemKey ->
val server = server() ?: return@let
val parser = ItemParser(server.registryAccess())
@@ -678,6 +694,7 @@ open class PokemonProperties {
originalTrainerType?.let { nbt.putInt(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE, it.ordinal) }
originalTrainer?.let { nbt.putString(DataKeys.POKEMON_ORIGINAL_TRAINER, it) }
moves?.let { nbt.putString(DataKeys.POKEMON_PROPERTIES_MOVES, it.joinToString(separator = ",")) }
movesetBuilders?.let { nbt.putString(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS, it.joinToString(separator = ",") { source -> source.toString() }) }
heldItem?.let {nbt.putString(DataKeys.POKEMON_PROPERTIES_HELDITEM, it)}
scaleModifier?.let { nbt.putFloat(DataKeys.POKEMON_SCALE_MODIFIER, it) }
val custom = ListTag()
@@ -712,6 +729,7 @@ open class PokemonProperties {
originalTrainerType = if (tag.contains(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE)) OriginalTrainerType.valueOf(tag.getString(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE)) else null
originalTrainer = if (tag.contains(DataKeys.POKEMON_ORIGINAL_TRAINER)) tag.getString(DataKeys.POKEMON_ORIGINAL_TRAINER) else null
moves = if (tag.contains(DataKeys.POKEMON_PROPERTIES_MOVES)) tag.getString(DataKeys.POKEMON_PROPERTIES_MOVES).split(",") else null
movesetBuilders = if (tag.contains(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS)) tag.getString(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS).split(",").map { it.asIdentifierDefaultingNamespace() } else null
heldItem = if (tag.contains(DataKeys.POKEMON_PROPERTIES_HELDITEM)) tag.getString(DataKeys.POKEMON_PROPERTIES_HELDITEM) else null
scaleModifier = if (tag.contains(DataKeys.POKEMON_SCALE_MODIFIER)) tag.getFloat(DataKeys.POKEMON_SCALE_MODIFIER) else null
val custom = tag.getList(DataKeys.POKEMON_PROPERTIES_CUSTOM, Tag.TAG_STRING.toInt())
@@ -747,6 +765,7 @@ open class PokemonProperties {
originalTrainerType?.let { json.addProperty(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE, it.name) }
originalTrainer?.let { json.addProperty(DataKeys.POKEMON_ORIGINAL_TRAINER, it) }
moves?.let { json.addProperty(DataKeys.POKEMON_PROPERTIES_MOVES, it.joinToString(separator = ",")) }
movesetBuilders?.let { json.addProperty(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS, it.joinToString(separator = ",") { source -> source.toString() }) }
heldItem?.let {json.addProperty(DataKeys.POKEMON_PROPERTIES_HELDITEM, it)}
scaleModifier?.let { json.addProperty(DataKeys.POKEMON_SCALE_MODIFIER, it) }
val custom = JsonArray()
@@ -781,6 +800,7 @@ open class PokemonProperties {
originalTrainerType = json.get(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE)?.asString?.let { OriginalTrainerType.valueOf(it) }
originalTrainer = json.get(DataKeys.POKEMON_ORIGINAL_TRAINER)?.asString
moves = json.get(DataKeys.POKEMON_PROPERTIES_MOVES)?.asString?.split(",")
movesetBuilders = json.get(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS)?.asString?.split(",")?.map { it.asIdentifierDefaultingNamespace() }
heldItem = json.get(DataKeys.POKEMON_PROPERTIES_HELDITEM)?.asString
scaleModifier = json.get(DataKeys.POKEMON_SCALE_MODIFIER)?.asFloat
val custom = json.get(DataKeys.POKEMON_PROPERTIES_CUSTOM)?.asJsonArray
@@ -820,6 +840,7 @@ open class PokemonProperties {
originalTrainer?.let { pieces.add("originaltrainer=$it") }
customProperties.forEach { pieces.add(it.asString()) }
moves?.let { pieces.add("moves=${it.joinToString(separator = ",")}") }
movesetBuilders?.let { pieces.add("movesetbuilders=${it.joinToString(separator = ",") { source -> source.toString() }}") }
heldItem?.let {pieces.add("helditem=$it")}
scaleModifier?.let { pieces.add("scale_modifier=$it") }
return pieces.joinToString(separator)
@@ -23,6 +23,7 @@ import com.cobblemon.mod.common.api.drop.ItemDropMethod
import com.cobblemon.mod.common.api.entity.EntityDimensionsAdapter
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.moves.adapters.MoveTemplateAdapter
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies.getByIdentifier
@@ -131,6 +132,7 @@ object PokemonSpecies : JsonDataRegistry<Species> {
.registerTypeAdapter(RidingBehaviourSettings::class.java, RidingBehaviourSettingsAdapter)
.registerTypeAdapter(RideSoundSettingsList::class.java, RideSoundSettingsListAdapter)
.registerTypeAdapter(ObtainableItemCondition::class.java, ObtainableItemConditionAdapter)
.registerTypeAdapter(MovesetBuilder::class.java, MovesetBuilderReferenceAdapter)
.disableHtmlEscaping()
.enableComplexMapKeySerialization()
.create()
@@ -314,11 +316,11 @@ object PokemonSpecies : JsonDataRegistry<Species> {
val result = mutableMapOf<String, String>()
this.species.forEach {species ->
val baseSpecies = ShowdownSpecies(species, null)
result[ShowdownIdentifiable.REGEX.replace(baseSpecies.name.lowercase(), "")] = this.gson.toJson(baseSpecies)
result[ShowdownIdentifiable.REGEX.replace(baseSpecies.name, "")] = this.gson.toJson(baseSpecies)
species.forms.forEach { form ->
if (form != species.standardForm) {
val formSpecies = ShowdownSpecies(species, form)
result[ShowdownIdentifiable.REGEX.replace(formSpecies.name.lowercase(), "")] = this.gson.toJson(formSpecies)
result[ShowdownIdentifiable.REGEX.replace(formSpecies.name, "")] = this.gson.toJson(formSpecies)
}
}
}
@@ -54,6 +54,8 @@ class RidingAnimationData(val ride: PokemonEntity)
fun update() {
val activeRide = ride.hasControllingPassenger()
if (!activeRide) return
var currRot = Vec3.ZERO
var angDelta = Vec3.ZERO
@@ -151,16 +153,14 @@ class RidingAnimationData(val ride: PokemonEntity)
/******************************************************
* Update springs
*****************************************************/
if (activeRide) {
val driver = ride.controllingPassenger
val vertInput = when {
driver != null && driver.jumping-> 1.0
driver != null && driver.isShiftKeyDown -> -1.0
else -> 0.0
}
val input = if(driver != null) Vec3(driver.xxa.toDouble() * -1, vertInput, driver.zza.toDouble()) else Vec3.ZERO
driverInputSpring.update(input, stiffness * 2, damping)
val driver = ride.controllingPassenger
val vertInput = when {
driver != null && driver.jumping-> 1.0
driver != null && driver.isShiftKeyDown -> -1.0
else -> 0.0
}
val input = if(driver != null) Vec3(driver.xxa.toDouble() * -1, vertInput, driver.zza.toDouble()) else Vec3.ZERO
driverInputSpring.update(input, stiffness * 2, damping)
rotDeltaSpring.update(angDelta, stiffness, damping)
velocitySpring.update(currentVelocity, stiffness, damping)
rotSpring.update(currRot, stiffness, damping)
@@ -44,7 +44,7 @@ import kotlin.random.Random
* @since July 10th, 2022
*/
open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelectionData> {
class SelectingSpawnInformation {
class SelectingSpawnInformation(val spawnDetail: SpawnDetail) {
val spawnablePositions = mutableMapOf<SpawnablePosition, Float>()
var highestWeight = 0F
@@ -58,61 +58,81 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
fun chooseSpawnablePosition() = spawnablePositions.entries.weightedSelection { it.value }!!.key
}
class SpawnablePositionSelectionData(
val spawnToSpawnablePosition: MutableMap<SpawnDetail, SelectingSpawnInformation>,
var percentSum: Float
): SpawnSelectionData {
val size: Int
get() = spawnToSpawnablePosition.size
class SpawnablePositionBucketSelectionData(
val selectingSpawnInformation: MutableList<SelectingSpawnInformation>
) {
var percentSum: Float = 0F
val seenSpawns = mutableListOf<SpawnDetail>()
fun add(spawnDetail: SpawnDetail, spawnablePosition: SpawnablePosition) {
// Only add to percentSum if this is the first time we've seen this SpawnDetail, otherwise
// the percentage will get amplified for every spawnable position the thing was possible, completely
// ruining the point of this pre-selection percentage.
if (spawnDetail.percentage > 0 && spawnDetail !in seenSpawns) {
seenSpawns.add(spawnDetail)
percentSum += spawnDetail.percentage
}
val spawnInformation = selectingSpawnInformation.find { it.spawnDetail == spawnDetail }
?: SelectingSpawnInformation(spawnDetail).also { selectingSpawnInformation.add(it) }
spawnInformation.add(spawnablePosition, spawnablePosition.getWeight(spawnDetail))
}
fun remove(spawnInformation: SelectingSpawnInformation) {
selectingSpawnInformation -= spawnInformation
if (spawnInformation.spawnDetail.percentage > 0) {
percentSum -= spawnInformation.spawnDetail.percentage
}
}
}
class SpawnablePositionSelectionData(
val spawner: Spawner,
val spawnablePositions: List<SpawnablePosition>,
val selectingSpawnInformation: MutableMap<SpawnBucket, SpawnablePositionBucketSelectionData>,
): SpawnSelectionData {
override val spawnActions = mutableListOf<SpawnAction<*>>()
override val context = mutableMapOf<String, Any>()
fun getDataForBucket(bucket: SpawnBucket): SpawnablePositionBucketSelectionData {
val existing = selectingSpawnInformation[bucket]
if (existing != null) {
return existing
}
val bucketSpawnInformation = SpawnablePositionBucketSelectionData(mutableListOf())
spawnablePositions.forEach { spawnablePosition ->
spawner.getMatchingSpawns(bucket, spawnablePosition).forEach {
bucketSpawnInformation.add(it, spawnablePosition)
}
}
selectingSpawnInformation[bucket] = bucketSpawnInformation
return bucketSpawnInformation
}
override fun removeSpawnDetails(shouldRemove: (SpawnDetail) -> Boolean) {
val toRemove = spawnToSpawnablePosition.entries.filter { shouldRemove(it.key) }
toRemove.forEach { spawnToSpawnablePosition.remove(it.key) }
percentSum -= toRemove.sumOf { it.key.percentage.toDouble().takeIf { it > 0 } ?: 0.0 }.toFloat()
selectingSpawnInformation.flatMap { it.value.selectingSpawnInformation.filter { shouldRemove(it.spawnDetail) } }
.forEach { selectingSpawnInformation[it.spawnDetail.bucket]?.remove(it) }
}
override fun removeSpawnablePositions(shouldRemove: (SpawnDetail, SpawnablePosition) -> Boolean) {
val toRemove = spawnToSpawnablePosition.entries.filter { (spawnDetail, positionData) ->
positionData.spawnablePositions.removeIf { shouldRemove(spawnDetail, it.key) }
val toRemove = selectingSpawnInformation.flatMap { it.value.selectingSpawnInformation }.filter { positionData ->
positionData.spawnablePositions.removeIf { shouldRemove(positionData.spawnDetail, it.key) }
positionData.highestWeight = positionData.spawnablePositions.maxOfOrNull { it.value } ?: 0F
positionData.spawnablePositions.isEmpty()
}
toRemove.forEach { spawnToSpawnablePosition.remove(it.key) }
percentSum -= toRemove.sumOf { it.key.percentage.toDouble().takeIf { it > 0 } ?: 0.0 }.toFloat()
toRemove.forEach { selectingSpawnInformation[it.spawnDetail.bucket]?.remove(it) }
}
}
override fun getSelectionData(
spawner: Spawner,
bucket: SpawnBucket,
spawnablePositions: List<SpawnablePosition>
): SpawnablePositionSelectionData {
val spawnToSpawnablePosition: MutableMap<SpawnDetail, SelectingSpawnInformation> = mutableMapOf()
var percentSum = 0F
val selectingSpawnInformation: MutableMap<SpawnBucket, SpawnablePositionBucketSelectionData> = mutableMapOf()
spawnablePositions.forEach { spawnablePosition ->
spawner.getMatchingSpawns(bucket, spawnablePosition).forEach {
// Only add to percentSum if this is the first time we've seen this SpawnDetail, otherwise
// the percentage will get amplified for every spawnable position the thing was possible, completely
// ruining the point of this pre-selection percentage.
if (it.percentage > 0 && !spawnToSpawnablePosition.containsKey(it)) {
percentSum += it.percentage
}
val selectingSpawnInformation = spawnToSpawnablePosition.getOrPut(
it,
::SelectingSpawnInformation
)
selectingSpawnInformation.add(spawnablePosition, spawnablePosition.getWeight(it))
}
}
return SpawnablePositionSelectionData(spawnToSpawnablePosition, percentSum)
return SpawnablePositionSelectionData(spawner, spawnablePositions, selectingSpawnInformation)
}
override fun selectSpawnAction(
@@ -120,8 +140,8 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
bucket: SpawnBucket,
selectionData: SpawnablePositionSelectionData,
): SpawnAction<*>? {
val spawnToSpawnablePosition = selectionData.spawnToSpawnablePosition
var percentSum = selectionData.percentSum
val selectingSpawnInformation = selectionData.getDataForBucket(bucket) ?: return null
var percentSum = selectingSpawnInformation.percentSum
// First pass is doing percentage checks.
if (percentSum > 0) {
@@ -129,7 +149,7 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
if (percentSum > 100) {
Cobblemon.LOGGER.warn(
"""
A spawn list for ${spawner.name} exceeded 100% on percentage sums...
A spawn list for ${spawner.name} exceeded 100% on percentage sums in bucket ${bucket.name}...
This means you don't understand how this option works.
""".trimIndent()
)
@@ -140,13 +160,13 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
* It's [0, 1) and I want (0, 1]
* See half-open intervals here https://en.wikipedia.org/wiki/Interval_(mathematics)#Terminology
*/
val selectedPercentage = 100 - Random.Default.nextFloat() * 100
val selectedPercentage = 100 - Random.nextFloat() * 100
percentSum = 0F
for ((spawnDetail, info) in spawnToSpawnablePosition) {
if (spawnDetail.percentage > 0) {
percentSum += spawnDetail.percentage
for (info in selectingSpawnInformation.selectingSpawnInformation) {
if (info.spawnDetail.percentage > 0) {
percentSum += info.spawnDetail.percentage
if (percentSum >= selectedPercentage) {
return spawnDetail.choose(
return info.spawnDetail.choose(
spawnablePosition = info.chooseSpawnablePosition(),
bucket = bucket,
selectionData = selectionData
@@ -156,9 +176,9 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
}
}
val selectedSpawn = spawnToSpawnablePosition.entries.toList().weightedSelection { it.value.highestWeight }!!
return selectedSpawn.key.choose(
spawnablePosition = selectedSpawn.value.chooseSpawnablePosition(),
val selectedSpawn = selectingSpawnInformation.selectingSpawnInformation.toList().weightedSelection { it.highestWeight }!!
return selectedSpawn.spawnDetail.choose(
spawnablePosition = selectedSpawn.chooseSpawnablePosition(),
bucket = bucket,
selectionData = selectionData
)
@@ -169,16 +189,16 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
bucket: SpawnBucket,
spawnablePositions: List<SpawnablePosition>
): Map<SpawnDetail, Float> {
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
val selectionData = getSelectionData(spawner, spawnablePositions).getDataForBucket(bucket)
if (selectionData.size == 0) {
if (selectionData.selectingSpawnInformation.isEmpty()) {
return emptyMap()
}
val totalWeights = mutableMapOf<SpawnDetail, Float>()
for ((spawnDetail, info) in selectionData.spawnToSpawnablePosition.entries) {
totalWeights[spawnDetail] = info.highestWeight
for (info in selectionData.selectingSpawnInformation) {
totalWeights[info.spawnDetail] = info.highestWeight
}
return totalWeights
@@ -18,7 +18,6 @@ import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.util.removeIf
import com.cobblemon.mod.common.util.weightedSelection
import kotlin.random.Random
import kotlin.reflect.full.createInstance
/**
* A spawning selector that compiles a distinct list of all spawn details that
@@ -33,9 +32,9 @@ import kotlin.reflect.full.createInstance
* of an entire spawnable position type to favour whichever are the more populous.
*
* The weight for a spawn when doing primary selection is whichever spawnable position-influenced
* weight is highest (as weight multipliers exist per spawnable position) and then the
* selection of which spawnable position to spawn the primary selected spawn uses the spawnable
* position-adjusted weight.
* weight is highest (as weight multipliers exist per spawnable position). After the spawn has been
* chosen, the selection of which spawnable position to spawn it at is selected via another weighted
* selection using spawnable position-adjusted weights.
*
* At a glance:
* - Spawn detail selection is flat across spawnable position quantity
@@ -45,7 +44,6 @@ import kotlin.reflect.full.createInstance
* @since July 10th, 2022
*/
open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnablePositionWeightedSelector.SeparatedSelectionData> {
open fun getWeight(spawnablePositionType: SpawnablePositionType<*>) = spawnablePositionType.getWeight()
class SelectingSpawnInformation {
val spawnablePositionWeights = mutableMapOf<SpawnablePosition, Float>()
@@ -62,13 +60,25 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
fun chooseSpawnablePosition() = spawnablePositionWeights.keys.toList().weightedSelection { spawnablePositionWeights[it]!! }!!
}
class SpawnablePositionSelectionData(
val spawnToSpawnablePosition: MutableMap<SpawnDetail, SelectingSpawnInformation>,
var percentSum: Float
) {
class SpawnablePositionSelectionData {
val spawnToSpawnablePosition = mutableMapOf<SpawnDetail, SelectingSpawnInformation>()
var percentSum: Float = 0F
val seenSpawns = mutableListOf<SpawnDetail>()
val size: Int
get() = spawnToSpawnablePosition.size
fun add(spawnDetail: SpawnDetail, spawnablePosition: SpawnablePosition) {
// Only add to percentSum if this is the first time we've seen this SpawnDetail for this spawnable
// position type, otherwise the percentage will get amplified for every spawnable position the thing
// was possible, completely ruining the point of this pre-selection percentage.
if (spawnDetail.percentage > 0 && spawnDetail !in seenSpawns) {
percentSum += spawnDetail.percentage
seenSpawns.add(spawnDetail)
}
val selectingSpawnInformation = spawnToSpawnablePosition.getOrPut(spawnDetail, ::SelectingSpawnInformation)
selectingSpawnInformation.add(spawnDetail, spawnablePosition, 1F)
}
fun removeSpawnDetails(shouldRemove: (SpawnDetail) -> Boolean) {
val toRemove = spawnToSpawnablePosition.entries.filter { shouldRemove(it.key) }
toRemove.forEach { spawnToSpawnablePosition.remove(it.key) }
@@ -89,67 +99,68 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
}
class SeparatedSelectionData(
val spawnablePositionTypeToSpawns: MutableMap<SpawnablePositionType<*>, SpawnablePositionSelectionData>
): SpawnSelectionData {
val spawner: Spawner,
val spawnablePositions: List<SpawnablePosition>
) : SpawnSelectionData {
override val spawnActions = mutableListOf<SpawnAction<*>>()
override val context = mutableMapOf<String, Any>()
val bucketData = mutableMapOf<SpawnBucket, MutableMap<SpawnablePositionType<*>, SpawnablePositionSelectionData>>()
override fun removeSpawnDetails(shouldRemove: (SpawnDetail) -> Boolean) {
spawnablePositionTypeToSpawns.values.forEach { it.removeSpawnDetails(shouldRemove) }
spawnablePositionTypeToSpawns.removeIf { it.value.size == 0 }
bucketData.values.forEach {
it.values.forEach { it.removeSpawnDetails(shouldRemove) }
it.removeIf { it.value.size == 0 }
}
}
override fun removeSpawnablePositions(shouldRemove: (SpawnDetail, SpawnablePosition) -> Boolean) {
spawnablePositionTypeToSpawns.values.forEach { it.removeSpawnablePositions(shouldRemove) }
spawnablePositionTypeToSpawns.removeIf { it.value.size == 0 }
bucketData.values.forEach {
it.values.forEach { it.removeSpawnablePositions(shouldRemove) }
it.removeIf { it.value.size == 0 }
}
}
fun getDataForBucket(bucket: SpawnBucket): MutableMap<SpawnablePositionType<*>, SpawnablePositionSelectionData> {
bucketData[bucket]?.let { return it }
val spawnablePositionTypeToSpawns = mutableMapOf<SpawnablePositionType<*>, SpawnablePositionSelectionData>()
spawnablePositions.forEach { spawnablePosition ->
val spawnablePositionType = SpawnablePosition.getByClass(spawnablePosition)!!
val possible = spawner.getMatchingSpawns(bucket, spawnablePosition)
if (possible.isNotEmpty()) {
val spawnablePositionSelectionData = spawnablePositionTypeToSpawns
.getOrPut(spawnablePositionType, ::SpawnablePositionSelectionData)
possible.forEach { spawnablePositionSelectionData.add(it, spawnablePosition) }
}
}
bucketData[bucket] = spawnablePositionTypeToSpawns
return spawnablePositionTypeToSpawns
}
}
override fun getSelectionData(
spawner: Spawner,
bucket: SpawnBucket,
spawnablePositions: List<SpawnablePosition>
): SeparatedSelectionData {
val spawnablePositionTypeToSpawns = mutableMapOf<SpawnablePositionType<*>, SpawnablePositionSelectionData>()
spawnablePositions.forEach { spawnablePosition ->
val spawnablePositionType = SpawnablePosition.getByClass(spawnablePosition)!!
val possible = spawner.getMatchingSpawns(bucket, spawnablePosition)
if (possible.isNotEmpty()) {
val spawnablePositionSelectionData = spawnablePositionTypeToSpawns.getOrPut(spawnablePositionType) { SpawnablePositionSelectionData(mutableMapOf(), 0F) }
possible.forEach {
// Only add to percentSum if this is the first time we've seen this SpawnDetail for this spawnable
// position type, otherwise the percentage will get amplified for every spawnable position the thing
// was possible, completely ruining the point of this pre-selection percentage.
if (it.percentage > 0 && !spawnablePositionSelectionData.spawnToSpawnablePosition.containsKey(it)) {
spawnablePositionSelectionData.percentSum += it.percentage
}
val selectingSpawnInformation = spawnablePositionSelectionData.spawnToSpawnablePosition.getOrPut(
it,
SelectingSpawnInformation::class::createInstance
)
selectingSpawnInformation.add(it, spawnablePosition, getWeight(spawnablePositionType))
}
}
}
return SeparatedSelectionData(spawnablePositionTypeToSpawns)
}
) = SeparatedSelectionData(spawner, spawnablePositions)
override fun selectSpawnAction(
spawner: Spawner,
bucket: SpawnBucket,
selectionData: SeparatedSelectionData
): SpawnAction<*>? {
if (selectionData.spawnablePositionTypeToSpawns.isEmpty()) {
val bucketData = selectionData.getDataForBucket(bucket)
if (bucketData.isEmpty()) {
return null
}
// Which spawnable position type should we use?
val spawnablePositionSelectionData = selectionData.spawnablePositionTypeToSpawns.entries.toList()
.weightedSelection { getWeight(it.key) * it.value.size }
val spawnablePositionSelectionData = bucketData.entries.toList()
.weightedSelection { it.key.getWeight() * it.value.size }
?.value
?: return null
@@ -161,7 +172,7 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
if (percentSum > 100) {
LOGGER.warn(
"""
A spawn list for ${spawner.name} exceeded 100% on percentage sums...
A spawn list for ${spawner.name} exceeded 100% on percentage sums in bucket ${bucket.name}...
This means you don't understand how this option works.
""".trimIndent()
)
@@ -173,7 +184,7 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
* It's [0, 1) and I want (0, 1]
* See half-open intervals here https://en.wikipedia.org/wiki/Interval_(mathematics)#Terminology
*/
val selectedPercentage = 100 - Random.Default.nextFloat() * 100
val selectedPercentage = 100 - Random.nextFloat() * 100
percentSum = 0F
for ((spawnDetail, info) in spawnToSpawnablePosition) {
if (spawnDetail.percentage > 0) {
@@ -224,7 +235,8 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
val percentageWeight = (rescaledTotalWeight - totalWeight) / percentSum
for ((spawnDetail, info) in spawnToSpawnablePosition.entries) {
totalWeights[spawnDetail] = info.highestWeight + if (spawnDetail.percentage > 0) spawnDetail.percentage * percentageWeight else 0F
totalWeights[spawnDetail] =
info.highestWeight + if (spawnDetail.percentage > 0) spawnDetail.percentage * percentageWeight else 0F
}
return totalWeights
@@ -235,18 +247,18 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
bucket: SpawnBucket,
spawnablePositions: List<SpawnablePosition>
): Map<SpawnDetail, Float> {
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
val selectionData = getSelectionData(spawner, spawnablePositions).getDataForBucket(bucket)
if (selectionData.spawnablePositionTypeToSpawns.isEmpty()) {
if (selectionData.isEmpty()) {
return mapOf()
}
val totalWeights = mutableMapOf<SpawnDetail, Float>()
val totalSpawnablePositionWeight = selectionData.spawnablePositionTypeToSpawns.keys.sumOf { getWeight(it).toDouble() }.toFloat()
val totalSpawnablePositionWeight = selectionData.keys.sumOf { it.getWeight().toDouble() }.toFloat()
for ((spawnablePositionType, spawnPositionSelectionData) in selectionData.spawnablePositionTypeToSpawns) {
val spawnPositionWeightCorrection = getWeight(spawnablePositionType) / totalSpawnablePositionWeight
for ((spawnablePositionType, spawnPositionSelectionData) in selectionData) {
val spawnPositionWeightCorrection = spawnablePositionType.getWeight() / totalSpawnablePositionWeight
val spawnPositionProbabilities = getProbabilitiesFromSpawnablePositionType(spawner, spawnPositionSelectionData)
spawnPositionProbabilities.entries.forEach {
@@ -29,7 +29,7 @@ interface SpawningSelector<T : SpawnSelectionData> {
val DEFAULT = FlatSpawnablePositionWeightedSelector()
}
fun getSelectionData(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>): T
fun getSelectionData(spawner: Spawner, spawnablePositions: List<SpawnablePosition>): T
fun selectSpawnAction(
spawner: Spawner,
@@ -37,12 +37,18 @@ interface SpawningSelector<T : SpawnSelectionData> {
selectionData: T
): SpawnAction<*>?
fun select(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>, maxSpawns: Int): List<SpawnAction<*>> {
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
fun select(
spawner: Spawner,
bucketSelector: () -> SpawnBucket,
spawnablePositions: List<SpawnablePosition>,
maxSpawns: Int
): List<SpawnAction<*>> {
val selectionData = getSelectionData(spawner, spawnablePositions)
val spawnActions = selectionData.spawnActions
while (spawnActions.size < maxSpawns) {
val bucket = bucketSelector()
val spawnAction = selectSpawnAction(
spawner = spawner,
bucket = bucket,
@@ -29,13 +29,6 @@ import com.cobblemon.mod.common.util.isBoxLoaded
import com.cobblemon.mod.common.util.squeezeWithinBounds
import com.cobblemon.mod.common.util.toVec3f
import com.cobblemon.mod.common.util.weightedSelection
import kotlin.Any
import kotlin.Boolean
import kotlin.Float
import kotlin.Int
import kotlin.Pair
import kotlin.String
import kotlin.collections.plus
import kotlin.math.max
import net.minecraft.core.BlockPos
import net.minecraft.core.SectionPos
@@ -95,10 +88,9 @@ interface Spawner {
): SpawnAction<*>? {
influences.removeIf { it.isExpired() }
spawnablePosition.influences.addAll(influences)
val bucket = chooseBucket(cause, spawnablePosition.influences)
return selector.select(
spawner = this,
bucket = bucket,
bucketSelector = { chooseBucket(cause, spawnablePosition.influences) },
spawnablePositions = listOf(spawnablePosition),
maxSpawns = 1
).firstOrNull()
@@ -116,9 +108,9 @@ interface Spawner {
val areaBox = AABB.ofSize(
Vec3(constrainedArea.getCenter().toVec3f()),
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2,
ENTITY_LIMIT_CHUNK_RANGE * 16.0,
1000.0,
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2
ENTITY_LIMIT_CHUNK_RANGE * 16.0
)
if (!constrainedArea.world.isBoxLoaded(areaBox)) {
@@ -140,11 +132,10 @@ interface Spawner {
val zone = generator.generate(this, constrainedArea)
val spawnablePositions = resolver.resolve(this, prioritizedAreaCalculators, zone)
val influences = influences + zone.unconditionalInfluences
val bucket = chooseBucket(zoneInput.cause, influences)
return selector.select(
spawner = this,
bucket = bucket,
bucketSelector = { chooseBucket(zoneInput.cause, influences) },
spawnablePositions = spawnablePositions,
maxSpawns = maxSpawns
)
@@ -94,8 +94,7 @@ enum class ShowdownActionResponseType(val loader: (RegistryFriendlyByteBuf) -> S
PASS({ PassActionResponse }),
SHIFT({ ShiftActionResponse()}),
HEAL_ITEM({ HealItemActionResponse("potion") }),
FORFEIT({ ForfeitActionResponse() }),
FLEE_ATTEMPT({ FleeAttemptActionResponse() }),
FORFEIT({ ForfeitActionResponse() });
}
abstract class ShowdownActionResponse(val type: ShowdownActionResponseType) {
@@ -281,11 +280,6 @@ class ForfeitActionResponse : ShowdownActionResponse(ShowdownActionResponseType.
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?) = "forfeit"
}
class FleeAttemptActionResponse : ShowdownActionResponse(ShowdownActionResponseType.FLEE_ATTEMPT) {
override fun isValid(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?, forceSwitch: Boolean) = true
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?) = "flee-attempt"
}
class ShowdownMoveset {
lateinit var moves: List<InBattleMove>
var trapped = false
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.battles
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.battles.runner.ShowdownService
import net.minecraft.CrashReport
import java.util.*
import java.util.concurrent.CountDownLatch
@@ -38,15 +37,8 @@ class ShowdownThread : Thread("Cobblemon Showdown") {
override fun run() {
LOGGER.info("Starting showdown service...")
try {
ShowdownService.service.openConnection()
LOGGER.info("Showdown has been started!")
} catch (e: Exception) {
LOGGER.error("Failed to start showdown service!", e)
} finally {
this.latch.countDown()
throw RuntimeException("Showdown service failed to initialize...")
}
ShowdownService.service.openConnection()
LOGGER.info("Showdown has been started!")
this.latch.countDown()
}
}
@@ -51,7 +51,6 @@ class FaintInstruction(battle: PokemonBattle, val message: BattleMessage) : Inte
preamble.thenAccept {
faintingPokemon.effectedPokemon.currentHealth = 0
faintingPokemon.faintedAt = battle.faintCounter++
faintingPokemon.sendUpdate()
CobblemonEvents.BATTLE_FAINTED.post(BattleFaintedEvent(battle, pokemon, context, pnx))
battle.getActorAndActiveSlotFromPNX(pnx).second.battlePokemon = null
@@ -110,9 +110,6 @@ open class BattlePokemon(
/** A set of all the BattlePokemon that they faced during the battle (for exp purposes) */
val facedOpponents = mutableSetOf<BattlePokemon>()
/** The faint sequence number within the battle, or null if this pokemon has not fainted. */
var faintedAt: Int? = null
/**
* The [HeldItemManager] backing this [BattlePokemon].
*/
@@ -151,15 +151,13 @@ class CampfireBlock(settings: Properties, val isSoul: Boolean) : BaseEntityBlock
override fun placeLiquid(level: LevelAccessor, pos: BlockPos, state: BlockState, fluidState: FluidState): Boolean {
val blockEntity = level.getBlockEntity(pos)
if (fluidState.type === Fluids.WATER && blockEntity is CampfireBlockEntity) {
if (level is Level) {
if (!level.isClientSide) {
removePotItem(blockEntity, state, level, pos, null, true)
level.playSoundServer(pos.center, SoundEvents.GENERIC_EXTINGUISH_FIRE, SoundSource.BLOCKS, 1.0F, 1.0F)
}
if (level.isClientSide) for (i in 0..19) MCCampfireBlock.makeParticles(level, pos, false, true)
if (!level.isClientSide) {
removePotItem(blockEntity, state, level as Level, pos, null, true)
level.playSoundServer(pos.center, SoundEvents.GENERIC_EXTINGUISH_FIRE, SoundSource.BLOCKS, 1.0F, 1.0F)
}
if (level.isClientSide) for (i in 0..19) MCCampfireBlock.makeParticles(level as Level, pos, false, true)
level.scheduleTick(pos, fluidState.type, fluidState.type.getTickDelay(level))
return true
} else {
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.client.gui.battle.subscreen
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
import com.cobblemon.mod.common.battles.FleeAttemptActionResponse
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.battle.SingleActionRequest
import com.cobblemon.mod.common.client.gui.battle.BattleGUI
@@ -66,7 +65,7 @@ class BattleGeneralActionSelection(
addOption(rank++, battleLang("ui.run"), BattleGUI.runResource) {
CobblemonClient.battle?.minimised = true
battleGUI.selectAction(request, FleeAttemptActionResponse())
Minecraft.getInstance().player?.displayClientMessage(battleLang("run_prompt"), false)
playDownSound(Minecraft.getInstance().soundManager)
}
} else {
@@ -138,7 +138,8 @@ abstract class BattleGimmickButton(gimmick: ShowdownMoveset.Gimmick, val x: Floa
pp = gimmickTemplate?.pp ?: moveTemplate.pp,
priority = gimmickTemplate?.priority ?: moveTemplate.priority,
critRatio = gimmickTemplate?.critRatio ?: moveTemplate.critRatio,
effectChances = gimmickTemplate?.effectChances ?: moveTemplate.effectChances
effectChances = gimmickTemplate?.effectChances ?: moveTemplate.effectChances,
weight = gimmickTemplate?.weight ?: moveTemplate.weight
)
}
}
@@ -132,4 +132,4 @@ abstract class InfoTextScrollWidget(val pX: Int, val pY: Int): ScrollingWidget<I
return Component.literal(text)
}
}
}
}
@@ -0,0 +1,46 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.pokemon.EVs
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.IVs
import com.cobblemon.mod.common.pokemon.Species
data class PokeEditDraft(
var species: Species,
var form: FormData = species.standardForm,
var shiny: Boolean = false,
var gender: Gender = defaultGenderFor(form),
var level: Int = 1,
var originalTrainer: String = "",
var abilityIndex: Int = 0,
var ivs: IVs = IVs(),
var evs: EVs = EVs(),
var cosmeticItem: String? = null,
var cosmeticAspects: Set<String> = emptySet(),
var heldItem: String? = null,
val forcedAspects: MutableSet<String> = mutableSetOf(),
val choiceFeatures: MutableMap<String, String> = mutableMapOf(),
val flagFeatures: MutableSet<String> = mutableSetOf(),
val activeMoves: MutableList<MoveTemplate?> = MutableList(4) { null },
val benchedMoves: MutableSet<MoveTemplate> = mutableSetOf()
) {
companion object {
fun defaultGenderFor(form: FormData): Gender {
return when {
form.maleRatio !in 0F..1F -> Gender.GENDERLESS
form.maleRatio == 0F -> Gender.FEMALE
else -> Gender.MALE
}
}
}
}
@@ -0,0 +1,600 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.client.gui.ExitButton
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.BASE_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.BASE_WIDTH
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCALE
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_ICON_SIZE
import com.cobblemon.mod.common.client.gui.pokedex.ScaledButton
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAnimationListWidget
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAnimationTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditEVIVTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditGeneralTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditLocatorListWidget
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditLocatorTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditMovesTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditImplementationBlankTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditWalkSpeedTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditSpeciesList
import com.cobblemon.mod.common.client.gui.pokedex.widgets.SearchWidget
import com.cobblemon.mod.common.client.gui.summary.SummaryButton
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditAction
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditActionPacket
import com.cobblemon.mod.common.api.pokemon.feature.ChoiceSpeciesFeatureProvider
import com.cobblemon.mod.common.api.pokemon.feature.FlagSpeciesFeatureProvider
import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeatures
import com.cobblemon.mod.common.pokemon.properties.AspectPropertyType
import com.cobblemon.mod.common.pokemon.OriginalTrainerType
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.gui.components.EditBox
import net.minecraft.client.gui.screens.Screen
import net.minecraft.network.chat.Component
class PokeEditGUI private constructor() : Screen(Component.translatable("cobblemon.ui.pokeedit.title")) {
companion object {
private val screenBackground = cobblemonResource("textures/gui/pokedex/pokedex_screen.png")
private val baseBackground = cobblemonResource("textures/gui/pokedex/pokedex_base_red.png")
private val tabSelectArrow = cobblemonResource("textures/gui/pokedex/select_arrow.png")
private val actionButtonResource = cobblemonResource("textures/gui/summary/summary_evolve_button.png")
private val tabIcons = arrayOf(
cobblemonResource("textures/gui/pokedex/tab_info.png"),
cobblemonResource("textures/gui/pokedex/tab_stats.png"),
cobblemonResource("textures/gui/pokedex/tab_moves.png"),
cobblemonResource("textures/gui/pokedex/tab_species.png"),
cobblemonResource("textures/gui/pokedex/tab_locations.png"),
)
const val TAB_GENERAL = 0
const val TAB_EV_IV = 1
const val TAB_MOVES = 2
const val TAB_ASPECTS = 3
const val TAB_IMPLEMENTATION_BUTTON = 4
const val IMPL_TAB_LOCATORS = 0
const val IMPL_TAB_ANIMATION = 1
const val IMPL_TAB_SCALE = 2
const val IMPL_TAB_WALK_SPEED = 3
fun open() {
Minecraft.getInstance().setScreen(PokeEditGUI())
}
}
private val allSpecies = PokemonSpecies.species.sortedBy { it.nationalPokedexNumber }
private var filteredSpecies = allSpecies
private lateinit var searchWidget: SearchWidget
private lateinit var speciesList: PokeEditSpeciesList
private lateinit var locatorListWidget: PokeEditLocatorListWidget
private lateinit var animationListWidget: PokeEditAnimationListWidget
private var highlightedLocator: String? = null
private var animationTab: PokeEditAnimationTab? = null
private val tabButtons: MutableList<ScaledButton> = mutableListOf()
private var tabIndex = TAB_GENERAL
private var implementationMode = false
private var implementationTabIndex = IMPL_TAB_LOCATORS
private lateinit var tabElement: AbstractWidget
private lateinit var createButton: SummaryButton
private lateinit var spawnButton: SummaryButton
private lateinit var exitButton: ExitButton
private lateinit var implementationButton: SummaryButton
private lateinit var implementationExitButton: SummaryButton
private val implementationTabButtons: MutableList<SummaryButton> = mutableListOf()
private val draft = PokeEditDraft(allSpecies.firstOrNull() ?: PokemonSpecies.random())
override fun renderBlurredBackground(delta: Float) {}
override fun renderMenuBackground(context: GuiGraphics) {}
override fun isPauseScreen() = false
override fun init() {
super.init()
clearWidgets()
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
if (::searchWidget.isInitialized) removeWidget(searchWidget)
searchWidget = SearchWidget(x + 26, y + 28, 128, 11, update = ::updateFilters)
addRenderableWidget(searchWidget)
if (::speciesList.isInitialized) removeWidget(speciesList)
speciesList = PokeEditSpeciesList(x + 26, y + 39, ::setSelectedSpecies)
if (::locatorListWidget.isInitialized) removeWidget(locatorListWidget)
locatorListWidget = PokeEditLocatorListWidget(
pX = x + 26,
pY = y + 190,
listWidth = 139,
listHeight = 151,
onSelectLocator = { name ->
highlightedLocator = name
(tabElement as? PokeEditLocatorTab)?.setHighlightedLocator(name)
}
)
locatorListWidget.setLocators(emptyList())
if (::animationListWidget.isInitialized) removeWidget(animationListWidget)
animationListWidget = PokeEditAnimationListWidget(
pX = x + 26,
pY = y + 190,
listWidth = 139,
listHeight = 151,
isPlaying = { name -> animationTab?.isAnimationPlaying(name) == true },
isLooping = { name -> animationTab?.isAnimationLooping(name) == true },
getDuration = { name -> animationTab?.getAnimationDuration(name) ?: 0F },
getProgress = { name -> animationTab?.getAnimationProgress(name) ?: 0F },
onPlay = { name -> animationTab?.playAnimation(name) },
onPause = { name -> animationTab?.pauseAnimation(name) },
onToggleLoop = { name, enabled -> animationTab?.setAnimationLooping(name, enabled) }
)
animationListWidget.setAnimations(emptyList())
setUpTabs()
setUpActionButtons()
setUpImplementationControls()
setImplementationMode(implementationMode, force = true)
updateFilters(true)
if (implementationMode) {
displayImplementationTab(implementationTabIndex, recreate = true)
} else {
displayTab(tabIndex, recreate = true)
}
}
override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val matrices = context.pose()
renderBackground(context, mouseX, mouseY, delta)
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
blitk(
matrixStack = matrices,
texture = baseBackground,
x = x,
y = y,
width = BASE_WIDTH,
height = BASE_HEIGHT
)
blitk(
matrixStack = matrices,
texture = screenBackground,
x = x,
y = y,
width = BASE_WIDTH,
height = BASE_HEIGHT
)
drawScaledText(
context = context,
text = lang("ui.pokeedit.title"),
x = x + 192,
y = y + 15,
shadow = true
)
super.render(context, mouseX, mouseY, delta)
tabButtons.forEach { it.render(context, mouseX, mouseY, delta) }
if (!implementationMode) {
// Draw the tab arrow above overlay widgets so it stays visible.
blitk(
matrixStack = matrices,
texture = tabSelectArrow,
x = (x + 198 + (25 * tabIndex)) / SCALE,
y = (y + 177) / SCALE,
width = 12,
height = 6,
scale = SCALE
)
}
}
private fun updateFilters(init: Boolean = false) {
val search = searchWidget.value.trim()
filteredSpecies = if (search.isEmpty()) {
allSpecies
} else {
allSpecies.filter { species ->
species.translatedName.string.contains(search, true)
|| species.resourceIdentifier.path.contains(search, true)
}
}
speciesList.setEntries(filteredSpecies)
if (init || draft.species !in filteredSpecies) {
filteredSpecies.firstOrNull()?.let { setSelectedSpecies(it) }
}
}
private fun setSelectedSpecies(species: com.cobblemon.mod.common.pokemon.Species) {
if (draft.species == species) return
draft.species = species
draft.form = species.standardForm
draft.gender = PokeEditDraft.defaultGenderFor(draft.form)
draft.shiny = false
draft.abilityIndex = 0
val aspectOptions = PokeEditAspectsTab.collectAspectOptions(
species,
draft.form,
draft.gender,
draft.shiny
)
draft.forcedAspects.retainAll(aspectOptions.toggles)
draft.flagFeatures.retainAll(aspectOptions.flagKeys)
draft.choiceFeatures.entries.removeIf { (key, choice) ->
val group = aspectOptions.choiceGroups.firstOrNull { it.key == key } ?: return@removeIf true
choice !in group.choices
}
val cosmetic = aspectOptions.cosmeticChoices.firstOrNull { it.itemId.toString() == draft.cosmeticItem }
draft.cosmeticItem = cosmetic?.itemId?.toString()
draft.cosmeticAspects = cosmetic?.aspects?.toSet() ?: emptySet()
if (implementationMode) {
displayImplementationTab(implementationTabIndex, recreate = true)
} else {
displayTab(tabIndex, recreate = true)
}
}
private fun setUpTabs() {
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
tabButtons.clear()
for (i in tabIcons.indices) {
tabButtons.add(
ScaledButton(
x + 197F + (i * 25F),
y + 181.5F,
TAB_ICON_SIZE,
TAB_ICON_SIZE,
resource = tabIcons[i],
clickAction = {
if (i == TAB_IMPLEMENTATION_BUTTON) {
setImplementationMode(true)
} else {
displayTab(i)
}
}
)
)
}
tabButtons.forEach(::addRenderableWidget)
}
private fun setUpActionButtons() {
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
val actionY = y + 200F
val exitX = x + 312F
val spawnX = exitX - 56F
val createX = spawnX - 56F
val implX = x + 26F
if (::createButton.isInitialized) removeWidget(createButton)
createButton = SummaryButton(
buttonX = createX,
buttonY = actionY,
buttonWidth = 54,
buttonHeight = 15,
clickAction = { sendPokeEditAction(PokeEditAction.CREATE) },
text = lang("ui.pokeedit.give"),
resource = actionButtonResource
)
addRenderableWidget(createButton)
if (::spawnButton.isInitialized) removeWidget(spawnButton)
spawnButton = SummaryButton(
buttonX = spawnX,
buttonY = actionY,
buttonWidth = 54,
buttonHeight = 15,
clickAction = { sendPokeEditAction(PokeEditAction.SPAWN) },
text = lang("ui.pokeedit.spawn"),
resource = actionButtonResource
)
addRenderableWidget(spawnButton)
if (::exitButton.isInitialized) removeWidget(exitButton)
exitButton = ExitButton(pX = exitX.toInt(), pY = y + 200) {
Minecraft.getInstance().setScreen(null)
}
addRenderableWidget(exitButton)
if (::implementationButton.isInitialized) removeWidget(implementationButton)
implementationButton = SummaryButton(
buttonX = implX,
buttonY = actionY,
buttonWidth = 54,
buttonHeight = 15,
clickAction = { setImplementationMode(true) },
text = Component.literal("Impl"),
resource = actionButtonResource,
renderRequirement = { !implementationMode },
clickRequirement = { !implementationMode },
largeText = false,
boldText = false,
textScale = 0.7F
)
addRenderableWidget(implementationButton)
}
private fun setUpImplementationControls() {
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
val sideX = (x - 60F).coerceAtLeast(6F)
if (::implementationExitButton.isInitialized) removeWidget(implementationExitButton)
implementationExitButton = SummaryButton(
buttonX = sideX,
buttonY = y + 28F,
buttonWidth = 54,
buttonHeight = 15,
clickAction = { setImplementationMode(false) },
text = Component.literal("Back"),
resource = actionButtonResource,
renderRequirement = { implementationMode },
clickRequirement = { implementationMode },
largeText = false,
boldText = false,
textScale = 0.7F
)
addRenderableWidget(implementationExitButton)
implementationTabButtons.forEach(::removeWidget)
implementationTabButtons.clear()
val baseY = y + 48F
val labels = listOf(
Component.literal("Locators"),
Component.literal("Animation"),
Component.literal("Scale"),
Component.literal("Walk Speed")
)
labels.forEachIndexed { index, label ->
val button = SummaryButton(
buttonX = sideX,
buttonY = baseY + (index * 18F),
buttonWidth = 54,
buttonHeight = 15,
clickAction = { displayImplementationTab(index) },
text = label,
resource = actionButtonResource,
renderRequirement = { implementationMode },
clickRequirement = { implementationMode },
largeText = false,
boldText = false,
textScale = 0.7F
)
implementationTabButtons.add(button)
addRenderableWidget(button)
}
}
private fun displayTab(newIndex: Int, recreate: Boolean = false) {
if (implementationMode) {
return
}
if (tabIndex == newIndex && ::tabElement.isInitialized && !recreate) {
return
}
tabIndex = newIndex
if (::tabElement.isInitialized) removeWidget(tabElement)
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
val tabX = x + 180
val tabY = y + 28
tabElement = when (newIndex) {
TAB_GENERAL -> PokeEditGeneralTab(tabX, tabY, draft) { displayTab(tabIndex, recreate = true) }
TAB_EV_IV -> PokeEditEVIVTab(tabX, tabY, draft)
TAB_MOVES -> PokeEditMovesTab(tabX, tabY, draft)
TAB_ASPECTS -> PokeEditAspectsTab(tabX, tabY, draft)
else -> PokeEditGeneralTab(tabX, tabY, draft) { displayTab(tabIndex, recreate = true) }
}
addRenderableWidget(tabElement)
bringControlsToFront()
}
private fun displayImplementationTab(newIndex: Int, recreate: Boolean = false) {
if (!implementationMode) {
return
}
if (implementationTabIndex == newIndex && ::tabElement.isInitialized && !recreate) {
return
}
implementationTabIndex = newIndex
if (::tabElement.isInitialized) removeWidget(tabElement)
if (::locatorListWidget.isInitialized) removeWidget(locatorListWidget)
if (::animationListWidget.isInitialized) removeWidget(animationListWidget)
animationTab = null
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
val tabX = x + 180
val tabY = y + 28
tabElement = when (newIndex) {
IMPL_TAB_LOCATORS -> PokeEditLocatorTab(
tabX,
tabY,
draft,
highlightedLocator,
::updateLocatorList
)
IMPL_TAB_ANIMATION -> PokeEditAnimationTab(tabX, tabY, draft) { animations ->
animationListWidget.setAnimations(animations)
}
IMPL_TAB_SCALE -> PokeEditImplementationBlankTab(tabX, tabY, draft, Component.literal("Scale"))
IMPL_TAB_WALK_SPEED -> PokeEditWalkSpeedTab(tabX, tabY, draft)
else -> PokeEditLocatorTab(
tabX,
tabY,
draft,
highlightedLocator,
::updateLocatorList
)
}
addRenderableWidget(tabElement)
when (newIndex) {
IMPL_TAB_LOCATORS -> addRenderableWidget(locatorListWidget)
IMPL_TAB_ANIMATION -> {
animationTab = tabElement as? PokeEditAnimationTab
animationListWidget.setAnimations(animationTab?.getAnimations().orEmpty())
addRenderableWidget(animationListWidget)
}
}
bringControlsToFront()
}
private fun setImplementationMode(enabled: Boolean, force: Boolean = false) {
if (!force && implementationMode == enabled) return
implementationMode = enabled
if (implementationMode) {
if (::searchWidget.isInitialized) removeWidget(searchWidget)
if (::speciesList.isInitialized) removeWidget(speciesList)
implementationTabIndex = IMPL_TAB_LOCATORS
displayImplementationTab(implementationTabIndex, recreate = true)
} else {
if (::searchWidget.isInitialized) addRenderableWidget(searchWidget)
if (::speciesList.isInitialized) addRenderableWidget(speciesList)
if (::locatorListWidget.isInitialized) removeWidget(locatorListWidget)
if (::animationListWidget.isInitialized) removeWidget(animationListWidget)
displayTab(tabIndex, recreate = true)
}
tabButtons.forEach { it.isVisible = !implementationMode }
if (!implementationMode) {
searchWidget.isFocused = false
}
bringControlsToFront()
}
private fun bringControlsToFront() {
if (::createButton.isInitialized) {
removeWidget(createButton)
addRenderableWidget(createButton)
}
if (::spawnButton.isInitialized) {
removeWidget(spawnButton)
addRenderableWidget(spawnButton)
}
if (::exitButton.isInitialized) {
removeWidget(exitButton)
addRenderableWidget(exitButton)
}
if (::implementationButton.isInitialized) {
removeWidget(implementationButton)
addRenderableWidget(implementationButton)
}
if (tabButtons.isEmpty()) return
tabButtons.forEach { removeWidget(it) }
tabButtons.forEach(::addRenderableWidget)
}
private fun sendPokeEditAction(action: PokeEditAction) {
val properties = buildDraftProperties()
val moveNames = draft.activeMoves.map { it?.name ?: "" }
val cosmeticItem = draft.cosmeticItem?.trim().orEmpty()
PokeEditActionPacket(action, properties.asString(), moveNames, cosmeticItem).sendToServer()
Minecraft.getInstance().setScreen(null)
}
private fun updateLocatorList(locators: Collection<String>) {
if (::locatorListWidget.isInitialized) {
locatorListWidget.setLocators(locators)
}
}
private fun buildDraftProperties(): PokemonProperties {
val props = PokemonProperties()
props.species = draft.species.resourceIdentifier.toString()
props.form = draft.form.formOnlyShowdownId()
props.shiny = draft.shiny
props.gender = draft.gender
props.level = draft.level
val ot = draft.originalTrainer.trim()
if (ot.isBlank()) {
props.originalTrainerType = OriginalTrainerType.NONE
} else {
props.originalTrainerType = OriginalTrainerType.NPC
props.originalTrainer = ot
}
props.heldItem = draft.heldItem?.takeIf { it.isNotBlank() }
props.ivs = draft.ivs
props.evs = draft.evs
val abilities = draft.form.abilities.map { it.template }
props.ability = abilities.getOrNull(draft.abilityIndex)?.name
val customProperties = mutableListOf<com.cobblemon.mod.common.api.properties.CustomPokemonProperty>()
draft.forcedAspects.mapNotNullTo(customProperties) { aspect -> AspectPropertyType.fromString(aspect) }
draft.flagFeatures.forEach { key ->
val provider = SpeciesFeatures.getFeature(key) as? FlagSpeciesFeatureProvider ?: return@forEach
provider.fromString("true")?.let { customProperties.add(it) }
}
draft.choiceFeatures.forEach { (key, choice) ->
val provider = SpeciesFeatures.getFeature(key) as? ChoiceSpeciesFeatureProvider ?: return@forEach
provider.fromString(choice)?.let { customProperties.add(it) }
}
props.customProperties = customProperties
props.updateAspects()
return props
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
if (focused !is EditBox && (minecraft?.options?.keyInventory?.matches(keyCode, scanCode) == true)) {
onClose()
return true
}
return super.keyPressed(keyCode, scanCode, modifiers)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
return try {
super.mouseClicked(mouseX, mouseY, button)
} catch (_: ConcurrentModificationException) {
false
}
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double, verticalAmount: Double): Boolean {
if (implementationMode) {
if (implementationTabIndex == IMPL_TAB_LOCATORS && ::locatorListWidget.isInitialized) {
if (locatorListWidget.mouseScrolled(mouseX, mouseY, amount, verticalAmount)) return true
}
if (implementationTabIndex == IMPL_TAB_ANIMATION && ::animationListWidget.isInitialized) {
if (animationListWidget.mouseScrolled(mouseX, mouseY, amount, verticalAmount)) return true
}
}
return super.mouseScrolled(mouseX, mouseY, amount, verticalAmount)
}
}
@@ -0,0 +1,100 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.cobblemon.mod.common.api.abilities.AbilityTemplate
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.pokedex.ScaledButton
import com.cobblemon.mod.common.client.gui.pokedex.widgets.InfoTextScrollWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component
class PokeEditAbilityTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft
) : SoundlessWidget(pX, pY, 139, 163, lang("ui.pokeedit.abilities")) {
companion object {
private val arrowLeft = cobblemonResource("textures/gui/pokedex/info_arrow_left.png")
private val arrowRight = cobblemonResource("textures/gui/pokedex/info_arrow_right.png")
}
private val description = object : InfoTextScrollWidget(pX, pY + 60) {}
private val leftButton = ScaledButton(
pX + 2.5F,
pY + 30F,
7,
10,
arrowLeft,
clickAction = { switchAbility(false) }
).also { addWidget(it) }
private val rightButton = ScaledButton(
pX + 133F,
pY + 30F,
7,
10,
arrowRight,
clickAction = { switchAbility(true) }
).also { addWidget(it) }
private var abilitiesList: List<AbilityTemplate> = emptyList()
init {
refreshAbilities()
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
refreshAbilities()
if (abilitiesList.isEmpty()) return
val ability = abilitiesList[draft.abilityIndex.coerceIn(0, abilitiesList.lastIndex)]
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = Component.translatable("cobblemon.ui.pokedex.info.ability", ability.displayName.asTranslated()).bold(),
x = x + 9,
y = y + 30,
shadow = true
)
description.setText(listOf(ability.description))
description.render(context, mouseX, mouseY, delta)
leftButton.render(context, mouseX, mouseY, delta)
rightButton.render(context, mouseX, mouseY, delta)
}
private fun refreshAbilities() {
val form = draft.form
abilitiesList = form.abilities.map { it.template }
if (draft.abilityIndex !in abilitiesList.indices) {
draft.abilityIndex = 0
}
}
private fun switchAbility(nextIndex: Boolean) {
if (abilitiesList.isEmpty()) return
val size = abilitiesList.size
draft.abilityIndex = if (nextIndex) {
(draft.abilityIndex + 1) % size
} else {
(draft.abilityIndex - 1 + size) % size
}
}
}
@@ -0,0 +1,362 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.gui.ScrollingWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component
import net.minecraft.util.Mth
import java.util.Locale
class PokeEditAnimationListWidget(
val pX: Int,
val pY: Int,
val listWidth: Int,
val listHeight: Int,
private val isPlaying: (String) -> Boolean,
private val isLooping: (String) -> Boolean,
private val getDuration: (String) -> Float,
private val getProgress: (String) -> Float,
private val onPlay: (String) -> Unit,
private val onPause: (String) -> Unit,
private val onToggleLoop: (String, Boolean) -> Unit
) : ScrollingWidget<PokeEditAnimationListWidget.AnimationRow>(
left = pX,
top = pY - listHeight,
width = listWidth,
height = listHeight,
slotHeight = ROW_HEIGHT
) {
companion object {
private const val ROW_HEIGHT = 12
private const val TEXT_SCALE = 0.5F
private const val BORDER_COLOR = 0xFF1B1B1B.toInt()
private const val FILL_COLOR = 0xFF3B3B3B.toInt()
private const val TEXT_COLOR = 0xFFFCFCFC.toInt()
private const val BUTTON_WIDTH = 12
private const val BUTTON_HEIGHT = 9
private const val BUTTON_GAP = 3
private const val BUTTON_PAD_RIGHT = 4
private const val BUTTON_PAD_TOP = 1
private const val BUTTON_ACTIVE = 0xFF6BD46B.toInt()
private const val BUTTON_INACTIVE = 0xFF5B5B5B.toInt()
private const val BUTTON_BORDER = 0xFF1B1B1B.toInt()
private const val LOOP_BOX_SIZE = 8
private const val LOOP_GAP = 3
private const val LOOP_LABEL_SCALE = 0.45F
private const val PROGRESS_HEIGHT = 2
private const val PROGRESS_BG = 0xFF2C2C2C.toInt()
private const val PROGRESS_FG = 0xFF8AD5FF.toInt()
private const val DURATION_SCALE = 0.45F
private const val DURATION_WIDTH = 28
}
private val listTop = pY - listHeight
private val listBottom = pY
private var draggingScrollbar = false
fun setAnimations(animations: Collection<String>) {
clearEntries()
animations.sorted().forEach { addEntry(AnimationRow(it)) }
scrollAmount = 0.0
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
context.fill(pX - 1, listTop - 1, pX + listWidth + 1, listBottom + 1, BORDER_COLOR)
context.fill(pX, listTop, pX + listWidth, listBottom, FILL_COLOR)
focused = if (mouseX in pX..(pX + listWidth) && mouseY in listTop..listBottom) {
getEntryAtPosition(mouseX.toDouble(), mouseY.toDouble())
} else {
null
}
context.enableScissor(pX, listTop, pX + listWidth, listBottom)
renderListItems(context, mouseX, mouseY, delta)
context.disableScissor()
if (maxScroll > 0) {
renderScrollbar(context, mouseX, mouseY, delta)
}
}
override fun renderListItems(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val rowX = rowLeft
for (index in 0 until itemCount) {
val rowY = getRowTop(index)
val rowBottom = getRowBottom(index)
if (rowBottom >= listTop && rowY <= listBottom) {
val entry = getEntry(index)
if (entry is AnimationRow) {
entry.isPlaying = isPlaying(entry.name)
entry.isLooping = isLooping(entry.name)
entry.durationSeconds = getDuration(entry.name)
entry.progress = getProgress(entry.name)
}
renderItem(context, mouseX, mouseY, delta, index, rowX, rowY, rowWidth, itemHeight)
}
}
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double, verticalAmount: Double): Boolean {
if (mouseX !in pX.toDouble()..(pX + listWidth).toDouble()
|| mouseY !in listTop.toDouble()..listBottom.toDouble()
) {
return false
}
val maxScroll = (itemCount * itemHeight - listHeight).coerceAtLeast(0)
if (maxScroll <= 0) return true
val step = if (verticalAmount != 0.0) verticalAmount else amount
scrollAmount = Mth.clamp(scrollAmount - step * itemHeight, 0.0, maxScroll.toDouble())
return true
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (mouseX !in pX.toDouble()..(pX + listWidth).toDouble()
|| mouseY !in listTop.toDouble()..listBottom.toDouble()
|| button != 0
) {
return false
}
if (isOverScrollbar(mouseX, mouseY) && maxScroll > 0) {
draggingScrollbar = true
return true
}
val localY = mouseY.toInt() - listTop + scrollAmount.toInt()
if (localY < 0) return false
val index = localY / itemHeight
if (index !in 0 until itemCount) return false
val entry = getEntry(index)
if (entry is AnimationRow) {
val rowX = rowLeft
val rowY = getRowTop(index)
val (playX, pauseX, loopX, buttonY, durationX) = buttonPositions(rowX, rowY)
val inPlay = mouseX.toInt() in playX..(playX + BUTTON_WIDTH)
&& mouseY.toInt() in buttonY..(buttonY + BUTTON_HEIGHT)
val inPause = mouseX.toInt() in pauseX..(pauseX + BUTTON_WIDTH)
&& mouseY.toInt() in buttonY..(buttonY + BUTTON_HEIGHT)
val inLoop = mouseX.toInt() in loopX..(loopX + LOOP_BOX_SIZE)
&& mouseY.toInt() in buttonY..(buttonY + LOOP_BOX_SIZE)
if (inPlay) {
onPlay(entry.name)
return true
}
if (inPause) {
onPause(entry.name)
return true
}
if (inLoop) {
val newValue = !isLooping(entry.name)
onToggleLoop(entry.name, newValue)
return true
}
}
return false
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (button == 0 && draggingScrollbar) {
draggingScrollbar = false
return true
}
return super.mouseReleased(mouseX, mouseY, button)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (!draggingScrollbar || maxScroll <= 0) {
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
val barHeight = listBottom - listTop
var thumbHeight = ((barHeight * barHeight).toFloat() / maxPosition.toFloat()).toInt()
thumbHeight = Mth.clamp(thumbHeight, 16, barHeight - 4)
val trackHeight = barHeight - thumbHeight
if (trackHeight <= 0) return true
val clampedY = Mth.clamp(mouseY.toInt() - listTop - thumbHeight / 2, 0, trackHeight)
scrollAmount = (clampedY.toDouble() * maxScroll.toDouble()) / trackHeight.toDouble()
return true
}
override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean {
return mouseX in pX.toDouble()..(pX + listWidth).toDouble()
&& mouseY in listTop.toDouble()..listBottom.toDouble()
}
override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val xLeft = this.scrollbarPosition
val xRight = xLeft + 3
val yStart = listTop
val barHeight = listBottom - listTop
var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
yBottom = Mth.clamp(yBottom, 16, barHeight - 4)
var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + yStart
if (yTop < yStart) yTop = yStart
context.fill(xLeft + 1, yStart, xRight - 1, listBottom, 0xFF7AEEF1.toInt())
context.fill(xLeft, yTop, xRight, yTop + yBottom, 0xFF3A96B6.toInt())
}
override fun getRowTop(index: Int): Int {
return listTop - scrollAmount.toInt() + (index * itemHeight)
}
override fun getRowBottom(index: Int): Int {
return getRowTop(index) + itemHeight
}
override fun getBottom(): Int {
return listBottom
}
private fun isOverScrollbar(mouseX: Double, mouseY: Double): Boolean {
val xLeft = scrollbarPosition
val xRight = xLeft + 3
return mouseX >= xLeft && mouseX <= xRight && mouseY >= listTop && mouseY <= listBottom
}
private fun buttonPositions(rowX: Int, rowY: Int): List<Int> {
val loopX = rowX + listWidth - BUTTON_PAD_RIGHT - LOOP_BOX_SIZE
val pauseX = loopX - LOOP_GAP - BUTTON_WIDTH
val playX = pauseX - BUTTON_GAP - BUTTON_WIDTH
val buttonY = rowY + BUTTON_PAD_TOP
val durationX = playX - BUTTON_GAP - DURATION_WIDTH
return listOf(playX, pauseX, loopX, buttonY, durationX)
}
class AnimationRow(val name: String) : Slot<AnimationRow>() {
var isPlaying: Boolean = false
var isLooping: Boolean = false
var durationSeconds: Float = 0F
var progress: Float = 0F
override fun render(
context: GuiGraphics,
index: Int,
y: Int,
x: Int,
entryWidth: Int,
entryHeight: Int,
mouseX: Int,
mouseY: Int,
hovered: Boolean,
tickDelta: Float
) {
val bg = if (hovered) 0xFF2F2F2F.toInt() else null
if (bg != null) {
context.fill(x + 1, y, x + entryWidth - 1, y + ROW_HEIGHT, bg)
}
val loopX = x + entryWidth - BUTTON_PAD_RIGHT - LOOP_BOX_SIZE
val pauseX = loopX - LOOP_GAP - BUTTON_WIDTH
val playX = pauseX - BUTTON_GAP - BUTTON_WIDTH
val durationX = playX - BUTTON_GAP - DURATION_WIDTH
val buttonY = y + BUTTON_PAD_TOP
drawButton(context, playX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT, !isPlaying)
drawPlayIcon(context, playX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT)
drawButton(context, pauseX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT, isPlaying)
drawPauseIcon(context, pauseX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT)
drawLoopBox(context, loopX, buttonY, isLooping)
drawDuration(context, durationX, buttonY, durationSeconds)
drawScaledText(
context = context,
text = name.text(),
x = x + 3,
y = y + 2,
scale = TEXT_SCALE,
colour = TEXT_COLOR,
shadow = false
)
drawProgressBar(context, x + 2, y + ROW_HEIGHT - PROGRESS_HEIGHT, entryWidth - 4, progress)
}
override fun getNarration(): Component {
return Component.literal(name)
}
private fun drawButton(
context: GuiGraphics,
x: Int,
y: Int,
width: Int,
height: Int,
active: Boolean
) {
val fill = if (active) BUTTON_ACTIVE else BUTTON_INACTIVE
context.fill(x - 1, y - 1, x + width + 1, y + height + 1, BUTTON_BORDER)
context.fill(x, y, x + width, y + height, fill)
}
private fun drawLoopBox(context: GuiGraphics, x: Int, y: Int, enabled: Boolean) {
val fill = if (enabled) BUTTON_ACTIVE else BUTTON_INACTIVE
context.fill(x - 1, y - 1, x + LOOP_BOX_SIZE + 1, y + LOOP_BOX_SIZE + 1, BUTTON_BORDER)
context.fill(x, y, x + LOOP_BOX_SIZE, y + LOOP_BOX_SIZE, fill)
drawLoopIcon(context, x, y, LOOP_BOX_SIZE)
}
private fun drawPlayIcon(context: GuiGraphics, x: Int, y: Int, width: Int, height: Int) {
val iconWidth = 6
val iconHeight = 7
val startX = x + (width - iconWidth) / 2
val startY = y + (height - iconHeight) / 2
for (i in 0 until iconWidth) {
val top = startY + i / 2
val bottom = startY + iconHeight - i / 2
context.fill(startX + i, top, startX + i + 1, bottom, TEXT_COLOR)
}
}
private fun drawPauseIcon(context: GuiGraphics, x: Int, y: Int, width: Int, height: Int) {
val barWidth = 2
val barHeight = 7
val gap = 2
val startX = x + (width - (barWidth * 2 + gap)) / 2
val startY = y + (height - barHeight) / 2
context.fill(startX, startY, startX + barWidth, startY + barHeight, TEXT_COLOR)
context.fill(startX + barWidth + gap, startY, startX + barWidth * 2 + gap, startY + barHeight, TEXT_COLOR)
}
private fun drawLoopIcon(context: GuiGraphics, x: Int, y: Int, size: Int) {
val startX = x + 1
val startY = y + 1
val endX = x + size - 1
val endY = y + size - 1
context.fill(startX, startY, endX, startY + 1, TEXT_COLOR)
context.fill(startX, endY - 1, endX, endY, TEXT_COLOR)
context.fill(startX, startY, startX + 1, endY, TEXT_COLOR)
context.fill(endX - 1, startY, endX, endY, TEXT_COLOR)
context.fill(endX - 2, startY, endX, startY + 2, TEXT_COLOR)
}
private fun drawDuration(context: GuiGraphics, x: Int, y: Int, duration: Float) {
val text = String.format(Locale.US, "%.2fs", duration.coerceAtLeast(0F))
drawScaledText(
context = context,
text = text.text(),
x = x + DURATION_WIDTH / 2,
y = y + 2,
scale = DURATION_SCALE,
colour = TEXT_COLOR,
maxCharacterWidth = DURATION_WIDTH,
centered = true,
shadow = false
)
}
private fun drawProgressBar(context: GuiGraphics, x: Int, y: Int, width: Int, progress: Float) {
val clamped = Mth.clamp(progress, 0F, 1F)
context.fill(x, y, x + width, y + PROGRESS_HEIGHT, PROGRESS_BG)
context.fill(x, y, x + (width * clamped).toInt(), y + PROGRESS_HEIGHT, PROGRESS_FG)
}
}
}
@@ -0,0 +1,496 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab.Companion.choiceAspectId
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.item.HeldItemRenderer
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
import com.cobblemon.mod.common.client.render.models.blockbench.animation.ActiveAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PrimaryAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import com.cobblemon.mod.common.util.toHex
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.platform.Lighting
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.renderer.LightTexture
import net.minecraft.client.renderer.RenderType
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.client.Minecraft
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
import org.joml.Quaternionf
import org.joml.Vector3f
class PokeEditAnimationTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft,
private val onAnimationsChanged: (List<String>) -> Unit
) : SoundlessWidget(pX, pY, HALF_OVERLAY_WIDTH, HALF_OVERLAY_HEIGHT, Component.literal("Animation")) {
companion object {
private val backgroundOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay.png")
private val pokeBallOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_poke_ball.png")
private val platformBase = cobblemonResource("textures/gui/pokedex/platform_base.png")
private val platformShadow = cobblemonResource("textures/gui/pokedex/platform_shadow.png")
private const val POKEBALL_OVERLAY_Y = 25
private const val PLATFORM_Y = 69
private const val SHADOW_X = 25
private const val SHADOW_Y = 76
private const val VIEW_SCALE = 1.2F
private const val VIEW_SHIFT_X = 0F
private const val VIEW_SHIFT_Y = 36F
private const val MODEL_VIEW_SCALE = 2.4F
private const val MODEL_DRAW_SCALE = 24F
private const val MODEL_OFFSET_Y = 20.0
}
private val state = FloatingState().also { state ->
state.runtime.environment.query.addFunction("is_holding_item") {
val held = currentHeldStack()
DoubleValue(!held.isEmpty && !held.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) && !held.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
}
state.runtime.environment.query.addFunction("is_wearing_hat") {
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS))
}
state.runtime.environment.query.addFunction("is_wearing_face") {
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
}
}
private val heldItemRenderer = HeldItemRenderer()
private var renderablePokemon: RenderablePokemon? = null
private var rotationY = 30F
private var poseList: Array<PoseType> = arrayOf(PoseType.PROFILE, PoseType.STAND, PoseType.WALK, PoseType.SLEEP)
private var selectedPoseIndex: Int = 0
private var oldDragPosX = 0.0
private var canDragRender = false
private var isDraggingRender = false
private var availableAnimations: List<String> = emptyList()
private val activeAnimationsByName = mutableMapOf<String, ActiveAnimation>()
private val loopingAnimations = mutableMapOf<String, Boolean>()
private val animationStartTimes = mutableMapOf<String, Float>()
private val animationDurations = mutableMapOf<String, Float>()
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
context.enableScissor(x, y, x + HALF_OVERLAY_WIDTH, y + HALF_OVERLAY_HEIGHT)
val viewWidth = HALF_OVERLAY_WIDTH * VIEW_SCALE
val viewHeight = HALF_OVERLAY_HEIGHT * VIEW_SCALE
val viewX = x + (HALF_OVERLAY_WIDTH - viewWidth) / 2F + VIEW_SHIFT_X
val viewY = y + (HALF_OVERLAY_HEIGHT - viewHeight) / 2F + VIEW_SHIFT_Y
val scaledBaseX = viewX / VIEW_SCALE
val scaledBaseY = viewY / VIEW_SCALE
blitk(
matrixStack = context.pose(),
texture = backgroundOverlay,
x = scaledBaseX,
y = scaledBaseY,
width = HALF_OVERLAY_WIDTH,
height = HALF_OVERLAY_HEIGHT,
scale = VIEW_SCALE
)
blitk(
matrixStack = context.pose(),
texture = pokeBallOverlay,
x = scaledBaseX + 15,
y = scaledBaseY + POKEBALL_OVERLAY_Y,
width = 109,
height = 68,
vOffset = 0,
textureHeight = 1744,
scale = VIEW_SCALE
)
blitk(
matrixStack = context.pose(),
texture = platformBase,
x = scaledBaseX + 13,
y = scaledBaseY + PLATFORM_Y,
width = 113,
height = 24,
textureHeight = 30,
scale = VIEW_SCALE
)
blitk(
matrixStack = context.pose(),
texture = platformShadow,
x = scaledBaseX + SHADOW_X,
y = scaledBaseY + SHADOW_Y,
width = 90,
height = 20,
scale = VIEW_SCALE
)
val fillTop = (viewY + 114F * VIEW_SCALE).toInt()
val fillBottom = (viewY + (HALF_OVERLAY_HEIGHT - 6) * VIEW_SCALE).toInt()
context.fill(viewX.toInt(), fillTop, (viewX + viewWidth).toInt(), fillBottom, 0xFF7AEEF1.toInt())
context.disableScissor()
renderPokemon(context, delta)
refreshAnimationList()
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isWithinViewport(mouseX, mouseY)) {
canDragRender = true
isDraggingRender = true
oldDragPosX = mouseX
return true
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
if (canDragRender) {
canDragRender = false
}
if (isDraggingRender) {
isDraggingRender = false
}
return super.mouseReleased(pMouseX, pMouseY, pButton)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val handledDrag = isDraggingRender && canDragRender
if (handledDrag) {
val dragOffsetY = (oldDragPosX - mouseX).toFloat()
rotationY = (((rotationY + dragOffsetY) % 360 + 360) % 360)
}
oldDragPosX = mouseX
return handledDrag || super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
fun getAnimations(): List<String> = availableAnimations
fun isAnimationPlaying(name: String): Boolean = activeAnimationsByName.containsKey(name)
fun isAnimationLooping(name: String): Boolean = loopingAnimations[name] == true
fun getAnimationDuration(name: String): Float {
return animationDurations[name] ?: resolveDuration(name).also { animationDurations[name] = it }
}
fun getAnimationProgress(name: String): Float {
val duration = getAnimationDuration(name)
if (duration <= 0F) return 0F
val start = animationStartTimes[name] ?: return 0F
val elapsed = state.animationSeconds - start
val raw = if (isAnimationLooping(name)) {
(elapsed % duration) / duration
} else {
elapsed / duration
}
return raw.coerceIn(0F, 1F)
}
fun playAnimation(name: String) {
if (activeAnimationsByName.containsKey(name)) return
val model = VaryingModelRepository.getPoser(draft.species.resourceIdentifier, state)
val animation = model.getAnimation(state, name, state.runtime)
val active = if (animation != null) {
if (animation is PrimaryAnimation) animation.animation else animation
} else {
val pose = resolvePoseAnimation(model, name) ?: return
PoseActiveAnimation(pose)
}
state.addActiveAnimation(active)
activeAnimationsByName[name] = active
animationStartTimes[name] = state.animationSeconds
animationDurations[name] = active.duration
}
fun pauseAnimation(name: String) {
val active = activeAnimationsByName.remove(name) ?: return
state.activeAnimations.remove(active)
animationStartTimes.remove(name)
}
fun setAnimationLooping(name: String, enabled: Boolean) {
loopingAnimations[name] = enabled
}
private fun refreshAnimationList() {
val model = VaryingModelRepository.getPoser(draft.species.resourceIdentifier, state)
val names = mutableSetOf<String>()
names.addAll(model.animations.keys)
model.poses.values.forEach { pose ->
names.addAll(pose.namedAnimations.keys)
names.add(pose.poseName)
}
val sorted = names.sorted()
if (sorted != availableAnimations) {
availableAnimations = sorted
loopingAnimations.keys.retainAll(sorted.toSet())
animationDurations.keys.retainAll(sorted.toSet())
animationStartTimes.keys.retainAll(sorted.toSet())
onAnimationsChanged(sorted)
}
}
private fun renderPokemon(context: GuiGraphics, delta: Float) {
buildRenderable()?.let { renderable ->
state.currentAspects = renderable.aspects
context.enableScissor(
x + 1,
y + 1,
x + HALF_OVERLAY_WIDTH - 1,
y + HALF_OVERLAY_HEIGHT - 1
)
val matrices = context.pose()
matrices.pushPose()
matrices.translate(
x.toDouble() + (HALF_OVERLAY_WIDTH.toDouble()) / 2,
y.toDouble() + MODEL_OFFSET_Y,
1000.0
)
matrices.scale(MODEL_VIEW_SCALE, MODEL_VIEW_SCALE, MODEL_VIEW_SCALE)
renderAnimationModel(context, renderable, delta)
if (!renderable.heldItem.isEmpty) {
heldItemRenderer.renderOnModel(
renderable.heldItem,
state,
matrices,
context.bufferSource(),
light = 0xF000F0,
frontLight = true
)
}
matrices.popPose()
context.disableScissor()
}
}
private fun renderAnimationModel(context: GuiGraphics, renderable: RenderablePokemon, delta: Float) {
val speciesId = renderable.species.resourceIdentifier
val model = VaryingModelRepository.getPoser(speciesId, state)
val texture = VaryingModelRepository.getTexture(speciesId, state)
val renderContext = RenderContext()
model.context = renderContext
VaryingModelRepository.getTextureNoSubstitute(speciesId, state).let { renderContext.put(RenderContext.TEXTURE, it) }
val baseScale = PokemonSpecies.getByIdentifier(speciesId)!!.getForm(state.currentAspects).baseScale
renderContext.put(RenderContext.SCALE, baseScale)
renderContext.put(RenderContext.SPECIES, speciesId)
renderContext.put(RenderContext.ASPECTS, state.currentAspects)
renderContext.put(RenderContext.RENDER_STATE, RenderContext.RenderState.PROFILE)
renderContext.put(RenderContext.POSABLE_STATE, state)
renderContext.put(RenderContext.DO_QUIRKS, false)
state.currentModel = model
if (state.currentPose == null) {
state.currentPose = model.getPose(PoseType.PROFILE)?.poseName
}
state.updatePartialTicks(delta)
RenderSystem.applyModelViewMatrix()
val matrices = context.pose()
val rotationVector = Vector3f(13F, rotationY, 0F)
val rotation = Quaternionf().fromEulerXYZDegrees(rotationVector)
matrices.pushPose()
matrices.scale(MODEL_DRAW_SCALE, MODEL_DRAW_SCALE, -MODEL_DRAW_SCALE)
matrices.translate(
model.profileTranslation.x,
model.profileTranslation.y + 1.5 * model.profileScale,
model.profileTranslation.z - 4.0
)
matrices.scale(model.profileScale, model.profileScale, 1 / model.profileScale)
matrices.mulPose(rotation)
model.setDefault()
state.preRender()
state.renderMarkers.clear()
runActiveAnimations(renderContext, model)
model.updateLocators(null, state)
Lighting.setupForEntityInInventory()
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
val buffer = bufferSource.getBuffer(RenderType.entityCutout(texture))
val light1 = Vector3f(-1F, 1F, 1.0F)
val light2 = Vector3f(1.3F, -1F, 1.0F)
RenderSystem.setShaderLights(light1, light2)
val packedLight = LightTexture.pack(11, 7)
val colour = toHex(1F, 1F, 1F, 1F)
model.withLayerContext(bufferSource, state, VaryingModelRepository.getLayers(speciesId, state)) {
model.render(renderContext, matrices, buffer, packedLight, OverlayTexture.NO_OVERLAY, colour)
bufferSource.endBatch()
}
model.setDefault()
Lighting.setupFor3DItems()
matrices.popPose()
}
private fun runActiveAnimations(context: RenderContext, model: com.cobblemon.mod.common.client.render.models.blockbench.PosableModel) {
val toRestart = mutableListOf<String>()
val iterator = activeAnimationsByName.entries.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
val animation = entry.value
val stillRunning = animation.run(
context,
model,
state,
0F,
0F,
state.animationSeconds,
0F,
0F,
1F
)
if (!stillRunning) {
state.activeAnimations.remove(animation)
iterator.remove()
animationStartTimes.remove(entry.key)
if (loopingAnimations[entry.key] == true) {
toRestart.add(entry.key)
}
}
}
if (toRestart.isNotEmpty()) {
toRestart.forEach { name ->
val animation = model.getAnimation(state, name, state.runtime)
val active = if (animation != null) {
if (animation is PrimaryAnimation) animation.animation else animation
} else {
val pose = resolvePoseAnimation(model, name) ?: return@forEach
PoseActiveAnimation(pose)
}
state.addActiveAnimation(active)
activeAnimationsByName[name] = active
animationStartTimes[name] = state.animationSeconds
animationDurations[name] = active.duration
}
}
}
private fun resolvePoseAnimation(
model: com.cobblemon.mod.common.client.render.models.blockbench.PosableModel,
name: String
): Pose? {
return model.poses[name] ?: model.poses.values.firstOrNull { it.poseName.equals(name, ignoreCase = true) }
}
private fun resolveDuration(name: String): Float {
val model = VaryingModelRepository.getPoser(draft.species.resourceIdentifier, state)
val animation = model.getAnimation(state, name, state.runtime)
val active = if (animation != null) {
if (animation is PrimaryAnimation) animation.animation else animation
} else {
return 0F
}
return active.duration
}
private class PoseActiveAnimation(private val pose: Pose) : ActiveAnimation {
override val isTransition: Boolean = false
override val duration: Float = 0F
override val enduresPrimaryAnimations: Boolean = true
override fun run(
context: RenderContext,
model: com.cobblemon.mod.common.client.render.models.blockbench.PosableModel,
state: com.cobblemon.mod.common.client.render.models.blockbench.PosableState,
limbSwing: Float,
limbSwingAmount: Float,
ageInTicks: Float,
headYaw: Float,
headPitch: Float,
intensity: Float
): Boolean {
if (!pose.isSuitable(state)) return true
pose.apply(context, model, state, limbSwing, limbSwingAmount, ageInTicks, headYaw, headPitch)
return true
}
}
private fun buildRenderable(): RenderablePokemon? {
val species = draft.species
val form = draft.form
val aspects = mutableSetOf<String>()
if (draft.shiny) aspects.add(SHINY_ASPECT.aspect)
aspects.addAll(form.aspects)
aspects.addAll(draft.forcedAspects)
aspects.addAll(draft.flagFeatures)
aspects.addAll(draft.cosmeticAspects)
draft.choiceFeatures.forEach { (key, choice) ->
choiceAspectId(key, choice)?.let { aspects.add(it) }
}
when (draft.gender) {
Gender.MALE -> aspects.add("male")
Gender.FEMALE -> aspects.add("female")
Gender.GENDERLESS -> aspects.add("genderless")
}
val heldStack = draft.heldItem
?.let { ResourceLocation.tryParse(it) }
?.let { id -> BuiltInRegistries.ITEM.getOptional(id).orElse(null) }
?.let { item -> ItemStack(item) }
?: ItemStack.EMPTY
val prev = renderablePokemon
if (prev == null || prev.species != species || prev.aspects != aspects) {
activeAnimationsByName.clear()
state.activeAnimations.clear()
}
renderablePokemon = RenderablePokemon(species, aspects, heldStack).also { recalculatePoses(it) }
return renderablePokemon
}
private fun recalculatePoses(renderablePokemon: RenderablePokemon) {
val poseState = FloatingState().also { it.currentAspects = renderablePokemon.aspects }
val poser = VaryingModelRepository.getPoser(renderablePokemon.species.resourceIdentifier, poseState)
poseList = poser.poses
.map { it.value.poseTypes.minBy { it.ordinal } }
.toSet()
.filterNot { it in PoseType.SHOULDER_POSES }
.sortedBy { it.ordinal }
.toTypedArray()
if (selectedPoseIndex !in poseList.indices) {
selectedPoseIndex = 0
}
}
private fun currentHeldStack(): ItemStack {
return renderablePokemon?.heldItem ?: ItemStack.EMPTY
}
private fun isWithinViewport(mouseX: Double, mouseY: Double): Boolean =
mouseX.toInt() in x..(x + HALF_OVERLAY_WIDTH)
&& mouseY.toInt() in y..(y + HALF_OVERLAY_HEIGHT)
&& children.none { child ->
child is AbstractWidget && child.visible && child.isMouseOver(mouseX, mouseY)
}
}
@@ -0,0 +1,620 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.cobblemon.mod.common.CobblemonCosmeticItems
import com.cobblemon.mod.common.api.conditional.RegistryLikeIdentifierCondition
import com.cobblemon.mod.common.api.conditional.RegistryLikeTagCondition
import com.cobblemon.mod.common.api.pokemon.feature.ChoiceSpeciesFeatureProvider
import com.cobblemon.mod.common.api.pokemon.feature.FlagSpeciesFeatureProvider
import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeatures
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.ScrollingWidget
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.substitute
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.resources.language.I18n
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.resources.ResourceLocation
import net.minecraft.tags.TagKey
import net.minecraft.util.Mth
class PokeEditAspectsTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft
) : SoundlessWidget(pX, pY, WIDTH, HEIGHT, lang("ui.aspects")) {
companion object {
private const val WIDTH = 139
private const val HEIGHT = 163
private const val SLOT_HEIGHT = 16
private const val SCALE = 0.7F
private const val TOP_MARGIN = 10
private const val BOTTOM_MARGIN = 10
private const val COLUMN_GAP = 6
data class ChoiceGroup(
val key: String,
val choices: List<String>,
val aspectFormat: String
)
data class CosmeticChoice(
val itemId: ResourceLocation,
val aspects: List<String>,
val label: MutableComponent
)
data class AspectOptions(
val toggles: Set<String>,
val choiceGroups: List<ChoiceGroup>,
val flagKeys: Set<String>,
val cosmeticChoices: List<CosmeticChoice>
)
fun collectAspectOptions(
species: Species,
form: FormData? = null,
gender: Gender? = null,
shiny: Boolean? = null
): AspectOptions {
val toggles = mutableSetOf<String>()
val choiceGroups = mutableListOf<ChoiceGroup>()
val flagKeys = mutableSetOf<String>()
val forms = if (species.forms.isEmpty()) listOf(species.standardForm) else species.forms
forms.forEach { form -> toggles.addAll(form.aspects) }
SpeciesFeatures.getFeaturesFor(species).forEach { provider ->
when (provider) {
is ChoiceSpeciesFeatureProvider -> {
if (provider.isAspect && provider.keys.isNotEmpty() && provider.choices.isNotEmpty()) {
choiceGroups.add(ChoiceGroup(provider.keys.first(), provider.choices, provider.aspectFormat))
}
}
is FlagSpeciesFeatureProvider -> {
if (provider.isAspect && provider.keys.isNotEmpty()) {
flagKeys.add(provider.keys.first())
}
}
}
}
toggles.removeAll(flagKeys)
val filteredChoiceGroups = choiceGroups
.filterNot { it.key in setOf("cosmetic", "cosmetic_item", "cosmeticitem") }
.sortedBy { it.key }
return AspectOptions(
toggles = toggles.filter { it.isNotBlank() && it != "cosmetic" && it != "cosmetic_item" && it != "cosmeticitem" }.toSet(),
choiceGroups = filteredChoiceGroups,
flagKeys = flagKeys.filter { it.isNotBlank() }.toSet(),
cosmeticChoices = collectCosmeticChoices(species, form, gender, shiny)
)
}
fun collectCosmeticChoices(
species: Species,
form: FormData?,
gender: Gender?,
shiny: Boolean?
): List<CosmeticChoice> {
val pokemon = Pokemon().also {
it.isClient = true
it.species = species
it.form = form ?: species.standardForm
if (gender != null) it.gender = gender
if (shiny != null) it.shiny = shiny
}
val registry = BuiltInRegistries.ITEM
val choices = linkedMapOf<ResourceLocation, CosmeticChoice>()
CobblemonCosmeticItems.findValidForPokemon(pokemon).forEach { assignment ->
assignment.cosmeticItems.forEach { cosmetic ->
val aspects = cosmetic.aspects.filter { it.isNotBlank() }
when (val condition = cosmetic.consumedItem) {
is RegistryLikeIdentifierCondition<*> -> {
val id = condition.identifier
val label = registry.getOptional(id)
.map { it.defaultInstance.hoverName.copy() }
.orElseGet { Component.literal(id.toString()) }
choices.putIfAbsent(id, CosmeticChoice(id, aspects, label))
}
is RegistryLikeTagCondition<*> -> {
@Suppress("UNCHECKED_CAST")
val tag = condition.tag as? TagKey<net.minecraft.world.item.Item> ?: return@forEach
val tagItems = registry.getTag(tag).orElse(null) ?: return@forEach
tagItems.forEach { holder ->
val item = holder.value()
val id = registry.getKey(item) ?: return@forEach
val label = item.defaultInstance.hoverName.copy()
choices.putIfAbsent(id, CosmeticChoice(id, aspects, label))
}
}
}
}
}
return choices.values.sortedBy { it.label.string.lowercase() }
}
fun choiceAspectId(key: String, choice: String): String? {
val provider = SpeciesFeatures.getFeature(key) as? ChoiceSpeciesFeatureProvider ?: return null
return provider.aspectFormat.substitute("choice", choice)
}
}
private sealed class AspectItem {
data class Group(val group: ChoiceGroup) : AspectItem()
data class Toggle(val id: String, val source: ToggleSource) : AspectItem()
data class Cosmetic(val choices: List<CosmeticChoice>) : AspectItem()
}
private sealed class OptionItem {
data class Choice(val group: ChoiceGroup, val choice: String) : OptionItem()
data class ClearChoice(val group: ChoiceGroup) : OptionItem()
data class Toggle(val aspect: AspectItem.Toggle, val enabled: Boolean) : OptionItem()
data class CosmeticChoice(val choice: PokeEditAspectsTab.Companion.CosmeticChoice) : OptionItem()
object ClearCosmetic : OptionItem()
}
private enum class ToggleSource {
FORCED_ASPECT,
FLAG_FEATURE
}
private val listX = x + 4
private val listBottom = y + HEIGHT - BOTTOM_MARGIN
private val listAreaWidth = WIDTH - 8
private val listTop = y + TOP_MARGIN
private val listHeight = listBottom - listTop
private val leftWidth = (listAreaWidth - COLUMN_GAP) / 2
private val rightWidth = listAreaWidth - leftWidth - COLUMN_GAP
private val leftX = listX
private val rightX = listX + leftWidth + COLUMN_GAP
private val aspectList = AspectList(
leftX,
listTop,
leftWidth,
listHeight
).also { addWidget(it) }
private val optionList = OptionList(
rightX,
listTop,
rightWidth,
listHeight
).also { addWidget(it) }
private var aspectOptions: AspectOptions = collectAspectOptions(
draft.species,
draft.form,
draft.gender,
draft.shiny
)
private var selectedAspect: AspectItem? = null
private var lastSpeciesId: ResourceLocation? = null
init {
rebuildAspectEntries()
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val currentSpeciesId = draft.species.resourceIdentifier
if (lastSpeciesId != currentSpeciesId) {
refreshAspects()
}
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = lang("ui.aspects").bold(),
x = x + (WIDTH / 2.0),
y = y + 2,
scale = SCALE,
centered = true
)
context.fill(
leftX,
listTop,
leftX + leftWidth,
listBottom,
0x22000000
)
context.fill(
rightX,
listTop,
rightX + rightWidth,
listBottom,
0x22000000
)
aspectList.render(context, mouseX, mouseY, delta)
optionList.render(context, mouseX, mouseY, delta)
}
private fun refreshAspects() {
aspectOptions = collectAspectOptions(
draft.species,
draft.form,
draft.gender,
draft.shiny
)
val cosmetic = aspectOptions.cosmeticChoices.firstOrNull { it.itemId.toString() == draft.cosmeticItem }
draft.cosmeticItem = cosmetic?.itemId?.toString()
draft.cosmeticAspects = cosmetic?.aspects?.toSet() ?: emptySet()
val valid = buildAspectEntries()
if (selectedAspect !in valid) {
selectedAspect = valid.firstOrNull()
}
rebuildAspectEntries()
lastSpeciesId = draft.species.resourceIdentifier
}
private fun buildAspectEntries(): List<AspectItem> {
val entries = mutableListOf<AspectItem>()
aspectOptions.choiceGroups.sortedBy { it.key }.forEach { entries.add(AspectItem.Group(it)) }
aspectOptions.flagKeys.sortedBy { it }.forEach { entries.add(AspectItem.Toggle(it, ToggleSource.FLAG_FEATURE)) }
aspectOptions.toggles.sortedBy { it }.forEach { entries.add(AspectItem.Toggle(it, ToggleSource.FORCED_ASPECT)) }
return entries
}
private fun rebuildAspectEntries() {
val entries = buildAspectEntries()
aspectList.setEntries(entries)
if (selectedAspect == null) {
selectedAspect = entries.firstOrNull()
}
rebuildOptionEntries()
}
private fun rebuildOptionEntries() {
optionList.setEntries(buildOptionEntries(selectedAspect))
}
private fun buildOptionEntries(aspect: AspectItem?): List<OptionItem> {
if (aspect == null) return emptyList()
return when (aspect) {
is AspectItem.Group -> {
val entries = mutableListOf<OptionItem>()
entries.add(OptionItem.ClearChoice(aspect.group))
aspect.group.choices.forEach { choice ->
entries.add(OptionItem.Choice(aspect.group, choice))
}
entries
}
is AspectItem.Cosmetic -> {
val entries = mutableListOf<OptionItem>()
entries.add(OptionItem.ClearCosmetic)
aspect.choices.forEach { choice ->
entries.add(OptionItem.CosmeticChoice(choice))
}
entries
}
is AspectItem.Toggle -> {
listOf(
OptionItem.Toggle(aspect, enabled = true),
OptionItem.Toggle(aspect, enabled = false)
)
}
}
}
private fun humanizeId(raw: String): String {
return raw
.replace('_', ' ')
.replace('-', ' ')
.split(" ")
.filter { it.isNotBlank() }
.joinToString(" ") { part ->
part.lowercase().replaceFirstChar { ch -> if (ch.isLowerCase()) ch.titlecase() else ch.toString() }
}
}
private fun labelForId(id: String): MutableComponent {
val key = "cobblemon.ui.aspect.$id"
return if (I18n.exists(key)) {
Component.translatable(key)
} else {
Component.literal(humanizeId(id))
}
}
private fun isAspectActive(entry: AspectItem): Boolean = when (entry) {
is AspectItem.Toggle -> when (entry.source) {
ToggleSource.FORCED_ASPECT -> entry.id in draft.forcedAspects
ToggleSource.FLAG_FEATURE -> entry.id in draft.flagFeatures
}
is AspectItem.Group -> entry.group.key in draft.choiceFeatures
is AspectItem.Cosmetic -> draft.cosmeticItem != null
}
private fun labelFor(entry: AspectItem): MutableComponent = when (entry) {
is AspectItem.Group -> {
val name = labelForId(entry.group.key).string
val choice = draft.choiceFeatures[entry.group.key]?.let { labelForId(it).string }
Component.literal(if (choice != null) "$name: $choice" else name)
}
is AspectItem.Toggle -> labelForId(entry.id)
is AspectItem.Cosmetic -> {
val base = lang("cosmetic_item").string
val choice = draft.cosmeticItem?.let { id ->
entry.choices.firstOrNull { it.itemId.toString() == id }?.label?.string
}
Component.literal(if (choice != null) "$base: $choice" else base)
}
}
private fun labelFor(option: OptionItem): MutableComponent = when (option) {
is OptionItem.ClearChoice -> lang("ui.none")
is OptionItem.Choice -> labelForId(option.choice)
is OptionItem.Toggle -> if (option.enabled) lang("ui.enabled") else lang("ui.disabled")
is OptionItem.CosmeticChoice -> option.choice.label
OptionItem.ClearCosmetic -> lang("ui.none")
}
private fun selectAspect(entry: AspectItem) {
if (selectedAspect == entry) return
selectedAspect = entry
rebuildOptionEntries()
}
private fun handleOption(option: OptionItem) {
when (option) {
is OptionItem.ClearChoice -> {
draft.choiceFeatures.remove(option.group.key)
}
is OptionItem.Choice -> {
draft.choiceFeatures[option.group.key] = option.choice
}
is OptionItem.Toggle -> {
when (option.aspect.source) {
ToggleSource.FORCED_ASPECT -> {
if (option.enabled) {
draft.forcedAspects.add(option.aspect.id)
} else {
draft.forcedAspects.remove(option.aspect.id)
}
}
ToggleSource.FLAG_FEATURE -> {
if (option.enabled) {
draft.flagFeatures.add(option.aspect.id)
} else {
draft.flagFeatures.remove(option.aspect.id)
}
}
}
}
is OptionItem.CosmeticChoice -> {
draft.cosmeticItem = option.choice.itemId.toString()
draft.cosmeticAspects = option.choice.aspects.toSet()
}
OptionItem.ClearCosmetic -> {
draft.cosmeticItem = null
draft.cosmeticAspects = emptySet()
}
}
}
private inner class AspectList(
pX: Int,
pY: Int,
width: Int,
height: Int
) : ScrollingWidget<AspectList.AspectEntry>(
width = width,
height = height,
left = pX,
top = pY - height,
slotHeight = SLOT_HEIGHT + 2,
scrollBarWidth = 3
) {
private val listX = pX
fun setEntries(aspects: List<AspectItem>) {
clearEntries()
aspects.forEach { aspect -> addEntry(AspectEntry(aspect)) }
scrollAmount = 0.0
}
override fun getScrollbarPosition(): Int = listX + width - 3
override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val xLeft = this.scrollbarPosition
val xRight = xLeft + 3
val barHeight = this.bottom - this.y
var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
yBottom = Mth.clamp(yBottom, 32, barHeight - 8)
var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + this.y
if (yTop < this.y) yTop = this.y
context.fill(xLeft, this.y + 3, xRight, this.bottom - 3, 0xFF3A96B6.toInt())
context.fill(xLeft, yTop + 3, xRight, yTop + yBottom - 3, 0xFFFCFCFC.toInt())
}
inner class AspectEntry(
private val entry: AspectItem
) : Slot<AspectEntry>() {
private var x: Int = 0
private var y: Int = 0
override fun render(
context: GuiGraphics,
index: Int,
y: Int,
x: Int,
entryWidth: Int,
entryHeight: Int,
mouseX: Int,
mouseY: Int,
hovered: Boolean,
tickDelta: Float
) {
this.x = x
this.y = y + 1
if (selectedAspect == entry) {
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x442A7FFF)
} else if (isAspectActive(entry)) {
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x4436B33F)
} else if (hovered) {
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x442A7FFF)
}
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = labelFor(entry),
x = this.x + 4,
y = this.y + 3,
scale = SCALE,
shadow = true
)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isMouseOver(mouseX, mouseY)) {
selectAspect(entry)
return true
}
return false
}
override fun getNarration(): Component {
return labelFor(entry)
}
}
}
private inner class OptionList(
pX: Int,
pY: Int,
width: Int,
height: Int
) : ScrollingWidget<OptionList.OptionEntry>(
width = width,
height = height,
left = pX,
top = pY - height,
slotHeight = SLOT_HEIGHT + 2,
scrollBarWidth = 3
) {
private val listX = pX
fun setEntries(options: List<OptionItem>) {
clearEntries()
options.forEach { option -> addEntry(OptionEntry(option)) }
scrollAmount = 0.0
}
override fun getScrollbarPosition(): Int = listX + width - 3
override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val xLeft = this.scrollbarPosition
val xRight = xLeft + 3
val barHeight = this.bottom - this.y
var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
yBottom = Mth.clamp(yBottom, 32, barHeight - 8)
var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + this.y
if (yTop < this.y) yTop = this.y
context.fill(xLeft, this.y + 3, xRight, this.bottom - 3, 0xFF3A96B6.toInt())
context.fill(xLeft, yTop + 3, xRight, yTop + yBottom - 3, 0xFFFCFCFC.toInt())
}
inner class OptionEntry(
private val option: OptionItem
) : Slot<OptionEntry>() {
private var x: Int = 0
private var y: Int = 0
private fun isSelectedOption(): Boolean = when (option) {
is OptionItem.ClearChoice -> {
draft.choiceFeatures[option.group.key] == null
}
is OptionItem.Choice -> {
draft.choiceFeatures[option.group.key] == option.choice
}
is OptionItem.Toggle -> {
val isEnabled = when (option.aspect.source) {
ToggleSource.FORCED_ASPECT -> option.aspect.id in draft.forcedAspects
ToggleSource.FLAG_FEATURE -> option.aspect.id in draft.flagFeatures
}
isEnabled == option.enabled
}
is OptionItem.CosmeticChoice -> {
draft.cosmeticItem == option.choice.itemId.toString()
}
OptionItem.ClearCosmetic -> {
draft.cosmeticItem == null
}
}
override fun render(
context: GuiGraphics,
index: Int,
y: Int,
x: Int,
entryWidth: Int,
entryHeight: Int,
mouseX: Int,
mouseY: Int,
hovered: Boolean,
tickDelta: Float
) {
this.x = x
this.y = y + 1
if (isSelectedOption()) {
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x4436B33F)
} else if (hovered) {
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x442A7FFF)
}
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = labelFor(option),
x = this.x + 4,
y = this.y + 3,
scale = SCALE,
shadow = true
)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isMouseOver(mouseX, mouseY)) {
handleOption(option)
return true
}
return false
}
override fun getNarration(): Component {
return labelFor(option)
}
}
}
}
@@ -0,0 +1,335 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.pokemon.EVs
import com.cobblemon.mod.common.pokemon.IVs
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.BufferUploader
import com.mojang.blaze3d.vertex.DefaultVertexFormat
import com.mojang.blaze3d.vertex.Tesselator
import com.mojang.blaze3d.vertex.VertexFormat
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.screens.Screen
import net.minecraft.network.chat.MutableComponent
import net.minecraft.world.phys.Vec2
import org.joml.Vector3f
class PokeEditEVIVTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft
) : SoundlessWidget(pX, pY, WIDTH, HEIGHT, lang("ui.pokeedit.stats")) {
companion object {
private const val WIDTH = 139
private const val HEIGHT = 163
private const val CHART_WIDTH = 134
private const val CHART_HEIGHT = 148
private const val SCALE = 0.5F
private const val TAB_MARKER_WIDTH = 8
private const val TAB_IV = 0
private const val TAB_EV = 1
private val statsBaseResource = cobblemonResource("textures/gui/summary/summary_stats_chart_base.png")
private val statsChartResource = cobblemonResource("textures/gui/summary/summary_stats_chart.png")
private val tabMarkerResource = cobblemonResource("textures/gui/summary/summary_stats_tab_marker.png")
private val statLabels = listOf(
lang("ui.stats.hp").bold() to Stats.HP,
lang("ui.stats.atk").bold() to Stats.ATTACK,
lang("ui.stats.def").bold() to Stats.DEFENCE,
lang("ui.stats.speed").bold() to Stats.SPEED,
lang("ui.stats.sp_def").bold() to Stats.SPECIAL_DEFENCE,
lang("ui.stats.sp_atk").bold() to Stats.SPECIAL_ATTACK
)
private val hexagonVerticesOffset = listOf(
Pair(67.0, 10.5), // 12 o'clock
Pair(122.0, 42.5), // 2 o'clock
Pair(122.0, 93.5), // 4 o'clock
Pair(67.0, 124.5), // 6 o'clock
Pair(12.0, 93.5), // 8 o'clock
Pair(12.0, 42.5) // 10 o'clock
)
}
private var tabIndex = TAB_IV
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val chartX = x + 2
val chartY = y + 6
blitk(
matrixStack = context.pose(),
texture = statsBaseResource,
x = chartX,
y = chartY,
width = CHART_WIDTH,
height = CHART_HEIGHT
)
blitk(
matrixStack = context.pose(),
texture = statsChartResource,
x = (chartX + 25.5) / SCALE,
y = (chartY + 22) / SCALE,
width = 166,
height = 192,
scale = SCALE
)
if (tabIndex == TAB_IV) {
drawStatPolygon(
chartX.toFloat(),
chartY.toFloat(),
statLabels.map { (_, stat) -> draft.ivs.getOrDefault(stat).toFloat() / IVs.MAX_VALUE.toFloat() },
colour = Vector3f(216F / 255F, 100F / 255F, 1F)
)
} else {
drawStatPolygon(
chartX.toFloat(),
chartY.toFloat(),
statLabels.map { (_, stat) -> draft.evs.getOrDefault(stat).toFloat() / EVs.MAX_STAT_VALUE.toFloat() },
colour = Vector3f(1F, 1F, 100F / 255F)
)
}
val labelsHovered = statLabelsHovered(chartX, chartY, mouseX, mouseY)
val labels = statLabels.map { (label, stat) ->
if (labelsHovered) {
if (tabIndex == TAB_IV) {
"${draft.ivs.getOrDefault(stat)}/${IVs.MAX_VALUE}".text()
} else {
"${draft.evs.getOrDefault(stat)}/${EVs.MAX_STAT_VALUE}".text()
}
} else {
label
}
}
renderPolygonLabels(
context = context,
chartX = chartX.toDouble(),
chartY = chartY.toDouble(),
labels = labels,
verticesOffset = hexagonVerticesOffset
)
val values = statLabels.map { (_, stat) ->
if (tabIndex == TAB_IV) {
draft.ivs.getOrDefault(stat).toString().text()
} else {
draft.evs.getOrDefault(stat).toString().text()
}
}
renderPolygonLabels(
context = context,
chartX = chartX.toDouble(),
chartY = chartY.toDouble(),
labels = values,
verticesOffset = hexagonVerticesOffset,
offsetY = 5.5
)
if (tabIndex == TAB_EV) {
drawScaledText(
context = context,
text = lang("ui.pokeedit.evs.total", draft.evs.total(), EVs.MAX_TOTAL_VALUE),
x = chartX + 10,
y = chartY + 126,
scale = SCALE,
shadow = true
)
}
renderTabs(context, chartX, chartY)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val tabIndexAtPos = getTabIndexFromPos(mouseX, mouseY)
if (tabIndexAtPos != -1 && tabIndexAtPos != tabIndex) {
tabIndex = tabIndexAtPos
return true
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
val chartX = x + 2
val chartY = y + 6
val stat = getHoveredStat(chartX, chartY, mouseX.toInt(), mouseY.toInt()) ?: return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
val step = when {
Screen.hasControlDown() -> 25
Screen.hasShiftDown() -> 10
else -> 1
}
val delta = if (verticalAmount > 0) step else -step
if (tabIndex == TAB_IV) {
val current = draft.ivs.getOrDefault(stat)
val next = (current + delta).coerceIn(0, IVs.MAX_VALUE)
draft.ivs[stat] = next
} else {
val current = draft.evs.getOrDefault(stat)
val totalWithout = draft.evs.total() - current
val maxForStat = minOf(EVs.MAX_STAT_VALUE, EVs.MAX_TOTAL_VALUE - totalWithout)
val next = (current + delta).coerceIn(0, maxForStat)
draft.evs[stat] = next
}
return true
}
private fun renderTabs(context: GuiGraphics, chartX: Int, chartY: Int) {
val tabWidth = 40
val tabLeft = chartX + ((CHART_WIDTH - (tabWidth * 2)) / 2)
val tabCenters = listOf(
tabLeft + (tabWidth / 2),
tabLeft + tabWidth + (tabWidth / 2)
)
val tabY = chartY + 137
drawScaledText(
context = context,
text = lang("ui.stats.ivs").bold(),
x = tabCenters[0],
y = tabY,
scale = SCALE,
colour = if (tabIndex == TAB_IV) 0x00FFFFFF else 0x008F8F8F,
centered = true
)
drawScaledText(
context = context,
text = lang("ui.stats.evs").bold(),
x = tabCenters[1],
y = tabY,
scale = SCALE,
colour = if (tabIndex == TAB_EV) 0x00FFFFFF else 0x008F8F8F,
centered = true
)
blitk(
matrixStack = context.pose(),
texture = tabMarkerResource,
x = (tabCenters[tabIndex] - (TAB_MARKER_WIDTH / 2)) / SCALE,
y = (tabY - 3) / SCALE,
width = TAB_MARKER_WIDTH,
height = 4,
scale = SCALE
)
}
private fun getTabIndexFromPos(mouseX: Double, mouseY: Double): Int {
val chartX = x + 2
val chartY = y + 6
val tabWidth = 40
val tabLeft = chartX + ((CHART_WIDTH - (tabWidth * 2)) / 2)
val tabTop = chartY + 134.0
val tabBottom = tabTop + 9.0
if (mouseY !in tabTop..tabBottom) return -1
if (mouseX in tabLeft.toDouble()..(tabLeft + tabWidth).toDouble()) return TAB_IV
if (mouseX in (tabLeft + tabWidth).toDouble()..(tabLeft + tabWidth * 2).toDouble()) return TAB_EV
return -1
}
private fun getHoveredStat(chartX: Int, chartY: Int, mouseX: Int, mouseY: Int): Stat? {
statLabels.forEachIndexed { index, (_, stat) ->
val pos = hexagonVerticesOffset[index]
if (mouseX.toDouble() in (chartX + pos.first - 12)..(chartX + pos.first + 12)
&& mouseY.toDouble() in (chartY + pos.second - 1)..(chartY + pos.second + 10)
) {
return stat
}
}
return null
}
private fun statLabelsHovered(chartX: Int, chartY: Int, mouseX: Int, mouseY: Int): Boolean {
return getHoveredStat(chartX, chartY, mouseX, mouseY) != null
}
private fun renderPolygonLabels(
context: GuiGraphics,
chartX: Double,
chartY: Double,
labels: List<MutableComponent>,
verticesOffset: List<Pair<Double, Double>>,
offsetY: Double = 0.0
) {
if (labels.size != verticesOffset.size) return
labels.forEachIndexed { index, label ->
drawScaledText(
context = context,
text = label,
x = chartX + verticesOffset[index].first,
y = chartY + verticesOffset[index].second + offsetY,
scale = SCALE,
centered = true
)
}
}
private fun drawTriangle(colour: Vector3f, v1: Vec2, v2: Vec2, v3: Vec2, opacity: Float = 0.6F) {
RenderSystem.setShaderTexture(0, com.cobblemon.mod.common.client.CobblemonResources.WHITE)
RenderSystem.setShaderColor(colour.x, colour.y, colour.z, opacity)
val bufferBuilder = Tesselator.getInstance().begin(VertexFormat.Mode.TRIANGLES, DefaultVertexFormat.POSITION)
bufferBuilder.addVertex(v1.x, v1.y, 10F)
bufferBuilder.addVertex(v2.x, v2.y, 10F)
bufferBuilder.addVertex(v3.x, v3.y, 10F)
BufferUploader.drawWithShader(bufferBuilder.buildOrThrow())
RenderSystem.setShaderColor(1F, 1F, 1F, 1F)
}
private fun drawStatPolygon(chartX: Float, chartY: Float, ratios: List<Float>, colour: Vector3f) {
if (ratios.size != 6) return
val topVertexY = chartY + 22F
val centerX = chartX + 67F
val centerY = topVertexY + 48F
val radius = centerY - topVertexY
val startAngleDeg = -90.0
val angleStepDeg = 360.0 / 6
val anglesDeg = List(6) { i -> startAngleDeg + i * angleStepDeg }
val anglesRad = anglesDeg.map { Math.toRadians(it) }
val maxPoints = anglesRad.map { angle ->
Vec2(
centerX + radius * Math.cos(angle).toFloat(),
centerY + radius * Math.sin(angle).toFloat()
)
}
val coercedRatios = ratios.map { it.coerceIn(5 / radius, 1F) }
val vertices = maxPoints.mapIndexed { index, maxPoint ->
Vec2(
centerX + (maxPoint.x - centerX) * coercedRatios[index],
centerY + (maxPoint.y - centerY) * coercedRatios[index]
)
}
val centerPoint = Vec2(centerX, centerY)
RenderSystem.disableDepthTest()
for (i in vertices.indices) {
val nextIndex = (i + 1) % vertices.size
drawTriangle(colour, vertices[i], centerPoint, vertices[nextIndex])
}
RenderSystem.enableDepthTest()
}
}
@@ -0,0 +1,354 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab.Companion.choiceAspectId
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.item.HeldItemRenderer
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import com.mojang.blaze3d.platform.Lighting
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.renderer.LightTexture
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.block.Blocks
import org.joml.Quaternionf
import org.joml.Vector3f
class PokeEditImplementationBlankTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft,
title: Component
) : SoundlessWidget(pX, pY, HALF_OVERLAY_WIDTH, HALF_OVERLAY_HEIGHT, title) {
companion object {
private val backgroundOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay.png")
private val pokeBallOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_poke_ball.png")
private val platformBase = cobblemonResource("textures/gui/pokedex/platform_base.png")
private val platformShadow = cobblemonResource("textures/gui/pokedex/platform_shadow.png")
private const val POKEBALL_OVERLAY_Y = 25
private const val PLATFORM_Y = 69
private const val SHADOW_X = 25
private const val SHADOW_Y = 76
private const val VIEW_SCALE = 1.2F
private const val VIEW_SHIFT_X = 0F
private const val VIEW_SHIFT_Y = 36F
private const val MODEL_VIEW_SCALE = 2.4F
private const val MODEL_DRAW_SCALE = 24F
private const val MODEL_OFFSET_Y = 20.0
private const val CUBE_BASE_OFFSET_X = 1.0F
private const val CUBE_SIZE_OFFSET_MULTIPLIER = 0.18F
private const val CUBE_OFFSET_Z = -0.5F
}
private val state = FloatingState().also { state ->
state.runtime.environment.query.addFunction("is_holding_item") {
DoubleValue(hasNonWearableAttachment())
}
state.runtime.environment.query.addFunction("is_wearing_hat") {
DoubleValue(hasWearableHatAttachment())
}
state.runtime.environment.query.addFunction("is_wearing_face") {
DoubleValue(hasWearableFaceAttachment())
}
}
private val heldItemRenderer = HeldItemRenderer()
private var renderablePokemon: RenderablePokemon? = null
private var attachmentStacks: List<ItemStack> = emptyList()
private var rotationY = 30F
private var poseList: Array<PoseType> = arrayOf(PoseType.PROFILE)
private var selectedPoseIndex: Int = 0
private var oldDragPosX = 0.0
private var canDragRender = false
private var isDraggingRender = false
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
context.enableScissor(x, y, x + HALF_OVERLAY_WIDTH, y + HALF_OVERLAY_HEIGHT)
val viewWidth = HALF_OVERLAY_WIDTH * VIEW_SCALE
val viewHeight = HALF_OVERLAY_HEIGHT * VIEW_SCALE
val viewX = x + (HALF_OVERLAY_WIDTH - viewWidth) / 2F + VIEW_SHIFT_X
val viewY = y + (HALF_OVERLAY_HEIGHT - viewHeight) / 2F + VIEW_SHIFT_Y
val scaledBaseX = viewX / VIEW_SCALE
val scaledBaseY = viewY / VIEW_SCALE
blitk(
matrixStack = context.pose(),
texture = backgroundOverlay,
x = scaledBaseX,
y = scaledBaseY,
width = HALF_OVERLAY_WIDTH,
height = HALF_OVERLAY_HEIGHT,
scale = VIEW_SCALE
)
blitk(
matrixStack = context.pose(),
texture = pokeBallOverlay,
x = scaledBaseX + 15,
y = scaledBaseY + POKEBALL_OVERLAY_Y,
width = 109,
height = 68,
vOffset = 0,
textureHeight = 1744,
scale = VIEW_SCALE
)
blitk(
matrixStack = context.pose(),
texture = platformBase,
x = scaledBaseX + 13,
y = scaledBaseY + PLATFORM_Y,
width = 113,
height = 24,
textureHeight = 30,
scale = VIEW_SCALE
)
blitk(
matrixStack = context.pose(),
texture = platformShadow,
x = scaledBaseX + SHADOW_X,
y = scaledBaseY + SHADOW_Y,
width = 90,
height = 20,
scale = VIEW_SCALE
)
val fillTop = (viewY + 114F * VIEW_SCALE).toInt()
val fillBottom = (viewY + (HALF_OVERLAY_HEIGHT - 6) * VIEW_SCALE).toInt()
context.fill(viewX.toInt(), fillTop, (viewX + viewWidth).toInt(), fillBottom, 0xFF7AEEF1.toInt())
context.disableScissor()
buildRenderable()?.let { renderable ->
context.enableScissor(
x + 1,
y + 1,
x + HALF_OVERLAY_WIDTH - 1,
y + HALF_OVERLAY_HEIGHT - 1
)
val matrices = context.pose()
matrices.pushPose()
matrices.translate(
x.toDouble() + (HALF_OVERLAY_WIDTH.toDouble()) / 2,
y.toDouble() + MODEL_OFFSET_Y,
1000.0
)
matrices.scale(MODEL_VIEW_SCALE, MODEL_VIEW_SCALE, MODEL_VIEW_SCALE)
val rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, rotationY, 0F))
val cubeOffsetX = getCubeOffsetX()
matrices.translate((-cubeOffsetX / 2F).toDouble(), 0.0, 0.0)
renderReferenceCube(renderable, matrices, rotation, cubeOffsetX)
drawProfilePokemon(
renderablePokemon = renderable,
poseType = poseList[selectedPoseIndex],
matrixStack = matrices,
partialTicks = delta,
rotation = Quaternionf(rotation),
state = state,
scale = MODEL_DRAW_SCALE
)
if (attachmentStacks.isNotEmpty()) {
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
attachmentStacks.forEach { stack ->
heldItemRenderer.renderOnModel(
stack,
state,
matrices,
bufferSource,
light = 0xF000F0,
frontLight = true
)
}
bufferSource.endBatch()
}
matrices.popPose()
context.disableScissor()
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isWithinViewport(mouseX, mouseY)) {
canDragRender = true
isDraggingRender = true
oldDragPosX = mouseX
return true
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
if (canDragRender) {
canDragRender = false
}
if (isDraggingRender) {
isDraggingRender = false
}
return super.mouseReleased(pMouseX, pMouseY, pButton)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val handledDrag = isDraggingRender && canDragRender
if (handledDrag) {
val dragOffsetY = (oldDragPosX - mouseX).toFloat()
rotationY = (((rotationY + dragOffsetY) % 360 + 360) % 360)
}
oldDragPosX = mouseX
return handledDrag || super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
private fun buildRenderable(): RenderablePokemon? {
val species = draft.species
val form = draft.form
val aspects = mutableSetOf<String>()
if (draft.shiny) aspects.add(SHINY_ASPECT.aspect)
aspects.addAll(form.aspects)
aspects.addAll(draft.forcedAspects)
aspects.addAll(draft.flagFeatures)
aspects.addAll(draft.cosmeticAspects)
draft.choiceFeatures.forEach { (key, choice) ->
choiceAspectId(key, choice)?.let { aspects.add(it) }
}
when (draft.gender) {
Gender.MALE -> aspects.add("male")
Gender.FEMALE -> aspects.add("female")
Gender.GENDERLESS -> aspects.add("genderless")
}
val heldStack = parseItemStack(draft.heldItem)
val cosmeticStack = parseItemStack(draft.cosmeticItem)
attachmentStacks = buildAttachmentStacks(heldStack, cosmeticStack)
renderablePokemon = RenderablePokemon(species, aspects, heldStack).also { recalculatePoses(it) }
return renderablePokemon
}
private fun recalculatePoses(renderablePokemon: RenderablePokemon) {
val poseState = FloatingState().also { it.currentAspects = renderablePokemon.aspects }
val poser = VaryingModelRepository.getPoser(renderablePokemon.species.resourceIdentifier, poseState)
poseList = poser.poses
.map { it.value.poseTypes.minBy { it.ordinal } }
.toSet()
.filterNot { it in PoseType.SHOULDER_POSES }
.sortedBy { it.ordinal }
.toTypedArray()
if (selectedPoseIndex !in poseList.indices) {
selectedPoseIndex = 0
}
}
private fun parseItemStack(rawId: String?): ItemStack {
return rawId
?.let { ResourceLocation.tryParse(it) }
?.let { id -> BuiltInRegistries.ITEM.getOptional(id).orElse(null) }
?.let { item -> ItemStack(item) }
?: ItemStack.EMPTY
}
private fun buildAttachmentStacks(heldStack: ItemStack, cosmeticStack: ItemStack): List<ItemStack> {
if (heldStack.isEmpty && cosmeticStack.isEmpty) return emptyList()
val stacks = mutableListOf<ItemStack>()
if (!heldStack.isEmpty) {
stacks.add(heldStack)
}
if (!cosmeticStack.isEmpty && (heldStack.isEmpty || cosmeticStack.item != heldStack.item)) {
stacks.add(cosmeticStack)
}
return stacks
}
private fun hasNonWearableAttachment(): Boolean {
return attachmentStacks.any { stack ->
!stack.isEmpty && !stack.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) && !stack.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS)
}
}
private fun hasWearableHatAttachment(): Boolean {
return attachmentStacks.any { stack -> !stack.isEmpty && stack.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) }
}
private fun hasWearableFaceAttachment(): Boolean {
return attachmentStacks.any { stack -> !stack.isEmpty && stack.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS) }
}
private fun renderReferenceCube(
renderable: RenderablePokemon,
matrices: com.mojang.blaze3d.vertex.PoseStack,
rotation: Quaternionf,
cubeOffsetX: Float
) {
val model = VaryingModelRepository.getPoser(renderable.species.resourceIdentifier, state)
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
matrices.pushPose()
matrices.scale(MODEL_DRAW_SCALE, MODEL_DRAW_SCALE, -MODEL_DRAW_SCALE)
matrices.translate(
model.profileTranslation.x,
model.profileTranslation.y + 1.5 * model.profileScale,
model.profileTranslation.z - 4.0
)
matrices.scale(model.profileScale, model.profileScale, 1 / model.profileScale)
matrices.mulPose(rotation)
matrices.mulPose(com.mojang.math.Axis.XP.rotationDegrees(180F))
matrices.translate(cubeOffsetX.toDouble(), 0.0, CUBE_OFFSET_Z.toDouble())
RenderSystem.applyModelViewMatrix()
RenderSystem.enableDepthTest()
RenderSystem.disableCull()
Lighting.setupForEntityInInventory()
Minecraft.getInstance().blockRenderer.renderSingleBlock(
Blocks.RED_WOOL.defaultBlockState(),
matrices,
bufferSource,
LightTexture.pack(15, 15),
OverlayTexture.NO_OVERLAY
)
bufferSource.endBatch()
Lighting.setupFor3DItems()
RenderSystem.enableCull()
matrices.popPose()
}
private fun getCubeOffsetX(): Float {
val hitboxWidth = draft.form.hitbox.width * draft.form.baseScale
val hitboxHeight = draft.form.hitbox.height * draft.form.baseScale
val hitboxSize = maxOf(hitboxWidth, hitboxHeight)
val extraOffset = (hitboxSize - 1.0F).coerceAtLeast(0.0F) * CUBE_SIZE_OFFSET_MULTIPLIER
return CUBE_BASE_OFFSET_X + extraOffset
}
private fun isWithinViewport(mouseX: Double, mouseY: Double): Boolean =
mouseX.toInt() in x..(x + HALF_OVERLAY_WIDTH)
&& mouseY.toInt() in y..(y + HALF_OVERLAY_HEIGHT)
&& children.none { child ->
child is AbstractWidget && child.visible && child.isMouseOver(mouseX, mouseY)
}
}
@@ -0,0 +1,227 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.gui.ScrollingWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component
import net.minecraft.util.Mth
class PokeEditLocatorListWidget(
val pX: Int,
val pY: Int,
val listWidth: Int,
val listHeight: Int,
private val onSelectLocator: (String?) -> Unit
) : ScrollingWidget<PokeEditLocatorListWidget.LocatorRow>(
left = pX,
top = pY - listHeight,
width = listWidth,
height = listHeight,
slotHeight = ROW_HEIGHT
) {
companion object {
private const val ROW_HEIGHT = 9
private const val TEXT_SCALE = 0.5F
private const val BORDER_COLOR = 0xFF1B1B1B.toInt()
private const val FILL_COLOR = 0xFF3B3B3B.toInt()
private const val TEXT_COLOR = 0xFFFCFCFC.toInt()
}
private val listTop = pY - listHeight
private val listBottom = pY
private var selectedLocator: String? = null
private var draggingScrollbar = false
fun setLocators(locators: Collection<String>) {
clearEntries()
locators.sorted().forEach { addEntry(LocatorRow(it)) }
if (selectedLocator != null && locators.none { it == selectedLocator }) {
selectedLocator = null
onSelectLocator(null)
}
scrollAmount = 0.0
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
context.fill(pX - 1, listTop - 1, pX + listWidth + 1, listBottom + 1, BORDER_COLOR)
context.fill(pX, listTop, pX + listWidth, listBottom, FILL_COLOR)
focused = if (mouseX in pX..(pX + listWidth) && mouseY in listTop..listBottom) {
getEntryAtPosition(mouseX.toDouble(), mouseY.toDouble())
} else {
null
}
context.enableScissor(pX, listTop, pX + listWidth, listBottom)
renderListItems(context, mouseX, mouseY, delta)
context.disableScissor()
if (maxScroll > 0) {
renderScrollbar(context, mouseX, mouseY, delta)
}
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double, verticalAmount: Double): Boolean {
if (mouseX !in pX.toDouble()..(pX + listWidth).toDouble()
|| mouseY !in listTop.toDouble()..listBottom.toDouble()
) {
return false
}
val maxScroll = (itemCount * itemHeight - listHeight).coerceAtLeast(0)
if (maxScroll <= 0) return true
val step = if (verticalAmount != 0.0) verticalAmount else amount
scrollAmount = Mth.clamp(scrollAmount - step * itemHeight, 0.0, maxScroll.toDouble())
return true
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (mouseX !in pX.toDouble()..(pX + listWidth).toDouble()
|| mouseY !in listTop.toDouble()..listBottom.toDouble()
) {
return false
}
if (button != 0) {
return false
}
if (isOverScrollbar(mouseX, mouseY) && maxScroll > 0) {
draggingScrollbar = true
return true
}
val localY = mouseY.toInt() - listTop + scrollAmount.toInt()
if (localY < 0) return false
val index = localY / itemHeight
if (index !in 0 until itemCount) return false
val entry = getEntry(index)
if (entry is LocatorRow) {
selectedLocator = entry.name
onSelectLocator(selectedLocator)
return true
}
return false
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (button == 0 && draggingScrollbar) {
draggingScrollbar = false
return true
}
return super.mouseReleased(mouseX, mouseY, button)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (!draggingScrollbar || maxScroll <= 0) {
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
val barHeight = listBottom - listTop
var thumbHeight = ((barHeight * barHeight).toFloat() / maxPosition.toFloat()).toInt()
thumbHeight = Mth.clamp(thumbHeight, 16, barHeight - 4)
val trackHeight = barHeight - thumbHeight
if (trackHeight <= 0) return true
val clampedY = Mth.clamp(mouseY.toInt() - listTop - thumbHeight / 2, 0, trackHeight)
scrollAmount = (clampedY.toDouble() * maxScroll.toDouble()) / trackHeight.toDouble()
return true
}
override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean {
return mouseX in pX.toDouble()..(pX + listWidth).toDouble()
&& mouseY in listTop.toDouble()..listBottom.toDouble()
}
override fun renderListItems(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val rowX = rowLeft
for (index in 0 until itemCount) {
val rowY = getRowTop(index)
val rowBottom = getRowBottom(index)
if (rowBottom >= listTop && rowY <= listBottom) {
val entry = getEntry(index)
if (entry is LocatorRow) {
entry.isSelected = entry.name == selectedLocator
}
renderItem(context, mouseX, mouseY, delta, index, rowX, rowY, rowWidth, itemHeight)
}
}
}
override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val xLeft = this.scrollbarPosition
val xRight = xLeft + 3
val yStart = listTop
val barHeight = listBottom - listTop
var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
yBottom = Mth.clamp(yBottom, 16, barHeight - 4)
var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + yStart
if (yTop < yStart) yTop = yStart
context.fill(xLeft + 1, yStart, xRight - 1, listBottom, 0xFF7AEEF1.toInt())
context.fill(xLeft, yTop, xRight, yTop + yBottom, 0xFF3A96B6.toInt())
}
override fun getRowTop(index: Int): Int {
return listTop - scrollAmount.toInt() + (index * itemHeight)
}
override fun getRowBottom(index: Int): Int {
return getRowTop(index) + itemHeight
}
override fun getBottom(): Int {
return listBottom
}
private fun isOverScrollbar(mouseX: Double, mouseY: Double): Boolean {
val xLeft = scrollbarPosition
val xRight = xLeft + 3
return mouseX >= xLeft && mouseX <= xRight && mouseY >= listTop && mouseY <= listBottom
}
class LocatorRow(val name: String) : Slot<LocatorRow>() {
var isSelected: Boolean = false
override fun render(
context: GuiGraphics,
index: Int,
y: Int,
x: Int,
entryWidth: Int,
entryHeight: Int,
mouseX: Int,
mouseY: Int,
hovered: Boolean,
tickDelta: Float
) {
val bg = when {
isSelected -> 0xFF4A4A4A.toInt()
hovered -> 0xFF2F2F2F.toInt()
else -> null
}
if (bg != null) {
context.fill(x + 1, y, x + entryWidth - 1, y + ROW_HEIGHT, bg)
}
drawScaledText(
context = context,
text = name.text(),
x = x + 3,
y = y + 2,
scale = TEXT_SCALE,
colour = TEXT_COLOR,
shadow = false
)
}
override fun getNarration(): Component {
return Component.literal(name)
}
}
}
@@ -0,0 +1,476 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
import com.cobblemon.mod.common.client.gui.summary.SummaryButton
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.item.HeldItemRenderer
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab.Companion.choiceAspectId
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.renderer.LevelRenderer
import net.minecraft.client.renderer.RenderType
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import org.joml.Matrix4f
import org.joml.Quaternionf
import org.joml.Vector4f
import org.joml.Vector3f
class PokeEditLocatorTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft,
private var highlightedLocator: String?,
private val onDumpLocators: (List<String>) -> Unit
) : SoundlessWidget(pX, pY, HALF_OVERLAY_WIDTH, HALF_OVERLAY_HEIGHT, "locators".text()) {
companion object {
private val backgroundOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay.png")
private val pokeBallOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_poke_ball.png")
private val platformBase = cobblemonResource("textures/gui/pokedex/platform_base.png")
private val platformShadow = cobblemonResource("textures/gui/pokedex/platform_shadow.png")
private val actionButtonResource = cobblemonResource("textures/gui/summary/summary_evolve_button.png")
private const val LOCATOR_CUBE_SIZE = 0.1
private const val LOCATOR_TOOLTIP_RADIUS = 8
private const val BUTTON_WIDTH = 54
private const val BUTTON_HEIGHT = 15
private const val POKEBALL_OVERLAY_Y = 25
private const val PLATFORM_Y = 69
private const val SHADOW_X = 25
private const val SHADOW_Y = 76
private const val VIEW_SCALE = 1.2F
private const val VIEW_SHIFT_X = 0F
private const val VIEW_SHIFT_Y = 36F
private const val MODEL_VIEW_SCALE = 2.4F
private const val MODEL_DRAW_SCALE = 24F
private const val MODEL_OFFSET_Y = 20.0
private const val AXIS_LINE_LENGTH = 0.35
private const val LEGEND_SCALE = 0.5F
private const val LEGEND_X = 6
private const val LEGEND_Y = 60
}
private val state = FloatingState().also { state ->
state.runtime.environment.query.addFunction("is_holding_item") {
val held = currentHeldStack()
DoubleValue(!held.isEmpty && !held.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) && !held.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
}
state.runtime.environment.query.addFunction("is_wearing_hat") {
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS))
}
state.runtime.environment.query.addFunction("is_wearing_face") {
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
}
}
private val heldItemRenderer = HeldItemRenderer()
private var renderablePokemon: RenderablePokemon? = null
private var rotationY = 30F
private var poseList: Array<PoseType> = arrayOf(PoseType.PROFILE)
private var selectedPoseIndex: Int = 0
private var showLocators = false
private var showLocatorAxes = false
private var oldDragPosX = 0.0
private var canDragRender = false
private var isDraggingRender = false
private var cachedLocatorNames: List<String> = emptyList()
private val showLocatorsButton = SummaryButton(
buttonX = pX + 6F,
buttonY = pY + 6F,
buttonWidth = BUTTON_WIDTH,
buttonHeight = BUTTON_HEIGHT,
clickAction = { showLocators = true },
text = "Locators On".text(),
resource = actionButtonResource,
renderRequirement = { !showLocators },
largeText = false,
boldText = false,
textScale = 0.7F
).also { addWidget(it) }
private val hideLocatorsButton = SummaryButton(
buttonX = pX + 66F,
buttonY = pY + 6F,
buttonWidth = BUTTON_WIDTH,
buttonHeight = BUTTON_HEIGHT,
clickAction = { showLocators = false },
text = "Locators Off".text(),
resource = actionButtonResource,
renderRequirement = { showLocators },
largeText = false,
boldText = false,
textScale = 0.7F
).also { addWidget(it) }
private val showLocatorAxesButton = SummaryButton(
buttonX = pX + 6F,
buttonY = pY + 24F,
buttonWidth = BUTTON_WIDTH,
buttonHeight = BUTTON_HEIGHT,
clickAction = { showLocatorAxes = true },
text = "Axes On".text(),
resource = actionButtonResource,
renderRequirement = { !showLocatorAxes },
largeText = false,
boldText = false,
textScale = 0.7F
).also { addWidget(it) }
private val hideLocatorAxesButton = SummaryButton(
buttonX = pX + 66F,
buttonY = pY + 24F,
buttonWidth = BUTTON_WIDTH,
buttonHeight = BUTTON_HEIGHT,
clickAction = { showLocatorAxes = false },
text = "Axes Off".text(),
resource = actionButtonResource,
renderRequirement = { showLocatorAxes },
largeText = false,
boldText = false,
textScale = 0.7F
).also { addWidget(it) }
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val matrices = context.pose()
showLocatorsButton.buttonActive = showLocators
hideLocatorsButton.buttonActive = !showLocators
showLocatorAxesButton.buttonActive = showLocatorAxes
hideLocatorAxesButton.buttonActive = !showLocatorAxes
context.enableScissor(x, y, x + HALF_OVERLAY_WIDTH, y + HALF_OVERLAY_HEIGHT)
val viewWidth = HALF_OVERLAY_WIDTH * VIEW_SCALE
val viewHeight = HALF_OVERLAY_HEIGHT * VIEW_SCALE
val viewX = x + (HALF_OVERLAY_WIDTH - viewWidth) / 2F + VIEW_SHIFT_X
val viewY = y + (HALF_OVERLAY_HEIGHT - viewHeight) / 2F + VIEW_SHIFT_Y
val scaledBaseX = viewX / VIEW_SCALE
val scaledBaseY = viewY / VIEW_SCALE
blitk(
matrixStack = matrices,
texture = backgroundOverlay,
x = scaledBaseX,
y = scaledBaseY,
width = HALF_OVERLAY_WIDTH,
height = HALF_OVERLAY_HEIGHT,
scale = VIEW_SCALE
)
blitk(
matrixStack = matrices,
texture = pokeBallOverlay,
x = scaledBaseX + 15,
y = scaledBaseY + POKEBALL_OVERLAY_Y,
width = 109,
height = 68,
vOffset = 0,
textureHeight = 1744,
scale = VIEW_SCALE
)
blitk(
matrixStack = matrices,
texture = platformBase,
x = scaledBaseX + 13,
y = scaledBaseY + PLATFORM_Y,
width = 113,
height = 24,
textureHeight = 30,
scale = VIEW_SCALE
)
blitk(
matrixStack = matrices,
texture = platformShadow,
x = scaledBaseX + SHADOW_X,
y = scaledBaseY + SHADOW_Y,
width = 90,
height = 20,
scale = VIEW_SCALE
)
val fillTop = (viewY + 114F * VIEW_SCALE).toInt()
val fillBottom = (viewY + (HALF_OVERLAY_HEIGHT - 6) * VIEW_SCALE).toInt()
context.fill(viewX.toInt(), fillTop, (viewX + viewWidth).toInt(), fillBottom, 0xFF7AEEF1.toInt())
context.disableScissor()
buildRenderable()?.let { renderable ->
context.enableScissor(
x + 1,
y + 1,
x + HALF_OVERLAY_WIDTH - 1,
y + HALF_OVERLAY_HEIGHT - 1
)
matrices.pushPose()
matrices.translate(
x.toDouble() + (HALF_OVERLAY_WIDTH.toDouble()) / 2,
y.toDouble() + MODEL_OFFSET_Y,
1000.0
)
matrices.scale(MODEL_VIEW_SCALE, MODEL_VIEW_SCALE, MODEL_VIEW_SCALE)
val rotationVector = Vector3f(13F, rotationY, 0F)
drawProfilePokemon(
renderablePokemon = renderable,
poseType = poseList[selectedPoseIndex],
matrixStack = matrices,
partialTicks = delta,
rotation = Quaternionf().fromEulerXYZDegrees(rotationVector),
state = state,
scale = MODEL_DRAW_SCALE
)
refreshLocatorList()
if (!renderable.heldItem.isEmpty) {
heldItemRenderer.renderOnModel(
renderable.heldItem,
state,
matrices,
context.bufferSource(),
light = 0xF000F0,
frontLight = true
)
}
if (showLocators) {
renderLocators(context, mouseX, mouseY, showLocatorAxes)
}
matrices.popPose()
context.disableScissor()
}
showLocatorsButton.render(context, mouseX, mouseY, delta)
hideLocatorsButton.render(context, mouseX, mouseY, delta)
showLocatorAxesButton.render(context, mouseX, mouseY, delta)
hideLocatorAxesButton.render(context, mouseX, mouseY, delta)
if (showLocators && showLocatorAxes) {
renderAxisLegend(context)
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isWithinViewport(mouseX, mouseY)) {
canDragRender = true
isDraggingRender = true
oldDragPosX = mouseX
return true
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
if (canDragRender) {
canDragRender = false
}
if (isDraggingRender) {
isDraggingRender = false
}
return super.mouseReleased(pMouseX, pMouseY, pButton)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val handledDrag = isDraggingRender && canDragRender
if (handledDrag) {
val dragOffsetY = (oldDragPosX - mouseX).toFloat()
rotationY = (((rotationY + dragOffsetY) % 360 + 360) % 360)
}
oldDragPosX = mouseX
return handledDrag || super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
private fun renderLocators(context: GuiGraphics, mouseX: Int, mouseY: Int, showAxes: Boolean) {
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
val buffer = bufferSource.getBuffer(RenderType.lines())
val baseMatrix = Matrix4f(context.pose().last().pose())
var closestName: String? = null
var closestDistance = LOCATOR_TOOLTIP_RADIUS.toFloat()
for ((name, matrix) in state.locatorStates.entries) {
val highlight = name == highlightedLocator
val origin = matrix.getOrigin()
context.pose().pushPose()
context.pose().mulPose(matrix.matrix)
LevelRenderer.renderLineBox(
context.pose(),
buffer,
AABB.ofSize(Vec3.ZERO, LOCATOR_CUBE_SIZE, LOCATOR_CUBE_SIZE, LOCATOR_CUBE_SIZE),
if (highlight) 1F else 0F,
if (highlight) 0F else 1F,
0F,
1F
)
if (showAxes) {
renderLocatorAxes(context, buffer)
}
context.pose().popPose()
val screenPos = projectToScreen(baseMatrix, origin) ?: continue
val dx = screenPos.first - mouseX
val dy = screenPos.second - mouseY
val dist = kotlin.math.sqrt((dx * dx + dy * dy).toDouble()).toFloat()
if (dist <= closestDistance) {
closestDistance = dist
closestName = name
}
}
bufferSource.endBatch(RenderType.lines())
if (closestName != null) {
context.renderTooltip(Minecraft.getInstance().font, Component.literal(closestName), mouseX, mouseY)
}
}
fun setHighlightedLocator(name: String?) {
highlightedLocator = name
}
private fun refreshLocatorList() {
val names = state.locatorStates.keys.sorted()
if (names != cachedLocatorNames) {
cachedLocatorNames = names
onDumpLocators(names)
}
}
private fun renderLocatorAxes(context: GuiGraphics, buffer: com.mojang.blaze3d.vertex.VertexConsumer) {
val pose = context.pose().last().pose()
val length = AXIS_LINE_LENGTH.toFloat()
addAxisLine(buffer, pose, 0f, 0f, 0f, length, 0f, 0f, 1f, 0f, 0f)
addAxisLine(buffer, pose, 0f, 0f, 0f, 0f, length, 0f, 0f, 1f, 0f)
addAxisLine(buffer, pose, 0f, 0f, 0f, 0f, 0f, length, 0f, 0f, 1f)
}
private fun addAxisLine(
buffer: com.mojang.blaze3d.vertex.VertexConsumer,
pose: Matrix4f,
x1: Float,
y1: Float,
z1: Float,
x2: Float,
y2: Float,
z2: Float,
r: Float,
g: Float,
b: Float
) {
buffer.addVertex(pose, x1, y1, z1).setColor(r, g, b, 1f).setNormal(0f, 1f, 0f)
buffer.addVertex(pose, x2, y2, z2).setColor(r, g, b, 1f).setNormal(0f, 1f, 0f)
}
private fun renderAxisLegend(context: GuiGraphics) {
val legendX = x + LEGEND_X
val legendY = y + LEGEND_Y
context.fill(legendX - 2, legendY - 2, legendX + 46, legendY + 16, 0xAA1B1B1B.toInt())
drawLegendLine(context, legendX, legendY, 0xFFFF4D4D.toInt(), "X")
drawLegendLine(context, legendX, legendY + 5, 0xFF4DFF4D.toInt(), "Y")
drawLegendLine(context, legendX, legendY + 10, 0xFF4D4DFF.toInt(), "Z")
}
private fun drawLegendLine(context: GuiGraphics, x: Int, y: Int, color: Int, label: String) {
context.fill(x, y + 2, x + 6, y + 3, color)
drawScaledText(
context = context,
text = Component.literal(label),
x = x + 8,
y = y,
scale = LEGEND_SCALE,
colour = 0xFFFFFFFF.toInt(),
shadow = false
)
}
private fun buildRenderable(): RenderablePokemon? {
val species = draft.species
val form = draft.form
val aspects = mutableSetOf<String>()
if (draft.shiny) aspects.add(SHINY_ASPECT.aspect)
aspects.addAll(form.aspects)
aspects.addAll(draft.forcedAspects)
aspects.addAll(draft.flagFeatures)
aspects.addAll(draft.cosmeticAspects)
draft.choiceFeatures.forEach { (key, choice) ->
choiceAspectId(key, choice)?.let { aspects.add(it) }
}
when (draft.gender) {
Gender.MALE -> aspects.add("male")
Gender.FEMALE -> aspects.add("female")
Gender.GENDERLESS -> aspects.add("genderless")
}
val heldStack = draft.heldItem
?.let { ResourceLocation.tryParse(it) }
?.let { id -> BuiltInRegistries.ITEM.getOptional(id).orElse(null) }
?.let { item -> ItemStack(item) }
?: ItemStack.EMPTY
renderablePokemon = RenderablePokemon(species, aspects, heldStack).also { recalculatePoses(it) }
return renderablePokemon
}
private fun recalculatePoses(renderablePokemon: RenderablePokemon) {
val poseState = FloatingState().also { it.currentAspects = renderablePokemon.aspects }
val poser = VaryingModelRepository.getPoser(renderablePokemon.species.resourceIdentifier, poseState)
poseList = poser.poses
.map { it.value.poseTypes.minBy { it.ordinal } }
.toSet()
.filterNot { it in PoseType.SHOULDER_POSES }
.sortedBy { it.ordinal }
.toTypedArray()
if (selectedPoseIndex !in poseList.indices) {
selectedPoseIndex = 0
}
}
private fun currentHeldStack(): ItemStack {
return renderablePokemon?.heldItem ?: ItemStack.EMPTY
}
private fun projectToScreen(baseMatrix: Matrix4f, position: Vec3): Pair<Float, Float>? {
val projection = RenderSystem.getProjectionMatrix()
val vec = Vector4f(position.x.toFloat(), position.y.toFloat(), position.z.toFloat(), 1f)
vec.mul(baseMatrix)
vec.mul(projection)
if (vec.w == 0f) return null
vec.div(vec.w)
val window = Minecraft.getInstance().window
val screenX = (vec.x * 0.5f + 0.5f) * window.guiScaledWidth.toFloat()
val screenY = (1f - (vec.y * 0.5f + 0.5f)) * window.guiScaledHeight.toFloat()
return Pair(screenX, screenY)
}
private fun isWithinViewport(mouseX: Double, mouseY: Double): Boolean =
mouseX.toInt() in x..(x + HALF_OVERLAY_WIDTH)
&& mouseY.toInt() in y..(y + HALF_OVERLAY_HEIGHT)
&& children.none { child ->
child is AbstractWidget && child.visible && child.isMouseOver(mouseX, mouseY)
}
}
@@ -0,0 +1,659 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.MoveCategoryIcon
import com.cobblemon.mod.common.client.gui.TypeIcon
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.gui.summary.widgets.common.SummaryScrollList
import com.cobblemon.mod.common.client.gui.summary.widgets.screens.moves.MoveDescriptionScrollList
import com.cobblemon.mod.common.client.gui.summary.widgets.screens.moves.ReorderMoveButton
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.math.toRGB
import java.math.RoundingMode
import java.text.DecimalFormat
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.util.Mth
class PokeEditMovesTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft
) : SoundlessWidget(pX, pY, WIDTH, HEIGHT, lang("ui.moves")) {
companion object {
private const val WIDTH = 139
private const val HEIGHT = 163
private const val BASE_WIDTH = 134
private const val BASE_HEIGHT = 148
private const val BASE_SCALE = 0.80F
private const val SLOT_SPACING = 3
private const val SCALE = 0.5F
private val decimalFormat = DecimalFormat("#.##").also {
it.roundingMode = RoundingMode.CEILING
}
private val movesBaseResource = cobblemonResource("textures/gui/summary/summary_moves_base.png")
private val moveResource = cobblemonResource("textures/gui/summary/summary_move.png")
private val moveOverlayResource = cobblemonResource("textures/gui/summary/summary_move_overlay.png")
private val moveSelectedOverlayResource = cobblemonResource("textures/gui/summary/summary_move_selected_overlay.png")
private val moveSwapButtonResource = cobblemonResource("textures/gui/summary/summary_move_swap.png")
private val moveAddButtonResource = cobblemonResource("textures/gui/summary/summary_move_add.png")
private val movesPowerIconResource = cobblemonResource("textures/gui/summary/summary_moves_icon_power.png")
private val movesAccuracyIconResource = cobblemonResource("textures/gui/summary/summary_moves_icon_accuracy.png")
private val movesEffectIconResource = cobblemonResource("textures/gui/summary/summary_moves_icon_effect.png")
private val moveListResource = cobblemonResource("textures/gui/summary/summary_move_condensed.png")
private val moveListOverlayResource = cobblemonResource("textures/gui/summary/summary_move_overlay_condensed.png")
private const val MOVE_WIDTH = 108
private const val MOVE_HEIGHT = 22
private const val MOVE_LIST_SLOT_HEIGHT = 18
private const val MOVE_LIST_SLOT_SPACING = 3
private const val MOVE_LIST_X_OFFSET = 0
private const val MOVE_LIST_Y_OFFSET = 0
}
private val baseScaledWidth = (BASE_WIDTH * BASE_SCALE).toInt()
private val baseScaledHeight = (BASE_HEIGHT * BASE_SCALE).toInt()
private val moveWidthScaled = (MOVE_WIDTH * BASE_SCALE).toInt()
private val moveHeightScaled = (MOVE_HEIGHT * BASE_SCALE).toInt()
private val slotSpacingScaled = (SLOT_SPACING * BASE_SCALE).toInt().coerceAtLeast(1)
private val baseX = x + (WIDTH - baseScaledWidth) / 2 - 10
private val baseY = y + 7
private val slotX = baseX + (13 * BASE_SCALE).toInt()
private val slotY = baseY + (6 * BASE_SCALE).toInt()
private val descriptionListX = (baseX + (69 * BASE_SCALE).toInt())
.coerceAtMost(baseX + baseScaledWidth - 60)
private val descriptionListY = baseY + (113 * BASE_SCALE).toInt()
private val slots = (0 until 4).map { index ->
MoveSlot(
slotX,
slotY + (moveHeightScaled + slotSpacingScaled) * index,
index
).also { addWidget(it) }
}
private val descriptionList = MoveDescriptionScrollList(
descriptionListX,
descriptionListY,
5
)
private val movePicker = MovePickerList(
baseX + baseScaledWidth + MOVE_LIST_X_OFFSET,
baseY + (11 * BASE_SCALE).toInt() + MOVE_LIST_Y_OFFSET
)
private var selectedSlotIndex = -1
private var pickerSlotIndex = -1
private var movePickerOpen = false
private var lastDescriptionKey: String? = null
init {
refreshMoves()
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val matrices = context.pose()
blitk(
matrixStack = matrices,
texture = movesBaseResource,
x = baseX,
y = baseY,
width = baseScaledWidth,
height = baseScaledHeight
)
slots.forEach { it.render(context, mouseX, mouseY, delta) }
renderMoveInfo(context)
renderDescription(context, mouseX, mouseY, delta)
if (movePickerOpen) {
movePicker.renderWidget(context, mouseX, mouseY, delta)
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (movePickerOpen) {
val handled = movePicker.mouseClicked(mouseX, mouseY, button)
if (handled) return true
if (!movePicker.isMouseOver(mouseX, mouseY)) {
closeMovePicker()
}
}
if (descriptionList.isHovered) {
descriptionList.mouseClicked(mouseX, mouseY, button)
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
if (movePickerOpen) {
if (movePicker.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)) return true
}
if (descriptionList.isHovered) {
descriptionList.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
}
return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (movePickerOpen) {
if (movePicker.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) return true
}
if (descriptionList.isHovered) {
descriptionList.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
private fun refreshMoves() {
val moves = Moves.all().sortedBy { it.displayName.string }
movePicker.setEntries(moves)
}
private fun selectedMove(): MoveTemplate? {
return if (selectedSlotIndex in draft.activeMoves.indices) draft.activeMoves[selectedSlotIndex] else null
}
private fun renderMoveInfo(context: GuiGraphics) {
val move = selectedMove()
val mcFont = Minecraft.getInstance().font
// Power
blitk(
matrixStack = context.pose(),
texture = movesPowerIconResource,
x = (baseX + (7 * BASE_SCALE)) / SCALE,
y = (baseY + (114.5 * BASE_SCALE)) / SCALE,
width = 10,
height = 10,
scale = SCALE
)
drawScaledText(
context = context,
text = lang("ui.power"),
x = baseX + (14 * BASE_SCALE),
y = baseY + (115 * BASE_SCALE),
scale = SCALE * BASE_SCALE,
shadow = true
)
val powerText = if (move != null && move.power.toInt() > 0) move.power.toInt().toString() else "-"
val powerComponent = Component.literal(powerText)
drawScaledText(
context = context,
text = powerComponent,
x = (baseX + (62.5 * BASE_SCALE)) - (mcFont.width(powerComponent) * SCALE * BASE_SCALE),
y = baseY + (115 * BASE_SCALE),
scale = SCALE * BASE_SCALE,
shadow = true
)
// Accuracy
blitk(
matrixStack = context.pose(),
texture = movesAccuracyIconResource,
x = (baseX + (7 * BASE_SCALE)) / SCALE,
y = (baseY + (125.5 * BASE_SCALE)) / SCALE,
width = 10,
height = 10,
scale = SCALE
)
drawScaledText(
context = context,
text = lang("ui.accuracy"),
x = baseX + (14 * BASE_SCALE),
y = baseY + (126 * BASE_SCALE),
scale = SCALE * BASE_SCALE,
shadow = true
)
val accuracyText = if (move != null) formatPercent(move.accuracy) else "-"
val accuracyComponent = Component.literal(accuracyText)
drawScaledText(
context = context,
text = accuracyComponent,
x = (baseX + (62.5 * BASE_SCALE)) - (mcFont.width(accuracyComponent) * SCALE * BASE_SCALE),
y = baseY + (126 * BASE_SCALE),
scale = SCALE * BASE_SCALE,
shadow = true
)
// Category
blitk(
matrixStack = context.pose(),
texture = movesEffectIconResource,
x = (baseX + (7 * BASE_SCALE)) / SCALE,
y = (baseY + (136.5 * BASE_SCALE)) / SCALE,
width = 10,
height = 10,
scale = SCALE
)
drawScaledText(
context = context,
text = lang("ui.category"),
x = baseX + (14 * BASE_SCALE),
y = baseY + (137 * BASE_SCALE),
scale = SCALE * BASE_SCALE,
shadow = true
)
if (move != null) {
MoveCategoryIcon(
x = baseX + (51 * BASE_SCALE),
y = baseY + (137 * BASE_SCALE),
category = move.damageCategory
).render(context)
drawScaledText(
context = context,
text = move.damageCategory.displayName.copy(),
x = baseX + (61 * BASE_SCALE),
y = baseY + (137 * BASE_SCALE),
scale = SCALE * BASE_SCALE,
shadow = true
)
}
}
private fun renderDescription(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val move = selectedMove()
val nextKey = move?.name ?: ""
if (nextKey != lastDescriptionKey) {
descriptionList.setMoveDescription(move?.description ?: Component.literal(""))
lastDescriptionKey = nextKey
}
if (move != null) {
descriptionList.renderWidget(context, mouseX, mouseY, delta)
}
}
private fun formatPercent(input: Double): String {
if (input <= 0) return "-"
return "${decimalFormat.format(input)}%"
}
private fun toggleMovePicker(slotIndex: Int) {
selectedSlotIndex = slotIndex
if (movePickerOpen && pickerSlotIndex == slotIndex) {
closeMovePicker()
return
}
movePickerOpen = true
pickerSlotIndex = slotIndex
}
private fun closeMovePicker() {
movePickerOpen = false
pickerSlotIndex = -1
}
private fun assignMove(slotIndex: Int, move: MoveTemplate) {
draft.activeMoves.indices.forEach { index ->
if (index != slotIndex && draft.activeMoves[index] == move) {
draft.activeMoves[index] = null
}
}
draft.activeMoves[slotIndex] = move
}
private fun reorderMove(slotIndex: Int, up: Boolean) {
val size = draft.activeMoves.size
if (size == 0) return
val target = if (up) {
if (slotIndex > 0) slotIndex - 1 else size - 1
} else {
if (slotIndex < size - 1) slotIndex + 1 else 0
}
val temp = draft.activeMoves[target]
draft.activeMoves[target] = draft.activeMoves[slotIndex]
draft.activeMoves[slotIndex] = temp
if (selectedSlotIndex == slotIndex) selectedSlotIndex = target
else if (selectedSlotIndex == target) selectedSlotIndex = slotIndex
if (pickerSlotIndex == slotIndex) pickerSlotIndex = target
else if (pickerSlotIndex == target) pickerSlotIndex = slotIndex
}
private inner class MoveSlot(
pX: Int,
pY: Int,
private val slotIndex: Int
) : SoundlessWidget(pX, pY, moveWidthScaled, moveHeightScaled, Component.literal("move-slot-$slotIndex")) {
private val moveUpButton = ReorderMoveButton(x, y, true) {
reorderMove(slotIndex, true)
}.apply { addWidget(this) }
private val moveDownButton = ReorderMoveButton(x, y, false) {
reorderMove(slotIndex, false)
}.apply { addWidget(this) }
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val move = draft.activeMoves[slotIndex]
val matrices = context.pose()
isHovered = mouseX >= x && mouseY >= y && mouseX < x + width && mouseY < y + height
if (move != null) {
val rgb = move.elementalType.hue.toRGB()
if (selectedSlotIndex == slotIndex) {
blitk(
matrixStack = matrices,
texture = moveSelectedOverlayResource,
x = x - 1,
y = y - 1,
width = moveWidthScaled + 2,
height = moveHeightScaled + 2
)
}
blitk(
matrixStack = matrices,
texture = moveResource,
x = x,
y = y,
width = moveWidthScaled,
height = moveHeightScaled,
vOffset = if (isHovered) moveHeightScaled else 0,
textureHeight = moveHeightScaled * 2,
red = rgb.first,
green = rgb.second,
blue = rgb.third
)
blitk(
matrixStack = matrices,
texture = moveOverlayResource,
x = x,
y = y,
width = moveWidthScaled,
height = moveHeightScaled
)
val ppText = Component.literal("${move.pp}/${move.maxPp}").bold()
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = ppText,
x = x + (93 * BASE_SCALE),
y = y + (13 * BASE_SCALE),
centered = true,
scale = BASE_SCALE
)
TypeIcon(
x = x + (2 * BASE_SCALE),
y = y + (2 * BASE_SCALE),
type = move.elementalType
).render(context)
MoveCategoryIcon(
x = x + (66 * BASE_SCALE),
y = y + (13.5 * BASE_SCALE),
category = move.damageCategory
).render(context)
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = move.displayName.bold(),
x = x + (28 * BASE_SCALE),
y = y + (2 * BASE_SCALE),
shadow = true,
scale = BASE_SCALE
)
moveUpButton.render(context, mouseX, mouseY, delta)
moveDownButton.render(context, mouseX, mouseY, delta)
}
renderSwapButton(context, mouseX, mouseY, move)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isSwapHovered(mouseX, mouseY)) {
toggleMovePicker(slotIndex)
return true
}
if (isHovered) {
selectedSlotIndex = slotIndex
}
return super.mouseClicked(mouseX, mouseY, button)
}
private fun renderSwapButton(context: GuiGraphics, mouseX: Int, mouseY: Int, move: MoveTemplate?) {
val selected = movePickerOpen && pickerSlotIndex == slotIndex
val swapOffsetX = 114.5F * BASE_SCALE
val swapOffsetY = 6.5F * BASE_SCALE
val swapWidth = 12F * BASE_SCALE
val swapHeight = 18F * BASE_SCALE
blitk(
matrixStack = context.pose(),
texture = if (move == null) moveAddButtonResource else moveSwapButtonResource,
x = (x + swapOffsetX) / SCALE,
y = (y + swapOffsetY) / SCALE,
width = swapWidth,
height = swapHeight,
vOffset = if (isSwapHovered(mouseX.toDouble(), mouseY.toDouble()) || selected) swapHeight else 0F,
textureHeight = swapHeight * 2,
scale = SCALE
)
}
private fun isSwapHovered(mouseX: Double, mouseY: Double): Boolean {
val swapOffsetX = 114.5F * BASE_SCALE
val swapOffsetY = 6.5F * BASE_SCALE
val swapWidth = 12F * BASE_SCALE
val swapHeight = 18F * BASE_SCALE
val swapX = x + swapOffsetX
val swapY = y + swapOffsetY
return mouseX.toFloat() in (swapX..(swapX + (swapWidth * SCALE)))
&& mouseY.toFloat() in (swapY..(swapY + (swapHeight * SCALE) - 0.5F))
}
}
private inner class MovePickerList(
listX: Int,
listY: Int
) : SummaryScrollList<MovePickerList.MoveEntry>(
listX,
listY,
lang("ui.moves.switch"),
MOVE_LIST_SLOT_HEIGHT + MOVE_LIST_SLOT_SPACING
) {
private var entries: List<MoveTemplate> = emptyList()
fun setEntries(moves: List<MoveTemplate>) {
clearEntries()
entries = moves
moves.forEach { move -> addEntry(MoveEntry(move)) }
scrollAmount = 0.0
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, partialTicks: Float) {
if (!movePickerOpen) return
super.renderWidget(context, mouseX, mouseY, partialTicks)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (!movePickerOpen) return false
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
if (!movePickerOpen) return false
return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (!movePickerOpen) return false
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
inner class MoveEntry(private val move: MoveTemplate) : Entry<MoveEntry>() {
override fun getNarration(): MutableComponent = move.displayName.copy()
override fun render(
context: GuiGraphics,
index: Int,
rowTop: Int,
rowLeft: Int,
rowWidth: Int,
rowHeight: Int,
mouseX: Int,
mouseY: Int,
isHovered: Boolean,
partialTicks: Float
) {
val matrices = context.pose()
val tweakedRowTop = rowTop - (MOVE_LIST_SLOT_SPACING / 2) + 1
val rgb = move.elementalType.hue.toRGB()
blitk(
matrixStack = matrices,
texture = moveListResource,
x = rowLeft,
y = tweakedRowTop,
height = MOVE_LIST_SLOT_HEIGHT,
width = rowWidth,
vOffset = if (isHovered) MOVE_LIST_SLOT_HEIGHT else 0,
textureHeight = MOVE_LIST_SLOT_HEIGHT * 2,
red = rgb.first,
green = rgb.second,
blue = rgb.third
)
blitk(
matrixStack = matrices,
texture = moveListOverlayResource,
x = rowLeft,
y = tweakedRowTop,
height = MOVE_LIST_SLOT_HEIGHT,
width = rowWidth
)
TypeIcon(
x = rowLeft - 9,
y = tweakedRowTop,
type = move.elementalType
).render(context)
MoveCategoryIcon(
x = rowLeft + 77,
y = tweakedRowTop + 1.5,
category = move.damageCategory
).render(context)
drawScaledText(
context = context,
text = move.displayName,
x = rowLeft + 14,
y = tweakedRowTop + 3.5,
scale = SCALE,
shadow = true
)
blitk(
matrixStack = matrices,
texture = movesPowerIconResource,
x = (rowLeft + 10) / SCALE,
y = (tweakedRowTop + 11) / SCALE,
width = 10,
height = 10,
scale = SCALE
)
blitk(
matrixStack = matrices,
texture = movesAccuracyIconResource,
x = (rowLeft + 30) / SCALE,
y = (tweakedRowTop + 11) / SCALE,
width = 10,
height = 10,
scale = SCALE
)
blitk(
matrixStack = matrices,
texture = movesEffectIconResource,
x = (rowLeft + 53.5) / SCALE,
y = (tweakedRowTop + 11) / SCALE,
width = 10,
height = 10,
scale = SCALE
)
val movePower = if (move.power.toInt() > 0) move.power.toInt().toString() else "-"
drawScaledText(
context = context,
text = Component.literal(movePower),
x = rowLeft + 16.5,
y = tweakedRowTop + 12,
scale = SCALE,
shadow = true
)
drawScaledText(
context = context,
text = Component.literal(formatPercent(move.accuracy)),
x = rowLeft + 37,
y = tweakedRowTop + 12,
scale = SCALE,
shadow = true
)
drawScaledText(
context = context,
text = Component.literal(formatPercent(move.effectChances.firstOrNull() ?: 0.0)),
x = rowLeft + 60.5,
y = tweakedRowTop + 12,
scale = SCALE,
shadow = true
)
val pp = move.maxPp
drawScaledText(
context = context,
text = lang("ui.moves.pp", pp),
x = rowLeft + 76,
y = tweakedRowTop + 12,
scale = SCALE,
shadow = true
)
}
override fun mouseClicked(d: Double, e: Double, i: Int): Boolean {
if (isMouseOver(d, e) && pickerSlotIndex >= 0) {
assignMove(pickerSlotIndex, move)
selectedSlotIndex = pickerSlotIndex
closeMovePicker()
return true
}
return false
}
}
}
}
@@ -0,0 +1,212 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.client.gui.ScrollingWidget
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_BASE_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_SLOT_SIZE
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_SLOT_SPACING
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCALE
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.network.chat.Component
import net.minecraft.util.Mth
import org.joml.Quaternionf
import org.joml.Vector3f
class PokeEditSpeciesList(
val pX: Int,
val pY: Int,
val onSelect: (Species) -> Unit
) : ScrollingWidget<PokeEditSpeciesList.SpeciesRow>(
width = 139,
height = SCROLL_BASE_HEIGHT,
left = pX,
top = pY - SCROLL_BASE_HEIGHT,
slotHeight = SCROLL_SLOT_SIZE + 2
) {
private var entries: List<Species> = emptyList()
fun setEntries(species: List<Species>) {
clearEntries()
entries = species
species.chunked(5).forEach { chunk ->
addEntry(SpeciesRow(chunk, onSelect))
}
scrollAmount = 0.0
}
override fun addEntry(entry: SpeciesRow): Int = super.addEntry(entry)
override fun getScrollbarPosition(): Int = pX + width - 3
override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val xLeft = this.scrollbarPosition
val xRight = xLeft + 3
val barHeight = this.bottom - this.y
var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
yBottom = Mth.clamp(yBottom, 32, barHeight - 8)
var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + this.y
if (yTop < this.y) yTop = this.y
context.fill(xLeft, this.y + 3, xRight, this.bottom - 3, 0xFF3A96B6.toInt())
context.fill(xLeft, yTop + 3, xRight, yTop + yBottom - 3, 0xFFFCFCFC.toInt())
}
class SpeciesRow(
private val speciesList: List<Species>,
private val onSelect: (Species) -> Unit
) : Slot<SpeciesRow>() {
companion object {
private val slotResource = cobblemonResource("textures/gui/pokedex/pokedex_slot.png")
private val slotHighlight = cobblemonResource("textures/gui/pokedex/slot_select.png")
}
private val runtime = MoLangRuntime().setupClient().setup().also {
it.environment.query.addFunction("get_pokedex") { null }
}
private var x: Int = 0
private var y: Int = 0
override fun render(
context: GuiGraphics,
index: Int,
y: Int,
x: Int,
entryWidth: Int,
entryHeight: Int,
mouseX: Int,
mouseY: Int,
hovered: Boolean,
tickDelta: Float
) {
this.x = x
this.y = y
speciesList.forEachIndexed { indexInRow, species ->
val startPosX = x + ((SCROLL_SLOT_SPACING + SCROLL_SLOT_SIZE) * indexInRow)
val startPosY = y + SCROLL_SLOT_SPACING + 1
blitk(
matrixStack = context.pose(),
texture = slotResource,
x = startPosX,
y = startPosY,
width = SCROLL_SLOT_SIZE,
height = SCROLL_SLOT_SIZE
)
if (getHoveredSlotIndex(mouseX, mouseY) == indexInRow) {
blitk(
matrixStack = context.pose(),
texture = slotHighlight,
x = startPosX,
y = startPosY,
width = SCROLL_SLOT_SIZE,
height = SCROLL_SLOT_SIZE,
vOffset = SCROLL_SLOT_SIZE,
textureHeight = SCROLL_SLOT_SIZE * 2
)
}
val form = species.standardForm
val aspects = mutableSetOf<String>()
aspects.addAll(form.aspects)
if (form.maleRatio in 0F..1F) {
aspects.add(Gender.MALE.name.lowercase())
} else {
aspects.add(Gender.GENDERLESS.name.lowercase())
}
context.enableScissor(
startPosX + 1,
startPosY + 1,
startPosX + SCROLL_SLOT_SIZE - 1,
startPosY + SCROLL_SLOT_SIZE - 2
)
val state = FloatingState().also { it.currentAspects = aspects }
val matrices = context.pose()
matrices.pushPose()
matrices.translate(startPosX + (SCROLL_SLOT_SIZE / 2.0), startPosY + 1.0, 0.0)
matrices.scale(2.5F, 2.5F, 1F)
drawProfilePokemon(
renderablePokemon = RenderablePokemon(species, aspects),
matrixStack = matrices,
rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, 35F, 0F)),
state = state,
partialTicks = 0F,
scale = 4.5F
)
matrices.popPose()
context.disableScissor()
matrices.pushPose()
matrices.translate(0.0, 0.0, 100.0)
val pokemonNumber = species.nationalPokedexNumber.toString().padStart(4, '0').text()
drawScaledText(
context = context,
text = pokemonNumber,
x = startPosX + 1.5,
y = startPosY + 2.5,
shadow = true,
scale = SCALE
)
matrices.popPose()
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val hoverIndex = getHoveredSlotIndex(mouseX.toInt(), mouseY.toInt())
if (hoverIndex in speciesList.indices) {
onSelect(speciesList[hoverIndex])
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(com.cobblemon.mod.common.CobblemonSounds.POKEDEX_CLICK, 1.0F))
}
return true
}
private fun getHoveredSlotIndex(mouseX: Int, mouseY: Int): Int {
speciesList.forEachIndexed { index, _ ->
val startPosX = x + ((SCROLL_SLOT_SPACING + SCROLL_SLOT_SIZE) * index)
val startPosY = y + SCROLL_SLOT_SPACING + 1
if (mouseX in startPosX..(startPosX + SCROLL_SLOT_SIZE)
&& mouseY in startPosY..(startPosY + SCROLL_SLOT_SIZE)
) {
return index
}
}
return -1
}
override fun getNarration(): Component {
return if (speciesList.isNotEmpty()) {
"${speciesList.first().name}-${speciesList.last().name}".text()
} else {
"".text()
}
}
}
}
@@ -0,0 +1,445 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab.Companion.choiceAspectId
import com.cobblemon.mod.common.client.gui.summary.SummaryButton
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.item.HeldItemRenderer
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import com.cobblemon.mod.common.util.resolveFloat
import com.mojang.math.Axis
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.platform.Lighting
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.gui.components.EditBox
import net.minecraft.client.renderer.LightTexture
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.block.Blocks
import org.joml.Quaternionf
import org.joml.Vector3f
import java.util.Locale
class PokeEditWalkSpeedTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft
) : SoundlessWidget(pX, pY, HALF_OVERLAY_WIDTH, HALF_OVERLAY_HEIGHT, Component.literal("Walk Speed")) {
companion object {
private val backgroundOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay.png")
private val pokeBallOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_poke_ball.png")
private val platformBase = cobblemonResource("textures/gui/pokedex/platform_base.png")
private val platformShadow = cobblemonResource("textures/gui/pokedex/platform_shadow.png")
private val actionButtonResource = cobblemonResource("textures/gui/summary/summary_evolve_button.png")
private const val POKEBALL_OVERLAY_Y = 25
private const val PLATFORM_Y = 69
private const val SHADOW_X = 25
private const val SHADOW_Y = 76
private const val VIEW_SCALE = 1.2F
private const val VIEW_SHIFT_X = 0F
private const val VIEW_SHIFT_Y = 36F
private const val MODEL_VIEW_SCALE = 2.4F
private const val MODEL_DRAW_SCALE = 24F
private const val MODEL_OFFSET_Y = 20.0
private const val SPEED_INPUT_WIDTH = 40
private const val SPEED_INPUT_HEIGHT = 10
private const val SPEED_LABEL_SCALE = 0.5F
private const val BUTTON_WIDTH = 54
private const val BUTTON_HEIGHT = 15
private const val BLOCK_SPACING = 1.0F
private const val BLOCK_PLANE_OFFSET = 2.3
private const val BLOCK_COLUMNS = 7
private const val BLOCK_ROWS = 3
}
private val state = FloatingState().also { state ->
state.runtime.environment.query.addFunction("is_holding_item") {
val held = currentHeldStack()
DoubleValue(!held.isEmpty && !held.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) && !held.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
}
state.runtime.environment.query.addFunction("is_wearing_hat") {
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS))
}
state.runtime.environment.query.addFunction("is_wearing_face") {
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
}
}
private val heldItemRenderer = HeldItemRenderer()
private var renderablePokemon: RenderablePokemon? = null
private var rotationY = 30F
private var poseList: Array<PoseType> = arrayOf(PoseType.WALK, PoseType.PROFILE)
private var selectedPoseIndex: Int = 0
private var oldDragPosX = 0.0
private var canDragRender = false
private var isDraggingRender = false
private var cachedSpeedKey: String? = null
private var defaultWalkSpeed = 0.35F
private var customWalkSpeed: Float? = null
private var syncingSpeedInput = false
private val walkSpeedInput = EditBox(
Minecraft.getInstance().font,
pX + 6,
pY + 18,
SPEED_INPUT_WIDTH,
SPEED_INPUT_HEIGHT,
Component.literal("speed")
).also { editBox ->
editBox.setBordered(false)
editBox.setTextColor(0xFFFFFF)
editBox.setTextColorUneditable(0xFFFFFF)
editBox.setMaxLength(6)
editBox.active = true
editBox.visible = true
editBox.setFilter { value ->
value.isBlank() || value.matches(Regex("\\d*(\\.\\d*)?"))
}
editBox.setResponder { value ->
if (syncingSpeedInput) return@setResponder
customWalkSpeed = value.toFloatOrNull()
}
addWidget(editBox)
}
private val resetSpeedButton = SummaryButton(
buttonX = pX + 52F,
buttonY = pY + 14F,
buttonWidth = BUTTON_WIDTH,
buttonHeight = BUTTON_HEIGHT,
clickAction = { resetWalkSpeed() },
text = Component.literal("Reset"),
resource = actionButtonResource,
largeText = false,
boldText = false,
textScale = 0.7F
).also { addWidget(it) }
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
refreshDefaultWalkSpeed()
walkSpeedInput.active = true
walkSpeedInput.visible = true
if (!walkSpeedInput.isFocused && customWalkSpeed == null) {
val formatted = formatSpeed(defaultWalkSpeed)
if (walkSpeedInput.value != formatted) {
syncingSpeedInput = true
walkSpeedInput.value = formatted
syncingSpeedInput = false
}
}
val matrices = context.pose()
context.enableScissor(x, y, x + HALF_OVERLAY_WIDTH, y + HALF_OVERLAY_HEIGHT)
val viewWidth = HALF_OVERLAY_WIDTH * VIEW_SCALE
val viewHeight = HALF_OVERLAY_HEIGHT * VIEW_SCALE
val viewX = x + (HALF_OVERLAY_WIDTH - viewWidth) / 2F + VIEW_SHIFT_X
val viewY = y + (HALF_OVERLAY_HEIGHT - viewHeight) / 2F + VIEW_SHIFT_Y
val scaledBaseX = viewX / VIEW_SCALE
val scaledBaseY = viewY / VIEW_SCALE
blitk(
matrixStack = matrices,
texture = backgroundOverlay,
x = scaledBaseX,
y = scaledBaseY,
width = HALF_OVERLAY_WIDTH,
height = HALF_OVERLAY_HEIGHT,
scale = VIEW_SCALE
)
blitk(
matrixStack = matrices,
texture = pokeBallOverlay,
x = scaledBaseX + 15,
y = scaledBaseY + POKEBALL_OVERLAY_Y,
width = 109,
height = 68,
vOffset = 0,
textureHeight = 1744,
scale = VIEW_SCALE
)
blitk(
matrixStack = matrices,
texture = platformBase,
x = scaledBaseX + 13,
y = scaledBaseY + PLATFORM_Y,
width = 113,
height = 24,
textureHeight = 30,
scale = VIEW_SCALE
)
blitk(
matrixStack = matrices,
texture = platformShadow,
x = scaledBaseX + SHADOW_X,
y = scaledBaseY + SHADOW_Y,
width = 90,
height = 20,
scale = VIEW_SCALE
)
val fillTop = (viewY + 114F * VIEW_SCALE).toInt()
val fillBottom = (viewY + (HALF_OVERLAY_HEIGHT - 6) * VIEW_SCALE).toInt()
context.fill(viewX.toInt(), fillTop, (viewX + viewWidth).toInt(), fillBottom, 0xFF7AEEF1.toInt())
context.disableScissor()
walkSpeedInput.render(context, mouseX, mouseY, delta)
resetSpeedButton.render(context, mouseX, mouseY, delta)
drawScaledText(
context = context,
text = Component.literal("Walk Speed"),
x = x + 6,
y = y + 6,
scale = SPEED_LABEL_SCALE,
colour = 0xFFFFFFFF.toInt(),
shadow = false
)
buildRenderable()?.let { renderable ->
state.currentAspects = renderable.aspects
context.enableScissor(
x + 1,
y + 1,
x + HALF_OVERLAY_WIDTH - 1,
y + HALF_OVERLAY_HEIGHT - 1
)
matrices.pushPose()
matrices.translate(
x.toDouble() + (HALF_OVERLAY_WIDTH.toDouble()) / 2,
y.toDouble() + MODEL_OFFSET_Y,
1000.0
)
matrices.scale(MODEL_VIEW_SCALE, MODEL_VIEW_SCALE, MODEL_VIEW_SCALE)
renderWalkBlocks(context, renderable, effectiveWalkSpeed(), delta)
val rotationVector = Vector3f(13F, rotationY, 0F)
drawProfilePokemon(
renderablePokemon = renderable,
poseType = poseList[selectedPoseIndex],
matrixStack = matrices,
partialTicks = delta,
rotation = Quaternionf().fromEulerXYZDegrees(rotationVector),
state = state,
scale = MODEL_DRAW_SCALE
)
if (!renderable.heldItem.isEmpty) {
heldItemRenderer.renderOnModel(
renderable.heldItem,
state,
matrices,
context.bufferSource(),
light = 0xF000F0,
frontLight = true
)
}
matrices.popPose()
context.disableScissor()
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isWithinViewport(mouseX, mouseY)) {
canDragRender = true
isDraggingRender = true
oldDragPosX = mouseX
return true
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
if (canDragRender) {
canDragRender = false
}
if (isDraggingRender) {
isDraggingRender = false
}
return super.mouseReleased(pMouseX, pMouseY, pButton)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val handledDrag = isDraggingRender && canDragRender
if (handledDrag) {
val dragOffsetY = (oldDragPosX - mouseX).toFloat()
rotationY = (((rotationY + dragOffsetY) % 360 + 360) % 360)
}
oldDragPosX = mouseX
return handledDrag || super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
private fun resetWalkSpeed() {
customWalkSpeed = null
syncingSpeedInput = true
walkSpeedInput.value = formatSpeed(defaultWalkSpeed)
syncingSpeedInput = false
}
private fun refreshDefaultWalkSpeed() {
val key = "${draft.species.resourceIdentifier}#${draft.form.formOnlyShowdownId()}"
if (key == cachedSpeedKey) return
cachedSpeedKey = key
defaultWalkSpeed = resolveDefaultWalkSpeed()
if (customWalkSpeed == null) {
syncingSpeedInput = true
walkSpeedInput.value = formatSpeed(defaultWalkSpeed)
syncingSpeedInput = false
}
}
private fun resolveDefaultWalkSpeed(): Float {
return try {
state.runtime.resolveFloat(draft.form.behaviour.moving.walk.walkSpeed).coerceAtLeast(0.0F)
} catch (_: Exception) {
0.35F
}
}
private fun effectiveWalkSpeed(): Float {
val speed = customWalkSpeed ?: defaultWalkSpeed
return if (speed.isFinite() && speed > 0F) speed else defaultWalkSpeed
}
private fun formatSpeed(value: Float): String = String.format(Locale.US, "%.2f", value)
private fun renderWalkBlocks(context: GuiGraphics, renderable: RenderablePokemon, speed: Float, delta: Float) {
val model = VaryingModelRepository.getPoser(renderable.species.resourceIdentifier, state)
val ticks = (Minecraft.getInstance().level?.gameTime ?: (System.currentTimeMillis() / 50L)).toFloat() + delta
val offset = (ticks * speed) % BLOCK_SPACING
val travel = offset
val halfCols = BLOCK_COLUMNS / 2
val halfRows = BLOCK_ROWS / 2
val blockState = Blocks.GRASS_BLOCK.defaultBlockState()
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
val packedLight = LightTexture.pack(15, 15)
val hitboxHeight = draft.form.hitbox.height * draft.form.baseScale
val blockPlaneY = (-hitboxHeight * 0.5F + BLOCK_PLANE_OFFSET).toDouble()
val matrices = context.pose()
matrices.pushPose()
matrices.scale(MODEL_DRAW_SCALE, MODEL_DRAW_SCALE, -MODEL_DRAW_SCALE)
matrices.translate(
model.profileTranslation.x,
model.profileTranslation.y + 1.5 * model.profileScale,
model.profileTranslation.z - 4.0
)
matrices.scale(model.profileScale, model.profileScale, 1 / model.profileScale)
matrices.translate(0.0, blockPlaneY, 0.0)
val rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, rotationY, 0F))
matrices.mulPose(rotation)
RenderSystem.applyModelViewMatrix()
RenderSystem.enableDepthTest()
RenderSystem.disableCull()
Lighting.setupForEntityInInventory()
for (xIndex in -halfRows..halfRows) {
for (zIndex in -halfCols..halfCols) {
val z = zIndex * BLOCK_SPACING + travel
matrices.pushPose()
matrices.translate((xIndex * BLOCK_SPACING).toDouble(), 0.0, z.toDouble())
matrices.mulPose(Axis.XP.rotationDegrees(180F))
Minecraft.getInstance().blockRenderer.renderSingleBlock(
blockState,
matrices,
bufferSource,
packedLight,
OverlayTexture.NO_OVERLAY
)
matrices.popPose()
}
}
bufferSource.endBatch()
Lighting.setupFor3DItems()
RenderSystem.enableCull()
matrices.popPose()
}
private fun buildRenderable(): RenderablePokemon? {
val species = draft.species
val form = draft.form
val aspects = mutableSetOf<String>()
if (draft.shiny) aspects.add(SHINY_ASPECT.aspect)
aspects.addAll(form.aspects)
aspects.addAll(draft.forcedAspects)
aspects.addAll(draft.flagFeatures)
aspects.addAll(draft.cosmeticAspects)
draft.choiceFeatures.forEach { (key, choice) ->
choiceAspectId(key, choice)?.let { aspects.add(it) }
}
when (draft.gender) {
Gender.MALE -> aspects.add("male")
Gender.FEMALE -> aspects.add("female")
Gender.GENDERLESS -> aspects.add("genderless")
}
val heldStack = draft.heldItem
?.let { ResourceLocation.tryParse(it) }
?.let { id -> BuiltInRegistries.ITEM.getOptional(id).orElse(null) }
?.let { item -> ItemStack(item) }
?: ItemStack.EMPTY
renderablePokemon = RenderablePokemon(species, aspects, heldStack).also { recalculatePoses(it) }
return renderablePokemon
}
private fun recalculatePoses(renderablePokemon: RenderablePokemon) {
val poseState = FloatingState().also { it.currentAspects = renderablePokemon.aspects }
val poser = VaryingModelRepository.getPoser(renderablePokemon.species.resourceIdentifier, poseState)
poseList = poser.poses
.map { it.value.poseTypes.minBy { it.ordinal } }
.toSet()
.filterNot { it in PoseType.SHOULDER_POSES }
.sortedBy { it.ordinal }
.toTypedArray()
if (PoseType.WALK in poseList) {
selectedPoseIndex = poseList.indexOf(PoseType.WALK)
} else if (selectedPoseIndex !in poseList.indices) {
selectedPoseIndex = 0
}
}
private fun currentHeldStack(): ItemStack {
return renderablePokemon?.heldItem ?: ItemStack.EMPTY
}
private fun isWithinViewport(mouseX: Double, mouseY: Double): Boolean =
mouseX.toInt() in x..(x + HALF_OVERLAY_WIDTH)
&& mouseY.toInt() in y..(y + HALF_OVERLAY_HEIGHT)
&& children.none { child ->
child is AbstractWidget && child.visible && child.isMouseOver(mouseX, mouseY)
}
}
@@ -1,25 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.debug
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.render.debug.LineState
import com.cobblemon.mod.common.client.render.debug.SteerForceRenderer
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
import net.minecraft.client.Minecraft
object S2CUpdateObsAvoidHandler : ClientNetworkPacketHandler<ClientboundUpdateObsAvoidPacket> {
override fun handle(packet: ClientboundUpdateObsAvoidPacket, client: Minecraft) {
client.executeIfPossible {
//ObjectAvoidanceRenderer.avoidanceLines[packet.entityId] = packet.lines
val lineState = SteerForceRenderer.lineStates.getOrPut(packet.entityId) { LineState() }
lineState.update(packet.lines)
}
}
}
@@ -1,116 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.debug
import com.cobblemon.mod.common.api.net.Encodable
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.blaze3d.vertex.VertexConsumer
import net.minecraft.client.Camera
import net.minecraft.client.Minecraft
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.util.Mth
import net.minecraft.world.phys.Vec3
/**
* Data class currently used for storing line data for rendering in [SteerForceRenderer]
*
* @author Jackson
* @since September 18th, 2025
*/
data class EntityDebugLine(
val vector: Vec3, // Direction/magnitude in world space, relative to entity origin
val r: Float,
val g: Float,
val b: Float,
val a: Float,
) : Encodable {
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeVec3(vector)
buffer.writeFloat(r)
buffer.writeFloat(g)
buffer.writeFloat(b)
buffer.writeFloat(a)
}
companion object {
operator fun invoke(vector: Vec3, r: Float = 1f, g: Float = 1f, b: Float = 1f, a: Float = 1f): EntityDebugLine {
return EntityDebugLine(vector, r.coerceIn(0f,1f), g.coerceIn(0f,1f), b.coerceIn(0f,1f), a.coerceIn(0f,1f))
}
fun decode(buffer: RegistryFriendlyByteBuf): EntityDebugLine {
val vector = buffer.readVec3()
return EntityDebugLine(vector, buffer.readFloat(), buffer.readFloat(), buffer.readFloat(), buffer.readFloat())
}
}
}
class LineState {
/**
* The comparison of the previous and current lines relies on the order being consistent
*/
var previousLines: List<EntityDebugLine> = emptyList()
var currentLines: List<EntityDebugLine> = emptyList()
var lastReceivedLineTick: Long? = null
fun tick() {
previousLines = currentLines
}
fun update(newLines: List<EntityDebugLine>) {
lastReceivedLineTick = Minecraft.getInstance().level?.gameTime
currentLines = newLines
if (previousLines.isEmpty()) previousLines = newLines
}
// entityPos: interpolated world position of the entity this tick
fun draw(vc: VertexConsumer, pose: PoseStack.Pose, partialTick: Float, camera: Camera, entity: PokemonEntity, partialTicks: Float) {
previousLines.zip(currentLines).forEach { (prev, curr) ->
val iVec = prev.vector.lerp(curr.vector, partialTick.toDouble())
val iR = prev.r.lerp(curr.r, partialTick)
val iG = prev.g.lerp(curr.g, partialTick)
val iB = prev.b.lerp(curr.b, partialTick)
val iA = prev.a.lerp(curr.a, partialTick)
// Interpolated world position — same method used everywhere in the renderer
val worldStart = Vec3(
Mth.lerp(partialTicks.toDouble(), entity.xOld, entity.x),
Mth.lerp(partialTicks.toDouble(), entity.yOld, entity.y),
Mth.lerp(partialTicks.toDouble(), entity.zOld, entity.z)
)
val worldEnd = worldStart.add(iVec)
val relStart = worldStart.subtract(camera.position)
val relEnd = worldEnd.subtract(camera.position)
// Normal is the normalized direction of the segment
val dir = iVec.normalize()
val nx = dir.x.toFloat()
val ny = dir.y.toFloat()
val nz = dir.z.toFloat()
vc.addVertex(pose.pose(), relStart.x.toFloat(), relStart.y.toFloat(), relStart.z.toFloat())
.setColor(iR, iG, iB, iA)
.setNormal(pose, nx, ny, nz)
vc.addVertex(pose.pose(), relEnd.x.toFloat(), relEnd.y.toFloat(), relEnd.z.toFloat())
.setColor(iR, iG, iB, iA)
.setNormal(pose, nx, ny, nz)
}
}
fun reset() {
previousLines = emptyList()
currentLines = emptyList()
}
}
private fun Float.lerp(end: Float, t: Float): Float = this + (end - this) * t.coerceIn(0f, 1f)
@@ -1,58 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.debug
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.mojang.blaze3d.vertex.PoseStack
import net.minecraft.client.Camera
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.client.renderer.RenderType
object SteerForceRenderer {
// Lines to render attached to the uuid of entities.
val lineStates = mutableMapOf<Int, LineState>()
fun registerTickHandler() {
// Advance line States
PlatformEvents.CLIENT_TICK_PRE.subscribe {
lineStates.forEach { uuid, lineState ->
lineState.tick()
}
}
}
fun render(poseStack: PoseStack, multiBufferSource: MultiBufferSource, partialTicks: Float, camera: Camera) {
val matrix = poseStack.last().pose()
val level = Minecraft.getInstance().level ?: return
val currTick = level.gameTime
val ticksUntilStale = 3
val keysToRemove = lineStates.keys.filter { entityId ->
val entity = level.getEntity(entityId) as? PokemonEntity
val lastRecTick = lineStates[entityId]?.lastReceivedLineTick
entity == null || (lastRecTick != null && (currTick - lastRecTick) > ticksUntilStale)
}
keysToRemove.forEach { lineStates.remove(it) }
// One buffer for all independent line segments
val lineBuffer = multiBufferSource.getBuffer(RenderType.lines())
lineStates.forEach { entityId, lineState ->
val entity = level.getEntity(entityId) as? PokemonEntity ?: return@forEach
lineState.draw(lineBuffer, poseStack.last(), partialTicks, camera, entity, partialTicks)
}
}
}
@@ -8,7 +8,6 @@
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen1
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PitchTiltAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.createTransformation
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BiWingedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BipedFrame
@@ -199,7 +198,6 @@ class PidgeottoModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame,
transformTicks = 10,
quirks = arrayOf(blink, flyQuirk1, flyQuirk2),
animations = arrayOf(
PitchTiltAnimation(bone = getPart("body"), maxChangePerTick = 1F),
singleBoneLook(),
bedrock("pidgeotto", "air_fly")
)
@@ -146,12 +146,6 @@ class CobblemonConfig {
@CobblemonConfigField(Category.Battles, lang = "experience_share_multiplier", SERVER)
var experienceShareMultiplier = .5
@CobblemonConfigField(Category.Battles, lang = "award_experience_to_fainted_pokemon", SERVER)
var awardExperienceToFaintedPokemon = false
@CobblemonConfigField(Category.Battles, lang = "award_experience_on_battle_loss", SERVER)
var awardExperienceOnBattleLoss = false
@CobblemonConfigField(Category.Battles, lang = "lucky_egg_multiplier", SERVER)
var luckyEggMultiplier = 1.5
@@ -14,6 +14,9 @@ import com.cobblemon.mod.common.CobblemonBehaviours
import com.cobblemon.mod.common.CobblemonCallbacks
import com.cobblemon.mod.common.CobblemonCosmeticItems
import com.cobblemon.mod.common.CobblemonMechanics
import com.cobblemon.mod.common.CobblemonMovesetBuilders
import com.cobblemon.mod.common.CobblemonPartyCompositions
import com.cobblemon.mod.common.CobblemonPartyPools
import com.cobblemon.mod.common.CobblemonRideSettings
import com.cobblemon.mod.common.CobblemonUnlockableWallpapers
import com.cobblemon.mod.common.api.abilities.Abilities
@@ -78,12 +81,15 @@ object CobblemonDataProvider : DataProvider {
this.register(SpeciesFeatureAssignments, reloadable = false)
this.register(ActionEffects, reloadable = true)
this.register(Moves, reloadable = false)
this.register(CobblemonMovesetBuilders, reloadable = false)
this.register(Abilities, reloadable = false)
this.register(CobblemonBehaviours, reloadable = false)
this.register(PokemonSpecies, reloadable = false)
this.register(SpeciesAdditions, reloadable = false)
this.register(PokeBalls, reloadable = false)
this.register(PropertiesCompletionProvider, reloadable = false)
this.register(CobblemonPartyPools, reloadable = false)
this.register(CobblemonPartyCompositions, reloadable = false)
this.register(SpawnDetailPresets, reloadable = true)
this.register(CobblemonSpawnRules, reloadable = true)
this.register(CobblemonMechanics, reloadable = true)
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.entity.ai
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.entity.OmniPathingEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.ai.OmniPathNodeMaker
import com.cobblemon.mod.common.util.deleteNode
import com.cobblemon.mod.common.util.getWaterAndLavaIn
@@ -34,7 +35,6 @@ import net.minecraft.world.phys.Vec3
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.acos
import kotlin.math.max
/**
* A navigator designed to work with the [OmniPathNodeMaker], allowing a path that can cross land, water, and air
@@ -49,8 +49,6 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
var cachedCurrentNode: Node? = null
var currentNodeDistance = 0F
var segmentIndex: Int = 0
var pathFollowTarget: Vec3? = null
data class NavigationContext(
val onHit: (damage: DamageSource) -> Unit = {},
@@ -162,65 +160,6 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
}
doStuckDetection(vec3d)
// Gather the steer target from the segment pathing
// TODO: this shouldn't live here
val currentPath = path ?: return
if (!currentPath.isDone) {
segmentIndex = segmentIndex.coerceIn(0, max(currentPath.nodeCount - 2, 0))
pathFollowTarget = getPathSteerTarget(currentPath, mob)
}
}
/**
* Gets a steering target for the current path
* This video explains it well: https://www.youtube.com/watch?v=rlZYT-uvmGQ
* Uses Reynolds path following for velocity steered entities
*/
fun getPathSteerTarget(
path: Path,
mob: Mob
): Vec3 {
// If this is the final segment then just seek towards the end
if (segmentIndex >= path.nodeCount - 1) return path.getNode(path.nodeCount - 1).asVec3().add(0.5, 0.0, 0.5)
// Determine future position to use in steering calculations
val lookAheadDist = 1.0 //TODO: Scale to look ahead a certain amount of ticks probably
val futurePos = mob.position().add(mob.deltaMovement.normalize().scale(lookAheadDist))
// Gather the current segment points
var pointS1 = path.getNode(segmentIndex).asVec3().add(0.5, 0.0, 0.5) // Add the 0.5's to get the blockface center as blockpos is on a corner
var pointS2 = path.getNode(segmentIndex + 1).asVec3().add(0.5, 0.0, 0.5)
// Project the future point onto the current segment
var vecA = futurePos.subtract(pointS1)
var vecB = pointS2.subtract(pointS1)
var normB = vecB.normalize()
val lenB = vecB.length()
var projAB = max(vecA.dot(normB), 0.0) // make at least 0 as negative means we are projecting behind the segment which we don't want
// If the projection goes off the front of the segment then advance to the next segment and recalculate the projection
if (projAB > lenB && segmentIndex < path.nodeCount - 2) {
segmentIndex++
pointS1 = path.getNode(segmentIndex).asVec3().add(0.5, 0.0, 0.5)
pointS2 = path.getNode(segmentIndex + 1).asVec3().add(0.5, 0.0, 0.5)
vecA = futurePos.subtract(pointS1)
vecB = pointS2.subtract(pointS1)
normB = vecB.normalize()
projAB = max(vecA.dot(normB), 0.0)
}
// Get the projected point on the segment
val projPoint = pointS1.add(normB.scale(projAB ))
// Determine if the future position is too far from the path by getting its distance from its projected position on the current path segment
val futureDistFromPath = projPoint.distanceTo(futurePos)
val acceptableDist = mob.bbWidth / 3.0 //TODO: figure out what this really needs to be
val pushForward = 0.1 // Advance the projected point along the segment to avoid going backwards
val seekPoint = if (futureDistFromPath < acceptableDist) futurePos.add(normB.scale(pushForward)) else projPoint.add(normB.scale(pushForward))
return seekPoint
}
fun isAirborne(world: Level, pos: BlockPos) =
@@ -284,8 +223,6 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
fun moveTo(x: Double, y: Double, z: Double, speed: Double = 1.0, navigationContext: NavigationContext) {
this.navigationContext = navigationContext
segmentIndex = 0
pathFollowTarget = null
this.moveTo(x, y, z, speed)
}
@@ -340,8 +277,6 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
}
override fun moveTo(path: Path?, speed: Double): Boolean {
segmentIndex = 0
pathFollowTarget = null
if (path != null && path.nodeCount > 0) {
val node = path.getNode(0)!!
// pokemonEntity.discard()
@@ -464,8 +399,6 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
this.cachedCurrentNode = null
path = null
nodeEvaluator.done()
segmentIndex = 0
pathFollowTarget = null
// In case a path is cancelled instead of completed, check if we should stop flying
if (pather.couldStopFlying() && !isAirborne(entity.level(), entity.blockPosition())) {
pather.setFlying(false)
@@ -1,77 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.entity.ai.steering
import com.cobblemon.mod.common.CobblemonNetwork.sendPacketToTrackingPlayers
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
import net.minecraft.world.phys.Vec3
// Runs a list of SteerRules in priority order, consuming a force budget.
// High priority rules get their full contribution first. Lower priority
// rules only get what budget remains.
class SteerArbitrator(
val entity: PokemonEntity,
val maxForce: Double,
val rules: MutableList<SteerRule> = mutableListOf()
) {
private var sortedRules: List<SteerRule> = emptyList()
private var dirty = true
fun addRule(rule: SteerRule) { rules.add(rule); dirty = true }
fun removeRule(rule: SteerRule) { rules.remove(rule); dirty = true }
fun clearRules() { rules.clear(); dirty = true }
fun calculate(debug: Boolean = false): Vec3 {
if (dirty) {
sortedRules = rules.sortedByDescending { it.priority }
dirty = false
}
var remaining = maxForce
var result = Vec3.ZERO
val debugLines = if (debug) mutableListOf<EntityDebugLine>() else null
for (rule in sortedRules) {
if (remaining <= 0.0) break
val force = rule.calculate() ?: continue
val scaled = force.scale(rule.weight.toDouble())
val len = scaled.length()
if (len <= 0.0) continue
val contribution = if (len <= remaining) {
remaining -= len
scaled
} else {
val partial = remaining
remaining = 0.0
scaled.normalize().scale(partial)
}
result = result.add(contribution)
debugLines?.let {
val (r, g, b) = rule.debugColor
it.add(EntityDebugLine(contribution.scale(50.0), r, g, b, 1.0f))
}
}
debugLines?.let {
if (result.lengthSqr() > 0.0)
it.add(EntityDebugLine(result.scale(50.0), 1f, 1f, 1f, 1.5f))
if (it.isNotEmpty())
sendPacketToTrackingPlayers(entity, ClientboundUpdateObsAvoidPacket(entity.id, it))
}
return result
}
}
@@ -1,34 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.entity.ai.steering
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import net.minecraft.world.phys.Vec3
// A per-tick steering force contributor evaluated by SteerArbitrator.
// Return null from calculate() if the rule has nothing to say this tick.
interface SteerRule {
val entity: PokemonEntity
// Higher = evaluated first, consumes force budget before lower priority rules.
// 90-100: obstacle avoidance
// 60-89: threat / boundary
// 30-59: social (separation, alignment, cohesion)
// 0-29: goal / wander
val priority: Int
// Multiplied against the rule output before budget accounting.
val weight: Float
// RGB 0-1 for the debug line drawn by SteerArbitrator.
val debugColor: Triple<Float, Float, Float> get() = Triple(1f, 1f, 1f)
fun calculate(): Vec3?
}
@@ -1,76 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.entity.ai.steering.rules
import com.cobblemon.mod.common.entity.ai.steering.SteerRule
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.entity.pokemon.extensions.steerToTarget
import net.minecraft.world.phys.Vec3
import kotlin.math.max
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.math.cos
// Forward AABB projection obstacle avoidance. Stateful — one instance per entity.
class ObstacleAvoidRule(
override val entity: PokemonEntity,
override val priority: Int = 100,
override val weight: Float = 1.0f,
override val debugColor: Triple<Float, Float, Float> = Triple(1f, 0f, 0f),
val maxForce: Double = 0.02,
val lookAheadTicks: Int = 10,
val smoothingAlpha: Double = 0.25
) : SteerRule {
private var prevForce: Vec3 = Vec3.ZERO
override fun calculate(): Vec3? {
val topSpeed = (entity.moveControl as? PokemonMoveControl)?.steerSpeed ?: 0.1
val forwardVec = if (entity.deltaMovement.horizontalDistanceSqr() < 0.001) {
val yaw = Math.toRadians(entity.yRot.toDouble())
Vec3(-sin(yaw), 0.0, cos(yaw))
} else {
Vec3(entity.deltaMovement.x, 0.0, entity.deltaMovement.z).normalize()
}
val lookAheadDist = max(entity.deltaMovement.length() * lookAheadTicks, entity.bbWidth / 2.0)
val collBox = entity.boundingBox.move(forwardVec.scale(lookAheadDist))
val shapes = entity.level().getBlockCollisions(entity, collBox)
val nearestShape = shapes.minByOrNull { shape ->
shape.bounds().center.distanceToSqr(entity.position())
}
if (nearestShape == null) {
prevForce = prevForce.lerp(Vec3.ZERO, smoothingAlpha)
return if (prevForce.lengthSqr() < 1e-6) null else prevForce
}
val obsCenter = nearestShape.bounds().center
val toObs = obsCenter.subtract(entity.position())
val projVec = forwardVec.scale(toObs.dot(forwardVec))
val lateralComponent = toObs.subtract(projVec)
val avoidForce = if (lateralComponent.lengthSqr() < 0.001) {
// Dead ahead — turn right
val rightVec = forwardVec.cross(Vec3(0.0, 1.0, 0.0))
entity.steerToTarget(entity.position().add(rightVec), topSpeed, maxForce, horzOnly = true)
} else {
var avoidanceVec = Vec3(lateralComponent.x * -1.0, 0.0, lateralComponent.z * -1.0)
val avoidStrength = sqrt(1.0 - (avoidanceVec.length() / (entity.bbWidth / 2.0 + 0.5)).coerceIn(0.0, 0.7))
val avoidancePoint = entity.position().add(avoidanceVec)
entity.steerToTarget(avoidancePoint, topSpeed, maxForce * avoidStrength, horzOnly = true)
}
prevForce = prevForce.lerp(avoidForce, smoothingAlpha)
return prevForce
}
}
@@ -1,35 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.entity.ai.steering.rules
import com.cobblemon.mod.common.entity.ai.steering.SteerRule
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.entity.pokemon.extensions.steerToTarget
import net.minecraft.world.phys.Vec3
// Steers toward a target position. Reads speed from PokemonMoveControl.steerSpeed.
// Return null from targetProvider to deactivate for a tick.
class SeekRule(
override val entity: PokemonEntity,
override val priority: Int = 10,
override val weight: Float = 1.0f,
override val debugColor: Triple<Float, Float, Float> = Triple(0f, 1f, 1f),
val maxForce: Double,
val horzOnly: Boolean = false,
val arrivalRange: Double? = null,
val targetProvider: (PokemonEntity) -> Vec3?
) : SteerRule {
override fun calculate(): Vec3? {
val target = targetProvider(entity) ?: return null
val speed = (entity.moveControl as? PokemonMoveControl)?.steerSpeed ?: 0.1
return entity.steerToTarget(target, speed, maxForce, arrivalRange, horzOnly)
}
}
@@ -1,44 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.entity.ai.steering.rules
import com.cobblemon.mod.common.entity.ai.steering.SteerRule
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.entity.pokemon.extensions.steerToTarget
import net.minecraft.world.phys.Vec3
import kotlin.math.sin
// Steers toward a point directly ahead of the entity's current velocity/yaw.
// Useful as a baseline test rule.
class WalkForwardRule(
override val entity: PokemonEntity,
override val priority: Int = 10,
override val weight: Float = 1.0f,
override val debugColor: Triple<Float, Float, Float> = Triple(0f, 1f, 0f),
val maxForce: Double = 0.02
) : SteerRule {
override fun calculate(): Vec3? {
val speed = (entity.moveControl as? PokemonMoveControl)?.steerSpeed ?: 0.1
val forwardVec = if (entity.deltaMovement.horizontalDistanceSqr() < 0.001) {
val yaw = Math.toRadians(entity.yRot.toDouble())
Vec3(-sin(yaw), 0.0, cos(yaw))
} else {
Vec3(entity.deltaMovement.x, 0.0, entity.deltaMovement.z).normalize()
}
val pointAhead = entity.position().add(forwardVec)
return entity.steerToTarget(pointAhead, speed, maxForce, horzOnly = true)
}
}
private fun cos(d: Double) = kotlin.math.cos(d)
private fun sin(d: Double) = kotlin.math.sin(d)
@@ -1,71 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.entity.ai.steering.rules
import com.cobblemon.mod.common.entity.ai.steering.SteerRule
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.entity.pokemon.extensions.steerToTarget
import net.minecraft.util.Mth
import net.minecraft.world.phys.Vec3
import kotlin.math.abs
import kotlin.math.sin
import kotlin.random.Random
// Reynolds wander — projects a sphere ahead and offsets a point on it each tick.
// Stateful: holds current wander angles, one instance per entity.
// https://slsdo.github.io/steering-behaviors/
class WanderRule(
override val entity: PokemonEntity,
override val priority: Int = 10,
override val weight: Float = 1.0f,
override val debugColor: Triple<Float, Float, Float> = Triple(1f, 0.5f, 0f),
val maxForce: Double = 0.02,
val wanderDisp: Double = 15.0,
val centerDistance: Double = 4.0,
val wanderRadius: Double = 4.0,
val horzOnly: Boolean = false //TODO: all of these should be checks if flying or walking
) : SteerRule {
var currHorzWander = 0.0
var currVertWander = 0.0
override fun calculate(): Vec3? {
val speed = (entity.moveControl as? PokemonMoveControl)?.steerSpeed ?: 0.1
val normVec = if (entity.deltaMovement.lengthSqr() > 0.0001) {
entity.lookAngle.normalize()
} else {
Vec3(
Random.nextDouble(-1.0, 1.0),
Random.nextDouble(-1.0, 1.0),
Random.nextDouble(-1.0, 1.0)
).normalize()
}
var wanderOff = normVec
// Clamp vertical wander when getting too steep
val currPitchS = sin(Mth.atan2(wanderOff.y, wanderOff.horizontalDistance()))
if (currPitchS >= 0.5f || currPitchS <= -0.5f) currVertWander = 0.0
wanderOff = wanderOff.xRot(Math.toRadians(currVertWander).toFloat())
wanderOff = wanderOff.yRot(Math.toRadians(currHorzWander).toFloat())
val wanderCenter = entity.position().add(normVec.scale(centerDistance))
val wanderPoint = wanderCenter.add(wanderOff.scale(wanderRadius))
// Advance wander angles
currVertWander = Mth.wrapDegrees(currVertWander + Random.nextDouble(-wanderDisp, wanderDisp))
currHorzWander = Mth.wrapDegrees(currHorzWander + Random.nextDouble(-wanderDisp, wanderDisp))
val force = entity.steerToTarget(wanderPoint, speed, maxForce)
return if (horzOnly) Vec3(force.x, 0.0, force.z) else force
}
}
@@ -191,7 +191,7 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
}
}
} else {
setNoGravity(false)
isNoGravity = false
deltaMovement = Vec3.ZERO
}
}
@@ -175,7 +175,10 @@ import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import java.util.*
import java.util.concurrent.CompletableFuture
import kotlin.math.*
import kotlin.math.PI
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
@Suppress("unused")
open class PokemonEntity(
@@ -211,7 +214,6 @@ open class PokemonEntity(
@JvmStatic var RIDE_BOOSTS = SynchedEntityData.defineId(PokemonEntity::class.java, RideBoostsDataSerializer)
@JvmStatic var RIDE_STAMINA = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.FLOAT)
@JvmStatic var SCALE_MODIFIER = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.FLOAT)
@JvmStatic val IS_SPRINTING = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.BOOLEAN)
const val BATTLE_LOCK = "battle"
const val EVOLUTION_LOCK = "evolving"
@@ -291,9 +293,6 @@ open class PokemonEntity(
var shownItem: ItemStack
get() = entityData.get(SHOWN_HELD_ITEM)
set(value) = entityData.set(SHOWN_HELD_ITEM, value)
var isPokemonSprinting: Boolean
get() = entityData.get(IS_SPRINTING)
set(value) { entityData.set(IS_SPRINTING, value) }
var lastLightningBoltUUID: UUID? = null
@@ -462,7 +461,6 @@ open class PokemonEntity(
builder.define(RIDE_BOOSTS, emptyMap())
builder.define(RIDE_STAMINA, 1F)
builder.define(SCALE_MODIFIER, 1F)
builder.define(IS_SPRINTING, false)
}
override fun onSyncedDataUpdated(data: EntityDataAccessor<*>) {
@@ -625,12 +623,8 @@ open class PokemonEntity(
isPokemonFlying = flyDist - flyDistO > 0.005F
isPokemonWalking = walkDist - walkDistO > 0.005F
// TODO: Figure out a better way to allow for this. Or maybe just always do the animations data accumulation its really not that bad
if (this.pokemon.species.name.lowercase() == "charizard" || (passengers.isNotEmpty() && level().isClientSide)) {
// This is so ugly why am I doing it like this... even if its for a test this is low
if (passengers.isNotEmpty() && level().isClientSide) {
rideSoundManager.tick()
}
if (passengers.isNotEmpty() && level().isClientSide) {
rideSoundManager.tick()
ridingAnimationData.update()
} else if (!passengers.isNotEmpty() && level().isClientSide) {
rideSoundManager.stop()
@@ -2157,9 +2151,7 @@ open class PokemonEntity(
override fun stopSeenByPlayer(player: ServerPlayer) {
if (this.ownerUUID == player.uuid && tethering == null) {
// queuedToDespawn = true
if (this.removalReason != RemovalReason.DISCARDED) {
this.remove(RemovalReason.DISCARDED)
}
this.remove(RemovalReason.DISCARDED)
return
}
}
@@ -1,547 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.entity.pokemon.ai
import com.cobblemon.mod.common.CobblemonActivities
import com.cobblemon.mod.common.CobblemonNetwork.sendPacketToTrackingPlayers
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.extensions.*
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
import com.cobblemon.mod.common.util.math.geometry.toRadians
import com.cobblemon.mod.common.util.resolveDouble
import net.minecraft.util.Mth
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.control.MoveControl
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.level.ClipContext
import net.minecraft.world.phys.HitResult
import net.minecraft.world.phys.Vec3
import kotlin.math.*
import kotlin.random.Random
class BoidsMoveControl(val pokemonEntity: PokemonEntity, val target: LivingEntity) : MoveControl(pokemonEntity) {
companion object {
const val VERY_CLOSE = 2.500000277905201E-3
const val MAX_SPEED = 0.3 / 2
const val MAX_FORCE = 0.02
const val PREDATOR_MAX_SPEED = 0.6
// const val MAX_SPEED = 0.2
// const val MAX_FORCE = 0.013
}
// TODO: might need to hold its own velocity so minecraft doesn't do its nonsense to it?
// Can handle collisions ourselves as well
var speed: Double? = null
var drawLines: MutableList<EntityDebugLine> = mutableListOf()
// wander vars
var currHorzWander = 0.0
var currVertWander = 0.0
val wanderDisp = 15.0 // The degrees of change from tick to tick in wander angle
override fun tick() {
// Clear since the info is sent and is now stale
drawLines.clear()
// Steering constants
val orbitRadius = 16.0
val targetPos = target.position().add(0.0, 4.0, 0.0)
val arrivalRange = 2.0 // in blocks
val species = pokemonEntity.pokemon.species.name
// Set this to ensure the flying pose is active
// ignore all the friction stuff from outside
speed?.let { pokemonEntity.setDeltaMovement(pokemonEntity.deltaMovement.normalize().scale(it)) }
// Constrain to rectangular boundary
val cubeWidth = 16.0
val cubeHeight = 16.0
val cubeCenter = target.position().add(0.0, cubeHeight / 2.0, 0.0)
//forceRectBoundary(cubeCenter, cubeWidth, cubeHeight)
// Apply boid forces
// Obstacle Avoidance
val whiskerRange = 1.0
val avoidanceAngle = 20.0 // x degrees to the left, right, up, and down
//val obsAvoidForce = whisObsAvoid(whiskerRange, avoidanceAngle)
// if (obsAvoidForce != null) {
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(obsAvoidForce.scale(0.5))
// }
// TODO: obviously don't hardcode each pokemon. Remove this after testing is finished.
if (pokemonEntity.pokemon.species.name.lowercase() == "charizard") {
pokemonEntity.setFlying(true)
pokemonEntity.resetFallDistance()
seekPrey(pokemonEntity)
} else if (pokemonEntity.pokemon.species.name.lowercase() == "garchomp") {
pokemonEntity.setFlying(false)
val walkSpeed = pokemonEntity.runtime.resolveDouble(pokemonEntity.behaviour.moving.walk.walkSpeed) * 1.5
val steerForce = pokemonEntity.steerToTarget(target.position(), walkSpeed, walkSpeed / 12)
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(steerForce.x, 0.0, steerForce.z)
// speed?.let { this.mob.setZza(speed!!.toFloat()) }
} else if (pokemonEntity.pokemon.species.name.lowercase() == "skiddo") {
pokemonEntity.setFlying(false)
//wander(pokemonEntity,1.0)
// val steerForce = pokemonEntity.steerToTargetForce(target.position(), MAX_SPEED, MAX_FORCE)
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(steerForce.x, 0.0, steerForce.z)
aviodPredators(pokemonEntity, includePlayer = true)
boid(isFlyingOrSwimming = false)
} else {
pokemonEntity.setFlying(true)
pokemonEntity.resetFallDistance()
wander(pokemonEntity,0.8)
boid()
aviodPredators(pokemonEntity, includePlayer = true)
// Force towards ground
forceRectBoundary(pokemonEntity, cubeCenter, cubeWidth, cubeHeight)
}
// val avoidForce = avoidTarget(target.position(), MAX_SPEED, MAX_FORCE, 10.0)
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(avoidForce.scale(1.0))
// Apply steering force to current movement
// val dm = pokemonEntity.deltaMovement
// val w_pos = pokemonEntity.position().add(Vec3(dm.x, 0.0, dm.z))
// val steerForce = steerToTargetForce(w_pos, MAX_SPEED, MAX_FORCE)
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(steerForce)
// Take note of speed again to avoid minecraft's friction logic
speed = pokemonEntity.deltaMovement.length() * 0.98
// Test out what this setting of speed does.
//speed?.let { this.mob.speed = speed!!.toFloat() }
// Limit upwards speed to be half of max speed. The boids kept bumping into the top of the box more so than birds
// would. Honestly birds probably mostly want to move in the x,z plane anyhow
// pokemonEntity.deltaMovement = Vec3(
// pokemonEntity.deltaMovement.x,
// min(pokemonEntity.deltaMovement.y, MAX_SPEED * 0.5),
// pokemonEntity.deltaMovement.y
// ).normalize().scale(speed!!)
pokemonEntity.orientEntityToTravel(0.4f)
if (false && drawLines.isNotEmpty()) { //TODO: Re-enable
sendPacketToTrackingPlayers(pokemonEntity, ClientboundUpdateObsAvoidPacket(pokemonEntity.id, drawLines))
}
}
private fun aviodPredators(entity: PokemonEntity, includePlayer: Boolean = false) {
val fearRadius = 5.0
val brain = entity.brain
val predators = brain.getMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES).map { nearbyEntities ->
nearbyEntities.filterIsInstance<PokemonEntity>().filter { pokemon ->
pokemon.brain.activeNonCoreActivity.orElse(null) == CobblemonActivities.POKEMON_BOIDS &&
pokemon.pokemon.species.name.lowercase() == "charizard" &&
entity.position().subtract(pokemon.position()).length() <= fearRadius
}
}.orElse(emptyList())
var totalEvasionVec = Vec3.ZERO
var predatorCount = predators.size
// If the player should be included in avoidance then add the player as a predator.
if (includePlayer) {
totalEvasionVec = totalEvasionVec.add(entity.avoidTarget(target.position(), MAX_SPEED * 1.2, MAX_FORCE * 2, fearRadius))
predatorCount += 1
}
// Average evasion forces for all predators in the area and apply it to the prey entity
if (predatorCount > 0) {
for (predator in predators) {
totalEvasionVec = totalEvasionVec.add(entity.avoidTarget(predator.position(), MAX_SPEED * 1.2, MAX_FORCE * 2, fearRadius))
}
totalEvasionVec = totalEvasionVec.scale(1.0 / predatorCount)
if (entity.isFlying() || entity.isSwimming) {
entity.deltaMovement = entity.deltaMovement.add(totalEvasionVec)
} else {
entity.deltaMovement = entity.deltaMovement.add(Vec3(totalEvasionVec.x, 0.0, totalEvasionVec.z))
}
}
}
/*************************************************************************************************************
Seek the closest prey and pursue
*************************************************************************************************************/
private fun seekPrey(predator: PokemonEntity) {
val SeekRadius = 20.0
val brain = predator.brain
// Do calcs for head height as it looks better this way than a charizard flying crotch first at a group of pidgeys
val kindaHeadHeight = predator.position()
val prey = brain.getMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES).map { nearbyEntities ->
nearbyEntities.filterIsInstance<PokemonEntity>().filter { pokemon ->
pokemon.brain.activeNonCoreActivity.orElse(null) == CobblemonActivities.POKEMON_BOIDS &&
(pokemon.pokemon.species.name.lowercase() == "pidgey" || pokemon.pokemon.species.name.lowercase() == "skiddo") &&
kindaHeadHeight.subtract(pokemon.position()).length() <= SeekRadius
}
}.orElse(emptyList())
// Find the closest prey and seek towards it
val nearestPrey = prey.minByOrNull { pokePrey ->
kindaHeadHeight.subtract(pokePrey.position()).length()
}
nearestPrey?.let { prey ->
// Do the actual attack
// TODO: make this not dumb
if(kindaHeadHeight.subtract(nearestPrey.position()).length() < 0.5 + predator.bbWidth + nearestPrey.bbWidth) {
//predator.doHurtTarget(nearestPrey)
}
// Steer towards the target
val steerForce = predator.steerToTarget(prey.position(), PREDATOR_MAX_SPEED, MAX_FORCE)
predator.deltaMovement = predator.deltaMovement.add(steerForce)
}
}
/*************************************************************************************************************
Whisker Obstacle Avoidance
This function ray casts in a cone in front of the entity
*************************************************************************************************************/
private fun whisObsAvoid(range: Double, lookWidth: Double): Vec3? {
// --- 1. Build the Orientation Matrix ---
val forward = pokemonEntity.lookAngle.scale(range)
// val tempUp = if (abs(forward.y) > 0.999) Vec3(0.0, 0.0, 1.0) else Vec3(0.0, 1.0, 0.0)
// val right = forward.cross(tempUp).normalize()
// val up = right.cross(forward).normalize()
//
// val orientation = Matrix3f()
// .setColumn(0, Vector3f(right.x.toFloat(), right.y.toFloat(), right.z.toFloat()))
// .setColumn(1, Vector3f(up.x.toFloat(), up.y.toFloat(), up.z.toFloat()))
// .setColumn(2, Vector3f(forward.x.toFloat(), forward.y.toFloat(), forward.z.toFloat()))
// --- 2. Define Whiskers in Simple, Local Space ---
val radLookWidth = lookWidth.toRadians().toFloat()
// val localWhiskers: List<Vector3f> = listOf(
// Vector3f(0f, 0f, 1f), // Forward
//// Vector3f(sin(radLookWidth), 0f, cos(radLookWidth)), // Left
//// Vector3f(-sin(radLookWidth), 0f, cos(radLookWidth)), // Right
//// Vector3f(0f, sin(radLookWidth), cos(radLookWidth)), // Up
//// Vector3f(0f, -sin(radLookWidth), cos(radLookWidth)) // Down
// )
//
// // --- 3. Transform Local Whiskers to World Space using the Matrix ---
// val whiskerOffsets = localWhiskers.map { localVec ->
// orientation.transform(localVec, Vector3f()).toVec3d().scale(range) // Transform and store in a new vector
// }
// Obtain all points to be raycasted to
val scaledLookVec = pokemonEntity.deltaMovement.normalize().scale(range)
val lookPoints: List<Vec3> = listOf(
forward,
// forward.yRot(radLookWidth),
// forward.yRot(-radLookWidth),
// forward.xRot(90.toRadians()),
// forward.xRot((-90).toRadians()),
)
// iterate through each point and raycast to it starting from the entities pos (may need to be from somewhere else)
// If a position is hit then add its avoidance force to the totalAvoidance forces.
var totalAvoidance: Vec3? = null
val level = pokemonEntity.level()
val startPos = pokemonEntity.position()
for (point in lookPoints) {
val endPos = startPos.add(point)
val hit = level.clip(
ClipContext(
startPos,
endPos,
ClipContext.Block.OUTLINE, // If this is being weird then go back to COLLIDER
ClipContext.Fluid.ANY, // Birds should probably avoid fluid
pokemonEntity
)
)
// Add the whisker to the debug lines to be drawn
drawLines.add(EntityDebugLine(endPos, 1.0f, 0.0f, 0.0f))
//visualizeRay(level, startPos, endPos, hit.type != HitResult.Type.MISS)
if (hit.type != HitResult.Type.MISS) {
// Get the aviodance vector as the vector from the block detection point
// to the pokemon.
// Also inversely scale the size of the vector based on the distance the pokemon
// is to the object. This means close obstacles are avoided more strongly
val hitDist = hit.location.distanceTo(pokemonEntity.eyePosition)
val avoidVec = startPos.subtract(endPos).normalize() //.scale(1.0 - (hitDist / range))
// Add avoid vec to total
totalAvoidance = totalAvoidance?.add(avoidVec) ?: avoidVec
}
}
// // Scale all forces down to the max foce
// if (totalAvoidance != null) {
// // Cap the max total avoidance desire
// if (totalAvoidance.length() > MAX_SPEED) totalAvoidance = totalAvoidance.normalize().scale(MAX_SPEED)
// // Convert to Reynolds steering force
// totalAvoidance = totalAvoidance.subtract(pokemonEntity.deltaMovement)
// // Cap the max total avoidance force
// if (totalAvoidance.length() > MAX_FORCE) totalAvoidance = totalAvoidance.normalize().scale(MAX_FORCE)
// } // Is this next one even cleaner?
return totalAvoidance?.let { desiredVelocity ->
// Cap the desired velocity (using lengthSqr is more efficient than length)
val cappedVelocity = if (desiredVelocity.lengthSqr() > MAX_SPEED * MAX_SPEED) {
desiredVelocity.normalize().scale(MAX_SPEED)
} else {
desiredVelocity
}
// Convert to Reynolds steering force
val steeringForce = cappedVelocity.subtract(pokemonEntity.deltaMovement)
// Cap the final steering force
if (steeringForce.lengthSqr() > MAX_FORCE * MAX_FORCE) {
steeringForce.normalize().scale(MAX_FORCE)
} else {
steeringForce
}
}
}
private fun wander(entity: PokemonEntity, forceScale: Double = 1.0) {
// Wander behaviour
/*************************************************************************************************************
* The wander behaviour is based of Reynolds wander behaviour which can be seen in the below link under wander
* This version however is adapted to 3dimensions.
* https://slsdo.github.io/steering-behaviors/
*************************************************************************************************************/
// Get curr steering positions in left/right and up/down offsets
val normVec = if(entity.deltaMovement.lengthSqr() > 0.0001) {
entity.lookAngle.normalize()
} else {
Vec3(
Random.nextDouble(-1.0, 1.0),
Random.nextDouble(-1.0, 1.0),
Random.nextDouble(-1.0, 1.0)
).normalize()
}
var wanderOff = normVec
val currPitchS = sin(Mth.atan2(wanderOff.y, wanderOff.horizontalDistance()))
if (currPitchS >= 0.5f || currPitchS <= -0.5f) { //TODO: make this y limit class wide
this.currVertWander = 0.0
}
wanderOff = wanderOff.xRot(this.currVertWander.toRadians())
wanderOff = wanderOff.yRot(this.currHorzWander.toRadians())
// Base the center off the current direction of travel
val centerDistance = 4.0 // How many blocks the wander 'sphere' is in front of the entity
val wanderCenter = entity.position().add(normVec.scale(centerDistance))
// Get the wander circle's radius
val wanderRadius = 4.0
val wanderPoint = wanderCenter.add(wanderOff.scale(wanderRadius))
// change wander points
val randVert = Random.nextDouble(-wanderDisp, wanderDisp)
val randHorz = Random.nextDouble(-wanderDisp, wanderDisp)
currVertWander = Mth.wrapDegrees(randVert + currVertWander)
currHorzWander = Mth.wrapDegrees(randHorz + currHorzWander)
drawLines.add(EntityDebugLine(wanderPoint.subtract(pokemonEntity.position()), 1.0f, 0.0f, 0.0f, 1.0f)) // Add WanderPoint
drawLines.add(EntityDebugLine(wanderCenter.subtract(pokemonEntity.position()), 0.0f, 1.0f, 0.0f, 1.0f)) // Add wander center
val wanderForce = entity.steerToTarget(wanderPoint, MAX_SPEED, MAX_FORCE)
if(pokemonEntity.isFlying() || pokemonEntity.isSwimming) {
entity.deltaMovement = pokemonEntity.deltaMovement.add(wanderForce.scale(forceScale))
} else {
entity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(wanderForce.x, 0.0, wanderForce.z).scale(forceScale))
}
}
private fun boid(isFlyingOrSwimming: Boolean = true) {
// TODO: maybe move this to a sensor that lives spins up on the boid activity creation
// Get a list of other boid entities in this entity's vicinity
val brain = pokemonEntity.brain
val boids = brain.getMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES).map { nearbyEntities ->
nearbyEntities.filterIsInstance<PokemonEntity>().filter { pokemon ->
pokemon.brain.activeNonCoreActivity.orElse(null) == CobblemonActivities.POKEMON_BOIDS
}
}.orElse(emptyList())
// separation
val separationForce = separation(boids)?.scale(0.8)
separationForce?.let { force ->
if( isFlyingOrSwimming) {
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(force)
} else {
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(force.x, 0.0, force.z))
}
}
// alignment
val alignmentForce = alignment(boids)?.scale(0.0)
alignmentForce?.let { force ->
if( isFlyingOrSwimming) {
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(force)
} else {
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(force.x, 0.0, force.z))
}
}
// cohesion
val cohesionForce = cohesion(boids)?.scale(0.6)
cohesionForce?.let { force ->
if( isFlyingOrSwimming) {
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(force)
} else {
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(force.x, 0.0, force.z))
}
}
}
private fun separation(boids: List<PokemonEntity>): Vec3? {
var seperationVec: Vec3? = null
// Gather the total separation vec of all close entities
boids.forEach { boid ->
val toTargetVec = pokemonEntity.position().subtract(boid.position())
val desiredSeperation = boid.bbWidth * 2.0 // Base the seperation distance on the model width
val currDist = toTargetVec.length()
if (currDist < desiredSeperation) {
if (seperationVec == null) seperationVec = Vec3.ZERO
// Make the seperation force inversely proprotional to the serperation distance
// Also, scale the force by the bbwidth of the model so
seperationVec = seperationVec.add(toTargetVec.normalize().scale((1.0 - (currDist / desiredSeperation)) ))
}
}
if (seperationVec != null) {
//if (seperationVec.length() > MAX_SPEED) seperationVec = seperationVec.normalize().scale(MAX_SPEED)
seperationVec = seperationVec.normalize().scale(MAX_SPEED)
seperationVec = seperationVec.subtract(pokemonEntity.deltaMovement)
if (seperationVec.length() > MAX_FORCE) {
seperationVec = seperationVec.normalize().scale(MAX_FORCE)
}
}
return seperationVec
}
private fun alignment(boids: List<PokemonEntity>): Vec3? {
val neighborDist = 4.0
var summedVel: Vec3? = null
var alignmentVec: Vec3? = null
var boidCount = 0
// Gather the average position of the boid
boids.forEach { boid ->
val vecToOther = boid.position().subtract(this.pokemonEntity.position())
val distToOther = vecToOther.length()
val widthOfViewInDeg = 180
val isInSightLine = pokemonEntity.deltaMovement.normalize().dot(vecToOther) > cos((widthOfViewInDeg / 2.0).toRadians())
if (isInSightLine && distToOther < neighborDist) {
if (summedVel == null) summedVel = Vec3.ZERO
summedVel = summedVel.add(boid.deltaMovement.normalize())
boidCount++
}
}
if (summedVel != null) {
// Get the average vel/alignment by dividing by the number of boids sampled
val averageVel = summedVel.scale(1.0 / boidCount)
//alignmentVec = if (averageVel.length() > MAX_SPEED) averageVel.normalize().scale(MAX_SPEED) else averageVel
alignmentVec = averageVel.normalize().scale(MAX_SPEED)
alignmentVec = alignmentVec.subtract(pokemonEntity.deltaMovement)
if (alignmentVec.length() > MAX_FORCE) {
alignmentVec = alignmentVec.normalize().scale(MAX_FORCE)
}
}
return alignmentVec
}
private fun cohesion(boids: List<PokemonEntity>): Vec3? {
//val neighborDist = 5.0
var summedPos: Vec3? = null
var cohesionVec: Vec3? = null
var boidCount = 0
val cohesionRadius = 8.0
// Gather the average position of the boid
boids.forEach { boid ->
val vecToOther = boid.position().subtract(this.pokemonEntity.position())
val distToOther = vecToOther.length()
val widthOfViewInDeg = 360
val isInSightLine = pokemonEntity.deltaMovement.normalize().dot(vecToOther) > cos((widthOfViewInDeg / 2.0).toRadians())
if (isInSightLine && distToOther < cohesionRadius) { // Only use members that are within a certain radius and eyeline
if (summedPos == null) summedPos = Vec3.ZERO
summedPos = summedPos.add(boid.position())
boidCount++
}
}
if (summedPos != null) {
// Get the average position by dividing by the number of boids sampled
val averagePos = summedPos.scale(1.0 / boidCount)
// Get the vector towards that average position and scale
cohesionVec = averagePos.subtract(pokemonEntity.position())
//if (cohesionVec.length() > MAX_SPEED) cohesionVec = cohesionVec.normalize().scale(MAX_SPEED)
cohesionVec = cohesionVec.normalize().scale(MAX_SPEED)
cohesionVec = cohesionVec.subtract(pokemonEntity.deltaMovement)
if (cohesionVec.length() > MAX_FORCE) {
cohesionVec = cohesionVec.normalize().scale(MAX_FORCE)
}
}
return cohesionVec
}
private fun forceRectBoundary(entity: PokemonEntity, basePos: Vec3, rectWidth: Double, rectHeight: Double) {
val maxBoundaryForce = MAX_FORCE //* 2.0
val correctiveForce = getRectCorrectiveForce(basePos, rectWidth, rectHeight)
if (correctiveForce != null) {
//val correctiveForce = steerToTargetForce(correctivePos, speedLimit, maxForce)
var steer = correctiveForce.subtract(entity.deltaMovement)
if (steer.length() > maxBoundaryForce) {
steer = steer.normalize().scale(maxBoundaryForce)
}
entity.setDeltaMovement(entity.deltaMovement.add(steer))
}
}
// Returns the desired force to correct the entity back into the rectangle area
private fun getRectCorrectiveForce( basePos: Vec3, rectWidth: Double, rectHeight: Double): Vec3? {
val entityPos = pokemonEntity.position()
val posFromBase = entityPos.subtract(basePos)
val cubeHalfWidth = rectWidth / 2.0 // Useful as the basePoint is in the middle of the cube
val cubeHalfHeight = rectHeight / 2.0
// Check the three possible faces for exceeding the boundary (x, y, and z faces of cube)
val xExceed = max(0.0, abs(posFromBase.x) - cubeHalfWidth)
val yExceed = max(0.0, abs(posFromBase.y) - cubeHalfHeight)
val zExceed = max(0.0, abs(posFromBase.z) - cubeHalfWidth)
return if ( xExceed == 0.0 && yExceed == 0.0 && zExceed == 0.0) {
null
} else {
var desiredPos = pokemonEntity.position().add(pokemonEntity.deltaMovement) // Add delta movement so we aren't slowing this guy down
if (xExceed != 0.0) desiredPos = Vec3( basePos.x, desiredPos.y, desiredPos.z)
if (yExceed != 0.0) desiredPos = Vec3( desiredPos.x, basePos.y, desiredPos.z)
if (zExceed != 0.0) desiredPos = Vec3( desiredPos.x, desiredPos.y, basePos.z)
return desiredPos.subtract(pokemonEntity.position()).normalize().scale(MAX_SPEED)
}
}
}
//val desiredScale = if (distToTarget > slowDownRadius) speedLimit else (distToTarget/slowDownRadius).pow(2) * speedLimit
//desiredVec = desiredVec.normalize().scale(desiredScale)
@@ -9,47 +9,30 @@
package com.cobblemon.mod.common.entity.pokemon.ai
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonNetwork.sendPacketToTrackingPlayers
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addStandardFunctions
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine.Companion.invoke
import com.cobblemon.mod.common.entity.PlatformType
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.ai.OmniPathNavigation
import com.cobblemon.mod.common.entity.ai.steering.SteerArbitrator
import com.cobblemon.mod.common.entity.ai.steering.rules.ObstacleAvoidRule
import com.cobblemon.mod.common.entity.ai.steering.rules.SeekRule
import com.cobblemon.mod.common.entity.ai.steering.rules.WanderRule
import com.cobblemon.mod.common.entity.ai.steering.rules.WalkForwardRule
import com.cobblemon.mod.common.entity.pokemon.PokemonBehaviourFlag
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.extensions.*
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
import com.cobblemon.mod.common.util.getWaterAndLavaIn
import com.cobblemon.mod.common.util.math.geometry.toDegrees
import com.cobblemon.mod.common.util.math.geometry.toRadians
import com.cobblemon.mod.common.util.resolveFloat
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.tags.BlockTags
import net.minecraft.tags.FluidTags
import net.minecraft.util.Mth
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.attributes.Attributes
import net.minecraft.world.entity.ai.control.MoveControl
import net.minecraft.world.level.material.FluidState
import net.minecraft.world.level.pathfinder.PathType
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.random.Random
class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemonEntity) {
companion object {
@@ -67,41 +50,6 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
var bankUpwardsBlocksPerTick = 0F
var bankRightDegreesPerTick = 1F
var obsCorrectDir = 0F
var drawLines: MutableList<EntityDebugLine> = mutableListOf()
var prevForce: Vec3? = null
var steerSpeed = this.gatherTopSpeed(pokemonEntity)
private val steerArbitrator by lazy {
val steerArbitrator = SteerArbitrator(
entity = pokemonEntity,
maxForce = 0.02,
rules = mutableListOf(
ObstacleAvoidRule(
entity = pokemonEntity,
debugColor = Triple(1f, 0f, 0f),
),
SeekRule(
entity = pokemonEntity,
priority = 10,
maxForce = 0.02,
debugColor = Triple(0f, 1f, 0f),
horzOnly = true,
targetProvider = { e ->
(e.navigation as? OmniPathNavigation)?.pathFollowTarget //TODO: maybe change this seems silly. Probably want a rule that checks explicitly?
}
),
WanderRule(
entity = pokemonEntity,
debugColor = Triple(0f, 1f, 1f),
horzOnly = true
)
)
)
steerArbitrator
}
fun startBanking(
forwardBlocksPerTick: Float = 0F,
upwardsBlocksPerTick: Float = 0F,
@@ -125,7 +73,6 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
}
override fun tick() {
mob.setDiscardFriction(false) // ignored while trying to move but if stopped or doing something else this needs reenabled
if (banking && operation != Operation.WAIT) {
banking = false
}
@@ -155,7 +102,6 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
val baseSpeed = mob.getAttributeValue(Attributes.MOVEMENT_SPEED).toFloat() * this.speedModifier.toFloat()
val adjustedSpeed = baseSpeed * mediumSpeed
if (banking) {
pokemonEntity.yRot += bankRightDegreesPerTick
pokemonEntity.travel(Vec3(0.0, bankUpwardsBlocksPerTick.toDouble(), bankForwardBlocksPerTick.toDouble()).scale(mediumSpeed.toDouble()))
@@ -164,142 +110,11 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
}
if (bankDurationTicks == 0) {
banking = false
operation = Operation.WAIT
}
}
// Here we call the wander on every pokemon no matter what then they wander then leave
// Just ignore all operatations and do whatever is being tested.
if (true) {
// val horzOnly = true
//
// var topSpeed = gatherTopSpeed(pokemonEntity)
// var maxForce = 0.02 // Arbitrarily chosen as of right now.
// pokemonEntity.isPokemonSprinting = false
//
// // grab forward travel vector direction
// val forwardVec = if (pokemonEntity.deltaMovement.horizontalDistanceSqr() < 0.001) {
// // No meaningful velocity so derive from yaw orientation instead
// val yaw = Math.toRadians(pokemonEntity.yRot.toDouble())
// Vec3(-sin(yaw), 0.0, cos(yaw))
// } else {
// if (horzOnly) Vec3(pokemonEntity.deltaMovement.x, 0.0, pokemonEntity.deltaMovement.z).normalize()
// else pokemonEntity.deltaMovement.normalize()
// }
// val pointAhead = pokemonEntity.position().add(forwardVec)
// val aheadForce = pokemonEntity.steerToTarget(pointAhead, topSpeed, maxForce, horzOnly = horzOnly)
//
//
//// val player = pokemonEntity.level().getNearestPlayer(pokemonEntity, 10.0) ?: return
//// // Flee direction — away from player, flattened to XZ
//// val toPlayerVec = pokemonEntity.position().subtract(player.position())
////
//// if (toPlayerVec.length() < 6.0) {
//// pokemonEntity.isPokemonSprinting = true
////// topSpeed = 0.4 // hardcoded high value
////// maxForce = 0.04
//// topSpeed *= 2.0f
//// maxForce *= 2
//// }
////
//// val fleePoint = pokemonEntity.position().add(toPlayerVec)
//// val aheadForce = pokemonEntity.steerToTarget(fleePoint, topSpeed, maxForce, horzOnly = horzOnly)
//
// // For now just move forward by seeking towards a point ahead
// val lookAheadTicks = 10 // How far in the future of this entities current movement to look ahead for collisions
// val lookAheadDist = max(pokemonEntity.deltaMovement.length() * lookAheadTicks, pokemonEntity.bbWidth / 2.0) // At least always look ahead the width of the mons own hitbox
//
// // Get the collision box
// val inflationAmount = 0.0 //pokemonEntity.bbWidth / 5.0 //TODO: is this needed? it seems to help a bit keeping them off walls but would stop them from entering gaps they truly could
// val collBox = pokemonEntity.boundingBox.move(forwardVec.scale(lookAheadDist)).inflate(inflationAmount, 0.0, inflationAmount) // Inflate a bit
// val shapes = pokemonEntity.level().getBlockCollisions(pokemonEntity, collBox)
//
// // Gather the nearest shape
// val nearestShape = shapes.minByOrNull { shape ->
// shape.bounds().center.distanceToSqr(pokemonEntity.position())
// }
//
// var avoidancForce = Vec3.ZERO
//
// if (nearestShape != null) {
// var hasLeftForce = false
// var hasRightForce = false
// val right = Vec3(-forwardVec.z, 0.0, forwardVec.x) //TODO: expand to 3 dimensions
//
// val obsCenter = nearestShape.bounds().center
// val toObs = obsCenter.subtract(pokemonEntity.position())
//
// // Get the lateral avoidance point to seek to (the avoidance force really)
// // It is the negated vector of toObs - Proj(toObs->forwardVec)
// val projVec = forwardVec.scale(toObs.dot(forwardVec)) // Can do this because forwardVec is normalized
// val lateralComponent = toObs.subtract(projVec) // TODO: moved back so that two opposing forces turn the dude around
//
// avoidancForce = if (lateralComponent.lengthSqr() < 0.001) { // If its dead ahead then just turn right
// val rightVec = forwardVec.cross(Vec3(0.0,1.0,0.0))
// obsCorrectDir = 1F
// pokemonEntity.steerToTarget(pokemonEntity.position().add(rightVec), topSpeed, maxForce, horzOnly = horzOnly)
// } else {
// // Scale the strength of the avoidance vec based off how close to being dead center it is
//
// var avoidanceVec = lateralComponent.scale(-1.0)
// if (horzOnly) avoidanceVec = Vec3(avoidanceVec.x, 0.0, avoidanceVec.z)
// //var avoidStrength = (abs(toObs.dot(avoidanceVec)) / (pokemonEntity.bbWidth / 2.0) ).coerceIn(0.0, 1.0)
//
// // 0.5 to add half the block width allowance since if it catches the corner of a block it still gets the center
// var avoidStrength = sqrt(1 - (avoidanceVec.length() / (pokemonEntity.bbWidth / 2.0 + 0.5) ).coerceIn(0.0, 0.7))
// // Track which side this force is pushing toward
// //val newObsCorrectDir = if (avoidanceVec.normalize().dot(right) > 0) 1F else -1f
//
// // If the new direction is opposite the last direction then invert it
//// if (obsCorrectDir == 0F) {
//// obsCorrectDir = newObsCorrectDir
//// } else if (newObsCorrectDir != obsCorrectDir) {
//// avoidanceVec = avoidanceVec.scale(-1.0)
//// //avoidStrength = 1.0
//// }
//
// val avoidancePoint = pokemonEntity.position().add(avoidanceVec)
// pokemonEntity.steerToTarget(avoidancePoint, topSpeed, maxForce * avoidStrength, horzOnly = horzOnly)
// }
// } else {
// obsCorrectDir = 0F
// }
//
// // accumulte forces
// // TODO: steertoTarget also checks stuff like this. Should that be different? The forces could compound here
// val accumForce = aheadForce.scale(0.5).add(avoidancForce.scale(1.0))
// var finalForce = if( avoidancForce == Vec3.ZERO) aheadForce
// else avoidancForce
//
// prevForce?.let { finalForce = it.lerp(finalForce, 0.1) }
// prevForce = finalForce
//
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(finalForce)
//
// // Must be called to make sure we have complete control over our entities velocity.
// mob.speed = 0f
// mob.setZza(0f)
// mob.setXxa(0f)
// mob.setDiscardFriction(true)
// pokemonEntity.orientEntityToTravel(1.0f)
// pokemonEntity.setFlying(false)
//
// drawLines.add(EntityDebugLine(forwardVec))
// if (drawLines.isNotEmpty()) {
// sendPacketToTrackingPlayers(pokemonEntity, ClientboundUpdateObsAvoidPacket(pokemonEntity.id, drawLines.toList()))
// drawLines.clear()
// }
steerSpeed = gatherTopSpeed(pokemonEntity)
val force = steerArbitrator.calculate(debug = true)
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(force)
mob.speed = 0f
mob.setZza(0f)
mob.setXxa(0f)
mob.setDiscardFriction(true)
pokemonEntity.orientEntityToTravel(1.0f)
} else if (operation == Operation.STRAFE) {
if (operation == Operation.STRAFE) {
var movingDistanceTotal = Mth.sqrt(strafeForwards * strafeForwards + strafeRight * strafeRight)
if (movingDistanceTotal < 1.0f) {
movingDistanceTotal = 1.0f
@@ -346,14 +161,10 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
val horizontalDistanceFromTarget = xDist * xDist + zDist * zDist
val closeHorizontally = horizontalDistanceFromTarget < VERY_CLOSE
if (!closeHorizontally) {
// NEW
pokemonEntity.orientEntityToTravel(1.0f)
// OLD
// val angleToTarget = Mth.atan2(zDist, xDist).toDegrees() - 90.0f
// val currentMovingAngle = mob.yRot
// val steppedAngle = Mth.approachDegrees(currentMovingAngle, angleToTarget, 100 * mediumSpeed)
// mob.yRot = steppedAngle
val angleToTarget = Mth.atan2(zDist, xDist).toDegrees() - 90.0f
val currentMovingAngle = mob.yRot
val steppedAngle = Mth.approachDegrees(currentMovingAngle, angleToTarget, 100 * mediumSpeed)
mob.yRot = steppedAngle
}
val (inWater, inLava) = mob.level().getWaterAndLavaIn(mob.boundingBox)
@@ -380,101 +191,15 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
val scale = min(adjustedSpeed.toDouble(), fullDistance.length())
val maxForce = 0.02 // 0.02
// Gather the point to be steered to
val nav = pokemonEntity.navigation
nav.pathFollowTarget?.let { rt ->
wantedX = rt.x
wantedZ = rt.z
wantedY = rt.y
}
// + 0.05 for dealing with swimming out of water, they otherwise get stuck on the lip
val steerForce = pokemonEntity.steerToTarget(Vec3(wantedX, refine(wantedY + 0.05), wantedZ), scale, maxForce)
mob.deltaMovement = mob.deltaMovement.add(steerForce)
//mob.deltaMovement = direction.scale(scale)
mob.deltaMovement = direction.scale(scale)
xDist = fullDistance.x
zDist = fullDistance.z
yDist = fullDistance.y
} else {
// division is to slow the speed down a bit so they don't overshoot when they get there.
var forwardSpeed = adjustedSpeed
var forceDiv = 1 / 5.0 // Portion of max speed that force can apply every tick
var minForce = 0.02 // If its not at least this it breaks quite a few things. looking at you snorlax
val finalGoal: BlockPos? = pokemonEntity.navigation.path?.target
val distanceToFinalGoal = finalGoal?.let {
pokemonEntity.position().distanceTo(it.center).toFloat()
} ?: 0.0f
if (distanceToFinalGoal > 8f) { //TODO: scale this to hitbox size
pokemonEntity.isPokemonSprinting = true
forwardSpeed *= 1.4f
forceDiv = 1 / 10.0
minForce *= 1.0 //TODO: figure out what to actually do for this
} else {
pokemonEntity.isPokemonSprinting = false
}
// New way
val blockFriction = mob.level()
.getBlockState(mob.getBlockPosBelowThatAffectsMyMovement())
.block.getFriction()
val frictionInfluencedSpeed = if (mob.onGround()) {
forwardSpeed * (0.21600002f / (blockFriction * blockFriction * blockFriction))
} else {
0.02f // getFlyingSpeed() default for non-player controlled. Also hopefully our checks above this caught this case
}
val baseForce = (forwardSpeed * 0.98) * frictionInfluencedSpeed
// We find where the equilibrium between vanilla friction and the input force usually is to get a topSpeed for this mon.
// Could be replaced in the future with direct topSpeed calculations in the pokemon species form
val frictionAppliedAfter = blockFriction * 0.91
val topSpeed = baseForce / (1 - frictionAppliedAfter)
val currSpeed = pokemonEntity.deltaMovement.length()
//TODO: is this needed?
// if (!pokemonEntity.isPokemonSprinting && currSpeed > topSpeed) { // TODO: this might mess up some stuff like tnt pushing mobs around
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.normalize().scale(lerp(currSpeed, topSpeed, 0.1))
// }
// Get a force that looks natural
val maxForce = minForce //max(baseForce * forceDiv, minForce)
// Gather the point to be steered to
val nav = pokemonEntity.navigation
nav.pathFollowTarget?.let { rt ->
wantedX = rt.x
wantedZ = rt.z
wantedY = rt.y
}
val steerForce = pokemonEntity.steerToTarget(
Vec3(wantedX, wantedY, wantedZ),
topSpeed, // Is never reached as minecraft relies on top speed being obtained when force=friction
maxForce,
horzOnly = true
)
// If this happens a lot then we have issues
val mdt = LivingEntity.MIN_MOVEMENT_DISTANCE
if ( abs(steerForce.x) < mdt || abs(steerForce.y) < mdt || abs(steerForce.z) < mdt) {
//Cobblemon.LOGGER.info("INFO: Steer Force dimension smaller than minimum movement distance value") //TODO: figure this out
}
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(steerForce)
//pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(0.0103766, 0.0, 0.0))
mob.speed = 0f
mob.setZza(0f)
mob.setXxa(0f)
mob.setDiscardFriction(true)
// Old way
// mob.speed = forwardSpeed
val forwardSpeed = min(adjustedSpeed, max(horizontalDistanceFromTarget.toFloat() / 2, 0.15F))
mob.speed = forwardSpeed
}
if (!verticalHandled) {
@@ -555,41 +280,6 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
}
}
fun gatherTopSpeed( pokemonEntity: PokemonEntity): Double {
val behaviour = pokemonEntity.behaviour
val mediumSpeed = runtime.resolveFloat(if (pokemonEntity.getCurrentPoseType() in setOf(PoseType.FLY, PoseType.HOVER)) {
behaviour.moving.fly.flySpeedHorizontal
} else if (pokemonEntity.isEyeInFluid(FluidTags.WATER) || pokemonEntity.isEyeInFluid(FluidTags.LAVA)) {
behaviour.moving.swim.swimSpeed
} else {
behaviour.moving.walk.walkSpeed
}) * 2.5F //TODO: idk why this is 2.5 but I think we're stuck with it
val baseSpeed = mob.getAttributeValue(Attributes.MOVEMENT_SPEED).toFloat() * 0.4f //TODO: does this ever need to not be hardcoded?
val adjustedSpeed = baseSpeed * mediumSpeed
val forwardSpeed = adjustedSpeed
val blockFriction = mob.level()
.getBlockState(mob.getBlockPosBelowThatAffectsMyMovement())
.block.getFriction()
val frictionInfluencedSpeed = if (mob.onGround()) {
forwardSpeed * (0.21600002f / (blockFriction * blockFriction * blockFriction))
} else {
0.02f // getFlyingSpeed() default for non-player controlled
}
// Force applied every tick through the usual vanilla route
val baseForce = (forwardSpeed * 0.98) * frictionInfluencedSpeed
// We find where the equilibrium between vanilla friction and the input force usually is to get a topSpeed for this mon.
// Could be replaced in the future with direct topSpeed calculations in the pokemon species form
val frictionAppliedAfter = blockFriction * 0.91
val topSpeed = baseForce / (1 - frictionAppliedAfter)
return topSpeed
}
/*
* Returns the ideal water level y position of a given pokemon during battle.
* This is separate for the eye height check and is dependent on if the pokemon is rafted or swimming.
@@ -1,56 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.entity.pokemon.ai.sensors
import com.cobblemon.mod.common.CobblemonMemories
import com.google.common.collect.ImmutableSet
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.sensing.Sensor
import net.minecraft.world.item.Items
import net.minecraft.world.entity.player.Player
class PlayerDebuggingSensor : Sensor<LivingEntity>(SCAN_INTERVAL) {
override fun doTick(level: ServerLevel, entity: LivingEntity) {
var diamondHolder: LivingEntity? = null
diamondHolder = entity.brain.getMemory(CobblemonMemories.DIAMOND_DEBUGGER).orElse(null)?.let {
if(it.mainHandItem.`is`(Items.DIAMOND)) {
it
} else {
null
}
}
if (diamondHolder == null) {
val nearEntities = entity.brain.getMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES).orElse(mutableListOf())
diamondHolder = nearEntities?.find { entity ->
entity.mainHandItem.`is`(Items.DIAMOND)
}
}
if (diamondHolder != null && diamondHolder is Player) {
entity.brain.setMemory(CobblemonMemories.DIAMOND_DEBUGGER, diamondHolder)
} else {
entity.brain.eraseMemory(CobblemonMemories.DIAMOND_DEBUGGER)
}
}
override fun requires(): Set<MemoryModuleType<*>> {
return setOf(
CobblemonMemories.DIAMOND_DEBUGGER,
MemoryModuleType.NEAREST_LIVING_ENTITIES
)
}
companion object {
const val SCAN_INTERVAL = 10
}
}
@@ -1,40 +0,0 @@
//
///*
// * Copyright (C) 2023 Cobblemon Contributors
// *
// * This Source Code Form is subject to the terms of the Mozilla Public
// * License, v. 2.0. If a copy of the MPL was not distributed with this
// * file, You can obtain one at https://mozilla.org/MPL/2.0/.
// */
//
//package com.cobblemon.mod.common.entity.pokemon.ai.tasks;
//
//import com.cobblemon.mod.common.CobblemonMemories
//import com.cobblemon.mod.common.api.pokemon.status.Statuses
//import com.cobblemon.mod.common.api.storage.party.PartyStore
//import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
//import net.minecraft.world.entity.ai.behavior.OneShot
//import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder
//import net.minecraft.world.entity.ai.behavior.declarative.Trigger
//import net.minecraft.world.entity.ai.memory.MemoryModuleType
//
//object BoidsTask {
//fun create(): OneShot<PokemonEntity> {
// return BehaviorBuilder.create {
// it.group(
// it.absent(CobblemonMemories.POKEMON_DROWSY)
// ).apply(it) { _ ->
// Trigger { world, entity, _ ->
// if (entity.pokemon.status?.status == Statuses.SLEEP && entity.pokemon.storeCoordinates.get()?.store !is PartyStore) {
// entity.pokemon.status = null
// entity.brain.eraseMemory(CobblemonMemories.POKEMON_SLEEPING)
// entity.brain.useDefaultActivity()
// return@Trigger true
// } else {
// return@Trigger false
// }
// }
// }
// }
//}
//}
@@ -1,111 +0,0 @@
package com.cobblemon.mod.common.entity.pokemon.extensions
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.math.geometry.toDegrees
import net.minecraft.util.Mth
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.phys.Vec3
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.sign
/*************************************************************************************************************
Steering to Target
Detailed in: https://www.red3d.com/cwr/steer/gdc99/
*************************************************************************************************************/
fun PokemonEntity.steerToTarget(
targetPos: Vec3,
maxSpeed: Double,
maxForce: Double,
arrivalRange: Double? = null,
horzOnly: Boolean = false
): Vec3 {
val cPos = if (horzOnly) Vec3(this.position().x, 0.0, this.position().z) else this.position()
val tPos = if (horzOnly) Vec3(targetPos.x, 0.0, targetPos.z) else targetPos
// Ensure we can't have a negative range here
val absArrivalRange = arrivalRange?.let { abs(arrivalRange) }
// Calculate the vector from the pokemons current position to the
// target position. Set its desired magnitude to max speed or a
// portion of it if within the arrivalRange
val toTargetVec = tPos.subtract(cPos)
val distToTarget = toTargetVec.length()
val desiredSpeed = if (absArrivalRange != null && distToTarget < absArrivalRange) (distToTarget/absArrivalRange) * maxSpeed else maxSpeed
val desiredVec = toTargetVec.normalize().scale(desiredSpeed)
var steer = if (horzOnly) desiredVec.subtract(Vec3(this.deltaMovement.x, 0.0, this.deltaMovement.z))
else desiredVec.subtract(this.deltaMovement)
if (steer.length() > maxForce) {
steer = steer.normalize().scale(maxForce)
}
return steer
}
/*************************************************************************************************************
Steering Target Avoidance
Detailed in: https://www.red3d.com/cwr/steer/gdc99/
*************************************************************************************************************/
fun PokemonEntity.avoidTarget(
targetPos: Vec3,
maxSpeed: Double,
maxForce: Double,
avoidanceRange: Double? = null
): Vec3 {
// Calculate the vector from the targets current position to the
// pokemon position. Set its desired magnitude to max speed scaled
// linearly from 1.0 when right next to the target and 0.0 when on the
// very edge of the avoidance Range
var fromTargetVec = this.position().subtract(targetPos)
// Calculate distance to the target
val distToTarget = fromTargetVec.length()
if (distToTarget < 1.0E-4) {fromTargetVec = Vec3(0.0, 1.0, 0.0)} // Ensure the avoidance vector can't be ZERO
// Return no force otherwise a desired vector will be calculated that slows the velocity.
if (avoidanceRange != null && distToTarget >= avoidanceRange) return Vec3.ZERO
// Scale the avoidance force linearly
val desiredSpeed = when {
avoidanceRange == null -> maxSpeed // If no avoidance range then just always flee as fast as you can
avoidanceRange == 0.0 -> maxSpeed // Avoid weird divide by zero nonsense
distToTarget < avoidanceRange -> (1.0 - (distToTarget / avoidanceRange)) * maxSpeed
else -> 0.0
}
//TODO: determine if this should be scaled by distance. Probably not
val desiredVec = fromTargetVec.normalize() //.scale(desiredSpeed)
// Steer the pokemon towards the desired vector
var steer = desiredVec.subtract(this.deltaMovement)
if (steer.length() > maxForce) {
steer = steer.normalize().scale(maxForce)
}
return steer
}
/*************************************************************************************************************
Lerp the mobs yRot to its direction of travel
*************************************************************************************************************/
fun PokemonEntity.orientEntityToTravel(k: Float = 1.0f) {
val interpAmount = k.coerceIn(0.0f, 1.0f) //TODO: This probably shouldn't be a lerp like this.
if (this.deltaMovement.length() < 0.01) return //LivingEntity.MIN_MOVEMENT_DISTANCE) return
// Ensure the entity is facing the direction it is moving in both pitch and yaw
val normVec = this.deltaMovement.normalize()
val targetAngle = Mth.atan2(normVec.z, normVec.x).toDegrees() - 90.0f
val currAngle = this.yRot
val angleDiff = Mth.wrapDegrees(targetAngle - currAngle)
val maxPerTickRotation = 30.0f // set really high to ignore for now 15.0f // TODO: is something like this needed or should it be a clientside spring or something?
val effRot = min(abs(angleDiff), maxPerTickRotation) * sign(angleDiff)
this.yRot = currAngle + (effRot * interpAmount)
//entity.xRot = asin(normVec.y.toFloat()).toDegrees()
}
@@ -763,6 +763,7 @@ object CobblemonItemGroups {
CobblemonItems.pokeBalls.forEach(entries::accept)
CobblemonItems.pokeRods.forEach(entries::accept)
CobblemonItems.pokedexes.forEach(entries::accept)
entries.accept(CobblemonItems.POKEEDIT_WAND)
}
private fun blocksInjections(injector: Injector) {
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.item.interactive
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditGUI
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.item.CobblemonItem
import net.minecraft.world.InteractionHand
import net.minecraft.world.InteractionResult
import net.minecraft.world.InteractionResultHolder
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.Level
class PokeEditWandItem : CobblemonItem(Properties().stacksTo(1)) {
override fun use(world: Level, player: Player, usedHand: InteractionHand): InteractionResultHolder<ItemStack> {
val stack = player.getItemInHand(usedHand)
if (!player.isCreative) return InteractionResultHolder.pass(stack)
if (world.isClientSide) {
PokeEditGUI.open()
}
return InteractionResultHolder.sidedSuccess(stack, world.isClientSide)
}
override fun interactLivingEntity(
stack: ItemStack,
player: Player,
entity: LivingEntity,
usedHand: InteractionHand
): InteractionResult {
if (!player.isCreative) return InteractionResult.PASS
if (entity is PokemonEntity) {
if (entity.level().isClientSide) {
PokeEditGUI.open()
}
return InteractionResult.sidedSuccess(entity.level().isClientSide)
}
return InteractionResult.PASS
}
}
@@ -13,7 +13,11 @@ import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.battles.MoveTarget
import com.cobblemon.mod.common.util.*
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readEnumConstant
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeEnumConstant
import com.cobblemon.mod.common.util.writeString
import net.minecraft.network.RegistryFriendlyByteBuf
class MovesRegistrySyncPacket(moves: List<MoveTemplate>) : DataRegistrySyncPacket<MoveTemplate, MovesRegistrySyncPacket>(moves) {
@@ -33,6 +37,7 @@ class MovesRegistrySyncPacket(moves: List<MoveTemplate>) : DataRegistrySyncPacke
buffer.writeDouble(entry.critRatio)
buffer.writeInt(entry.effectChances.size)
entry.effectChances.forEach { chance -> buffer.writeDouble(chance) }
buffer.writeFloat(entry.weight)
}
override fun decodeEntry(buffer: RegistryFriendlyByteBuf): MoveTemplate {
@@ -50,7 +55,21 @@ class MovesRegistrySyncPacket(moves: List<MoveTemplate>) : DataRegistrySyncPacke
repeat(buffer.readInt()) {
effectChances += buffer.readDouble()
}
return MoveTemplate(name, num, type, damageCategory, power, target, accuracy, pp, priority, critRatio, effectChances.toTypedArray())
val weight = buffer.readFloat()
return MoveTemplate(
name = name,
num = num,
elementalType = type,
damageCategory = damageCategory,
power = power,
target = target,
accuracy = accuracy,
pp = pp,
priority = priority,
critRatio = critRatio,
effectChances = effectChances.toTypedArray(),
weight = weight
)
}
override fun synchronizeDecoded(entries: Collection<MoveTemplate>) {
@@ -1,49 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.debug
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeCollection
import com.cobblemon.mod.common.util.writeString
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.phys.Vec3
/**
* Packet sent to the client to update the obstacle avoidance lines to be rendered
* while debugging
*
* @author Jackson
* @since September 15th, 2025
*/
class ClientboundUpdateObsAvoidPacket(val entityId: Int, val lines: List<EntityDebugLine>) : NetworkPacket<ClientboundUpdateObsAvoidPacket> {
override val id: ResourceLocation = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeInt(entityId)
buffer.writeCollection(lines) { _, linePair ->
linePair.encode(buffer)
}
}
companion object {
val ID = cobblemonResource("obstacle_avoidance_lines")
fun decode(buffer: RegistryFriendlyByteBuf): ClientboundUpdateObsAvoidPacket {
val entityId = buffer.readInt()
val lines = buffer.readList { buf ->
EntityDebugLine.decode(buf as RegistryFriendlyByteBuf)
}
return ClientboundUpdateObsAvoidPacket(entityId, lines)
}
}
}
@@ -0,0 +1,51 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.server.pokeedit
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readEnumConstant
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeEnumConstant
import com.cobblemon.mod.common.util.writeString
import net.minecraft.network.RegistryFriendlyByteBuf
enum class PokeEditAction {
CREATE,
SPAWN
}
class PokeEditActionPacket(
val action: PokeEditAction,
val properties: String,
val moves: List<String>,
val cosmeticItem: String
) : NetworkPacket<PokeEditActionPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeEnumConstant(action)
buffer.writeString(properties)
buffer.writeInt(moves.size)
moves.forEach(buffer::writeString)
buffer.writeString(cosmeticItem)
}
companion object {
val ID = cobblemonResource("pokeedit_action")
fun decode(buffer: RegistryFriendlyByteBuf): PokeEditActionPacket {
val action = buffer.readEnumConstant(PokeEditAction::class.java)
val properties = buffer.readString()
val moves = List(buffer.readInt()) { buffer.readString() }
val cosmeticItem = buffer.readString()
return PokeEditActionPacket(action, properties, moves, cosmeticItem)
}
}
}
@@ -0,0 +1,130 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.serverhandling.pokeedit
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.moves.MoveSet
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.api.permission.CobblemonPermissions
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.CobblemonCosmeticItems
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditAction
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditActionPacket
import com.cobblemon.mod.common.util.commandLang
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.toBlockPos
import net.minecraft.network.chat.Component
import net.minecraft.commands.arguments.item.ItemParser
import com.mojang.brigadier.StringReader
import net.minecraft.world.item.ItemStack
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.MobSpawnType
import net.minecraft.world.level.Level
object PokeEditActionHandler : ServerNetworkPacketHandler<PokeEditActionPacket> {
override fun handle(packet: PokeEditActionPacket, server: MinecraftServer, player: ServerPlayer) {
val properties = PokemonProperties.parse(packet.properties)
if (properties.species == null) {
player.sendSystemMessage(commandLang("spawnpokemon.nospecies").red())
return
}
when (packet.action) {
PokeEditAction.CREATE -> {
if (!Cobblemon.permissionValidator.hasPermission(player, CobblemonPermissions.GIVE_POKEMON_SELF)) {
player.sendSystemMessage(lang("requires_permission").red())
return
}
val pokemon = properties.create(player)
applyMovesIfProvided(pokemon, packet.moves)
applyCosmeticIfProvided(pokemon, packet.cosmeticItem, server, player)
Cobblemon.storage.getParty(player).add(pokemon)
}
PokeEditAction.SPAWN -> {
if (!Cobblemon.permissionValidator.hasPermission(player, CobblemonPermissions.SPAWN_POKEMON)) {
player.sendSystemMessage(lang("requires_permission").red())
return
}
val world = player.serverLevel()
val pos = player.position()
val blockPos = pos.toBlockPos()
if (!Level.isInSpawnableBounds(blockPos)) {
player.sendSystemMessage(Component.literal("Invalid position").red())
return
}
try {
val pokemonEntity = properties.createEntity(world, player)
applyMovesIfProvided(pokemonEntity.pokemon, packet.moves)
applyCosmeticIfProvided(pokemonEntity.pokemon, packet.cosmeticItem, server, player)
pokemonEntity.moveTo(pos.x, pos.y, pos.z, pokemonEntity.yRot, pokemonEntity.xRot)
pokemonEntity.entityData.set(PokemonEntity.SPAWN_DIRECTION, pokemonEntity.random.nextFloat() * 360F)
pokemonEntity.finalizeSpawn(world, world.getCurrentDifficultyAt(blockPos), MobSpawnType.COMMAND, null)
world.addFreshEntity(pokemonEntity)
} catch (e: Exception) {
e.printStackTrace()
player.sendSystemMessage(Component.literal("Unable to spawn at the given position").red())
}
}
}
}
private fun applyMovesIfProvided(pokemon: com.cobblemon.mod.common.pokemon.Pokemon, moves: List<String>) {
if (moves.none { it.isNotBlank() }) return
val templates = moves.take(MoveSet.MOVE_COUNT).map { name ->
if (name.isBlank()) null else Moves.getByName(name)
}
pokemon.moveSet.doWithoutEmitting {
for (i in 0 until MoveSet.MOVE_COUNT) {
pokemon.moveSet.setMove(i, null)
}
templates.forEachIndexed { index, template ->
template?.let { pokemon.moveSet.setMove(index, it.create()) }
}
}
pokemon.moveSet.update()
}
private fun applyCosmeticIfProvided(
pokemon: com.cobblemon.mod.common.pokemon.Pokemon,
cosmeticItem: String,
server: MinecraftServer,
player: ServerPlayer
) {
val trimmed = cosmeticItem.trim()
if (trimmed.isEmpty()) return
val parser = ItemParser(server.registryAccess())
val result = try {
parser.parse(StringReader(trimmed))
} catch (_: Exception) {
player.sendSystemMessage(Component.literal("Invalid cosmetic item: $trimmed").red())
return
}
val stack = ItemStack(result.item)
stack.applyComponents(result.components)
if (stack.isEmpty) return
val isValid = CobblemonCosmeticItems.findValidCosmeticForPokemonAndItem(
server.registryAccess(),
pokemon,
stack
) != null
if (!isValid) {
player.sendSystemMessage(Component.literal("Item is not a valid cosmetic for this pokemon.").red())
return
}
pokemon.swapCosmeticItem(stack, decrement = false)
}
}
@@ -104,6 +104,10 @@ class FormData(
private var _baseAI: MutableList<BehaviourConfig>? = null,
@SerializedName("ai")
private var _ai: MutableList<BehaviourConfig>? = null,
@SerializedName("signatureMoves")
private var _signatureMoves: MutableList<MoveTemplate>? = null,
@SerializedName("defaultWildMovesetBuilder")
private val _defaultWildMovesetBuilder: ResourceLocation? = null,
val requiredMove: String? = null,
val requiredItem: String? = null,
/** For forms that can accept different items (e.g. Arceus-Grass: Meadow Plate or Grassium-Z). */
@@ -235,6 +239,10 @@ class FormData(
get() = _baseAI ?: species.baseAI
val ai: List<BehaviourConfig>
get() = _ai ?: species.ai
val signatureMoves: List<MoveTemplate>
get() = _signatureMoves ?: species.signatureMoves
val defaultWildMovesetBuilder: ResourceLocation
get() = _defaultWildMovesetBuilder ?: species.defaultWildMovesetBuilder
fun eyeHeight(entity: PokemonEntity): Float {
return this.resolveEyeHeight(entity) ?: return this.species.eyeHeight(entity)
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.pokemon
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonMovesetBuilders
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.CobblemonNetwork.sendPacketToPlayers
import com.cobblemon.mod.common.CobblemonSounds
@@ -34,6 +35,7 @@ import com.cobblemon.mod.common.api.moves.Move
import com.cobblemon.mod.common.api.moves.MoveSet
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.pokeball.PokeBalls
import com.cobblemon.mod.common.api.pokemon.Natures
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
@@ -594,6 +596,9 @@ open class Pokemon : ShowdownIdentifiable {
onChange(MarkingsUpdatePacket({ this }, value))
}
// Last flower fed to a Mooshtank
var lastFlowerFed: ItemStack = ItemStack.EMPTY
fun asRenderablePokemon() = RenderablePokemon(species, aspects, if (heldItemVisible) heldItem else ItemStack.EMPTY)
/**
@@ -1564,10 +1569,8 @@ open class Pokemon : ShowdownIdentifiable {
originalTrainer = fakeTrainerName
}
fun refreshOriginalTrainer()
{
when (originalTrainerType)
{
fun refreshOriginalTrainer() {
when (originalTrainerType) {
OriginalTrainerType.PLAYER -> {
UUID.fromString(originalTrainer)?.let { uuid ->
server()?.profileCache?.get(uuid)?.orElse(null)?.name?.let {
@@ -1584,8 +1587,7 @@ open class Pokemon : ShowdownIdentifiable {
}
}
fun removeOriginalTrainer()
{
fun removeOriginalTrainer() {
originalTrainer = null
originalTrainerType = OriginalTrainerType.NONE
originalTrainerName = null
@@ -1627,14 +1629,11 @@ open class Pokemon : ShowdownIdentifiable {
species = species
checkGender()
if (moveSet.getMoves().isEmpty()) {
initializeMoveset()
initializeMovesetFromDefault()
}
return this
}
// Last flower fed to a Mooshtank
var lastFlowerFed: ItemStack = ItemStack.EMPTY
fun checkGender() {
var reassess = false
if (form.maleRatio !in 0F..1F && gender != Gender.GENDERLESS) {
@@ -1810,6 +1809,20 @@ open class Pokemon : ShowdownIdentifiable {
moveSet.update()
}
fun initializeMovesetFromDefault() {
initializeMovesetFrom(movesetBuilder = CobblemonMovesetBuilders.getOrThrow(form.defaultWildMovesetBuilder))
}
fun initializeMovesetFrom(movesetBuilder: MovesetBuilder) {
val newMoveset = movesetBuilder.build(form = form, level = level)
moveSet.copyFrom(newMoveset)
}
@Deprecated(
message = "Will be removed within potentially 1 title update",
replaceWith = ReplaceWith("initializeMovesetFromDefault"),
level = DeprecationLevel.ERROR
)
fun initializeMoveset(preferLatest: Boolean = true) {
val possibleMoves = form.moves.getLevelUpMovesUpTo(level).toMutableList()
moveSet.doWithoutEmitting {
@@ -22,6 +22,7 @@ import com.cobblemon.mod.common.api.drop.DropTable
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addSpeciesFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addStandardFunctions
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.pokemon.effect.ShoulderEffect
@@ -42,6 +43,7 @@ import com.cobblemon.mod.common.net.IntSize
import com.cobblemon.mod.common.pokemon.abilities.HiddenAbility
import com.cobblemon.mod.common.pokemon.ai.PokemonBehaviour
import com.cobblemon.mod.common.pokemon.lighthing.LightingData
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readEntityDimensions
import com.cobblemon.mod.common.util.readEnumConstant
import com.cobblemon.mod.common.util.readIdentifier
@@ -111,6 +113,8 @@ class Species : ClientDataSynchronizer<Species>, ShowdownIdentifiable {
var implemented = false
var baseAI: MutableList<BehaviourConfig>? = null
var ai = mutableListOf<BehaviourConfig>()
var signatureMoves = mutableListOf<MoveTemplate>()
var defaultWildMovesetBuilder = cobblemonResource("wild")
/**
* The height in decimeters
@@ -77,8 +77,7 @@ object PokemonBrain {
CobblemonSensors.NEARBY_GROWABLE_CROPS,
CobblemonSensors.NEARBY_BEE_HIVE,
CobblemonSensors.NEARBY_FLOWER,
CobblemonSensors.NEARBY_SWEET_BERRY_BUSH,
CobblemonSensors.PLAYER_DEBUGGING
CobblemonSensors.NEARBY_SWEET_BERRY_BUSH
// CobblemonSensors.BATTLING_POKEMON,
// CobblemonSensors.NPC_BATTLING
@@ -180,7 +179,6 @@ object PokemonBrain {
MemoryModuleType.HURT_BY_ENTITY,
MemoryModuleType.NEAREST_PLAYERS,
MemoryModuleType.NEAREST_VISIBLE_PLAYER,
MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER,
MemoryModuleType.ANGRY_AT,
MemoryModuleType.ATTACK_COOLING_DOWN,
CobblemonMemories.POKEMON_DROWSY,
@@ -203,8 +201,7 @@ object PokemonBrain {
CobblemonMemories.HERD_LEADER,
CobblemonMemories.HERD_SIZE,
CobblemonMemories.ATTACK_TARGET_DATA,
CobblemonMemories.NEARBY_SWEET_BERRY_BUSH,
CobblemonMemories.DIAMOND_DEBUGGER
CobblemonMemories.NEARBY_SWEET_BERRY_BUSH
)
private fun coreTasks(pokemon: Pokemon) = buildList<Pair<Int, BehaviorControl<in PokemonEntity>>> {
@@ -87,6 +87,7 @@ object DataKeys {
const val POKEMON_PROPERTIES_MOVES = "Moves"
const val POKEMON_PROPERTIES_HELDITEM = "HeldItem"
const val POKEMON_PROPERTIES_MOVESET_BUILDERS = "MovesetBuilders"
// Entity effects
const val ENTITY_EFFECTS = "EntityEffects"
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.util
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.molang.ExpressionLike
/**
* An IntRange where the minimum and maximum values are defined by MoLang expressions. When deserializing from
* JSON the [com.cobblemon.mod.common.util.adapters.ScriptableIntRangeAdapter] will figure out if it's a single
* value in which case it's not using any scripting stuff. Otherwise, if it's an object with "min" and "max" properties
* it will respect them as expressions.
*
* @author Hiroku
* @since December 15th, 2025
*/
class ScriptableIntRange(
val min: ExpressionLike = "0".asExpressionLike(),
val max: ExpressionLike = "0".asExpressionLike()
) {
constructor(min: Int, max: Int): this(
min = min.toString().asExpressionLike(),
max = max.toString().asExpressionLike()
)
fun resolve(runtime: MoLangRuntime): IntRange {
val minimum = min.resolveInt(runtime)
val maximum = max.resolveInt(runtime)
if (minimum > maximum) {
Cobblemon.LOGGER.error("ScriptableIntRange(${min.getString()}, ${max.getString()}) resolved a minimum ($minimum) greater than maximum ($maximum). Returning empty range.")
return IntRange.EMPTY
}
return minimum..maximum
}
}
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.util.adapters
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import java.lang.reflect.Type
object MovesetBuilderAdapter : JsonDeserializer<MovesetBuilder> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): MovesetBuilder {
val jsonObject = json.asJsonObject
val type = jsonObject.get("type")?.asString ?: "cobblemon:default"
val clazz = MovesetBuilder.movesetBuilderTypes[type.asIdentifierDefaultingNamespace()]
?: throw IllegalArgumentException("Unknown MovesetBuilder type: $type")
return context.deserialize(json, clazz)
}
}
@@ -0,0 +1,23 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.util.adapters
import com.cobblemon.mod.common.CobblemonMovesetBuilders
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import java.lang.reflect.Type
object MovesetBuilderReferenceAdapter : JsonDeserializer<MovesetBuilder> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): MovesetBuilder {
return CobblemonMovesetBuilders.getOrThrow(json.asString.asIdentifierDefaultingNamespace())
}
}
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.util.adapters
import com.cobblemon.mod.common.api.npc.NPCPartyProvider
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
@@ -16,8 +17,8 @@ import java.lang.reflect.Type
object NPCPartyProviderAdapter : JsonDeserializer<NPCPartyProvider> {
override fun deserialize(json: JsonElement, type: Type, ctx: JsonDeserializationContext): NPCPartyProvider {
val typeName = if (json.isJsonPrimitive) json.asString else json.asJsonObject.get("type").asString
return NPCPartyProvider.types[typeName]?.invoke(typeName)?.also { it.loadFromJSON(json) }
val typeName = (if (json.isJsonPrimitive) json.asString else json.asJsonObject.get("type").asString).asIdentifierDefaultingNamespace()
return NPCPartyProvider.types[typeName]?.invoke()?.also { it.loadFromJSON(json) }
?: throw IllegalStateException("Unable to find party provider by type: $typeName")
}
}
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.util.adapters
import com.cobblemon.mod.common.util.ScriptableIntRange
import com.cobblemon.mod.common.util.asExpressionLike
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
import java.lang.reflect.Type
/**
* Parses a [ScriptableIntRange] by two paths. One is if it's a primitive in which
* case they're just doing a normal [IntRange] and it will parse it using the same
* method as IntRange normally gets and then returns a ScriptableIntRange of literals.
*
* The other path is if they provide an object with "min" and "max" in which case
* each property is deserialized as an [ExpressionLike].
*
* @author Hiroku
* @since December 15th, 2025
*/
object ScriptableIntRangeAdapter : JsonDeserializer<ScriptableIntRange> {
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): ScriptableIntRange {
if (json is JsonPrimitive) {
IntRangeAdapter.deserialize(json, typeOfT, context).let {
return ScriptableIntRange(
it.first.toString().asExpressionLike(),
it.last.toString().asExpressionLike()
)
}
} else {
val obj = json.asJsonObject
val min = obj.get("min").asString
val max = obj.get("max").asString
return ScriptableIntRange(
min.toString().asExpressionLike(),
max.toString().asExpressionLike()
)
}
}
}
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.util.adapters
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import java.lang.reflect.Type
/**
* A simple adapter that maps string values to objects based on a provided mapping.
*
* I could swear I already made something like this before.
*
* @author Hiroku
* @since December 5th, 2025
*/
class StringToObjectAdapter<T>(
val mapping: MutableMap<String, T>
) : JsonDeserializer<T> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): T = mapping[json.asString]
?: throw IllegalArgumentException("Unknown mapping for string: ${json.asString}")
}
Loaded 100 of 127 files, more files were not shown because too many files have changed in this diff. Show more