Compare commits

..
361 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
CyrusandPolymeta 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. McQuiggandPolymeta 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 BowesandPolymeta 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
GertimoshkaandPolymeta 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
GertimoshkaandPolymeta 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
GitoidoandPolymeta 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
TerrainwaxandPolymeta 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
CeejayandHiroku 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
AramandPolymeta 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
阿鱼andWeblate 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
阿鱼andWeblate 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 GhastandWeblate 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 GhastandWeblate 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
geumuandWeblate 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 GhastandWeblate 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
geumuandWeblate 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 GhastandWeblate 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 GhastandWeblate 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 GhastandWeblate 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
TeggyandWeblate 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
YasitoandWeblate 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
YasitoandWeblate 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
YasitoandWeblate 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
YasitoandWeblate 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
YasitoandWeblate 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
YasitoandWeblate 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 BunnuaandWeblate 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
TeggyandWeblate 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 BunnuaandWeblate 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 BunnuaandWeblate 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 PuerariandWeblate 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
TeggyandWeblate 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
geumuandWeblate 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 iteiteandWeblate 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
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
AramandPolymeta 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
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
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
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
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
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
Hiroku b6a7051b2c Fixed galarian weezing blink crash, closes #1800 2026-01-11 04:28:37 +00:00
Jerzykv 2cb1cf2f83 tiny fix on Togekiss 2026-01-10 13:36:40 +01:00
阿鱼andWeblate 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
Jerzykv 8dbb350230 minor fix to apricorn textures 2026-01-05 21:56:02 +01:00
Jerzykv 52f9ffb1f6 fixed Politoed's drop again. thanks scraper 2026-01-05 12:23:07 +01:00
LucasandWeblate 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
YasitoandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
YasitoandWeblate 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
LucasandWeblate 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
YasitoandWeblate 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
YasitoandWeblate 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
Konoha fade5502d9 molang additions: Move struct accessor and player inventory utils 2026-01-01 15:31:40 -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
RedGenesectNinja f4a7c6e873 Implemented the remodels for Chingling and Chimecho 2025-12-21 22:40:17 -06:00
geumuandWeblate 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
geumuandWeblate 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
arvid6andWeblate 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
geumuandWeblate 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
阿鱼andWeblate 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 iteiteandWeblate 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
geumuandWeblate 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
geumuandWeblate 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
geumuandWeblate 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
Alexis SalgueiroandWeblate 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
geumuandWeblate 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
geumuandWeblate 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
geumuandWeblate 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
geumuandWeblate 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
Watuneni iteiteandWeblate 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
arvid6andWeblate 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
geumuandWeblate 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
geumuandWeblate 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
Nico MerenqandWeblate 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
geumuandWeblate 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
geumuandWeblate 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
geumuandWeblate 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
Deltric 3cf9425247 Fixed memory leak from battles not being cleaned up after ending. 2025-12-06 01:35:53 -06:00
Carmendar b5578e72bd Merge branch 'main' into implement_pokemon 2025-12-05 01:30:51 -06:00
Carmendar 3aea0909a4 Merge branch 'main' into implement_pokemon 2025-12-05 01:27:25 -06:00
TeggyandWeblate 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
geumuandWeblate 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
geumuandWeblate 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
geumuandWeblate 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
geumuandWeblate 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
geumuandWeblate 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
geumuandWeblate 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
geumuandWeblate 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
geumuandWeblate 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
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
Zack the GhastandWeblate 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
geumuandWeblate 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 iteiteandWeblate 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
geumuandWeblate 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
geumuandWeblate 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
geumuandWeblate 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
geumuandWeblate 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
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
geumuandWeblate 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
geumuandWeblate 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 iteiteandWeblate 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
阿鱼andWeblate 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
Watuneni iteiteandWeblate 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
阿鱼andWeblate 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
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
810 changed files with 50099 additions and 8964 deletions
+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
+37 -71
View File
@@ -1,85 +1,51 @@
# Changelog
## [1.7.2 (MONTH Xth, YEAR)](#1-7-2)
## [1.7.4 (MONTH Xth, 2026)](#1-7-4)
### Additions
- Added riding statistics: `Distance by Pokémon on Land`, `Distance by Pokémon in Air` and `Distance by Pokémon in Liquid`.
- Added the Partner Mark, which party Pokémon have a very small chance to earn as the player walks, provided they have enough friendship.
- Added new effects
- Mental Restoration: Reduces "Time Since Last Rest" statistic, which controls Insomnia, by half a minute per second per effect level
- Negative Effect Cleanse: Removes all negative effects
- Effect Cleanse: Removes all effects
- Added more seasonings for sinister tea
- Mental Herb - Mental Restoration I for 10 seconds
- White Herb - Negative Effect Cleanse
- Milk - Effect Cleanse
- Moomoo Milk - Effect Cleanse
- Added `label` as an alternate key to the `tag` property.
- Added Saccharine wood furniture for Adorn.
- Added block models for existing items: Potion, Super Potion, Hyper Potion, Max Potion, Full Restore, Antidote, Awakening, Burn Heal, Ice Heal, Paralyze Heal, Full Heal, Ether, Elixir, Max Ether, Max Elixir, X Accuracy, X Attack, X Defence, X Special Attack, X Special Defence, X Speed, Dire Hit, Guard Spec, Weakness Policy, Blunder Policy, Cleanse Tag, and Spell Tag.
- Added decorative sign blocks, Plaques.
- 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}
- Added missing crops to Botany Pots integration
### New rideable Pokémon
### Pokémon Added
### Model updates for the following Pokémon
### Animation updates for the following Pokémon
### Changes
- Ride sounds have been separated into stereo for passengers and mono for other players. Riding will now sound more spacious.
- Added a new config option, `Enable In-Flight Dismounting` (default: off), which lets you dismount while riding a Pokémon in the air.
- Battle AI now uses a smarter threshold for switching, reducing unnecessary switches.
- AI will always use the most damaging move when at low HP and when it is not switching, improving endgame decision-making.
- Added support for optional message variants in battle activate instructions for more context-specific battle text.
- Example: `this.add('-activate', pokemon, 'ability: example', '[msg]message1');` will parse to lang key `cobblemon.battle.activate.example.message1`
- Restructured Botany Pots integration folder structure
- Decrease default pastured mon wander radius to 32.
- Factor in the weight of pokemon when calculating pushing forces between hitboxes.
- Remade the Starter Selection Screen with new assets
- Refactored berry trees to be less impactful on TPS (more performant)
### Fixes
- Fixed an issue in the OmniPathNodeMaker/Navigation that prevented Combee from pathing into and out of Saccharine Leaves.
- Fixed orientation not getting reset after crashing while flying and getting back on the Pokémon.
- Fixed Furfrou not being trimmable on NeoForge.
- Fixed shiny Pokémon not respecting the silent flag on spawn.
- Fixed honey from Saccharine Leaves being harvestable at less than the max age.
- Items given from interacting with Saccharine Leaves are placed in the active hotbar slot if possible.
- Fix all Pokémon being saved to chunks and never despawning.
- Fixed possible error on world generation when Combee are placed in naturally generated bee nests.
- Fixed Poké Snacks crashing if there was no available spawn.
- Fixed Pokémon spawn influences not being specific to regional forms.
- Fixed crashing when viewing another player in spectator mode.
- Fixed missing apricorn textures for Adorn blocks.
- Fixed crashing when riding due to malformed json file instead of resetting it.
- Fixed mochi items consuming two items at a time.
- Fix item interaction sometimes not working properly when playing on servers.
- Fixed Soothe Bell not being properly tagged as a held item.
- Fixed Mint Leaves not being usable for filling the Resurrection Machine.
- Added Saccharine Boats and Saccharine Boat with Chests to relevant boat tags.
- Fixed the `run_script` Molang function to not fail if the environment's context is null.
- Fixed battle item duplication issue.
- Fixed the movesets of certain Pokémon being incorrectly sanitized upon reload.
- Fixed the ordering of Aprijuice's riding stat boosts to always have the same order.
- Fixed the Molang functions `date_local_time`, `date_of`, `date_is_after` to use the correct date format.
- Fixed the bottom half of pasture blocks having an incorrect hit-box when facing north or south.
- Fixed Galarian Mr. Mime incorrectly being rideable.
- Fixed gimmicks not being usable in double / triple battles.
- Fixed gimmick buttons showing when already used in the same turn in double / triple battles.
- Fixed Apricorn block model rotation for counters/sinks/cupboards/drawers to match that of most recent version of Adorn.
- Fixed Fresh Start Mochi EV reset not syncing health changes properly.
- Fixed requirements such as `biomeCondition` crashing the game when used to define Pokémon interactions.
- Fixed Pokémon interactions not playing sound effects when the volume was not specified.
- Fixed Pokémon interaction cooldowns taking longer than intended when the Pokémon was in the player’s party.
- Fixed Pokémon interactions occasionally crashing the game when spamming an interaction with a 0 cooldown.
- Fixed Pokémon fullness decreasing more than intended when the Pokémon was pastured.
- Fixed crashing when sending a Pokémon out after editing its form.
- Fixed Super Potion recipes incorrectly using Hondew Berry instead of Aguav Berry.
- Fixed `/calculateseatpositions` expecting a locator format that even we don't use. It needs an underscore after "seat".
- Fixed invulnerability-bypassed damage being resisted by invulnerable NPC entities.
- Fixed selected leading Pokémon in battle not being used when levels are raised.
- Fixed killer not being set early enough on a wild wild Pokémon entity from WinInstruction.
- Fixes https://gitlab.com/cable-mc/cobblemon/-/issues/1943
- Fix occasional crash on retrieving revived fossils
- Fixed a compatibility issue with World Game Rules which caused some log spam in specific cases.
- Fixed improper validation of Pokémon when performing a trade.
- Fixed issue where particle like beam can disappear when you don't see the beginning of the beam or you look away
- Fixes https://gitlab.com/cable-mc/cobblemon/-/issues/1943
- Fix occasional crash on retrieving revived fossils
- Fix brushing not working on Neoforge
- [Various Submarine Ride Fixes](https://gitlab.com/cable-mc/cobblemon/-/merge_requests/2157)
- Fix Pastured mons despawning and running away.
- Fix Apricorn trees crashing when other mods bonemeal it
- [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
### Developer
- Changed the `owner` parameter in the `OwnerQueryRequirement` interface from `ServerPlayer` to `Player`. This method is now also called on the client to verify whether a Pokémon interaction succeeded, so make sure to update your implementations to handle both server and client contexts.
- Added `pnx` to the `BattleFaintedEvent` and `FormeChangeEvent`.
- Added register methods for custom instructions to `ShowdownInterpreter`.
- Changed callback operations in `BattlePokemon` to allow multiple callbacks and is now mutable.
- Added `PokeSnackSpawnerFactory` which allows influence registration on PokeSnack encounters
- Added `FishingSpawnerFactory` which allows influence registration on fishing encounters
### Molang & Datapacks
- Ride sounds can now be set as exclusive to passengers.
- Ride sounds no longer play when submerged unless specified with a new setting.
- Exception handling has been added to `run_molang`, resolving some crashes caused by malformed MoLang expressions.
### Particles
- q.item.is_enchanted()
- q.item.has_enchantment(minecraft:sharpness, 3)
- Added `has_chosen_starter` & `get_starter_uuid` as available Molang functions
### Localization
- Updated translations for:
+137
View File
@@ -0,0 +1,137 @@
# Changelog
## [1.7.2 (January 25th, 2026)](#1-7-2)
### Additions
- Added Saccharine wood furniture for Adorn.
- Added the Partner Mark, which party Pokémon have a very small chance to earn as the player walks, provided they have enough friendship.
- Added decorative block models for existing items: Potion, Super Potion, Hyper Potion, Max Potion, Full Restore, Antidote, Awakening, Burn Heal, Ice Heal, Paralyze Heal, Full Heal, Ether, Elixir, Max Ether, Max Elixir, X Accuracy, X Attack, X Defence, X Special Attack, X Special Defence, X Speed, Dire Hit, Guard Spec, Weakness Policy, Blunder Policy, Cleanse Tag, and Spell Tag.
- Added decorative sign blocks called Plaques.
- Added ride style labels to ride stats in Pokédex, as well as ride style icons in the summary GUI.
- Added more seasonings for sinister tea:
- Mental Herb - Mental Restoration I for 10 seconds, which staves off Insomnia.
- White Herb - Removes negative effects.
- Milk, Moomoo Milk - Removes effects.
- Added dispenser support for:
- Applying a Honey Bottle to Saccharine Logs.
- Washing honey off of Saccharine Slathered Logs.
- Applying and removing honey on Saccharine Leaves.
- Added riding statistics: `Distance by Pokémon on Land`, `Distance by Pokémon in Air` and `Distance by Pokémon in Liquid`.
### New rideable Pokémon
- Honchkrow
- Mantine
### Pokémon Added
#### Gen 5
- Throh
- Sawk
#### Gen 6
- Litleo
- Pyroar
- Spritzee
- Aromatisse
- Swirlix
- Slurpuff
### Model updates for the following Pokémon
- Falinks
- Cottonee
- Whimsicott
- Ferroseed
- Ferrothorn
- Chingling
- Chimecho
### Animation updates for the following Pokémon
- Mantine (ride animations)
- Flygon (ride jump animation)
- Honchkrow (ride animations)
- Croagunk
- Toxicroak
### Changes
- Added a new config option, `Enable In-Flight Dismounting` (default: off), which lets you dismount while riding a Pokémon in the air. This is the textbook definition of caveat emptor.
- Ride sounds have been separated into stereo for passengers and mono for other players. Riding will now sound more spacious.
- Battle AI now uses a smarter threshold for switching, reducing unnecessary switches.
- AI will always use the most damaging move when at low HP and when it is not switching, improving endgame decision-making.
- Items given from interacting with Saccharine Leaves are placed in the active hotbar slot if possible.
- Added `label` as an alternate key to the `tag` property.
### Fixes
- Fixed crashing when viewing another player in spectator mode. Some said this was reason to release this update a month ago. They were probably right.
- Fixed crashing when sending a Pokémon out after editing its form.
- Fixed all Pokémon being saved to chunks and never despawning even when they should be.
- Fixed Poké Snacks crashing if there was no available spawn.
- Fixed Poké Snack / Bait effects so that EV Yield, Type, and Egg Group filters now all apply together when weighting spawns. And you thought you were going crazy.
- Fixed Furfrou not being trimmable on NeoForge specifically.
- Fixed honey from Saccharine Leaves being harvestable at less than the max age.
- Fixed possible error on world generation when Combee are placed in naturally generated bee nests.
- Fixed Galarian Weezing crashing the world if it decides to blink the wrong way. The same could happen to you!
- Fixed Galarian Mr. Mime incorrectly being rideable. Some would argue that regular Mr. Mime shouldn't be rideable, but that would make them wrong.
- Fixed missing apricorn textures for Adorn blocks.
- Fixed Apricorn block model rotation for counters/sinks/cupboards/drawers to match that of most recent version of Adorn.
- Fixed item interaction sometimes not working properly when playing on servers.
- Fixed Mint Leaves not being usable for filling the Resurrection Machine.
- Fixed Super Potion recipes incorrectly using Hondew Berry instead of Aguav Berry. They look similar, to be fair.
- Fixed Wishiwashi not schooling properly.
- Fixed battle item duplication issue. How did it work? Wouldn't you like to know, weather boy.
- Fixed the ordering of Aprijuice's riding stat boosts so they always have the same order.
- Fixed the movesets of certain Pokémon being incorrectly sanitized upon reload, causing some forms to keep losing moves.
- Fixed selected leading Pokémon in battle not being used when levels are set.
- Fixed the bottom half of pasture blocks having an incorrect hit-box when facing north or south.
- Fixed gimmicks not being usable in double and triple battles.
- Fixed gimmick buttons showing when already used in the same turn in double and triple battles.
- Fixed mochi items consuming two items at a time.
- Fixed Fresh Start Mochi's EV reset not syncing health changes properly.
- Fixed Pokémon spawn influences not being specific to regional forms.
- Fixed memory leak from battles not being cleaned up after ending.
- Fixed orientation not getting reset after crashing while flying and getting back on the Pokémon.
- Fixed shiny Pokémon not respecting the `silent` flag on spawn.
- Fixed 'Plain' prefix not being applied to all low-quality Aprijuice.
- Fixed Pokémon interactions not playing sound effects when the volume was not specified.
- Fixed Pokémon interaction cooldowns taking longer than intended when the Pokémon was in the player’s party.
- Fixed Pokémon interactions occasionally crashing the game when spamming an interaction with a 0 cooldown.
- Fixed Pokémon fullness decreasing more than intended when the Pokémon was pastured.
- Fixed Scraggy line, Milcery line, and Morpeko spawning on unnatural blocks.
- Fixed invulnerability-bypassed damage being resisted by invulnerable NPC entities.
- Fixed Fabric network packets not being handled immediately, fixing incompatibilities with Supplementaries, Prometheus, Sleep Tight, and others.
- Fixed killer not being set early enough on a wild Pokémon entity from `WinInstruction`.
- Fixed requirements such as `biomeCondition` crashing the game when used to define Pokémon interactions.
- Fixed an issue that prevented registering custom dispenser behaviours (no longer overrides vanilla behaviour).
- Fixed an issue navigation that prevented Combee from pathing into and out of Saccharine Leaves.
- Added Saccharine Boats and Saccharine Boat with Chests to relevant boat tags.
- Fixed Soothe Bell not being properly tagged as a held item.
- Fixed `/calculateseatpositions` expecting a locator format that even we don't use. It needs an underscore after "seat".
- Fixed the `run_script` Molang function to not fail if the environment's context is null.
- Fixed the Molang functions `date_local_time`, `date_of`, `date_is_after` to use the correct date format.
### Developer
- Changed the `owner` parameter in the `OwnerQueryRequirement` interface from `ServerPlayer` to `Player`. This method is now also called on the client to verify whether a Pokémon interaction succeeded, so make sure to update your implementations to handle both server and client contexts.
- Added the `pnx` to the `BattleFaintedEvent` and `FormeChangeEvent`.
- Added register methods for custom instructions to `ShowdownInterpreter`.
- Changed callback operations in `BattlePokemon` to allow multiple callbacks and be mutable.
- Added `display_name`, `description`, and `max_pp` functions to the `MoveTemplate` struct.
- Changed the implementations of `AIBattleActor` to be open classes to allow extension.
- Changed the resource location of Poké Ball icons in the GUI to use the balls namespace instead of Cobblemon's, allowing mods to properly use their own namespace for these textures.
### Molang & Datapacks
- Ride sounds can now be set as exclusive to passengers.
- Ride sounds no longer play when submerged unless specified with a new setting.
- Exception handling has been added to `run_molang`, resolving some crashes caused by malformed MoLang expressions.
- Added the `create_itemstack`, `has_inventory_space`, `set_inventory_slot`, and `give_item` MoLang functions for item and inventory utility.
- Added the `get_move_from_id` MoLang function for general move info queries without requiring a Moveset object to access.
- Added support for optional message variants in battle activate instructions for more context-specific battle text.
- Example: `this.add('-activate', pokemon, 'ability: example', '[msg]message1');` will parse to lang key `cobblemon.battle.activate.example.message1`.
### Localization
- Updated translations for:
- French
- Canadian French
- Simplified Chinese
- Ukrainian
- Brazilian Portuguese
- Spanish
- Korean
- Japanese
+22
View File
@@ -0,0 +1,22 @@
# Changelog
## [1.7.3 (January 31st, 2026)](#1-7-3)
### Fixes
- Fixed mounted Pokémon not playing sounds.
- Fixed players failing to join servers randomly.
- Fixed some Pokémon having no stamina for flying.
- Fixed `/pctake` not respecting the actual size of the PC.
### Developer
- Poké Ball GUI textures now respect the ball's namespace, enabling proper texture loading for custom balls from other mods.
### Molang & Datapacks
- Added a `chance` requirement type for Pokémon interactions.
### Localization
- Updated translations for:
- French
- Canadian French
- Korean
- Portuguese
- Simplified Chinese
- Spanish
@@ -15,6 +15,8 @@ import com.cobblemon.mod.common.api.events.CobblemonEvents;
import com.cobblemon.mod.common.api.events.item.LeftoversCreatedEvent;
import com.cobblemon.mod.common.api.orientation.OrientationController;
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies;
import com.cobblemon.mod.common.api.riding.RidingStyle;
import com.cobblemon.mod.common.api.riding.behaviour.types.air.HoverBehaviour;
import com.cobblemon.mod.common.api.storage.party.PartyStore;
import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore;
import com.cobblemon.mod.common.api.tags.CobblemonItemTags;
@@ -59,6 +61,7 @@ import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@@ -169,6 +172,32 @@ public abstract class PlayerMixin extends LivingEntity implements ScannableEntit
}
}
@Redirect(
method = "getDestroySpeed",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/player/Player;onGround()Z"
)
)
private boolean cobblemon$considerHoverOnGround(Player player) {
// If riding a Hover Mount, return true to bypass the 5x slowdown
if (player.getVehicle() instanceof PokemonEntity vehicle) {
var ridingController = vehicle.getRidingController();
if (ridingController == null) player.onGround();
var activeContext = ridingController.getContext();
if (activeContext == null) player.onGround();
if (activeContext.getStyle() == RidingStyle.AIR &&
activeContext.getBehaviour().equals(HoverBehaviour.Companion.getKEY())) {
return true;
}
}
// Otherwise, return the actual value
return player.onGround();
}
@Inject(
method = "eat",
at = @At(
@@ -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.mixin.accessor;
import net.minecraft.world.item.Item;
import net.minecraft.core.dispenser.DispenseItemBehavior;
import net.minecraft.world.level.block.DispenserBlock;
import java.util.Map;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(DispenserBlock.class)
public interface DispenserBlockAccessor {
@Accessor("DISPENSER_REGISTRY")
static Map<Item, DispenseItemBehavior> getItemBehaviors() {
throw new AssertionError();
}
}
@@ -35,6 +35,7 @@ import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import com.cobblemon.mod.common.client.gui.party.PartyTutorialToasts;
import static com.google.common.primitives.Floats.min;
import static net.minecraft.util.Mth.lerp;
@@ -85,11 +86,13 @@ public abstract class MouseHandlerMixin {
CobblemonClient.INSTANCE.getStorage().shiftSelected(false);
ci.cancel();
PartySendBinding.INSTANCE.actioned();
PartyTutorialToasts.INSTANCE.onHoldKeyScrollSwitchedSlot();
} else if (i < 0) {
accumulatedScrollY -= i;
CobblemonClient.INSTANCE.getStorage().shiftSelected(true);
ci.cancel();
PartySendBinding.INSTANCE.actioned();
PartyTutorialToasts.INSTANCE.onHoldKeyScrollSwitchedSlot();
}
}
}
@@ -310,7 +313,7 @@ public abstract class MouseHandlerMixin {
var playerRotater = (RidePassenger)player;
playerRotater.cobblemon$setRideXRot(playerRotater.cobblemon$getRideXRot() + f);
playerRotater.cobblemon$setRideYRot(playerRotater.cobblemon$getRideYRot() + g);
playerRotater.cobblemon$setRideXRot(Mth.clamp(playerRotater.cobblemon$getRideXRot(), -90.0F, 90.0F));
playerRotater.cobblemon$setRideXRot(Mth.clamp(playerRotater.cobblemon$getRideXRot(), -89.0F, 89.0F));
playerRotater.cobblemon$setRideYRot(Mth.clamp(playerRotater.cobblemon$getRideYRot(), -105.0F, 105.0F));
if (player.getVehicle() != null) {
player.getVehicle().onPassengerTurned(player);
@@ -14,9 +14,14 @@ import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.pokemon.Pokemon;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef;
import com.llamalad7.mixinextras.sugar.ref.LocalFloatRef;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
@@ -89,4 +94,61 @@ public abstract class EntityMixin {
cir.setReturnValue(customEyePos);
}
@WrapOperation(
method = "push(Lnet/minecraft/world/entity/Entity;)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;push(DDD)V", ordinal = 0)
)
public void cobblemon$customPusherLogic(Entity pusher, double x, double y, double z,
Operation<Void> original,
@Local(argsOnly = true) Entity pushee,
@Local(name = "e") double e, @Local(name = "d") double d,
@Share("pusherIsPlayerOrPokemon") LocalBooleanRef pusherIsPlayerOrPokemon,
@Share("pusheeIsPlayerOrPokemon") LocalBooleanRef pusheeIsPlayerOrPokemon,
@Share("pusheeReceivedForce")LocalFloatRef pusheeReceivedForce) {
pusherIsPlayerOrPokemon.set(pusher instanceof PokemonEntity || pusher instanceof Player);
pusheeIsPlayerOrPokemon.set(pushee instanceof PokemonEntity || pushee instanceof Player);
if (!pusheeIsPlayerOrPokemon.get() || !pusherIsPlayerOrPokemon.get()) {
original.call(pusher, x, y, z);
return;
}
/**********************************************
* Custom portion of the code:
* Handle collisions with weight in mind. Use
* the weight value of pokemon to determine
* which side gets what portion of the total
* force.
*********************************************/
// Weights in hectograms
// Check to see if the pusher or pushee is a player. 200ish pounds. Steve's like a 6'6 miner
var pusherWeight = pusher instanceof PokemonEntity pusherPokemon ? pusherPokemon.getPokemon().getSpecies().getWeight() : 900F;
var pusheeWeight = pushee instanceof PokemonEntity pusheePokemon ? pusheePokemon.getPokemon().getSpecies().getWeight() : 900F;
// Calculate the portion of force received for both entities. Bigger receives less from the
// collision than the smaller one.
var totalWeight = pusherWeight + pusheeWeight;
var pusherReceivedForce = pusheeWeight / totalWeight * 2F;
pusheeReceivedForce.set((pusherWeight / totalWeight) * 2F);
original.call(pusher, -d * pusherReceivedForce, 0.0, -e * pusherReceivedForce);
}
@WrapOperation(
method = "push(Lnet/minecraft/world/entity/Entity;)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;push(DDD)V", ordinal = 1)
)
public void cobblemon$customPusheeLogic(Entity pusher, double x, double y, double z, Operation<Void> original,
@Local(name = "e") double e, @Local(name = "d") double d,
@Share("pusherIsPlayerOrPokemon") LocalBooleanRef pusherIsPlayerOrPokemon,
@Share("pusheeIsPlayerOrPokemon") LocalBooleanRef pusheeIsPlayerOrPokemon,
@Share("pusheeReceivedForce")LocalFloatRef pusheeReceivedForce) {
if (!pusheeIsPlayerOrPokemon.get() || !pusherIsPlayerOrPokemon.get()) {
original.call(pusher, x, y, z);
return;
}
original.call(pusher, d * pusheeReceivedForce.get(), 0.0, e * pusheeReceivedForce.get());
}
}
@@ -65,10 +65,14 @@ import com.cobblemon.mod.common.api.storage.player.adapter.PlayerDataMongoBacken
import com.cobblemon.mod.common.api.storage.player.factory.CachedPlayerDataStoreFactory
import com.cobblemon.mod.common.api.tags.CobblemonEntityTypeTags
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.battles.*
import com.cobblemon.mod.common.battles.BagItems
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.ShowdownThread
import com.cobblemon.mod.common.battles.actor.PokemonBattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.block.DispenserBehaviorRegistry
import com.cobblemon.mod.common.block.dispenser.DispenserBehaviorRegistry
import com.cobblemon.mod.common.client.CobblemonPack
import com.cobblemon.mod.common.command.argument.*
import com.cobblemon.mod.common.config.CobblemonConfig
@@ -76,7 +80,12 @@ import com.cobblemon.mod.common.config.LastChangedVersion
import com.cobblemon.mod.common.config.constraint.IntConstraint
import com.cobblemon.mod.common.config.starter.StarterConfig
import com.cobblemon.mod.common.data.CobblemonDataProvider
import com.cobblemon.mod.common.events.*
import com.cobblemon.mod.common.events.AdvancementHandler
import com.cobblemon.mod.common.events.CallbackHandler
import com.cobblemon.mod.common.events.EntityCallbackHandler
import com.cobblemon.mod.common.events.PokedexHandler
import com.cobblemon.mod.common.events.ServerTickHandler
import com.cobblemon.mod.common.events.StatHandler
import com.cobblemon.mod.common.net.messages.client.settings.ServerSettingsPacket
import com.cobblemon.mod.common.permission.LaxPermissionValidator
import com.cobblemon.mod.common.platform.events.PlatformEvents
@@ -90,11 +99,23 @@ import com.cobblemon.mod.common.pokemon.feature.SlowpokeTailRegrowthSpeciesFeatu
import com.cobblemon.mod.common.pokemon.feature.SlowpokeTailRegrowthSpeciesFeatureProvider
import com.cobblemon.mod.common.pokemon.feature.TagSeasonResolver
import com.cobblemon.mod.common.pokemon.helditem.CobblemonHeldItemManager
import com.cobblemon.mod.common.pokemon.properties.*
import com.cobblemon.mod.common.pokemon.properties.AspectPropertyType
import com.cobblemon.mod.common.pokemon.properties.BattleCloneProperty
import com.cobblemon.mod.common.pokemon.properties.FreezeFrameProperty
import com.cobblemon.mod.common.pokemon.properties.HiddenAbilityPropertyType
import com.cobblemon.mod.common.pokemon.properties.LabelProperty
import com.cobblemon.mod.common.pokemon.properties.NoAIProperty
import com.cobblemon.mod.common.pokemon.properties.UnaspectPropertyType
import com.cobblemon.mod.common.pokemon.properties.UncatchableProperty
import com.cobblemon.mod.common.pokemon.stat.CobblemonStatProvider
import com.cobblemon.mod.common.starter.CobblemonStarterHandler
import com.cobblemon.mod.common.util.*
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.ifDedicatedServer
import com.cobblemon.mod.common.util.isLaterVersion
import com.cobblemon.mod.common.util.party
import com.cobblemon.mod.common.util.requestWallpapers
import com.cobblemon.mod.common.util.server
import com.cobblemon.mod.common.world.feature.CobblemonPlacedFeatures
import com.cobblemon.mod.common.world.feature.ore.CobblemonOrePlacedFeatures
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
@@ -102,6 +123,17 @@ import com.mongodb.ConnectionString
import com.mongodb.MongoClientSettings
import com.mongodb.client.MongoClient
import com.mongodb.client.MongoClients
import java.io.File
import java.io.FileReader
import java.io.FileWriter
import java.io.PrintWriter
import java.util.UUID
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
import kotlin.properties.Delegates
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaField
import net.minecraft.client.Minecraft
import net.minecraft.commands.synchronization.SingletonArgumentInfo
import net.minecraft.resources.ResourceKey
@@ -114,17 +146,6 @@ import net.minecraft.world.level.Level
import net.minecraft.world.level.storage.LevelResource
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import java.io.File
import java.io.FileReader
import java.io.FileWriter
import java.io.PrintWriter
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
import kotlin.properties.Delegates
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaField
object Cobblemon {
const val MODID = CobblemonBuildDetails.MOD_ID
@@ -165,7 +186,7 @@ object Cobblemon {
val statistics: CobblemonStats = CobblemonStats
@JvmStatic
val builtinPacks = listOf<CobblemonPack>(
val builtinPacks = listOf(
CobblemonPack(id = "adorncompatibility", name = "Adorn Compatibility", packType = PackType.CLIENT_RESOURCES, activationBehaviour = ResourcePackActivationBehaviour.ALWAYS_ENABLED, neededMods = setOf("adorn")),
CobblemonPack(id = "gyaradosjump", name = "Gyarados Jump Patterns", packType = PackType.CLIENT_RESOURCES, activationBehaviour = ResourcePackActivationBehaviour.DEFAULT_ENABLED),
CobblemonPack(id = "regionbiasforms", name = "Region Bias Forms", packType = PackType.CLIENT_RESOURCES, activationBehaviour = ResourcePackActivationBehaviour.DEFAULT_ENABLED),
@@ -451,7 +472,7 @@ object Cobblemon {
server()?.getLevel(dimension)
} else {
val mc = Minecraft.getInstance()
return mc.singleplayerServer?.getLevel(dimension) ?: mc.level
mc.singleplayerServer?.getLevel(dimension) ?: mc.level
}
}
@@ -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>()
@@ -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)
}
}
@@ -21,12 +21,15 @@ import com.cobblemon.mod.common.client.net.callback.partymove.OpenPartyMoveCallb
import com.cobblemon.mod.common.client.net.cooking.ToggleCookingPotLidHandler
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.PokemonInteractionsSyncHandler
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.dialogue.DialogueClosedHandler
import com.cobblemon.mod.common.client.net.dialogue.DialogueOpenedHandler
import com.cobblemon.mod.common.client.net.effect.*
import com.cobblemon.mod.common.client.net.effect.PokeSnackBlockParticlesHandler
import com.cobblemon.mod.common.client.net.effect.RunPosableMoLangHandler
import com.cobblemon.mod.common.client.net.effect.SaccharineLogBlockParticlesHandler
import com.cobblemon.mod.common.client.net.effect.SpawnSnowstormEntityParticleHandler
import com.cobblemon.mod.common.client.net.effect.SpawnSnowstormParticleHandler
import com.cobblemon.mod.common.client.net.gui.ExpGainedDataPacketHandler
import com.cobblemon.mod.common.client.net.gui.InteractPokemonUIPacketHandler
import com.cobblemon.mod.common.client.net.gui.PokedexUIPacketHandler
@@ -34,7 +37,11 @@ import com.cobblemon.mod.common.client.net.gui.SummaryUIPacketHandler
import com.cobblemon.mod.common.client.net.npc.CloseNPCEditorHandler
import com.cobblemon.mod.common.client.net.npc.OpenNPCEditorHandler
import com.cobblemon.mod.common.client.net.orientation.ClientboundUpdateOrientationHandler
import com.cobblemon.mod.common.client.net.pasture.*
import com.cobblemon.mod.common.client.net.pasture.ClosePastureHandler
import com.cobblemon.mod.common.client.net.pasture.OpenPastureHandler
import com.cobblemon.mod.common.client.net.pasture.PokemonPasturedHandler
import com.cobblemon.mod.common.client.net.pasture.PokemonUnpasturedHandler
import com.cobblemon.mod.common.client.net.pasture.UpdatePastureConflictFlagHandler
import com.cobblemon.mod.common.client.net.pokedex.ServerConfirmedRegisterHandler
import com.cobblemon.mod.common.client.net.pokemon.update.ClientboundUpdateRidingStateHandler
import com.cobblemon.mod.common.client.net.pokemon.update.PokemonUpdatePacketHandler
@@ -52,7 +59,14 @@ import com.cobblemon.mod.common.client.net.storage.party.SetPartyPokemonHandler
import com.cobblemon.mod.common.client.net.storage.party.SetPartyReferenceHandler
import com.cobblemon.mod.common.client.net.storage.pc.*
import com.cobblemon.mod.common.client.net.toast.ToastPacketHandler
import com.cobblemon.mod.common.client.net.trade.*
import com.cobblemon.mod.common.client.net.trade.TradeAcceptanceChangedHandler
import com.cobblemon.mod.common.client.net.trade.TradeCancelledHandler
import com.cobblemon.mod.common.client.net.trade.TradeCompletedHandler
import com.cobblemon.mod.common.client.net.trade.TradeOfferExpiredHandler
import com.cobblemon.mod.common.client.net.trade.TradeOfferNotificationHandler
import com.cobblemon.mod.common.client.net.trade.TradeProcessStartedHandler
import com.cobblemon.mod.common.client.net.trade.TradeStartedHandler
import com.cobblemon.mod.common.client.net.trade.TradeUpdatedHandler
import com.cobblemon.mod.common.net.PacketRegisterInfo
import com.cobblemon.mod.common.net.messages.client.CalculateSeatPositionsPacket
import com.cobblemon.mod.common.net.messages.client.OpenBehaviourEditorPacket
@@ -69,12 +83,20 @@ import com.cobblemon.mod.common.net.messages.client.data.*
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
import com.cobblemon.mod.common.net.messages.client.effect.*
import com.cobblemon.mod.common.net.messages.client.effect.PokeSnackBlockParticlesPacket
import com.cobblemon.mod.common.net.messages.client.effect.RunPosableMoLangPacket
import com.cobblemon.mod.common.net.messages.client.effect.SaccharineLogBlockParticlesPacket
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormEntityParticlePacket
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormParticlePacket
import com.cobblemon.mod.common.net.messages.client.npc.CloseNPCEditorPacket
import com.cobblemon.mod.common.net.messages.client.npc.OpenNPCEditorPacket
import com.cobblemon.mod.common.net.messages.client.orientation.ClientboundUpdateDriverInputPacket
import com.cobblemon.mod.common.net.messages.client.orientation.ClientboundUpdateOrientationPacket
import com.cobblemon.mod.common.net.messages.client.pasture.*
import com.cobblemon.mod.common.net.messages.client.pasture.ClosePasturePacket
import com.cobblemon.mod.common.net.messages.client.pasture.OpenPasturePacket
import com.cobblemon.mod.common.net.messages.client.pasture.PokemonPasturedPacket
import com.cobblemon.mod.common.net.messages.client.pasture.PokemonUnpasturedPacket
import com.cobblemon.mod.common.net.messages.client.pasture.UpdatePastureConflictFlagPacket
import com.cobblemon.mod.common.net.messages.client.pokedex.ServerConfirmedRegisterPacket
import com.cobblemon.mod.common.net.messages.client.pokemon.update.*
import com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution.AddEvolutionPacket
@@ -94,19 +116,37 @@ import com.cobblemon.mod.common.net.messages.client.storage.party.InitializePart
import com.cobblemon.mod.common.net.messages.client.storage.party.MoveClientPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.party.SetPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.party.SetPartyReferencePacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.*
import com.cobblemon.mod.common.net.messages.client.storage.pc.ClosePCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.InitializePCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.MoveClientPCPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.OpenPCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.RenamePCBoxPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.SetPCBoxPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.SetPCPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.ChangePCBoxWallpaperPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.RequestPCBoxWallpapersPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.SetPCBoxWallpapersPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.UnlockPCBoxWallpaperPacket
import com.cobblemon.mod.common.net.messages.client.toast.ToastPacket
import com.cobblemon.mod.common.net.messages.client.trade.*
import com.cobblemon.mod.common.net.messages.client.trade.TradeAcceptanceChangedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeCancelledPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeCompletedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeOfferExpiredPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeOfferNotificationPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeProcessStartedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeStartedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeUpdatedPacket
import com.cobblemon.mod.common.net.messages.client.ui.ExpGainedDataPacket
import com.cobblemon.mod.common.net.messages.client.ui.InteractPokemonUIPacket
import com.cobblemon.mod.common.net.messages.client.ui.PokedexUIPacket
import com.cobblemon.mod.common.net.messages.client.ui.SummaryUIPacket
import com.cobblemon.mod.common.net.messages.server.*
import com.cobblemon.mod.common.net.messages.server.battle.*
import com.cobblemon.mod.common.net.messages.server.battle.BattleSelectActionsPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamLeavePacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamRequestPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamResponsePacket
import com.cobblemon.mod.common.net.messages.server.battle.RemoveSpectatorPacket
import com.cobblemon.mod.common.net.messages.server.battle.SpectateBattlePacket
import com.cobblemon.mod.common.net.messages.server.behaviour.DamageOnCollisionPacket
import com.cobblemon.mod.common.net.messages.server.behaviour.SetEntityBehaviourPacket
import com.cobblemon.mod.common.net.messages.server.block.AdjustBlockEntityViewerCountPacket
@@ -130,8 +170,13 @@ 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.*
import com.cobblemon.mod.common.net.messages.server.pokemon.update.ServerboundUpdateRidingStatePacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetActiveMarkPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetItemHiddenPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetMarkingsPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetNicknamePacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.evolution.AcceptEvolutionPacket
import com.cobblemon.mod.common.net.messages.server.riding.DismountPokemonPacket
import com.cobblemon.mod.common.net.messages.server.riding.ServerboundUpdateDriverInputPacket
@@ -139,14 +184,25 @@ import com.cobblemon.mod.common.net.messages.server.riding.ServerboundUpdateRide
import com.cobblemon.mod.common.net.messages.server.starter.RequestStarterScreenPacket
import com.cobblemon.mod.common.net.messages.server.storage.SwapPCPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.party.MovePartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.party.PartyTutorialPacket
import com.cobblemon.mod.common.net.messages.server.storage.party.ReleasePartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.party.SwapPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.pc.*
import com.cobblemon.mod.common.net.messages.server.trade.*
import com.cobblemon.mod.common.net.messages.server.trade.AcceptTradeRequestPacket
import com.cobblemon.mod.common.net.messages.server.trade.CancelTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.ChangeTradeAcceptancePacket
import com.cobblemon.mod.common.net.messages.server.trade.OfferTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.PerformTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.UpdateTradeOfferPacket
import com.cobblemon.mod.common.net.serverhandling.ChallengeHandler
import com.cobblemon.mod.common.net.serverhandling.ChallengeResponseHandler
import com.cobblemon.mod.common.net.serverhandling.RequestInteractionsHandler
import com.cobblemon.mod.common.net.serverhandling.battle.*
import com.cobblemon.mod.common.net.serverhandling.battle.BattleSelectActionsHandler
import com.cobblemon.mod.common.net.serverhandling.battle.RemoveSpectatorHandler
import com.cobblemon.mod.common.net.serverhandling.battle.SpectateBattleHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamLeaveHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamRequestHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamRequestResponseHandler
import com.cobblemon.mod.common.net.serverhandling.behaviour.DamageOnCollisionPacketHandler
import com.cobblemon.mod.common.net.serverhandling.behaviour.SetEntityBehaviourHandler
import com.cobblemon.mod.common.net.serverhandling.block.AdjustBlockEntityViewerCountHandler
@@ -165,14 +221,20 @@ import com.cobblemon.mod.common.net.serverhandling.dialogue.InputToDialogueHandl
import com.cobblemon.mod.common.net.serverhandling.evolution.AcceptEvolutionHandler
import com.cobblemon.mod.common.net.serverhandling.npc.SaveNPCHandler
import com.cobblemon.mod.common.net.serverhandling.orientation.ServerboundUpdateOrientationHandler
import com.cobblemon.mod.common.net.serverhandling.storage.party.PartyTutorialPacketHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.PasturePokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.SetPastureConflictHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.UnpastureAllPokemonHandler
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.*
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.ServerboundUpdateRidingStateHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetActiveMarkHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetItemHiddenHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetMarkingsHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetNicknameHandler
import com.cobblemon.mod.common.net.serverhandling.riding.DismountPokemonPacketHandler
import com.cobblemon.mod.common.net.serverhandling.riding.DriverInputPacketHandler
import com.cobblemon.mod.common.net.serverhandling.riding.ServerboundUpdateRiderRotationHandler
@@ -186,7 +248,12 @@ import com.cobblemon.mod.common.net.serverhandling.storage.party.MovePartyPokemo
import com.cobblemon.mod.common.net.serverhandling.storage.party.ReleasePCPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.storage.party.SwapPartyPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.storage.pc.*
import com.cobblemon.mod.common.net.serverhandling.trade.*
import com.cobblemon.mod.common.net.serverhandling.trade.AcceptTradeRequestHandler
import com.cobblemon.mod.common.net.serverhandling.trade.CancelTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.ChangeTradeAcceptanceHandler
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.ServerPlayer
@@ -332,7 +399,7 @@ object CobblemonNetwork {
// Data registries
list.add(PacketRegisterInfo(AbilityRegistrySyncPacket.ID, AbilityRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(CobblemonMechanicsSyncPacket.ID, CobblemonMechanicsSyncPacket::decode, CobblemonMechanicsSyncHandler))
list.add(PacketRegisterInfo(PokemonInteractionsSyncPacket.ID, PokemonInteractionsSyncPacket::decode, PokemonInteractionsSyncHandler))
list.add(PacketRegisterInfo(PokemonInteractionsSyncPacket.ID, PokemonInteractionsSyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(MovesRegistrySyncPacket.ID, MovesRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(BerryRegistrySyncPacket.ID, BerryRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(SpeciesRegistrySyncPacket.ID, SpeciesRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
@@ -460,7 +527,7 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(ReleasePCPokemonPacket.ID, ReleasePCPokemonPacket::decode, ReleasePCPokemonHandler))
list.add(PacketRegisterInfo(UnlinkPlayerFromPCPacket.ID, UnlinkPlayerFromPCPacket::decode, UnlinkPlayerFromPCHandler))
list.add(PacketRegisterInfo(RequestRenamePCBoxPacket.Companion.ID, RequestRenamePCBoxPacket.Companion::decode, RequestRenamePCBoxHandler))
list.add(PacketRegisterInfo(RequestRenamePCBoxPacket.ID, RequestRenamePCBoxPacket::decode, RequestRenamePCBoxHandler))
list.add(PacketRegisterInfo(PCBoxWallpapersPacket.ID, PCBoxWallpapersPacket::decode, PCBoxWallpapersHandler))
list.add(PacketRegisterInfo(RequestChangePCBoxWallpaperPacket.ID, RequestChangePCBoxWallpaperPacket::decode, RequestChangePCBoxWallpaperHandler))
list.add(PacketRegisterInfo(MarkPCBoxWallpapersSeenPacket.ID, MarkPCBoxWallpapersSeenPacket::decode, MarkPCBoxWallpapersSeenHandler))
@@ -496,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))
@@ -518,6 +588,9 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(PartyPokemonMoveSelectedPacket.ID, PartyPokemonMoveSelectedPacket::decode, PartyPokemonMoveSelectedHandler))
list.add(PacketRegisterInfo(PartyMoveSelectCancelledPacket.ID, PartyMoveSelectCancelledPacket::decode, PartyMoveSelectCancelledHandler))
//Party move tutorial toasts packets
list.add(PacketRegisterInfo(PartyTutorialPacket.ID, PartyTutorialPacket::decode, PartyTutorialPacketHandler))
// Dialogue packets
list.add(PacketRegisterInfo(EscapeDialoguePacket.ID, EscapeDialoguePacket::decode, EscapeDialogueHandler))
list.add(PacketRegisterInfo(InputToDialoguePacket.ID, InputToDialoguePacket::decode, InputToDialogueHandler))
@@ -551,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)
}
}
@@ -17,6 +17,7 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangV
import com.cobblemon.mod.common.entity.OmniPathingEntity
import com.cobblemon.mod.common.entity.ai.CobblemonRandomSurfacePos
import com.cobblemon.mod.common.entity.ai.CobblemonWalkTarget
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.mainThreadRuntime
import com.cobblemon.mod.common.util.resolveFloat
@@ -184,6 +185,12 @@ class WanderTaskConfig : SingleTaskConfig {
if (targetVec == null) continue
// Ignore pathing targets outside of the pasture zone if the pokemon is pastured.
val tethering = (entity as? PokemonEntity)?.tethering
if (tethering?.canRoamTo(BlockPos.containing(targetVec)) == false) {
continue
}
pos = applyHeightConstraints(
pos = BlockPos.containing(targetVec),
minimumHeight = minimumHeight,
@@ -499,8 +499,8 @@ open class PokemonBattle(
}
fun stop() {
end()
writeShowdownAction(">forcetie") // This will terminate the Showdown connection
end()
}
fun checkForInputDispatch() {
@@ -59,40 +59,32 @@ object PokemonInteractions : JsonDataRegistry<PokemonInteractionSet> {
.setPrettyPrinting()
.create()
val speciesInteractions = mutableListOf<PokemonInteractionSet>()
val generalInteractions = mutableListOf<PokemonInteractionSet>()
val interactions = mutableListOf<PokemonInteractionSet>()
override fun sync(player: ServerPlayer) {
PokemonInteractionsSyncPacket(
speciesInteractions,
generalInteractions,
).sendToPlayer(player)
PokemonInteractionsSyncPacket(interactions).sendToPlayer(player)
}
override fun reload(data: Map<ResourceLocation, PokemonInteractionSet>) {
speciesInteractions.clear()
generalInteractions.clear()
val split = data.entries.partition { it.value.requirements.any { requirement -> requirement is PokemonPropertiesRequirement && requirement.target.species != null } }
speciesInteractions.addAll(split.first.map {it.value})
generalInteractions.addAll(split.second.map {it.value})
Cobblemon.LOGGER.info("Loaded {} Pokémon interaction sets", data.size)
interactions.clear()
val split = data.entries
.partition {
it.value.requirements.any { requirement -> requirement is PokemonPropertiesRequirement && requirement.target.species != null }
}
// Put the species specific interactions first as they take priority
interactions.addAll(split.first.map {it.value})
interactions.addAll(split.second.map {it.value})
Cobblemon.LOGGER.info("Loaded {} Pokémon interaction sets", interactions.size)
}
fun findInteraction(pokemon: PokemonEntity): PokemonInteraction? {
val setCheck: (PokemonInteractionSet) -> Boolean = { it.requirements.all { req -> req.check(pokemon.pokemon) }}
val interactionCheck: (PokemonInteraction) -> Boolean = { it.requirements.all { req -> req.check(pokemon.pokemon) } && !pokemon.pokemon.isOnInteractionCooldown(it.grouping) }
// species-specific interactions take priority
val validInteractions = speciesInteractions
return interactions
.filter(setCheck)
.flatMap { it.interactions }
.filter(interactionCheck)
.toMutableList()
if (validInteractions.isEmpty()) { // if all species-specific interactions are absent/on cooldown, fallback to more generic ones
validInteractions.addAll(generalInteractions.filter(setCheck).flatMap { it.interactions }
.filter(interactionCheck)
)
}
return validInteractions.randomOrNull()
.firstOrNull(interactionCheck)
}
}
@@ -43,17 +43,9 @@ data class PokemonInteractionSet(
companion object {
fun decode(buffer: FriendlyByteBuf): PokemonInteractionSet {
val requirements = buffer.readList { it ->
CobblemonRequirementAdapter.decode(it)
}
val interactions = buffer.readList { it ->
PokemonInteraction.decode(it)
}
return PokemonInteractionSet(
requirements,
interactions,
)
val requirements = buffer.readList(CobblemonRequirementAdapter::decode)
val interactions = buffer.readList(PokemonInteraction::decode)
return PokemonInteractionSet(requirements, interactions)
}
}
}
@@ -145,6 +145,7 @@ import net.minecraft.world.entity.item.ItemEntity
import net.minecraft.world.entity.monster.Monster
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.enchantment.EnchantmentHelper
import net.minecraft.world.level.ClipContext
import net.minecraft.world.level.GameType
import net.minecraft.world.level.Level
@@ -368,6 +369,11 @@ object MoLangFunctions {
val b = format.parse(dateB)
DoubleValue(a.after(b))
},
"get_move_from_id" to java.util.function.Function { params ->
val moveId = params.getString(0)
val moveTemplate = Moves.getByName(moveId)
return@Function moveTemplate?.struct ?: DoubleValue.ZERO
},
"create_simple_party_provider" to java.util.function.Function { params ->
val partyProvider = SimplePartyProvider()
return@Function partyProvider.struct
@@ -378,6 +384,13 @@ object MoLangFunctions {
val pickupItem = ObtainableItem(item = item, pickupPriority = pickupPriority)
return@Function pickupItem.struct
},
"create_itemstack" to java.util.function.Function { params ->
val itemId = params.getString(0).asIdentifierDefaultingNamespace()
val item = BuiltInRegistries.ITEM.get(itemId) ?: return@Function DoubleValue.ZERO
val count = params.getIntOrNull(1) ?: 1
val itemStack = ItemStack(item, count)
return@Function ObjectValue(itemStack)
},
"file" to java.util.function.Function { MoLangLoadedFilesCache.struct }
)
val biomeFunctions = mutableListOf<(Holder<Biome>) -> HashMap<String, java.util.function.Function<MoParams, Any>>>()
@@ -583,6 +596,62 @@ object MoLangFunctions {
}
return@put items
}
map.put("has_inventory_space") { _ ->
val inventory = player.inventory
return@put if (inventory.getFreeSlot() != -1) DoubleValue.ONE else DoubleValue.ZERO
}
map.put("set_inventory_slot") { params ->
val slot = params.getInt(0)
val inventory = player.inventory
if (slot !in 0 until inventory.containerSize) return@put DoubleValue.ZERO
val value = params.getOrNull<MoValue>(1) ?: return@put DoubleValue.ZERO
val stack: ItemStack? = when (value) {
is ObjectValue<*> -> value.obj as? ItemStack
is StringValue -> {
val id = ResourceLocation.parse(value.value)
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(id) ?: return@put DoubleValue.ZERO
ItemStack(item)
}
else -> null
}
if (stack == null) return@put DoubleValue.ZERO
inventory.setItem(slot, stack)
return@put DoubleValue.ONE
}
map.put("give_item") { params ->
val inventory = player.inventory
val freeSlot = inventory.getFreeSlot()
val value = params.getOrNull<MoValue>(0) ?: return@put DoubleValue.ZERO
val dropIfFull = params.getBooleanOrNull(1) ?: false
val stack: ItemStack? = when (value) {
is ObjectValue<*> -> value.obj as? ItemStack
is StringValue -> {
val id = ResourceLocation.parse(value.value)
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(id) ?: return@put DoubleValue.ZERO
ItemStack(item)
}
else -> null
}
if (stack == null) return@put DoubleValue.ZERO
if (freeSlot == -1) {
if (dropIfFull) {
player.drop(stack, false)
return@put DoubleValue.ONE
} else {
return@put DoubleValue.ZERO
}
}
inventory.setItem(freeSlot, stack)
return@put DoubleValue.ONE
}
map.put("face") { params -> ObjectValue(PlayerDialogueFaceProvider(player.uuid, params.getBooleanOrNull(0) != false)) }
map.put("swing_hand") { _ -> player.swing(player.usedItemHand) }
map.put("food_level") { _ -> DoubleValue(player.foodData.foodLevel) }
@@ -626,8 +695,7 @@ object MoLangFunctions {
}
map.put("active_dialogue") { _ ->
if (player.isInDialogue) {
player.activeDialogue?.dialogueId.toString()
return@put DoubleValue.ONE
return@put player.activeDialogue?.toMoLangStruct() ?: DoubleValue.ZERO
} else {
DoubleValue.ZERO
}
@@ -804,6 +872,8 @@ object MoLangFunctions {
DoubleValue(value)
}
map.put("get_starter_uuid") { _ -> Cobblemon.playerDataManager.getGenericData(player).starterUUID?.let{ StringValue(it.toString()) } ?: DoubleValue.ZERO }
map.put("has_chosen_starter") { _ -> DoubleValue(Cobblemon.playerDataManager.getGenericData(player).starterSelected) }
}
map
}
@@ -824,6 +894,15 @@ object MoLangFunctions {
map.put("is_of") { params -> DoubleValue(holder.`is`(params.getString(0).asIdentifierDefaultingNamespace())) }
map.put("is_in") { params -> DoubleValue(holder.`is`(TagKey.create(Registries.ITEM, params.getString(0).replace("#", "").asIdentifierDefaultingNamespace()))) }
map.put("is_food") { params -> DoubleValue(stack.has(DataComponents.FOOD)) }
map.put("is_enchanted") { params -> DoubleValue(stack.hasFoil()) }
map.put("has_enchantment") { params ->
val enchantmentId = params.getString(0).asIdentifierDefaultingNamespace()
val minLevel = params.getIntOrNull(1) ?: 1
val enchantmentRegistry = registryAccess.registryOrThrow(Registries.ENCHANTMENT)
val holder = enchantmentRegistry.getHolder(enchantmentId).orElse(null) ?: return@put DoubleValue.ZERO
val level = EnchantmentHelper.getItemEnchantmentLevel(holder, stack)
return@put DoubleValue(level >= minLevel)
}
return@mutableListOf map
}
)
@@ -1862,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 ->
@@ -2748,8 +2826,8 @@ object MoLangFunctions {
}
variable
}
else -> null
} ?: throw IllegalArgumentException("Invalid NBT element type: ${nbt.type}")
else -> VariableStruct()
}
}
}
@@ -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,22 +53,26 @@ 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())
.addFunction("name") { StringValue(name) }
.addFunction("display_name") { StringValue(displayName.string) }
.addFunction("description") { StringValue(description.string) }
.addFunction("type") { StringValue(elementalType.showdownId) }
.addFunction("damage_category") { StringValue(damageCategory.name) }
.addFunction("power") { DoubleValue(power) }
.addFunction("target") { StringValue(target.name) }
.addFunction("accuracy") { DoubleValue(accuracy) }
.addFunction("pp") { DoubleValue(pp) }
.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
@@ -86,7 +90,8 @@ open class MoveTemplate(
pp = 5,
priority = 0,
critRatio = 0.0,
effectChances = emptyArray()
effectChances = emptyArray(),
weight = 0F,
)
companion object {
@@ -157,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
@@ -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()
@@ -119,7 +119,10 @@ class RidingController(
private fun canTransitionToLand(): Boolean {
if ((entity.isUnderWater)) return false
if ((entity.isEyeInFluid(FluidTags.WATER) || entity.isEyeInFluid(FluidTags.LAVA))) return false
return entity.onGround()
// If the currently in the air but has no land style then don't transition and render the ride useless.
// Hover is a good example for this.
val shouldStayInAir = entity.pokemon.riding.behaviours?.get(RidingStyle.LAND) == null && context?.style == RidingStyle.AIR
return entity.onGround() && !shouldStayInAir
}
private fun canTransitionToLiquid(): Boolean {
@@ -213,10 +213,23 @@ class HoverBehaviour : RidingBehaviour<HoverSettings, HoverState> {
val accel = topSpeed / (vehicle.runtime.resolveDouble(settings.accelerationExpr ?: globalHover.accelerationExpr!!) * 20.0)
var newVelocity = state.rideVelocity.get()
val unrotateVel = vehicle.deltaMovement.yRot(vehicle.yRot)
val vehicleFrontVec = Vec3(0.0, 0.0, 1.0)
val faceVel = Vec3(vehicle.deltaMovement.x, 0.0, vehicle.deltaMovement.z)
val d = vehicle.deltaMovement.horizontalDistance()
val v2 = Vec3(faceVel.dot(Vec3(1.0, 0.0, 0.0)) * d, newVelocity.y, faceVel.dot(Vec3(0.0, 0.0, 1.0)) * d)
// Check for collisions and if found, update the speed to reflect it
if (vehicle.verticalCollision || vehicle.horizontalCollision) {
state.speed.set(vehicle.deltaMovement.length())
if (vehicle.horizontalCollision) {
val smackingIntoWallSpeed = max( vehicle.deltaMovement.horizontalDistance(), 0.2)
newVelocity = Vec3(newVelocity.x, 0.0, newVelocity.z).normalize().scale(smackingIntoWallSpeed).add(0.0, newVelocity.y, 0.0)
}
if (vehicle.verticalCollision) {
newVelocity = Vec3(newVelocity.x, 0.0, newVelocity.z)
}
// align the velocity vector to be in local vehicle space
@@ -41,10 +41,6 @@ interface CompositeRidingStrategy<T : CompositeSettings> {
toState: RidingBehaviourState,
toSettings: RidingBehaviourSettings
) {
if (vehicle.level().isClientSide) {
val fromSettings = if (settings.defaultBehaviour == toSettings) settings.alternateBehaviour else settings.defaultBehaviour
}
toState.stamina.set(fromState.stamina.get())
toState.rideVelocity.set(fromState.rideVelocity.get())
fromState.reset()
@@ -187,17 +187,16 @@ class HorseBehaviour : RidingBehaviour<HorseSettings, HorseState> {
return
}
var newStam = stam
val stamDrainRate = (1.0f / vehicle.runtime.resolveDouble(settings.staminaExpr ?: globalHorse.staminaExpr!!)).toFloat() / 20.0f
if (state.sprinting.get()) {
newStam = max(0.0f,stam - stamDrainRate)
val newStam = if (state.sprinting.get()) {
max(0.0f,stam - stamDrainRate)
} else {
newStam = min(1.0f,stam + stamDrainRate * 4)
min(1.0f,stam + stamDrainRate * 4)
}
state.stamina.set(newStam)
state.stamina.set(newStam.coerceIn(0F, 1F))
}
override fun updatePassengerRotation(
@@ -100,7 +100,7 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
driver: Player,
input: Vec3
) {
if(vehicle.level().isClientSide) {
if (vehicle.level().isClientSide) {
handleBoosting(state, vehicle)
tickStamina(settings, state, vehicle)
}
@@ -147,7 +147,7 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
val stamDrainRate = (1.0f / boostTime).toFloat()
val newStam = if (state.boosting.get()) max(0.0f,stam - stamDrainRate)
else min(1.0f,stam + stamDrainRate)
else (stam + stamDrainRate).coerceIn(0F, 1F)
state.stamina.set(newStam)
}
@@ -60,7 +60,7 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
val poseProvider = PoseProvider<SubmarineSettings, SubmarineState>(PoseType.FLOAT)
.with(PoseOption(PoseType.SWIM) { _, state, entity -> state.rideVelocity.get().z > 0.05 && !state.onSurface.get()})
.with(PoseOption(PoseType.STAND) { _, state, entity -> state.rideVelocity.get().z <= 0.05 && state.onSurface.get()})
.with(PoseOption(PoseType.STAND) { _, state, entity -> state.rideVelocity.get().z > 0.05 && state.onSurface.get()})
.with(PoseOption(PoseType.SWIM) { _, state, entity -> state.rideVelocity.get().z > 0.05 && state.onSurface.get()})
override fun isActive(settings: SubmarineSettings, state: SubmarineState, vehicle: PokemonEntity): Boolean {
return vehicle.isInWater || !vehicle.onGround()
@@ -191,11 +191,17 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
* Calculate Forward Velocity
*********************************************/
val currZVel = state.rideVelocity.get().z
val newZVel = when {
var newZVel = when {
driver.zza > 0.0 -> min(topSpeed, currZVel + accel)
driver.zza < 0.0 -> max( -strafeSpeed, currZVel - strafeAccel) //TODO: Might need to be applied in addition to the normal braking force?
else -> lerp(currZVel, 0.0, 0.05)
driver.zza < 0.0 -> max( -strafeSpeed, currZVel - strafeAccel)
else -> lerp(currZVel, 0.0, 0.02)
}
// If trying to brake then add the friction to the braking force
if (driver.zza != 0.0f && driver.zza.sign.toDouble() != newZVel.sign) {
newZVel = lerp(newZVel, 0.0, 0.02)
}
currVel = Vec3(currVel.x, currVel.y, newZVel)
/*********************************************
@@ -206,14 +212,20 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
val gravity = (9.8 / ( 20.0)) * 0.2 * 0.4
val terminalYVel = 2.0
val driverYInput = if(driver.jumping) 1.0 else if (driver.isShiftKeyDown) -1.0 else 0.0
val newYVel = when {
var newYVel = when {
(!vehicle.isInWater) -> max(-terminalYVel, currYVel - gravity)
//(vehicle.isInWater && !vehicle.isUnderWater && currYVel > 0.001) -> lerp(currYVel, 0.0, 0.1) // Make sure that yVel is above a threshold before doing this otherwise it'll block submerging.
//driverYInput != 0.0 && currYVel > 0.001 && driverYInput.sign != currYVel.sign -> lerp(currYVel, 0.0, 0.05)
driverYInput > 0.0 -> min(vertTopSpeed, currYVel + strafeAccel)
driverYInput < 0.0 -> max(-vertTopSpeed, currYVel - strafeAccel)
else -> lerp(currYVel, 0.0, 0.05)
else -> lerp(currYVel, 0.0, 0.02)
}
// If trying to brake then add the friction to the braking force
if (driverYInput != 0.0 && driverYInput != newYVel.sign) {
newYVel = lerp(newYVel, 0.0, 0.02)
}
currVel = Vec3(currVel.x, newYVel, currVel.z)
/*********************************************
@@ -221,11 +233,17 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
*********************************************/
val currXVel = state.rideVelocity.get().x
val horzTopSpeed = strafeSpeed
val newXVel = when {
var newXVel = when {
driver.xxa > 0.0 -> min(horzTopSpeed, currXVel + strafeAccel)
driver.xxa < 0.0 -> max( -horzTopSpeed, currXVel - strafeAccel)
else -> lerp(currXVel, 0.0, 0.01)
else -> lerp(currXVel, 0.0, 0.02)
}
// If trying to brake then add the friction to the braking force
if (driver.xxa != 0.0f && driver.xxa.sign.toDouble() != newZVel.sign) {
newXVel = lerp(newXVel, 0.0, 0.02)
}
currVel = Vec3(newXVel, currVel.y, currVel.z)
// Set the new velocity
@@ -305,81 +323,6 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
}
private fun correctOrientation(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity,
controller: OrientationController,
driver: Player,
deltaTime: Double
) {
// TODO: remove if not needed
// // Calculate correcting roll force.
// // If the ride is moving then have the correction rate be an equal proportion of:
// // - how rolled are you
// // - how long has it been since last mouse input (higher is stronger correction)
// // Also the correction is proportional as a whole to how NOT pitched are you so you don't spin endlessly when looking up or down
// val howRolledAmI = sqrt(abs((cos(controller.roll.toRadians()) - 1.0f) * 0.5f)) // sqrt so it trends towards one pretty quickly
// val maxRollCorrectionRate =
// if (state.rideVelocity.get().length() < 0.2) 10.0f * abs(cos(controller.pitch.toRadians()))
// else 10.0f * cos(controller.pitch.toRadians()).pow(2) * (
// howRolledAmI*1.0f +
// (state.noInputTime.get() - rollCorrectionTimer).coerceIn(0.0, 1.0).pow(2).toFloat()*0.5f
// )
// val maxRollForce = maxRollCorrectionRate / 40.0f
// val rollArrivalDeg = 60.0f
// val desiredRoll = 0.0f
//
// // Calculate the signed error (how far and in what direction)
// val rollError = Mth.wrapDegrees(desiredRoll.angleDifference(controller.roll))
//
// // Use the absolute error to calculate the magnitude of the influence
// val influenceMagnitude = (min(rollArrivalDeg, abs(rollError)) / rollArrivalDeg)
// val desiredRollForce = maxRollCorrectionRate * influenceMagnitude * -rollError.sign
// var steeredRollForce = desiredRollForce - state.currRollCorrectionForce.get()
//
// // Ensure the 'steering' correction force doesn't exceed our maxForce
// if (abs(steeredRollForce) > maxRollForce) {
// steeredRollForce = steeredRollForce.sign * maxRollForce
// }
//
// // Apply the 'steered' force (its really not a steering force I need a better name)
// state.currRollCorrectionForce.set(
// state.currRollCorrectionForce.get() + steeredRollForce
// )
//Calculate correcting pitch force.
// val rollCorrectionTimer = vehicle.runtime.resolveDouble(settings.timeToRollCorrect ?: globalSubmarine.timeToRollCorrect!!)
// val maxPitchCorrectionRate = 5.0f * (state.noInputTime.get() - rollCorrectionTimer).coerceIn(0.0, 1.0).pow(2).toFloat()*0.5f
// val maxPitchForce = maxPitchCorrectionRate / 40.0f
// val pitchArrivalDeg = 45.0f
// val desiredPitch = 0.0f
//
// if(state.rideVelocity.get().length() < 0.1 && state.noInputTime.get() > rollCorrectionTimer) {
// val pitchErrror = Mth.wrapDegrees(desiredPitch.angleDifference(controller.pitch))
// val arrivalInfluence = (min(pitchArrivalDeg,abs(pitchErrror)) / pitchArrivalDeg)
// val desiredPitchForce = maxPitchCorrectionRate * arrivalInfluence * -pitchErrror.sign
// var steeredPitchForce = desiredPitchForce - state.currPitchCorrectionForce.get()
//
// // Ensure the 'steering' correction force doesn't exceed our maxForce
// if (abs(steeredPitchForce) > maxPitchForce) {
// steeredPitchForce = steeredPitchForce.sign * maxPitchForce
// }
//
// // Apply the 'steered' force (its really not a steering force I need a better name)
// state.currPitchCorrectionForce.set(
// state.currPitchCorrectionForce.get() + steeredPitchForce
// )
// } else {
// state.currPitchCorrectionForce.set(
// //reduce rollCorrection gradually
// lerp(state.currPitchCorrectionForce.get(), 0.0, deltaTime * 0.98)
// )
// }
}
override fun rotationOnMouseXY(
settings: SubmarineSettings,
state: SubmarineState,
@@ -44,6 +44,7 @@ import com.cobblemon.mod.common.api.spawning.preset.BestSpawnerConfig
import com.cobblemon.mod.common.api.spawning.preset.PokemonSpawnDetailPreset
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import com.cobblemon.mod.common.api.spawning.spawner.BasicSpawner
import com.cobblemon.mod.common.api.spawning.spawner.FishingSpawnerFactory
import com.cobblemon.mod.common.api.spawning.spawner.FixedAreaSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory
@@ -139,19 +140,6 @@ object BestSpawner {
fun onServerStarted(server: MinecraftServer) {
CobblemonSpawnPools.onServerLoad(server)
fishingSpawner = BasicSpawner(
name = "fishing",
spawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL
).also {
it.influences.add(
BucketMultiplyingInfluence(
multipliers = mapOf(
"uncommon" to 2.25f,
"rare" to 5.5f,
"ultra-rare" to 5.5f,
)
)
)
}
fishingSpawner = FishingSpawnerFactory.createSharedSpawner()
}
}
@@ -26,7 +26,7 @@ import net.minecraft.world.entity.Entity
/**
* A [SpawningInfluence] that applies some number of SpawnBait effects.
*
* @author Hiroku, Plastered_Crab
* @author Hiroku, Plastered_Crab, ppVon
* @since March 18th, 2025
*/
open class SpawnBaitInfluence(val effects: List<SpawnBait.Effect>, val onUsed: (time: Int, entity: PokemonEntity?) -> Unit = { _, _ -> }) : SpawningInfluence {
@@ -50,85 +50,84 @@ open class SpawnBaitInfluence(val effects: List<SpawnBait.Effect>, val onUsed: (
}
}
// EV related bait effects
// EV, Type, and Egg Group related bait effects
override fun affectWeight(detail: SpawnDetail, spawnablePosition: SpawnablePosition, weight: Float): Float {
val merged = SpawnBaitUtils.mergeEffects(effects)
val hasRelevantEffects =
merged.any { it.type == Effects.EV } ||
merged.any { it.type == Effects.TYPING } ||
merged.any { it.type == Effects.EGG_GROUP }
if (!hasRelevantEffects) {
return super.affectWeight(detail, spawnablePosition, weight)
}
val pokemonDetail = detail as? PokemonSpawnDetail
?: return super.affectWeight(detail, spawnablePosition, weight)
val detailSpecies = pokemonDetail.pokemon.species
?.let { PokemonSpecies.getByName(it) }
?: return super.affectWeight(detail, spawnablePosition, weight)
val formData = detailSpecies.getForm(pokemonDetail.pokemon.aspects)
var newWeight = weight
var used = false
// if bait exists and any effects are related to EV yields
if (merged.any { it.type == Effects.EV }){
if (detail is PokemonSpawnDetail) {
val detailSpecies = detail.pokemon.species?.let { PokemonSpecies.getByName(it) }
val baitEVStat = effects.firstOrNull { it.type == Effects.EV }?.subcategory?.path?.let { Stats.getStat(it) }
if (merged.any { it.type == Effects.EV }) {
val evEffect = effects.firstOrNull { it.type == Effects.EV }
val baitEVStat = evEffect?.subcategory?.path?.let { Stats.getStat(it) }
if (detailSpecies != null && baitEVStat != null) {
val detailAspects = detail.pokemon.aspects
val formData = detailSpecies.getForm(detailAspects)
val evYieldValue = formData.evYield[baitEVStat]?.toFloat() ?: 0f
return when {
evYieldValue > 0 -> {
markUsed()
super.affectWeight(detail, spawnablePosition, weight)
}
else -> {
markUsed()
super.affectWeight(detail, spawnablePosition, 0f)
}
}
if (baitEVStat != null) {
val evYieldValue = formData.evYield[baitEVStat]?.toFloat() ?: 0f
used = true
if (evYieldValue <= 0f) {
newWeight = 0f
} else {
LOGGER.debug("Boosted weight value");
}
}
}
// if bait exists and any effects are related to Typing
if (merged.any { it.type == Effects.TYPING }){
if (detail is PokemonSpawnDetail) {
val detailSpecies = detail.pokemon.species?.let { PokemonSpecies.getByName(it) }
val baitEffect = effects.firstOrNull { it.type == Effects.TYPING }
val baitTypingEffect = baitEffect?.subcategory?.path?.let { ElementalTypes.get(it) }
if (newWeight > 0f && merged.any { it.type == Effects.TYPING }) {
val baitEffect = effects.firstOrNull { it.type == Effects.TYPING }
val baitTypingEffect = baitEffect?.subcategory?.path?.let { ElementalTypes.get(it) }
if (detailSpecies != null && baitTypingEffect != null) {
val detailAspects = detail.pokemon.aspects
val formData = detailSpecies.getForm(detailAspects)
val isMatchingType = formData.types.contains(baitTypingEffect)
return when {
isMatchingType -> {
markUsed()
super.affectWeight(detail, spawnablePosition, weight * baitEffect.value.toFloat())
}
else -> super.affectWeight(detail, spawnablePosition, weight)
}
if (baitEffect != null && baitTypingEffect != null) {
val isMatchingType = formData.types.contains(baitTypingEffect)
used = true
if (isMatchingType) {
newWeight *= baitEffect.value.toFloat()
}
}
}
// if bait exists and any effects are related to Egg Groups
if (merged.any { it.type == Effects.EGG_GROUP }) {
if (detail is PokemonSpawnDetail) {
val detailSpecies = detail.pokemon.species?.let { PokemonSpecies.getByName(it) }
if (newWeight > 0f && merged.any { it.type == Effects.EGG_GROUP }) {
val eggGroupEffects = effects.filter { it.type == Effects.EGG_GROUP }
if (detailSpecies != null) {
val detailAspects = detail.pokemon.aspects
val formData = detailSpecies.getForm(detailAspects)
// Collect all the egg group effects
val eggGroupEffects = effects.filter { it.type == Effects.EGG_GROUP }
// Check if any of the egg group effects match the species' egg groups
val matchingEffect = eggGroupEffects.firstOrNull { effect ->
val effectEggGroupKey = effect.subcategory?.path ?: return@firstOrNull false
val eggGroup = EggGroup.fromIdentifier(effectEggGroupKey)
if (eggGroup == null) {
LOGGER.warn("Unknown egg group identifier: $effectEggGroupKey")
return@firstOrNull false
}
formData.eggGroups.contains(eggGroup)
}
if (matchingEffect != null) {
markUsed()
val multiplier = matchingEffect.value
return super.affectWeight(detail, spawnablePosition, (weight * multiplier).toFloat())
}
val matchingEffect = eggGroupEffects.firstOrNull { effect ->
val key = effect.subcategory?.path ?: return@firstOrNull false
val eggGroup = EggGroup.fromIdentifier(key)
if (eggGroup == null) {
LOGGER.warn("Unknown egg group identifier: $key")
return@firstOrNull false
}
formData.eggGroups.contains(eggGroup)
}
if (matchingEffect != null) {
used = true
newWeight *= matchingEffect.value.toFloat()
}
}
return super.affectWeight(detail, spawnablePosition, weight)
if (used) markUsed()
return super.affectWeight(detail, spawnablePosition, newWeight)
}
}
@@ -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,
@@ -0,0 +1,86 @@
/*
* 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.spawning.spawner
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnRules
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.BucketMultiplyingInfluence
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence.Companion.TYPICAL_VARIATION
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.util.mutableLazy
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.item.ItemStack
import com.cobblemon.mod.common.api.spawning.BestSpawner;
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
/**
* This isn't really a factory, one FishingSpawner is created as a field on [BestSpawner] on
* server start. However, this emulates the systems that PlayerSpawner uses so extenders can
* add influences that affect all fishing encounters.
*
* Responsible for creating [BasicSpawner]s for Fishing with whatever appropriate settings.
* You can swap over the spawn pool and the influences here, and it will apply to all newly-generated
* [BasicSpawner]s for Fishing. Has a global every-cast influence list, as well as a context-sensitive
* influence list.
*
* @author Andy
* @since January 23rd, 2026
*/
object FishingSpawnerFactory {
var spawnPool: SpawnPool by mutableLazy {
CobblemonSpawnPools.WORLD_SPAWN_POOL
}
data class Context(
val player: ServerPlayer,
val rodStack: ItemStack,
val lureLevel: Int,
val luckOfTheSeaLevel: Int,
val world: ServerLevel,
val pos: BlockPos,
val spawner: BasicSpawner
)
/**
* List of influences for a new cast of the fishing rod. Context is provided at
* build time in [buildPositionInfluences]
*/
var positionInfluenceBuilders = mutableListOf<(ctx: Context) -> SpawningInfluence?>(
{ ctx -> PlayerLevelRangeInfluence(ctx.player, TYPICAL_VARIATION) },
{ ctx -> BucketNormalizingInfluence(tier = ctx.lureLevel + ctx.luckOfTheSeaLevel) }
)
fun createSharedSpawner(): BasicSpawner {
return BasicSpawner(
name = "fishing",
spawnPool = spawnPool
).also { spawner ->
spawner.influences += BucketMultiplyingInfluence(
multipliers = mapOf(
"uncommon" to 2.25f,
"rare" to 5.5f,
"ultra-rare" to 5.5f,
)
)
}
}
/**
* Builds the per-cast influences for [FishingSpawnablePosition]
*/
fun buildPositionInfluences(ctx: Context): MutableList<SpawningInfluence> {
return positionInfluenceBuilders.mapNotNullTo(mutableListOf()) { it(ctx) }
}
}
@@ -0,0 +1,101 @@
/*
* 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.spawning.spawner
import com.cobblemon.mod.common.api.fishing.SpawnBait
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnRules
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.BucketMultiplyingInfluence
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawnBaitInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.rules.SpawnRule
import com.cobblemon.mod.common.util.mutableLazy
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
/**
* Responsible for creating [FixedAreaSpawner]s for PokeSnacks with whatever appropriate settings.
* You can swap over the spawn pool and the influences here, and it will apply to all newly-generated
* [FixedAreaSpawner]s for PokeSnacks.
*
* @author Andy
* @since January 23rd, 2026
*/
object PokeSnackSpawnerFactory {
var spawnPool: SpawnPool by mutableLazy {
CobblemonSpawnPools.WORLD_SPAWN_POOL
}
data class Context(
val world: ServerLevel,
val pos: BlockPos,
val horizontalRadius: Int,
val verticalRadius: Int,
val maxPokemonPerChunk: Float,
val name: String,
val baitEffects: List<SpawnBait.Effect>
)
/**
* The list of influences for a new [FixedAreaSpawner] for a PokeSnack.
* The influences will be added to the spawner in the order they are added to this list.
*/
var influenceBuilders = mutableListOf<(ctx: Context) -> SpawningInfluence?>(
{ ctx ->
val stackedLureTier = ctx.baitEffects
.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }
.sumOf { it.value }
.toInt()
if (stackedLureTier <= 0) null else BucketNormalizingInfluence(
tier = stackedLureTier,
gradient = 0.2F,
firstTier = 1.2F
)
},
{ _ ->
BucketMultiplyingInfluence(
mapOf(
"uncommon" to 2.25f,
"rare" to 5.5f,
"ultra-rare" to 5.5f,
)
)
},
{ ctx ->
SpawnBaitInfluence(effects = ctx.baitEffects)
},
)
fun create(ctx: Context): FixedAreaSpawner {
val influences = influenceBuilders.mapNotNull { it(ctx) }
return FixedAreaSpawner(
name = ctx.name,
spawnPool = spawnPool,
world = ctx.world,
position = ctx.pos,
horizontalRadius = ctx.horizontalRadius,
verticalRadius = ctx.verticalRadius,
maxPokemonPerChunk = ctx.maxPokemonPerChunk
).also { spawner ->
spawner.influences.addAll(influences)
// TODO(Andy): PokeSnacks currently bypass pokemon spawn rules, should that be true?
// Adding this rule will change that
spawner.influences.addAll(
CobblemonSpawnRules.rules.values
.filter(SpawnRule::enabled)
.flatMap(SpawnRule::components)
)
}
}
}
@@ -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
)
@@ -28,6 +28,7 @@ data class GeneralPlayerData(
var starterUUID: UUID?,
var keyItems: MutableSet<ResourceLocation>,
var battleTheme: ResourceLocation?,
var partySelectTutorialDone: Boolean = false,
val extraData: MutableMap<String, PlayerDataExtension>,
) : InstancedPlayerData {
var advancementData: PlayerAdvancementData = PlayerAdvancementData()
@@ -53,7 +54,8 @@ data class GeneralPlayerData(
starterSelected,
starterUUID,
advancementData.totalBattleVictoryCount == 0,
battleTheme
battleTheme,
partySelectTutorialDone
)
}
@@ -36,7 +36,8 @@ class PlayerDataJsonBackend: JsonBackedPlayerDataStoreBackend<GeneralPlayerData>
starterUUID = null,
keyItems = Cobblemon.config.defaultKeyItems,
extraData = mutableMapOf(),
battleTheme = CobblemonSounds.PVP_BATTLE.location
battleTheme = CobblemonSounds.PVP_BATTLE.location,
partySelectTutorialDone = false
)}
override val gson = GsonBuilder()
@@ -37,7 +37,8 @@ class PlayerDataMongoBackend(mongoClient: MongoClient, databaseName: String, col
starterUUID = null,
keyItems = Cobblemon.config.defaultKeyItems,
extraData = mutableMapOf(),
battleTheme = CobblemonSounds.PVP_BATTLE.location
battleTheme = CobblemonSounds.PVP_BATTLE.location,
partySelectTutorialDone = false
)}
override val gson = GsonBuilder()
@@ -34,7 +34,8 @@ data class ClientGeneralPlayerData(
var starterSelected: Boolean = false,
var starterUUID: UUID? = null,
var showChallengeLabel: Boolean = true,
val battleTheme: ResourceLocation? = null
val battleTheme: ResourceLocation? = null,
var partySelectTutorialDone: Boolean = false
) : ClientInstancedPlayerData {
override fun encode(buf: RegistryFriendlyByteBuf) {
@@ -46,6 +47,7 @@ data class ClientGeneralPlayerData(
buf.writeNullable(starterUUID) { pb, value -> pb.writeString(value.toString()) }
buf.writeNullable(resetStarters) { pb, value -> pb.writeBoolean(value) }
buf.writeNullable(battleTheme) {pb, value -> pb.writeIdentifier(value)}
buf.writeBoolean(partySelectTutorialDone)
}
companion object {
fun decode(buffer: RegistryFriendlyByteBuf): SetClientPlayerDataPacket {
@@ -57,6 +59,7 @@ data class ClientGeneralPlayerData(
val starterUUID = buffer.readNullable { it.readString() }?.let { UUID.fromString(it) }
val resetStarterPrompt = buffer.readNullable { it.readBoolean() }
val battleTheme = buffer.readNullable { it.readIdentifier() }
val partySelectTutorialDone = buffer.readBoolean()
val data = ClientGeneralPlayerData(
resetStarterPrompt,
promptStarter,
@@ -64,7 +67,8 @@ data class ClientGeneralPlayerData(
starterSelected,
starterUUID,
showChallengeLabel,
battleTheme
battleTheme,
partySelectTutorialDone
)
//Weird to do this, but since the flag doesn't get passed to the decoded obj, do it here
//Should be fine, as long as decode doesn't get run on the server for some reason
@@ -221,6 +221,7 @@ object BattleRegistry {
fun closeBattle(battle: PokemonBattle) {
battle.onEndHandlers.forEach { it(battle) }
battleMap.remove(battle.battleId)
ShowdownService.service.endBattle(battle)
}
@JvmStatic
@@ -18,7 +18,7 @@ import java.util.UUID
import net.minecraft.network.chat.MutableComponent
import net.minecraft.network.chat.Component
class MultiPokemonBattleActor(
open class MultiPokemonBattleActor(
pokemonList: List<BattlePokemon>,
artificialDecider: BattleAI = RandomBattleAI(),
uuid: UUID = UUID.randomUUID()
@@ -24,17 +24,14 @@ import net.minecraft.network.chat.MutableComponent
import net.minecraft.network.chat.Component
import net.minecraft.world.phys.Vec3
class PokemonBattleActor(
open class PokemonBattleActor(
uuid: UUID,
val pokemon: BattlePokemon,
override val fleeDistance: Float,
artificialDecider: BattleAI = RandomBattleAI()
artificialDecider: BattleAI = RandomBattleAI(),
) : AIBattleActor(uuid, listOf(pokemon), artificialDecider), EntityBackedBattleActor<PokemonEntity>, FleeableBattleActor {
override val initialPos: Vec3?
init {
initialPos = entity?.position()
}
override val initialPos: Vec3? = pokemon.entity?.position()
override fun getName() = pokemon.effectedPokemon.species.translatedName
override fun nameOwned(name: String): MutableComponent = Component.literal(name)
override fun getWorldAndPosition(): Pair<ServerLevel, Vec3>? {
@@ -17,7 +17,7 @@ import com.cobblemon.mod.common.util.battleLang
import java.util.UUID
import net.minecraft.network.chat.MutableComponent
class TrainerBattleActor(
open class TrainerBattleActor(
val trainerName: String,
uuid: UUID,
pokemonList: List<BattlePokemon>,
@@ -31,7 +31,6 @@ import kotlin.collections.associateWith
import kotlin.collections.partition
import kotlin.math.absoluteValue
/**
* AI that tries to choose the best move for the given situations. Based off of the Pokemon Trainer Tournament Simulator Github
* https://github.com/cRz-Shadows/Pokemon_Trainer_Tournament_Simulator/blob/main/pokemon-showdown/sim/examples/Simulation-test-1.ts#L330
@@ -49,7 +48,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
private val hpWeightConsideration = 0.25 // how much HP difference is a consideration for switchins
private val hpFractionCoefficient = 0.4 // how much HP differences should be taken into account for switch ins
private val boostWeightCoefficient = 1 // the amount of boosts considered a baseline to be removed
private val switchOutMatchupThreshold = 0 // todo change this to get it feeling just right (-7 never switches)
private val switchOutMatchupThreshold = -2 // todo change this to get it feeling just right (-7 never switches)
private val selfKoMoveMatchupThreshold = 0.3
private val trickRoomThreshold = 85
private val recoveryMoveThreshold = 0.50
@@ -219,19 +218,14 @@ class StrongBattleAI(skill: Int) : BattleAI {
.filter { it.second.canBeSentOut() }
if (forceSwitch || activeBattlePokemon.isGone()) {
if (battle.turn == 1) {
val switchTo = activeBattlePokemon.actor.pokemonList.filter { it.canBeSentOut() }.randomOrNull()
?: return DefaultActionResponse()
switchTo.willBeSwitchedIn = true
return SwitchActionResponse(switchTo.uuid)
}
else {
val bestEstimation = availableSwitches.maxByOrNull { estimateMatchup(activeBattlePokemon, aiSide, battle, it.first) }
?: return PassActionResponse
// When forced to switch, pick the Pokemon with the best matchup estimation
val bestSwitchScore = availableSwitches.maxOfOrNull { estimateMatchup(activeBattlePokemon, aiSide, battle, it.first) }
?: return PassActionResponse
val bestSwitch = availableSwitches.firstOrNull { estimateMatchup(activeBattlePokemon, aiSide, battle, it.first) == bestSwitchScore }
?: return PassActionResponse
bestEstimation.second.willBeSwitchedIn = true
return SwitchActionResponse(bestEstimation.second.uuid)
}
bestSwitch.second.willBeSwitchedIn = true
return SwitchActionResponse(bestSwitch.second.uuid)
}
if (moveset == null) {
return PassActionResponse
@@ -259,9 +253,9 @@ class StrongBattleAI(skill: Int) : BattleAI {
if (!checkSkillLevel()){
val move = availableMoves
.filter { it.first.target.targetList(activeBattlePokemon)?.isEmpty() != true }
.randomOrNull()
?: return MoveActionResponse("struggle")
.filter { it.first.target.targetList(activeBattlePokemon)?.isEmpty() != true }
.randomOrNull()
?: return MoveActionResponse("struggle")
return chooseMove(move.first, activeBattlePokemon)
}
@@ -270,7 +264,6 @@ class StrongBattleAI(skill: Int) : BattleAI {
if (checkSwitchOutSkill() && shouldSwitchOut(aiSide, battle, activeBattlePokemon, moveset)) {
considerSwitching(activeBattlePokemon, activeTrackerPokemon, opponents, availableMoves, availableSwitches, battle, aiSide)
}
activeTrackerPokemon.firstTurn = false
// If HP is below 30% and not switching out, always use the most damaging move
if (activeTrackerPokemon.currentHpPercent < 0.3 && !shouldSwitchOut(aiSide, battle, activeBattlePokemon, moveset)) {
@@ -296,8 +289,6 @@ class StrongBattleAI(skill: Int) : BattleAI {
return chooseMove(fakeOut.first, activeBattlePokemon, targets.firstOrNull { validTarget != null && validTarget.id == it.battlePokemon!!.uuid})
}
activeTrackerPokemon.firstTurn = false
// Explosion/Self destruct
availableMoves.firstOrNull {
@@ -315,7 +306,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Deal with non-weather related field changing effects
for (move in availableMoves) {
// Tailwind
if (move.first.id == "tailwind" && move.first.id != activeTracker.alliedSide.tailwindCondition &&
availableSwitches.size > 2) {
@@ -324,21 +315,21 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Trick room
if (move.first.id == "trickroom" && move.first.id != activeTracker.currentRoom
&& availableSwitches.count { statEstimationActive(it.first, Stats.SPEED) <= trickRoomThreshold } >= 2) {
&& availableSwitches.count { statEstimationActive(it.first, Stats.SPEED) <= trickRoomThreshold } >= 2) {
return chooseMove(move.first, activeBattlePokemon)
}
// todo find a way to get list of active screens
// Aurora veil
if (move.first.id == "auroraveil" && move.first.id != activeTracker.alliedSide.screenCondition
&& activeTracker.currentWeather in listOf("hail", "snow")) {
&& activeTracker.currentWeather in listOf("hail", "snow")) {
return chooseMove(move.first, activeBattlePokemon)
}
// todo find a way to get list of active screens
// Light Screen
if (move.first.id == "lightscreen" && move.first.id != activeTracker.alliedSide.screenCondition
&& opponents.any { (it.species?.baseStats?.get(Stats.SPECIAL_ATTACK) ?: 0) > (it.species?.baseStats?.get(Stats.ATTACK) ?: 0) }
&& opponents.any { (it.species?.baseStats?.get(Stats.SPECIAL_ATTACK) ?: 0) > (it.species?.baseStats?.get(Stats.ATTACK) ?: 0) }
&& availableSwitches.size > 1) {
return chooseMove(move.first, activeBattlePokemon)
}
@@ -356,13 +347,13 @@ class StrongBattleAI(skill: Int) : BattleAI {
for (move in availableMoves) {
// Setup
if (nOppRemainingMons >= 3 && move.first.id in AIUtility.entryHazards
&& !activeTracker.opponentSide.sideHazards.contains(move.first.id)) {
&& !activeTracker.opponentSide.sideHazards.contains(move.first.id)) {
return chooseMove(move.first, activeBattlePokemon)
}
// Removal
if (nRemainingMons >= 2 && move.first.id in AIUtility.antiHazardsMoves
&& activeTracker.alliedSide.sideHazards.isNotEmpty()) {
&& activeTracker.alliedSide.sideHazards.isNotEmpty()) {
return chooseMove(move.first, activeBattlePokemon)
}
}
@@ -372,10 +363,10 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Court Change
for (move in availableMoves) {
if (move.first.id == "courtchange"
&& (!AIUtility.entryHazards.none { it in activeTracker.alliedSide.sideHazards }
|| setOf("tailwind", "lightscreen", "reflect").any { it in activeTracker.opponentSide.sideHazards })
&& setOf("tailwind", "lightscreen", "reflect").none { it in activeTracker.opponentSide.sideHazards }
&& AIUtility.entryHazards.none { it in activeTracker.opponentSide.sideHazards }) {
&& (!AIUtility.entryHazards.none { it in activeTracker.alliedSide.sideHazards }
|| setOf("tailwind", "lightscreen", "reflect").any { it in activeTracker.opponentSide.sideHazards })
&& setOf("tailwind", "lightscreen", "reflect").none { it in activeTracker.opponentSide.sideHazards }
&& AIUtility.entryHazards.none { it in activeTracker.opponentSide.sideHazards }) {
return chooseMove(move.first, activeBattlePokemon)
}
}
@@ -383,7 +374,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Strength Sap
for (move in availableMoves) {
if (move.first.id == "strengthsap" && activeTrackerPokemon.currentHpPercent < 0.5
&& activeTrackerPokemon.species!!.baseStats.getOrDefault(Stats.ATTACK, 0) > 80) {
&& activeTrackerPokemon.species!!.baseStats.getOrDefault(Stats.ATTACK, 0) > 80) {
return chooseMove(move.first, activeBattlePokemon)
}
}
@@ -391,10 +382,10 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Belly Drum
for (move in availableMoves) {
if (move.first.id == "bellydrum"
&& (activeTrackerPokemon.currentHpPercent > 0.6
&& activeTrackerPokemon.pokemon!!.heldItem().item == CobblemonItems.SITRUS_BERRY
|| activeTrackerPokemon.currentHpPercent > 0.8)
&& activeTrackerPokemon.boosts.getOrDefault(Stats.ATTACK, 0) < 1) {
&& (activeTrackerPokemon.currentHpPercent > 0.6
&& activeTrackerPokemon.pokemon!!.heldItem().item == CobblemonItems.SITRUS_BERRY
|| activeTrackerPokemon.currentHpPercent > 0.8)
&& activeTrackerPokemon.boosts.getOrDefault(Stats.ATTACK, 0) < 1) {
return chooseMove(move.first, activeBattlePokemon)
}
}
@@ -404,8 +395,8 @@ class StrongBattleAI(skill: Int) : BattleAI {
for (move in availableMoves) {
AIUtility.weatherSetupMoves[move.first.id]?.let { requiredWeather ->
if (activeTracker.currentWeather != requiredWeather.lowercase() &&
!(activeTracker.currentWeather == "primordialsea" && requiredWeather == "raindance") &&
!(activeTracker.currentWeather == "desolateland" && requiredWeather == "sunnyday")) {
!(activeTracker.currentWeather == "primordialsea" && requiredWeather == "raindance") &&
!(activeTracker.currentWeather == "desolateland" && requiredWeather == "sunnyday")) {
return chooseMove(move.first, activeBattlePokemon)
}
}
@@ -431,8 +422,8 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Stall out side conditions
if ((activeTracker.opponentSide.screenCondition != null || activeTracker.opponentSide.tailwindCondition != null) &&
(activeTracker.alliedSide.screenCondition == null || activeTracker.alliedSide.tailwindCondition == null) ||
opponents.any { it.currentStatus == null } &&
activeTrackerPokemon.protectCount == 0 && opponents.none { it.currentAbility?.name == "unseenfist"}) {
opponents.any { it.currentStatus == null } &&
activeTrackerPokemon.protectCount == 0 && opponents.none { it.currentAbility?.name == "unseenfist"}) {
activeTrackerPokemon.protectCount = 3
return chooseMove(move.first, activeBattlePokemon)
}
@@ -460,10 +451,10 @@ class StrongBattleAI(skill: Int) : BattleAI {
activeTrackerPokemon.firstTurn = false
val move = availableMoves
.filter { it.first.canBeUsed() }
.filter { it.first.mustBeUsed() || it.first.target.targetList(activeBattlePokemon)?.isEmpty() != true }
.randomOrNull()
?: return MoveActionResponse("struggle")
.filter { it.first.canBeUsed() }
.filter { it.first.mustBeUsed() || it.first.target.targetList(activeBattlePokemon)?.isEmpty() != true }
.randomOrNull()
?: return MoveActionResponse("struggle")
return chooseMove(move.first, activeBattlePokemon)
}
@@ -520,6 +511,10 @@ class StrongBattleAI(skill: Int) : BattleAI {
return false
val activeTrackerPokemon = activeTracker.alliedSide.activePokemon.first { it.id == activeBattlePokemon.battlePokemon!!.uuid }
// Don't switch out if we just switched in
if (activeTrackerPokemon.firstTurn)
return false
val actorTracker = activeTracker.alliedSide.actors.first { activeTrackerPokemon in it.activePokemon }
val availableSwitches = actorTracker.party.filter { it.currentHp!! > 0 }
val currentAbility = activeTrackerPokemon.pokemon!!.ability
@@ -532,7 +527,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Only switch if a switch-in is significantly better than the current Pokémon
val currentScore = estimateMatchup(activeBattlePokemon, side, battle)
val bestSwitchScore = availableSwitches.maxOfOrNull { estimateMatchup(activeBattlePokemon, side, battle, it) } ?: currentScore
val improvementThreshold = currentScore.absoluteValue * 0.25 + 2
val improvementThreshold = currentScore.absoluteValue * 0.5 + 3
if (bestSwitchScore <= currentScore + improvementThreshold) {
return false
}
@@ -567,7 +562,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
if (availableSwitches.isEmpty() || availableSwitches.none { estimateMatchup(activeBattlePokemon, side, battle, it) > 0 })
return false
// ...and a 'good' reason to switch out
if (opponentActiveTracker.any { it.boosts.getOrDefault(Stats.ACCURACY, 0) <= accuracySwitchThreshold } ||
@@ -995,6 +990,4 @@ class StrongBattleAI(skill: Int) : BattleAI {
return MoveActionResponse(move.id, chosenTarget.getPNX())
}
}
}
}
@@ -132,6 +132,7 @@ class TrackerActor(val uuid: UUID) {
actor.activePokemon.filter { newActive -> activePokemon.none { it.id == newActive.battlePokemon?.uuid } }.forEach {
val active = party.firstOrNull { trackMon -> trackMon.id == it.battlePokemon?.uuid } ?: return@forEach
party.remove(active)
active.firstTurn = true // Reset firstTurn when switching in
activePokemon.add(active)
}
val swappedOut = activePokemon.filter { trackMon -> actor.activePokemon.none { it.battlePokemon?.uuid == trackMon.id } }
@@ -145,6 +146,7 @@ class TrackerActor(val uuid: UUID) {
actor.activePokemon.filter { newActive -> activePokemon.none { it.id == newActive.battlePokemon?.uuid } }.forEach {
val active = party.firstOrNull { trackMon -> trackMon.id == it.battlePokemon?.uuid } ?: return@forEach
party.remove(active)
active.firstTurn = true // Reset firstTurn when switching in
activePokemon.add(active)
}
val swappedOut = activePokemon.filter { trackMon -> actor.activePokemon.none { it.battlePokemon?.uuid == trackMon.id } }
@@ -27,6 +27,7 @@ interface ShowdownService {
fun openConnection()
fun closeConnection()
fun startBattle(battle: PokemonBattle, messages: Array<String>)
fun endBattle(battle: PokemonBattle)
fun send(battleId: UUID, messages: Array<String>)
fun getRegistryData(type: String): JsonArray
fun sendRegistryData(data: Map<String, String>, type: String)
@@ -129,6 +129,11 @@ class GraalShowdownService : ShowdownService {
startBattleFunction.execute(this, battle.battleId.toString(), messages)
}
override fun endBattle(battle: PokemonBattle) {
val endBattleFunction = context.getBindings("js").getMember("endBattle")
endBattleFunction.execute(battle.battleId.toString())
}
override fun send(battleId: UUID, messages: Array<String>) {
sendToShowdown(battleId, messages)
}
@@ -62,6 +62,11 @@ class SocketShowdownService(val host: String = "localhost", val port: Int = 1846
send(battle.battleId, messages)
}
override fun endBattle(battle: PokemonBattle) {
writer.write(">endbattle ${battle.battleId}\n")
acknowledge { Cobblemon.LOGGER.error("Failed to end battle ${battle.battleId}!") }
}
override fun send(battleId: UUID, messages: Array<String>) {
for (message in messages) {
writer.write("$battleId~$message\n")
@@ -133,7 +133,10 @@ class ApricornBlock(settings: Properties, val apricorn: Apricorn) : HorizontalDi
override fun isBonemealSuccess(world: Level, random: RandomSource, pos: BlockPos, state: BlockState) = true
override fun performBonemeal(world: ServerLevel, random: RandomSource, pos: BlockPos, state: BlockState) {
world.setBlock(pos, state.setValue(AGE, state.getValue(AGE) + 1), 2)
val curAge = state.getValue(AGE)
val newAge = curAge + 1
if (newAge > MAX_AGE) return
world.setBlock(pos, state.setValue(AGE, newAge), UPDATE_CLIENTS)
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
@@ -186,17 +189,17 @@ class ApricornBlock(settings: Properties, val apricorn: Apricorn) : HorizontalDi
* This uses [Block.dropResources] to handle the drops.
* It will also reset the [BlockState] of this block at the given location to the start of growth.
*
* @param world The [World] the apricorn is in.
* @param world The [Level] the apricorn is in.
* @param state The [BlockState] of the apricorn.
* @param pos The [BlockPos] of the apricorn.
* @return The [BlockState] after harvest.
*/
fun harvest(world: Level, state: BlockState, pos: BlockPos): BlockState {
// Uses loot tables, to change the drops use 'data/cobblemon/loot_tables/blocks/<color>_apricorn.json'
Block.dropResources(state, world, pos)
dropResources(state, world, pos)
// Don't use default as we want to keep the facing
val resetState = state.setValue(AGE, MIN_AGE)
world.setBlock(pos, resetState, Block.UPDATE_CLIENTS)
world.setBlock(pos, resetState, UPDATE_CLIENTS)
return resetState
}
@@ -81,7 +81,32 @@ class BerryBlock(private val berryIdentifier: ResourceLocation, settings: Proper
override fun isBonemealSuccess(world: Level, random: RandomSource, pos: BlockPos, state: BlockState) = !this.isMaxAge(state)
override fun <T : BlockEntity> getTicker(world: Level, blockState: BlockState, blockWithEntityType: BlockEntityType<T>): BlockEntityTicker<T>? = createTickerHelper(blockWithEntityType, CobblemonBlockEntities.BERRY, BerryBlockEntity.TICKER)
// Use Random Tick instead of BlockEntityTicker for performance optimization
override fun <T : BlockEntity> getTicker(world: Level, blockState: BlockState, blockWithEntityType: BlockEntityType<T>): BlockEntityTicker<T>? = null
// Enable Random Tick: only immature and non-rooted berries need to tick
override fun isRandomlyTicking(state: BlockState): Boolean {
return state.getValue(AGE) < FRUIT_AGE && !state.getValue(IS_ROOTED)
}
// Random Tick handler: uses time compensation to ensure correct growth speed
@Deprecated("Deprecated in Java")
override fun randomTick(state: BlockState, world: ServerLevel, pos: BlockPos, random: RandomSource) {
val blockEntity = world.getBlockEntity(pos) as? BerryBlockEntity ?: return
// processTick calculates elapsed time and updates timer, returns whether growth is needed
if (blockEntity.processTick(world, pos, state) && !world.getBlockTicks().hasScheduledTick(pos, this)) {
// Schedule growth for next tick to avoid tick scheduler conflicts during randomTick
world.scheduleTick(pos, this, 1)
}
}
// Scheduled tick handler: performs the actual growth
@Deprecated("Deprecated in Java")
override fun tick(state: BlockState, world: ServerLevel, pos: BlockPos, random: RandomSource) {
if (state.getValue(IS_ROOTED)) return
growHelper(world, random, pos, state)
}
init {
registerDefaultState(stateDefinition.any()
@@ -1,42 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.block
import net.minecraft.world.item.Item
import net.minecraft.core.dispenser.DispenseItemBehavior
import net.minecraft.world.item.Items
import net.minecraft.world.item.alchemy.PotionContents
import net.minecraft.world.item.alchemy.Potions
import net.minecraft.world.level.block.DispenserBlock
object DispenserBehaviorRegistry {
private val behaviors = mutableMapOf<Item, DispenseItemBehavior>()
fun register(items: List<Item>, behaviorProvider: (Item) -> DispenseItemBehavior) {
for (item in items) {
behaviors[item] = behaviorProvider(item)
}
}
fun applyBehaviors() {
behaviors.forEach { (item, behavior) ->
DispenserBlock.registerBehavior(item, behavior)
}
}
// TODO: Seems like there can only 1 behavior per item, so registering the leaves with the honey bottle overrode the honey bottle behavior for the log.
// Registering a new behavior with `DispenserBlock.registerBehavior` will override any existing ones for that item, possibly even ones from Minecraft and other mods
// Will probably need to use an alternate way of implementing this to avoid conflicts.
fun registerDispenserBehaviors() {
// register(listOf(PotionContents.createItemStack(Items.POTION, Potions.WATER).item)) { SaccharineLogSlatheredBlock.createBehavior() }
// register(listOf(Items.HONEY_BOTTLE)) { SaccharineLogBlock.createBehavior() }
// register(listOf(Items.HONEY_BOTTLE, Items.GLASS_BOTTLE)) { item -> SaccharineLeafBlock.createBehavior(item)}
applyBehaviors()
}
}
@@ -28,6 +28,8 @@ import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import net.minecraft.world.item.alchemy.PotionContents
import net.minecraft.world.item.alchemy.Potions
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.Level
import net.minecraft.world.level.LevelAccessor
@@ -67,9 +69,16 @@ class SaccharineLeafBlock(settings: Properties) : LeavesBlock(settings) {
if (blockState.block is SaccharineLeafBlock) {
val currentAge = blockState.getValue(AGE)
val newAge = when {
item == Items.HONEY_BOTTLE && currentAge < MAX_AGE -> (currentAge + 2).coerceAtMost(MAX_AGE)
item == Items.GLASS_BOTTLE && currentAge > MIN_AGE -> (currentAge - 2).coerceAtLeast(MIN_AGE)
val waterBottle = PotionContents.createItemStack(Items.POTION, Potions.WATER).item
val newAge = when (item) {
Items.HONEY_BOTTLE if currentAge < MAX_AGE ->
(currentAge + 2).coerceAtMost(MAX_AGE)
waterBottle if currentAge > MIN_AGE ->
(currentAge - 2).coerceAtLeast(MIN_AGE)
else -> currentAge
}
@@ -79,21 +88,23 @@ class SaccharineLeafBlock(settings: Properties) : LeavesBlock(settings) {
stack.shrink(1)
val dispenserEntity = source.blockEntity
val outputItem = if (item == Items.HONEY_BOTTLE) Items.GLASS_BOTTLE else Items.HONEY_BOTTLE
val outputItem = Items.GLASS_BOTTLE
val outputStack = ItemStack(outputItem)
var added = false
for (i in 0 until dispenserEntity.containerSize) {
val slotStack = dispenserEntity.getItem(i)
if (dispenserEntity != null) {
for (i in 0 until dispenserEntity.containerSize) {
val slotStack = dispenserEntity.getItem(i)
if (slotStack.isEmpty) {
dispenserEntity.setItem(i, outputStack.copy())
added = true
break
} else if (slotStack.`is`(outputItem) && slotStack.count < slotStack.maxStackSize) {
slotStack.grow(1)
added = true
break
if (slotStack.isEmpty) {
dispenserEntity.setItem(i, outputStack.copy())
added = true
break
} else if (slotStack.`is`(outputItem) && slotStack.count < slotStack.maxStackSize) {
slotStack.grow(1)
added = true
break
}
}
}
@@ -25,6 +25,7 @@ import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.DispenserBlock
import net.minecraft.world.level.block.HorizontalDirectionalBlock
import net.minecraft.world.level.block.RotatedPillarBlock
import net.minecraft.world.level.block.state.BlockState
@@ -37,7 +38,7 @@ class SaccharineLogBlock(properties: Properties) : RotatedPillarBlock(properties
fun createBehavior(): DispenseItemBehavior {
return DispenseItemBehavior { source, stack ->
val level = source.level
val facing = source.state.getValue(HorizontalDirectionalBlock.FACING)
val facing = source.state.getValue(DispenserBlock.FACING)
val pos = source.pos.relative(facing)
val blockState = level.getBlockState(pos)
@@ -50,7 +51,13 @@ class SaccharineLogBlock(properties: Properties) : RotatedPillarBlock(properties
}
}
fun changeLogTypeDispenser(level: ServerLevel, pos: BlockPos, newState: BlockState, stack: ItemStack, source: BlockSource) {
fun changeLogTypeDispenser(
level: ServerLevel,
pos: BlockPos,
newState: BlockState,
stack: ItemStack,
source: BlockSource
) {
level.setBlock(pos, newState, 3)
level.gameEvent(null, GameEvent.BLOCK_CHANGE, pos)
@@ -75,11 +82,23 @@ class SaccharineLogBlock(properties: Properties) : RotatedPillarBlock(properties
}
if (!added) {
Containers.dropItemStack(level, source.pos.x.toDouble(), source.pos.y.toDouble(), source.pos.z.toDouble(), glassBottle)
Containers.dropItemStack(
level,
source.pos.x.toDouble(),
source.pos.y.toDouble(),
source.pos.z.toDouble(),
glassBottle
)
}
}
fun changeLogType(level: Level, pos: BlockPos, newState: BlockState, player: Player? = null, itemStack: ItemStack? = null) {
fun changeLogType(
level: Level,
pos: BlockPos,
newState: BlockState,
player: Player? = null,
itemStack: ItemStack? = null
) {
level.setBlock(pos, newState, 3)
level.gameEvent(null, GameEvent.BLOCK_CHANGE, pos)
@@ -93,7 +112,15 @@ class SaccharineLogBlock(properties: Properties) : RotatedPillarBlock(properties
}
}
override fun useItemOn(stack: ItemStack, state: BlockState, level: Level, pos: BlockPos, player: Player, hand: InteractionHand, hitResult: BlockHitResult): ItemInteractionResult {
override fun useItemOn(
stack: ItemStack,
state: BlockState,
level: Level,
pos: BlockPos,
player: Player,
hand: InteractionHand,
hitResult: BlockHitResult
): ItemInteractionResult {
val axis = state.getValue(AXIS)
val itemStack = player.getItemInHand(hand)
if (!level.isClientSide && itemStack.`is`(Items.HONEY_BOTTLE) && axis == Direction.Axis.Y) {
@@ -16,7 +16,9 @@ import net.minecraft.core.Direction
import net.minecraft.util.StringRepresentable
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.LevelAccessor
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.HorizontalDirectionalBlock
import net.minecraft.world.level.block.SimpleWaterloggedBlock
import net.minecraft.world.level.block.state.BlockState
@@ -24,6 +26,8 @@ import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_FACING
import net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED
import net.minecraft.world.level.block.state.properties.IntegerProperty
import net.minecraft.world.level.material.FluidState
import net.minecraft.world.level.material.Fluids
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
@@ -45,7 +49,7 @@ class StackableItemBlock(settings: Properties, val type: StackableItemBlockType)
}
companion object {
val MAX_AMOUNT: Int = 8
val MAX_AMOUNT: Int = 4
val AMOUNT: IntegerProperty = IntegerProperty.create("amount", 1, MAX_AMOUNT)
val CODEC: MapCodec<StackableItemBlock> = RecordCodecBuilder.mapCodec { it.group(
@@ -163,6 +167,16 @@ class StackableItemBlock(settings: Properties, val type: StackableItemBlockType)
builder.add(HORIZONTAL_FACING, AMOUNT, WATERLOGGED)
}
override fun getFluidState(blockState: BlockState): FluidState? = if (blockState.getValue(WATERLOGGED)) Fluids.WATER.getSource(false) else super.getFluidState(blockState)
override fun updateShape(state: BlockState, direction: Direction, neighborState: BlockState, world: LevelAccessor, pos: BlockPos, neighborPos: BlockPos): BlockState {
if (state.getValue(WATERLOGGED)) {
world.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(world))
}
return if (state.getValue(HORIZONTAL_FACING).opposite == direction && !state.canSurvive(world, pos)) Blocks.AIR.defaultBlockState()
else super.updateShape(state, direction, neighborState, world, pos, neighborPos)
}
override fun getCollisionShape(state: BlockState, level: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape {
val direction = state.getValue(HORIZONTAL_FACING)
val count = state.getValue(AMOUNT)
@@ -175,8 +189,9 @@ class StackableItemBlock(settings: Properties, val type: StackableItemBlockType)
override fun getStateForPlacement(context: BlockPlaceContext): BlockState? {
val blockState = context.level.getBlockState(context.clickedPos)
val fluidState = context.level.getFluidState(context.clickedPos)
return if (blockState.`is`(this)) blockState.setValue(AMOUNT, min(getMaxAmount(type), (blockState.getValue(AMOUNT) + 1)))
else super.getStateForPlacement(context)?.setValue(HORIZONTAL_FACING, context.horizontalDirection.opposite)
else super.getStateForPlacement(context)?.setValue(HORIZONTAL_FACING, context.horizontalDirection.opposite)?.setValue(WATERLOGGED, fluidState.type === Fluids.WATER)
}
override fun canBeReplaced(state: BlockState, useContext: BlockPlaceContext): Boolean {
@@ -8,8 +8,6 @@
package com.cobblemon.mod.common.block
import com.cobblemon.mod.common.block.StackableItemBlock.Companion.AMOUNT
import com.cobblemon.mod.common.block.StackableItemBlock.Companion.MAX_AMOUNT
import com.cobblemon.mod.common.util.rotateShape
import com.mojang.serialization.MapCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
@@ -23,6 +21,7 @@ import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_FACING
import net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED
import net.minecraft.world.level.block.state.properties.IntegerProperty
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
@@ -40,6 +39,9 @@ class WallAttachedStackableItemBlock(settings: Properties, val type: WallStackab
}
companion object {
val MAX_AMOUNT: Int = 8
val AMOUNT: IntegerProperty = IntegerProperty.create("amount", 1, MAX_AMOUNT)
val CODEC: MapCodec<WallAttachedStackableItemBlock> = RecordCodecBuilder.mapCodec { it.group(
propertiesCodec(),
WallStackableItemBlockType.CODEC.fieldOf("type").forGetter(WallAttachedStackableItemBlock::type)
@@ -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.block.dispenser
import net.minecraft.core.dispenser.BlockSource
import net.minecraft.core.dispenser.DispenseItemBehavior
import net.minecraft.world.item.ItemStack
import java.util.concurrent.CopyOnWriteArrayList
class ChainedDispenseItemBehavior(private val base: DispenseItemBehavior) : DispenseItemBehavior {
private val extras = CopyOnWriteArrayList<DispenseItemBehavior>()
fun add(behavior: DispenseItemBehavior) {
extras.add(behavior)
}
override fun dispense(blockSource: BlockSource, itemStack: ItemStack): ItemStack {
var current = itemStack
for (b in extras) {
val prev = current.copy()
val next = try {
b.dispense(blockSource, current)
} catch (t: Throwable) {
continue
}
current = next
if (handled(prev, current)) {
return current
}
}
val beforeBase = current.copy()
val afterBase = try {
base.dispense(blockSource, current)
} catch (_: Throwable) {
return current
}
if (handled(beforeBase, afterBase)) {
return afterBase
}
return afterBase
}
private fun handled(before: ItemStack, after: ItemStack): Boolean {
return !ItemStack.matches(before, after)
}
}
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.block.dispenser
import com.cobblemon.mod.common.block.SaccharineLeafBlock
import com.cobblemon.mod.common.block.SaccharineLogBlock
import com.cobblemon.mod.common.block.SaccharineLogSlatheredBlock
import com.cobblemon.mod.common.mixin.accessor.DispenserBlockAccessor
import net.minecraft.core.dispenser.DispenseItemBehavior
import net.minecraft.world.item.Item
import net.minecraft.world.item.Items
import net.minecraft.world.item.alchemy.PotionContents
import net.minecraft.world.item.alchemy.Potions
import net.minecraft.world.level.block.DispenserBlock
object DispenserBehaviorRegistry {
private val behaviors = mutableMapOf<Item, DispenseItemBehavior>()
fun register(items: List<Item>, behaviorProvider: (Item) -> DispenseItemBehavior) {
register(behaviorProvider, items = items)
}
fun register(vararg behaviorProviders: (Item) -> DispenseItemBehavior, items: List<Item>) {
for (item in items) {
val providedBehaviors = behaviorProviders.map { provider -> provider(item) }
val existingLocal = behaviors[item]
if (existingLocal is ChainedDispenseItemBehavior) {
providedBehaviors.forEach { existingLocal.add(it) }
continue
}
val currentRegistered = getCurrentRegisteredBehavior(item)
if (currentRegistered is ChainedDispenseItemBehavior) {
behaviors[item] = currentRegistered
providedBehaviors.forEach { currentRegistered.add(it) }
continue
}
val base = currentRegistered ?: DispenseItemBehavior.NOOP
val chained = ChainedDispenseItemBehavior(base)
providedBehaviors.forEach { chained.add(it) }
behaviors[item] = chained
DispenserBlock.registerBehavior(item, chained)
}
}
private fun getCurrentRegisteredBehavior(item: Item): DispenseItemBehavior? {
return try {
val map = DispenserBlockAccessor.getItemBehaviors()
map[item]
} catch (t: Throwable) {
DispenseItemBehavior.LOGGER.error("Failed to read existing dispenser behaviors via mixin accessor", t)
null
}
}
fun registerDispenserBehaviors() {
register(
{ SaccharineLogSlatheredBlock.createBehavior() },
items = listOf(
PotionContents.createItemStack(
Items.POTION,
Potions.WATER
).item
)
)
register(
{ SaccharineLogBlock.createBehavior() },
items = listOf(Items.HONEY_BOTTLE)
)
register(
{ item -> SaccharineLeafBlock.createBehavior(item) },
items = listOf(
Items.HONEY_BOTTLE,
PotionContents.createItemStack(
Items.POTION,
Potions.WATER
).item
)
)
}
}
@@ -65,6 +65,9 @@ class BerryBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Cobblemon
private val growthPoints = arrayListOf<ResourceLocation>()
var mulchVariant = MulchVariant.NONE
// For Random Tick compensation: records the game time of last tick
var lastTickTime: Long = 0L
/**
* The idea behind the growth point sequence is it's a 16-long string of
* hexadecimal numbers. They represent the order of the growth points that
@@ -267,6 +270,7 @@ class BerryBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Cobblemon
} catch (ignored: ResourceLocationException) {}
}
this.mulchDuration = nbt.getInt(MULCH_DURATION)
this.lastTickTime = nbt.getLong(LAST_TICK_TIME)
this.wasLoading = false
if (nbt.contains(GROWTH_POINTS_SEQUENCE)) {
growthPointSequence = nbt.getString(GROWTH_POINTS_SEQUENCE)
@@ -287,6 +291,7 @@ class BerryBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Cobblemon
nbt.putInt(MULCH_DURATION, mulchDuration)
nbt.putString(GROWTH_POINTS_SEQUENCE, growthPointSequence)
nbt.putString(MULCH_VARIANT, mulchVariant.toString())
nbt.putLong(LAST_TICK_TIME, lastTickTime)
}
override fun setChanged() {
@@ -346,24 +351,39 @@ class BerryBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Cobblemon
return
}
/**
* Process Random Tick compensation logic.
* Calculates elapsed time since last tick and batch-decrements stageTimer.
*/
fun processTick(world: ServerLevel, pos: BlockPos, state: BlockState): Boolean {
if (state.getValue(BerryBlock.IS_ROOTED)) return false
val currentTime = world.gameTime
val elapsed = if (lastTickTime > 0L) {
(currentTime - lastTickTime).toInt().coerceAtLeast(1)
} else {
1 // First tick, only decrement by 1
}
lastTickTime = currentTime
// Batch decrement the timer
if (stageTimer > 0) {
stageTimer = (stageTimer - elapsed).coerceAtLeast(0)
}
// Return whether growth is needed
return stageTimer <= 0
}
//Using a similar approach to ComputerCraft, implement this client side
interface RenderState : AutoCloseable {
var needsRebuild: Boolean
}
companion object {
internal val TICKER = BlockEntityTicker<BerryBlockEntity> { world, pos, state, blockEntity ->
if (world.isClientSide) return@BlockEntityTicker
if (state.getValue(BerryBlock.IS_ROOTED)) return@BlockEntityTicker
if (blockEntity.stageTimer >= 0) {
blockEntity.stageTimer--
}
if (blockEntity.stageTimer <= 0) {
(state.block as BerryBlock).growHelper(world as ServerLevel, world.random, pos, state)
}
}
//private const val LIFE_CYCLES = "life_cycles"
private const val GROWTH_POINTS = "GrowthPoints"
private const val LAST_TICK_TIME = "LastTickTime"
private const val GROWTH_POINTS_SEQUENCE = "GrowthPointsSequence"
private const val GROWTH_TIMER = "GrowthTimer"
private const val STAGE_TIMER = "StageTimer"
@@ -29,6 +29,7 @@ import com.cobblemon.mod.common.api.spawning.influence.SpawnBaitInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.spawner.FixedAreaSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PokeSnackSpawnerFactory
import com.cobblemon.mod.common.block.PokeSnackBlock
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.item.components.BaitEffectsComponent
@@ -62,41 +63,22 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
}
val spawner: FixedAreaSpawner by lazy {
FixedAreaSpawner(
name = "poke_snack_spawner_${level?.dimension()?.location()}}_$blockPos",
spawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL,
world = level as ServerLevel,
position = blockPos,
val server = level as ServerLevel
val baitEffects = getBaitEffects()
val ctx = PokeSnackSpawnerFactory.Context(
world = server,
pos = blockPos,
horizontalRadius = RADIUS,
verticalRadius = RADIUS,
maxPokemonPerChunk = Cobblemon.config.pokeSnackPokemonPerChunk
).also {
it.influences.add(this)
maxPokemonPerChunk = Cobblemon.config.pokeSnackPokemonPerChunk,
name = "poke_snack_spawner_${server.dimension().location()}_$blockPos",
baitEffects = baitEffects
)
val baitEffects = getBaitEffects()
val stackedLureTier = baitEffects
.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }
.sumOf { it.value }
.toInt()
if (stackedLureTier > 0) {
it.influences += BucketNormalizingInfluence(
tier = stackedLureTier,
gradient = 0.2F,
firstTier = 1.2F
)
}
it.influences += BucketMultiplyingInfluence(
mapOf(
"uncommon" to 2.25f,
"rare" to 5.5f,
"ultra-rare" to 5.5f,
)
)
it.influences += SpawnBaitInfluence(effects = baitEffects)
PokeSnackSpawnerFactory.create(ctx).also { spawner ->
spawner.influences.add(this)
}
}
@@ -128,11 +128,11 @@ class FossilMultiblockStructure (
if(pokemon != null) {
pokemon.caughtBall = ballType
player.party().add(pokemon)
this.fossilState.growthState = "Taken"
player.playSound(CobblemonSounds.FOSSIL_MACHINE_RETRIEVE_POKEMON, 1.0F, 1.0F)
CobblemonEvents.FOSSIL_REVIVED.post(FossilRevivedEvent(pokemon, player))
CobblemonCriteria.RESURRECT_POKEMON.trigger(player, pokemon)
player.party().add(pokemon)
}
// Turn the monitor off
@@ -27,6 +27,7 @@ import com.cobblemon.mod.common.client.gui.PartyOverlayDataControl
import com.cobblemon.mod.common.client.gui.RideControlsOverlay
import com.cobblemon.mod.common.client.gui.battle.BattleOverlay
import com.cobblemon.mod.common.client.gui.cookingpot.CookingPotScreen
import com.cobblemon.mod.common.client.gui.party.PartyTutorialToasts
import com.cobblemon.mod.common.client.particle.BedrockParticleOptionsRepository
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.client.render.DeferredRenderer
@@ -128,6 +129,8 @@ object CobblemonClient {
battleOverlay.onLogout()
ClientTaskTracker.clear()
checkedStarterScreen = false
overlay.resetAttachedToast()
PartyTutorialToasts.reset()
// CobblemonDataProvider.canReload = true
DeferredRenderer.clearAll()
ClientPlayerIcon.clear()
@@ -165,13 +168,12 @@ object CobblemonClient {
PlatformEvents.CLIENT_TICK_POST.subscribe { event ->
val player = event.client.player
if (player != null) {
var selectedItem = player.inventory.getItem(player.inventory.selected)
val selectedItem = player.inventory.getItem(player.inventory.selected)
if (pokedexUsageContext.scanningGuiOpen &&
!(selectedItem.`is`(CobblemonItemTags.POKEDEX)) &&
!(player.offhandItem.`is`(CobblemonItemTags.POKEDEX) &&
player.isUsingItem == true &&
player.usedItemHand == InteractionHand.OFF_HAND
)
player.isUsingItem &&
player.usedItemHand == InteractionHand.OFF_HAND)
) {
// Stop using Pokédex in main hand if player switches to a different slot in hotbar
pokedexUsageContext.stopUsing(PokedexUsageContext.OPEN_SCANNER_BUFFER_TICKS + 1)
@@ -216,7 +218,7 @@ object CobblemonClient {
}
fun registerColors() {
this.implementation.registerBlockColors(BlockColor { blockState, view, blockPos, tintIndex ->
this.implementation.registerBlockColors(BlockColor { _, view, blockPos, _ ->
blockPos?.let { pos ->
view?.getBlockEntity(pos)?.let { blockEntity ->
if (blockEntity is TintBlockEntity) return@BlockColor blockEntity.getTint()
@@ -507,9 +507,7 @@ class PokemonClientDelegate : PosableState(), PokemonSideDelegate {
override fun positionRider(passenger: Entity, positionUpdater: MoveFunction) {
val locatorName = getSeatLocator(passenger)
val locator = this.locatorStates[locatorName]
if (locator == null) return
val locator = this.locatorStates[locatorName] ?: return
val offset = locator.matrix.getTranslation(Vector3f())
.sub(
@@ -12,16 +12,17 @@ import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.gui.drawPosablePortrait
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.darkGray
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.battle.BattleGUI
import com.cobblemon.mod.common.client.gui.party.PartyTutorialToasts
import com.cobblemon.mod.common.client.gui.toast.CobblemonToast
import com.cobblemon.mod.common.client.keybind.boundKey
import com.cobblemon.mod.common.client.keybind.keybinds.HidePartyBinding
import com.cobblemon.mod.common.client.keybind.keybinds.SummaryBinding
import com.cobblemon.mod.common.client.keybind.keybinds.*
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.getDepletableRedGreen
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
@@ -29,6 +30,7 @@ import com.cobblemon.mod.common.client.render.renderScaledGuiItemIcon
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.toAssetPath
import java.util.UUID
import net.minecraft.client.DeltaTracker
import net.minecraft.client.Minecraft
@@ -38,6 +40,7 @@ import net.minecraft.client.gui.components.toasts.AdvancementToast
import net.minecraft.client.gui.components.toasts.Toast
import net.minecraft.client.gui.screens.ChatScreen
import net.minecraft.client.gui.screens.Screen
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.Mth
import kotlin.math.ceil
import kotlin.math.floor
@@ -148,6 +151,9 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
val portraitFrameOffsetY = 2
val selectedSlot = CobblemonClient.storage.selectedSlot
val partySize = party.slots.count { it != null }
PartyTutorialToasts.onOverlayRender(partySize)
party.forEachIndexed { index, pokemon ->
if (pokemon != null) {
val selectedOffsetX = if (selectedSlot == index) 6 else 0
@@ -224,7 +230,7 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
var levelScrollOffset = 0F
var levelScrollPositionOffset = 0F
var levelScrollSize = 17F
if (expGainedData != null && expGainedData.oldLevel != null) {
if (expGainedData?.oldLevel != null) {
val ticksWithDelta = (expGainedData.ticks + partialDeltaTicks) - (PartyOverlayDataControl.BAR_UPDATE_BEFORE_TIME + PartyOverlayDataControl.BAR_FLASH_TIME)
val transition = ticksWithDelta / PartyOverlayDataControl.LEVEL_UP_PORTRAIT_TIME
if ((0F..<1F).contains(transition)) {
@@ -398,10 +404,48 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
blue = expBlue
)
//Handling EXP Text
var expGained = 0
var expTextTransparency = 0F
var expTextOffset = 0F
if (expGainedData != null) {
expGained = expGainedData.expGained
val ticksWithDelta = expGainedData.ticks + partialDeltaTicks
if (ticksWithDelta <= PartyOverlayDataControl.EXP_POPUP_TIME.fadeIn) {
val transition = ticksWithDelta / PartyOverlayDataControl.EXP_POPUP_TIME.fadeIn
val step = (floor(transition * 3)) + 1
expTextTransparency = step * 0.25F
expTextOffset = 4 - step
} else if (ticksWithDelta <= PartyOverlayDataControl.EXP_POPUP_TIME.fadeIn + PartyOverlayDataControl.EXP_POPUP_TIME.hold) {
expTextTransparency = 1F
expTextOffset = 0F
} else {
val adjustedTicks = ticksWithDelta - (PartyOverlayDataControl.EXP_POPUP_TIME.fadeIn + PartyOverlayDataControl.EXP_POPUP_TIME.hold)
val transition = adjustedTicks / PartyOverlayDataControl.EXP_POPUP_TIME.fadeOut
val step = (floor(transition * 3)) + 1
expTextTransparency = 1F - (step * 0.25F)
expTextOffset = -step
}
}
if (expTextTransparency > 0) {
drawScaledText(
context = context,
text = lang("ui.exp.number", expGained.toString()).bold(),
x = panelX + selectedOffsetX + 57F,
y = indexY + 17F + expTextOffset,
scale = SCALE,
shadow = true,
opacity = expTextTransparency
)
}
//Handling Popups
var ticksForMove = -1F
var ticksForEvo = -1F
//Handling Popups
if (expGainedData != null) {
val ticksWithDelta = expGainedData.ticks + partialDeltaTicks
val ticks = ticksWithDelta - (PartyOverlayDataControl.BAR_UPDATE_BEFORE_TIME + PartyOverlayDataControl.BAR_FLASH_TIME)
@@ -432,7 +476,7 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
return 0F to 1F
} else {
val adjustedPopupTicks = ticks - (PartyOverlayDataControl.POPUP_TIME.fadeIn + PartyOverlayDataControl.POPUP_TIME.hold)
val transition = adjustedPopupTicks / PartyOverlayDataControl.POPUP_TIME.fadeIn
val transition = adjustedPopupTicks / PartyOverlayDataControl.POPUP_TIME.fadeOut
val step = (floor(transition * 3)) + 1
return (-step) to (1F - (step * 0.25F))
}
@@ -446,7 +490,7 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
blitk(
matrixStack = matrices,
texture = newEvoPopup,
x = (panelX + selectedOffsetX + 56.5F) / SCALE,
x = (panelX + selectedOffsetX + (if (expTextTransparency > 0 && movePopupTransparency > 0) 78F else 56.5F)) / SCALE,
y = (indexY + 4 + evoPopupYOffset) / SCALE,
width = 37,
height = 20,
@@ -459,7 +503,7 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
matrixStack = matrices,
texture = newMovesPopup,
x = (panelX + selectedOffsetX + 56.5F) / SCALE,
y = (indexY + 17 + movePopupYOffset) / SCALE,
y = (indexY + (if (expTextTransparency > 0) 4 else 17) + movePopupYOffset) / SCALE,
width = 37,
height = 20,
scale = SCALE,
@@ -467,7 +511,7 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
)
}
val ballIcon = cobblemonResource("textures/gui/ball/" + pokemon.caughtBall.name.path + ".png")
val ballIcon = pokemon.caughtBall.name.toAssetPath("textures/gui/ball/", ".png")
val ballHeight = 22
blitk(
matrixStack = matrices,
@@ -19,10 +19,10 @@ object PartyOverlayDataControl {
const val BAR_UPDATE_BEFORE_TIME = 15
const val BAR_UPDATE_AFTER_TIME = 15
const val BAR_FLASH_TIME = 2
const val BAR_UPDATE_NO_LEVEL_TIME = BAR_UPDATE_BEFORE_TIME + BAR_UPDATE_AFTER_TIME + BAR_FLASH_TIME
val POPUP_TIME = FadeTimings(3, 40, 3)
const val LEVEL_UP_PORTRAIT_TIME = 10
val BAR_UPDATE_NO_LEVEL_TIME = BAR_UPDATE_BEFORE_TIME + BAR_UPDATE_AFTER_TIME + BAR_FLASH_TIME
val EXP_POPUP_TIME = FadeTimings(3, BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME + POPUP_TIME.total() - 6, 3)
private enum class RenderTarget {
NONE,
@@ -35,21 +35,19 @@ object PartyOverlayDataControl {
private var barFillupSound: SoundInstance? = null
private var levelUpJingleSound: SoundInstance? = null
private var levelUpJingleSoundType: SoundEvent? = null
fun pokemonGainedExp(pokemonUuid : UUID, oldLevel: Int?, expGained: Int, countOfMovesLearned: Int, countOfEvosUnlocked: Int = 0) {
var time = BAR_UPDATE_NO_LEVEL_TIME
if (countOfMovesLearned > 0 || countOfEvosUnlocked > 0) {
time = BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME + POPUP_TIME.total()
}
val data = overlayData[pokemonUuid]
val newData = ExpGainedData(oldLevel, expGained, countOfMovesLearned, countOfEvosUnlocked).also { it.ticksMax = time }
val newData = ExpGainedData(oldLevel, expGained, countOfMovesLearned, countOfEvosUnlocked).also { it.ticksMax = BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME + POPUP_TIME.total() }
if (data != null) {
if (data.movesGainedData != null) {
if (data.movesGainedData != null) { //Absorb Data
newData.countOfMovesLearned += data.movesGainedData!!.movesCount
data.movesGainedData = null
}
if (data.evoGainedData != null) {
if (data.evoGainedData != null) { //Absorb Data
newData.countOfEvosUnlocked += data.evoGainedData!!.evolutionCount
data.evoGainedData = null
}
val expData = data.expGainedData
@@ -72,8 +70,7 @@ object PartyOverlayDataControl {
val data = overlayData[pokemonUuid]
val newData = EvolutionGainedData(evoCount).also { it.ticksMax = POPUP_TIME.total() }
if (data != null) {
if (data.expGainedData != null && data.expGainedData!!.ticks < BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME) {
data.expGainedData!!.ticksMax = BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME + POPUP_TIME.total()
if (data.expGainedData != null) {
data.expGainedData!!.countOfEvosUnlocked += evoCount
return
}
@@ -93,8 +90,7 @@ object PartyOverlayDataControl {
val data = overlayData[pokemonUuid]
val newData = MovesGainedData(movesCount).also { it.ticksMax = POPUP_TIME.total() }
if (data != null) {
if (data.expGainedData != null && data.expGainedData!!.ticks < BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME) {
data.expGainedData!!.ticksMax = BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME + POPUP_TIME.total()
if (data.expGainedData != null) {
data.expGainedData!!.countOfMovesLearned += movesCount
return
}
@@ -137,13 +133,28 @@ object PartyOverlayDataControl {
fun tick(paused: Boolean) {
val deletion = mutableListOf<UUID>()
if (paused) return
if (levelUpJingleSound != null && !Minecraft.getInstance().soundManager.isActive(levelUpJingleSound!!)) {
levelUpJingleSound = null
levelUpJingleSoundType = null
}
overlayData.forEach { (id, data) ->
if (data.expGainedData != null) { // We update this in order and render these in order, and as such we don't tick others
val expGainedData = data.expGainedData!!
expGainedData.ticks += 1
if (expGainedData.ticks == BAR_UPDATE_BEFORE_TIME && expGainedData.oldLevel != null) {
stopSound(levelUpJingleSound)
levelUpJingleSound = playSound(CobblemonSounds.LEVELUP)
if (expGainedData.countOfEvosUnlocked > 0) {
stopSound(levelUpJingleSound)
levelUpJingleSound = playSound(CobblemonSounds.EVOLUTION_NOTIFICATION)
levelUpJingleSoundType = CobblemonSounds.EVOLUTION_NOTIFICATION
} else {
if (levelUpJingleSoundType != CobblemonSounds.EVOLUTION_NOTIFICATION) {
stopSound(levelUpJingleSound)
levelUpJingleSound = playSound(CobblemonSounds.LEVELUP)
levelUpJingleSoundType = CobblemonSounds.LEVELUP
}
}
}
if (expGainedData.ticks >= expGainedData.ticksMax) {
data.expGainedData = null
@@ -166,6 +177,7 @@ object PartyOverlayDataControl {
}
if (getCurrentRenderTarget(id) == RenderTarget.NONE) deletion.add(id)
}
deletion.forEach {
overlayData.remove(it)
}
@@ -216,7 +228,7 @@ object PartyOverlayDataControl {
//SOUNDS
private fun playSound(soundEvent: SoundEvent): SoundInstance? {
if (!PartyOverlay.canRender()) return null
val soundInstance = SimpleSoundInstance.forUI(soundEvent, 1F)
val soundInstance = SimpleSoundInstance.forUI(soundEvent, 1F, 1F)
Minecraft.getInstance().soundManager.play(soundInstance)
return soundInstance
}
@@ -47,6 +47,7 @@ fun drawProfilePokemon(
a: Float = 1F,
headYaw: Float = 0f,
headPitch: Float = 0f,
blockLight: Int = 13
) = drawProfilePokemon(
species = renderablePokemon.species.resourceIdentifier,
matrixStack = matrixStack,
@@ -63,6 +64,7 @@ fun drawProfilePokemon(
a = a,
headYaw = headYaw,
headPitch = headPitch,
blockLight = blockLight
)
fun drawProfilePokemon(
@@ -82,6 +84,7 @@ fun drawProfilePokemon(
a: Float = 1F,
headYaw: Float = 0f,
headPitch: Float = 0f,
blockLight: Int = 13
) {
RenderSystem.applyModelViewMatrix()
matrixStack.scale(scale, scale, -scale)
@@ -138,7 +141,7 @@ fun drawProfilePokemon(
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 packedLight = LightTexture.pack(blockLight, 0)
val colour = toHex(r, g, b, a)
model.withLayerContext(bufferSource, state, VaryingModelRepository.getLayers(species, state)) {
@@ -0,0 +1,153 @@
/*
* 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
import com.cobblemon.mod.common.api.gui.blitk
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.gui.components.events.GuiEventListener
import net.minecraft.client.gui.narration.NarrationElementOutput
import net.minecraft.network.chat.CommonComponents
import net.minecraft.resources.ResourceLocation
import kotlin.math.floor
open class VariableObjectSelectionList(x: Int, y: Int, width: Int, height: Int): AbstractWidget(x, y, width, height, CommonComponents.EMPTY) {
val children = mutableListOf<Entry>()
var listHeight = 0
var scrollAmount = 0.0
fun updateScroll(offset: Double) {
setScroll(scrollAmount + offset)
}
fun getMaxScroll(): Int {
return (listHeight - height).coerceAtLeast(0)
}
fun setScroll(scroll: Double) {
scrollAmount = scroll.coerceIn(0.0, getMaxScroll().toDouble())
}
fun removeEntry(index: Int) {
listHeight -= children[index].height
children.removeAt(index)
updatePositions()
}
fun removeEntry(entry: Entry) {
listHeight -= entry.height
children.remove(entry)
updatePositions()
}
fun removeLastEntry() {
listHeight -= children.last().height
children.removeLast()
}
fun updatePositions() {
var stackHeight = 0
children.forEach {
it.scrolledY = stackHeight
stackHeight += it.height
}
updateScroll(0.0)
}
fun addEntry(entry: Entry) {
children.add(entry)
entry.scrolledY = listHeight
listHeight += entry.height
}
override fun renderWidget(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
var stackHeight = -scrollAmount.toInt()
guiGraphics.enableScissor(x, y, x + width, y + height)
children.forEachIndexed { index, entry ->
if (stackHeight in 1..<height || stackHeight + entry.height in 1..<height) {
entry.render(guiGraphics, index, x, y + stackHeight, mouseX, mouseY, partialTick)
}
stackHeight += entry.height
}
guiGraphics.disableScissor()
}
fun getEntryAtPosition(mouseX: Int, mouseY: Int): Entry? {
if (mouseX in x..(x + width) && mouseY in y..(y + height)) {
var stackHeight = -scrollAmount
children.forEach {
if (mouseY.toDouble() in (y + stackHeight)..(y + stackHeight + it.height)) {
return it
}
stackHeight += it.height
}
}
return null
}
fun ensureVisible(index: Int) {
val entry = children[index]
if (entry.scrolledY - scrollAmount < 0) {
setScroll(entry.scrolledY.toDouble())
} else if (entry.scrolledY + entry.height > height) {
setScroll(entry.scrolledY.toDouble() + entry.height)
}
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
updateScroll(-verticalAmount * (listHeight / children.size / 2))
return true
}
override fun mouseClicked(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
val entry = getEntryAtPosition(pMouseX.toInt(), pMouseY.toInt())
if (entry != null) {
return entry.mouseClicked(pMouseX, pMouseY, pButton)
}
return false
}
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
return children.any {
it.mouseReleased(pMouseX, pMouseY, pButton)
} || super.mouseReleased(pMouseX, pMouseY, pButton)
}
fun getRowTop(entry: Entry): Int {
var stackHeight = -scrollAmount.toInt()
children.forEach {
if (it == entry) {
return y + stackHeight
}
stackHeight += it.height
}
return y
}
override fun defaultButtonNarrationText(pNarrationElementOutput: NarrationElementOutput) {}
override fun updateWidgetNarration(narrationElementOutput: NarrationElementOutput) {}
abstract inner class Entry(var height: Int): GuiEventListener {
var scrolledY: Int = 0
private var widgetFocused = false
override fun setFocused(focused: Boolean) {
this.widgetFocused = focused
}
override fun isFocused(): Boolean = widgetFocused
abstract fun render(guiGraphics: GuiGraphics, index: Int, x: Int, y: Int, mouseX: Int, mouseY: Int, partialTick: Float)
}
inner class EmptySeparator(height: Int): Entry(height) {
override fun render(guiGraphics: GuiGraphics, index: Int, x: Int, y: Int, mouseX: Int, mouseY: Int, partialTick: Float) {}
}
}
@@ -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
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.gui.narration.NarrationElementOutput
import net.minecraft.client.sounds.SoundManager
import net.minecraft.network.chat.CommonComponents
import net.minecraft.resources.ResourceLocation
class VariableObjectSelectionScrollbar(var parent: VariableObjectSelectionList, x: Int, y: Int, width: Int, height: Int): AbstractWidget(x, y, width, height, CommonComponents.EMPTY) {
companion object {
private val scrollerResource: ResourceLocation = ResourceLocation("minecraft", "textures/gui/sprites/widget/scroller.png")
private val scrollerBackgroundResource: ResourceLocation = ResourceLocation("minecraft", "textures/gui/sprites/widget/scroller_background.png")
private const val SCROLLER_HEIGHT = 32
}
private var scrolling = false
private var scrollHoldOffset = 0.0
fun scrollbarVisible(): Boolean = parent.getMaxScroll() > 0
override fun renderWidget(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
if (scrollbarVisible()) {
val scrollPosition = (parent.scrollAmount / parent.getMaxScroll()) * (height - SCROLLER_HEIGHT)
blitk(
matrixStack = guiGraphics.pose(),
texture = scrollerBackgroundResource,
x = x,
y = y,
width = width,
height = height,
)
blitk(
matrixStack = guiGraphics.pose(),
texture = scrollerResource,
x = x,
y = y + scrollPosition,
width = width,
height = SCROLLER_HEIGHT,
textureWidth = 6,
textureHeight = SCROLLER_HEIGHT
)
}
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, scrollX: Double, scrollY: Double): Boolean {
return parent.mouseScrolled(mouseX, mouseY, scrollX, scrollY)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (mouseX.toInt() in x..(x + width) && mouseY.toInt() in y..(y + height)) {
if (button == 0) {
var scrollPosition = (parent.scrollAmount / parent.getMaxScroll()) * (height - SCROLLER_HEIGHT)
if (mouseY !in (y + scrollPosition)..(y + scrollPosition + SCROLLER_HEIGHT)) {
val scrollRatio =
((mouseY - y - SCROLLER_HEIGHT / 2) / (height - SCROLLER_HEIGHT)).coerceIn(0.0, 1.0)
parent.setScroll(scrollRatio * parent.getMaxScroll())
scrollPosition = (parent.scrollAmount / parent.getMaxScroll()) * (height - SCROLLER_HEIGHT)
}
scrollHoldOffset = mouseY - y - scrollPosition
scrolling = true
return true
}
}
return false
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, dragX: Double, dragY: Double): Boolean {
if (button == 0 && scrolling) {
val scrollRatio = ((mouseY - y - scrollHoldOffset) / (height - SCROLLER_HEIGHT)).coerceIn(0.0,1.0)
parent.setScroll(scrollRatio * parent.getMaxScroll())
return true
}
return false
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (button == 0 && scrolling) {
scrolling = false
return true
}
return false
}
override fun playDownSound(handler: SoundManager) {}
override fun updateWidgetNarration(narrationElementOutput: NarrationElementOutput) {}
}
@@ -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
)
}
}
@@ -31,12 +31,14 @@ import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import com.cobblemon.mod.common.util.toAssetPath
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.narration.NarratableEntry
import net.minecraft.client.gui.narration.NarrationElementOutput
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.client.sounds.SoundManager
import net.minecraft.resources.ResourceLocation
import org.joml.Quaternionf
import org.joml.Vector3f
@@ -231,7 +233,7 @@ class BattleSwitchPokemonSelection(
textureHeight = SELECT_HEIGHT * 2,
)
val ballIcon = cobblemonResource("textures/gui/ball/" + pokemon.caughtBall.name.path + ".png")
val ballIcon = pokemon.caughtBall.name.toAssetPath("textures/gui/ball/", ".png")
val ballHeight = 22
blitk(
matrixStack = matrixStack,
@@ -24,11 +24,13 @@ import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import com.cobblemon.mod.common.util.toAssetPath
import com.google.common.collect.Lists
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.Button
import net.minecraft.client.sounds.SoundManager
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.FormattedCharSequence
import net.minecraft.world.item.ItemStack
import org.joml.Quaternionf
@@ -111,7 +113,7 @@ class PartySlotButton(
)
context.pose().popPose()
val ballIcon = cobblemonResource("textures/gui/ball/" + pokemon.pokeball!!.asIdentifierDefaultingNamespace().path + ".png")
val ballIcon = pokemon.pokeball!!.asIdentifierDefaultingNamespace().toAssetPath("textures/gui/ball/", ".png")
val ballHeight = 22
blitk(
matrixStack = matrices,
@@ -0,0 +1,99 @@
/*
* 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.party
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.api.text.darkGray
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.gui.toast.CobblemonToast
import com.cobblemon.mod.common.client.keybind.boundKey
import com.cobblemon.mod.common.client.keybind.keybinds.DownShiftPartyBinding
import com.cobblemon.mod.common.client.keybind.keybinds.PartySendBinding
import com.cobblemon.mod.common.client.keybind.keybinds.UpShiftPartyBinding
import com.cobblemon.mod.common.net.messages.server.storage.party.PartyTutorialPacket
import com.cobblemon.mod.common.util.lang
import com.mojang.blaze3d.platform.InputConstants
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.components.toasts.Toast
import net.minecraft.client.gui.components.toasts.AdvancementToast
import net.minecraft.network.chat.Component
import net.minecraft.util.Mth
object PartyTutorialToasts {
private var toastAttached = false
val upKey = UpShiftPartyBinding.boundKey()
val downKey = DownShiftPartyBinding.boundKey()
val upDisplay =
if (upKey.type == InputConstants.Type.KEYSYM && upKey.value == InputConstants.KEY_UP)
Component.literal("↑")
else upKey.displayName
val downDisplay =
if (downKey.type == InputConstants.Type.KEYSYM && downKey.value == InputConstants.KEY_DOWN)
Component.literal("↓")
else downKey.displayName
private val toast = CobblemonToast(
Mth.createInsecureUUID(),
CobblemonItems.POKEDEX_RED.defaultInstance,
lang("ui.party.party_select_title").red(),
lang(
"ui.party.party_select_description",
upDisplay,
downDisplay,
PartySendBinding.boundKey().displayName
).darkGray(),
AdvancementToast.BACKGROUND_SPRITE,
-1F,
0
)
fun reset() {
toastAttached = false
}
fun onOverlayRender(partySize: Int) {
if (partySize <= 1) return
if (!CobblemonClient.clientPlayerData.partySelectTutorialDone) {
showToast()
}
}
fun onArrowKeySwitchedSlot() {
if (CobblemonClient.clientPlayerData.partySelectTutorialDone) return
if (CobblemonClient.storage.party.count { it != null } < 2) return
PartyTutorialPacket().sendToServer()
hideToast()
}
fun onHoldKeyScrollSwitchedSlot() {
if (CobblemonClient.clientPlayerData.partySelectTutorialDone) return
if (CobblemonClient.storage.party.count { it != null } < 2) return
PartyTutorialPacket().sendToServer()
hideToast()
}
private fun showToast() {
val minecraft = Minecraft.getInstance()
if (!toastAttached) {
toast.nextVisibility = Toast.Visibility.SHOW
minecraft.toasts.addToast(toast)
toastAttached = true
}
}
private fun hideToast() {
if (toastAttached) toast.nextVisibility = Toast.Visibility.HIDE
}
}
@@ -40,6 +40,7 @@ import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.isInventoryKeyPressed
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.toAssetPath
import com.mojang.blaze3d.platform.InputConstants
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
@@ -463,7 +464,7 @@ class PCGUI(
)
// Poké Ball
val ballResource = cobblemonResource("textures/item/poke_balls/" + pokemon.caughtBall.name.path + ".png")
val ballResource = pokemon.caughtBall.name.toAssetPath("textures/item/poke_balls/", ".png")
blitk(
matrixStack = matrices,
texture = ballResource,
@@ -132,4 +132,4 @@ abstract class InfoTextScrollWidget(val pX: Int, val pY: Int): ScrollingWidget<I
return Component.literal(text)
}
}
}
}
@@ -375,7 +375,8 @@ class PokemonInfoWidget(val pX: Int, val pY: Int, val updateForm: (PokedexForm)
matrixStack = matrices,
partialTicks = delta,
rotation = Quaternionf().fromEulerXYZDegrees(rotationVector),
state = state
state = state,
blockLight = 15
)
matrices.popPose()
@@ -237,9 +237,11 @@ class StatsWidget(val pX: Int, val pY: Int) : SoundlessWidget(
blitk(matrixStack = matrices, texture = divider, x = pX + 51, y = pY + 10, height = 27.5, width = 1)
blitk(matrixStack = matrices, texture = divider, x = pX + 9, y = pY + 2, height = 6, width = 121)
val rideStyle = selectedBehaviour.value.key.path.toString().split("/").last().lowercase()
drawScaledText(
context = context,
text = lang("ui.ride_style.${selectedBehaviour.key.name.lowercase()}").bold(),
text = lang("ui.ride_style.${selectedBehaviour.key.toString().lowercase()}.${rideStyle}")
.append(" | ").append(lang("ui.ride_style.${selectedBehaviour.key.toString().lowercase()}")).bold(),
x = pX + 69.5,
y = pY + 3,
colour = 0x3A96B6,
@@ -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
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More