Compare commits

...
112 Commits
Author SHA1 Message Date
Hiroku 20ddab8722 Merge branch 'feature/minor-fixes' into 'main'
fix evs not updating client side

Closes #1152

See merge request cable-mc/cobblemon!1267
2024-11-10 01:26:10 +00:00
Polymeta 5327ff54b5 fix evs not updating client side 2024-11-10 01:26:10 +00:00
Hiroku b5f8181118 Merge branch 'sterr/bugfixes' into 'main'
Shiny particle fixes

See merge request cable-mc/cobblemon!1269
2024-11-10 01:26:05 +00:00
Paul 64b164bfca Updated locator prefix to dodge convention issues 2024-11-10 11:22:42 +10:00
Hiroku 93cb0f40f3 Merge branch 'get-vivil-on-with-it' into 'main'
Egg Advancement code functional and fixed Aspect tracking.

See merge request cable-mc/cobblemon!1268
2024-11-10 01:19:53 +00:00
MoeBoy76 20a56b1ff0 Egg Advancement code functional and fixed Aspect tracking. 2024-11-10 12:03:32 +11:00
Sterrezo 97dd08c35d Fixes for shiny particles
Changed how shiny particles are played, fixing Illusion and switching in not playing them
2024-11-10 02:03:22 +01:00
Hiroku 25c64841fe Merge branch 'fix-molang' into 'main'
proposed fix for client-side is_wild

See merge request cable-mc/cobblemon!1236
2024-11-10 00:34:43 +00:00
Hiroku dc5f616c90 Merge branch 'feature/dex-descirptions' into 'main'
fix up some missing dex entries and order

Closes #1107

See merge request cable-mc/cobblemon!1249
2024-11-10 00:27:03 +00:00
Polymeta ff9033f6c4 fix up some missing dex entries and order 2024-11-10 00:27:03 +00:00
Hiroku 9f937f6b3b Merge branch 'bugfix/genders-and-dex' into 'main'
Fixed client showing incorrect gender for genderless mons, fixed dex not...

Closes #1142 and #1139

See merge request cable-mc/cobblemon!1258
2024-11-10 00:26:16 +00:00
Hiroku c5c6bc340a Handle case of null entity so non-sent-out-pokemon don't pass the wild check 2024-11-10 00:26:03 +00:00
Hiroku 90c7e5ab61 Merge branch 'DontCrossThePokeRods' into 'main'
Iris Compatibility for PokeRods

Closes #1004

See merge request cable-mc/cobblemon!1265
2024-11-09 23:36:34 +00:00
Sterrezo b3028dc45d Removed pitch variation from fishing rod sounds, adjusted volumes 2024-11-10 00:29:34 +01:00
JazzMcNade f64b9e2fe7 Fixed PokeRod fishing lines connecting between players when Iris is used. 2024-11-09 17:23:11 -06:00
Sterrezo 1e7ffedd36 Merge branch 'main' into sound 2024-11-10 00:03:47 +01:00
Test e4fab76f0b rm println 2024-11-09 08:12:59 -05:00
Test 5a7b186478 Merge branch 'main' of https://gitlab.com/cable-mc/cobblemon 2024-11-09 08:12:27 -05:00
Hiroku 79d30f4fa5 Merge branch 'cleanup-interfaces' into 'main'
Cleanup interfaces

See merge request cable-mc/cobblemon!1262
2024-11-09 09:59:32 +00:00
Test 56dab6c18b Cleanup interfaces 2024-11-09 09:59:32 +00:00
Test 6546a5ae05 hide shiny icon if none registered 2024-11-08 21:37:14 -05:00
Test 57c243bd34 clean up scrolling UIs 2024-11-08 20:01:22 -05:00
Hiroku f2cd3fc435 Merge branch 'sound' into 'main'
Status sound updates

See merge request cable-mc/cobblemon!1261
2024-11-08 22:41:32 +00:00
Sterrezo 9b75b1fb93 Status sound updates 2024-11-08 22:41:32 +00:00
Polymeta 81f4b6e680 slightly tweaked boost and unboost pauses
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2024-11-08 22:05:25 +00:00
Sterrezo de783b6948 Volume adjustments to all status sounds 2024-11-08 17:28:34 +01:00
Sterrezo 53c6f707e6 Merge branch 'main' into sound 2024-11-08 17:08:27 +01:00
Sterrezo 23142c6ece Implemented burn status sound, fixed broken status sounds 2024-11-08 17:07:54 +01:00
Sterrezo 1fa3170355 Updated stat increase/decrease particles to use the final sounds 2024-11-08 16:40:32 +01:00
Hiroku 9025b7b825 Merge branch 'CommanderPassFix' into 'main'
Fix Dondozo/Tatsugiri Commander Interaction for Doubles

See merge request cable-mc/cobblemon!1260
2024-11-08 04:17:18 +00:00
JazzMcNade 1487ae1036 Pokemon with the "commanding" state now auto pass their turn. 2024-11-07 22:07:12 -06:00
Hiroku dab1240d19 Merge branch 'implement_pokemon' into 'main'
Couple additions & some fixes

Closes #1137

See merge request cable-mc/cobblemon!1256
2024-11-08 02:51:55 +00:00
Carmendar feb6622762 Couple additions & some fixes 2024-11-08 02:51:55 +00:00
Paul 3c4e1c59b1 Clarified some error logging now that abilities sync 2024-11-08 08:55:32 +10:00
Hiroku 3cd30681d7 Merge branch 'feature/fix-mixin-compat' into 'main'
exlude known fields and classes used by mixins into modelpart when deserializing models to prevent crashes

See merge request cable-mc/cobblemon!1259
2024-11-07 22:07:49 +00:00
Polymeta 308a4e9c06 introduce MixinCompatibilityExclusionStrategy to skip known fields and classes used by 3rd parties to prevent crashes when said 3rd parties are present with cobblemon
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2024-11-07 20:15:47 +00:00
Paul 34cc6da0a9 Oops I missed a thing 2024-11-07 22:13:18 +10:00
Paul 6f1b7edca4 Cleaned up Squirtle line, magikarp, Dragonair, Vaporeon pose handling for touching water, closes #1139 2024-11-07 22:04:25 +10:00
Paul fdc7a3d61a Fixed client showing incorrect gender for genderless mons, fixed dex not showing drops+abilities on servers, closes #1142 2024-11-07 19:19:01 +10:00
Paul 9798ba28ec Fixed deathloop that could occur with specific form configurations of gender ratios 2024-11-07 19:17:49 +10:00
Paul c8522488be Changelog deduplication 2024-11-07 19:11:25 +10:00
Hiroku f951e93875 Merge branch 'sound' into 'main'
Corrected the sound for owned shiny Pokemon particles

See merge request cable-mc/cobblemon!1257
2024-11-07 02:23:11 +00:00
Sterrezo d51c623168 Corrected the sound for owned shiny Pokemon particles
Reverted the volume to 1.0 because it's specified in the sounds.json
2024-11-07 01:57:55 +01:00
Paul fe57312d4f Merge remote-tracking branch 'origin/main' 2024-11-06 08:52:43 +10:00
Paul ee148954c0 Changelog squashing [skip-ci] 2024-11-06 08:52:37 +10:00
Hiroku 6ffc7d45c3 Merge branch 'evolution-message-fix' into 'main'
fix: post-evolution message uses correct names

See merge request cable-mc/cobblemon!1255
2024-11-05 22:51:55 +00:00
Hiroku a0301c85e8 Merge branch 'ihatebugtypes1.6part2' into 'main'
Ihatebugtypes1.6part2

Closes #1038, #1113, and #1114

See merge request cable-mc/cobblemon!1254
2024-11-05 22:51:22 +00:00
mia 6928b9a017 fix: post-evolution message uses correct names 2024-11-05 16:57:25 -05:00
Segfault Guy ce0d32a857 Fix status not updating for party pokemon during battle on infliciton. 2024-11-05 13:18:08 -05:00
Segfault Guy 717e861c1d Lingering aroma, aroma veil, stockpile lang fixes. 2024-11-05 13:13:07 -05:00
Segfault Guy 541dd1e284 Fix PVE interactions. 2024-11-05 12:51:09 -05:00
Paul f68384a505 Fixed duplication in changelog [skip-ci] 2024-11-05 18:58:40 +10:00
Hiroku f406e6da6b Merge branch 'validate-nickname-server-side' into 'main'
Implemented a simple server-side check for when changing Pokemon their...

Closes #1136

See merge request cable-mc/cobblemon!1238
2024-11-05 08:28:00 +00:00
Hiroku c75c5e4901 Merge branch 'implement_pokemon' into 'main'
added west sea forms & right claw forms for shellos line & clauncher line respectively

Closes #1137

See merge request cable-mc/cobblemon!1253
2024-11-05 08:26:59 +00:00
Carmendar b4aedf9ec9 added west sea forms & right claw forms for shellos line & clauncher line respectively 2024-11-05 08:26:58 +00:00
Hiroku cba00fba13 Merge branch 'request_icons' into 'main'
Player Interaction Request Icons

Closes #1067, #1059, #1066, and #1065

See merge request cable-mc/cobblemon!1181
2024-11-05 07:52:28 +00:00
Hiroku 90403d111b Merge branch 'MultiBattleFlatLevelBattlingFix' into 'main'
MultiBattleFlatLevelBattlingFix

See merge request cable-mc/cobblemon!1252
2024-11-05 07:45:06 +00:00
Segfault Guy 47460ef884 Merge remote-tracking branch 'origin/main' into player_icons 2024-11-05 00:23:12 -05:00
Segfault Guy e48d44c6e6 Correcting overlooked code. 2024-11-05 00:22:31 -05:00
JazzMcNade 8e522e3dd2 Merge branch 'main' into MultiBattleFlatLevelBattlingFix 2024-11-04 22:49:13 -06:00
JazzMcNade d9e0429ff4 Fixed party cloning for multibattles when the flat level rule is active. 2024-11-04 22:43:46 -06:00
Hiroku bd35fc3c9a Merge branch 'implement_pokemon' into 'main'
a few tweaks and fixes

Closes #1137

See merge request cable-mc/cobblemon!1251
2024-11-05 03:12:00 +00:00
Carmendar 80d882a437 a few tweaks and fixes 2024-11-05 03:12:00 +00:00
Hiroku 610f9dccc1 Merge branch 'npc-party-time' into 'main'
NPC party upgrades

See merge request cable-mc/cobblemon!1248
2024-11-05 00:07:23 +00:00
Paul bddb5b9f40 Removed excessive backcompat for NPC party 2024-11-05 09:59:19 +10:00
Hiroku 84fc94cf4b Merge branch 'Fishing-Boat-Fix' into 'main'
Fishing boat fix

See merge request cable-mc/cobblemon!1250
2024-11-04 22:45:40 +00:00
Conan e1186ebf2b Removed scope functions. 2024-11-04 22:56:47 +01:00
Slippy e56a778246 Adjusted structure json for fishing boats 2024-11-04 11:17:14 -06:00
Slippy a2d89f320f More new structure adjustments. Vug fixes to Cove generation 2024-11-04 11:16:32 -06:00
Test 7e11c4d47b Merge branch 'main' of https://gitlab.com/cable-mc/cobblemon 2024-11-03 22:52:03 -05:00
Test cc70fd18d5 change dex item order for creative menu 2024-11-03 22:51:31 -05:00
Segfault Guy 334b47a778 Merge branch 'main' into player_icons 2024-11-03 22:38:29 -05:00
Segfault Guy a0c6ec7918 changelog 2024-11-03 22:04:21 -05:00
Segfault Guy 665883474d Add licenses. 2024-11-03 22:01:26 -05:00
Segfault Guy e0fce5386e Merge branch 'main' into player_icons 2024-11-03 21:59:00 -05:00
Hiroku d00829e609 Merge branch 'ihatebugtypes1.6' into 'main'
Ihatebugtypes1.6

Closes #1108, #1074, and #1110

See merge request cable-mc/cobblemon!1220
2024-11-04 02:57:12 +00:00
Paul de5b1e33fe Expose a set of aspects with mock effects, fixes #1108 2024-11-04 12:49:36 +10:00
Segfault Guy f2e6ca1439 Multi battle & team interaction bug fixes. 2024-11-03 21:37:36 -05:00
Paul 974bf73516 Merge remote-tracking branch 'origin/main' into ihatebugtypes1.6
# Conflicts:
#	common/src/main/resources/assets/cobblemon/lang/en_us.json
2024-11-04 12:14:56 +10:00
Paul 3c8fe3bff7 Added level arg to spawnnpc and spawnnpcat 2024-11-04 11:20:38 +10:00
Paul 795a14a291 Fixed PartyStore loading actually being busted when the slot count isn't 6 (lol this has been in for ages) 2024-11-04 11:09:27 +10:00
Paul 0dca4a26e5 Merge remote-tracking branch 'origin/main' into npc-party-time 2024-11-04 10:19:41 +10:00
Hiroku edb98ae078 Merge branch 'implement_pokemon' into 'main'
remove extra pose from gyarados

Closes #1137

See merge request cable-mc/cobblemon!1244
2024-11-03 22:25:14 +00:00
Carmendar 02ba40facb remove extra pose from gyarados 2024-11-03 22:25:14 +00:00
Paul 6fc87fdafd Added proper multi-locator support for action effects 2024-11-03 22:21:34 +10:00
Conan d430498243 Fixed potential desync problem 2024-11-03 12:12:11 +01:00
Paul 2a17f76869 Fixed evolutions not being synced to the client unless calculated while in the party without going to PC 2024-11-03 19:39:54 +10:00
Paul d5adfb70ea Finished pool party provider and built example 2024-11-03 17:20:49 +10:00
Hiroku 23b422bed6 Merge branch 'Shipwreck-Cove-Changes' into 'main'
Shipwreck cove changes

See merge request cable-mc/cobblemon!1247
2024-11-03 07:19:07 +00:00
Slippy f346b8db0c Merge branch 'main' of https://gitlab.com/cable-mc/cobblemon into Shipwrecks-and-Stuff 2024-11-03 01:10:03 -06:00
Slippy a951bd8d25 Loot table changes for Shipwreck Coves and Fishing Boats. Made fishing boats more common. 2024-11-03 01:09:25 -06:00
Segfault Guy 0d7ff74bd2 Trade interaction cleanup. 2024-11-02 23:42:41 -04:00
Conan f1fcf60924 Fixed an oopsie, also moved MAX_NAME_LENGTH to SetNicknameHandler. 2024-11-03 00:30:54 +01:00
Conan a227ef1001 Implemented a simple server-side check for when changing Pokemon their nickname before sending the packet. 2024-11-02 19:21:07 +01:00
tmetcalfe89 2d13b85488 proposed fix for client-side is_wild 2024-11-01 22:29:59 -07:00
Paul a2edfcfdcb Merge remote-tracking branch 'origin/main' into npc-party-time 2024-11-02 15:17:23 +10:00
Paul 63ab04d3d8 Allow weight -1 in pool function and add has_aspect to NPC molang 2024-11-02 15:11:51 +10:00
Paul 21b6c1dc29 Implemented pool-based party generation for NPCs 2024-11-01 14:21:59 +10:00
Segfault Guy 29a8522a86 Player interaction request redesign. 2024-11-01 00:15:27 -04:00
apion 73af1863ec Fixes #1074 2024-10-31 18:02:47 -04:00
apion 9cd8f0e7c2 Fixes #1110 2024-10-31 16:07:17 -04:00
Segfault Guy ec3458b717 Remove unnecessary delay from generic moves. 2024-10-21 00:47:26 -04:00
Segfault Guy 1117792bf7 Misc. battle lang fixes + additions. 2024-10-21 00:46:54 -04:00
Segfault Guy 1649b9790b Only render icons outside of battle. 2024-10-05 08:47:11 -04:00
Segfault Guy b23c9b2133 Improve billboarding of icon to align with player nametags. Fix shifting of icon with respect to camera pitch. 2024-10-03 21:09:15 -04:00
Test eaeb918b33 update assets 2024-10-01 23:49:58 -04:00
Test f05af3d427 licensing, icons 2024-10-01 20:42:52 -04:00
Segfault Guy a73c5fac86 Normalize request notification messages. 2024-09-29 21:40:59 -04:00
Segfault Guy 79be2e64ae Remove CompoundIcon due to design issues. Test unique icon textures. 2024-09-29 08:24:30 -04:00
Segfault Guy e66951debc A player can only have one of each request. 2024-09-26 21:04:26 -04:00
Segfault Guy 1e8a8d0896 Fix request expiry. 2024-09-25 23:25:23 -04:00
Segfault Guy f9def3acf5 Implement DeferredRenderer for rendering request icons after translucent layer in LevelRender. 2024-09-22 11:49:17 -04:00
1303 changed files with 10941 additions and 15682 deletions

No files matched your search

-3
View File
@@ -1,3 +0,0 @@
### Additions
- Added the Poké Dex as a craftable item.
- Poké Dexes can be placed on lecterns.
-6
View File
@@ -1,6 +0,0 @@
### Additions
- Added `moves` option to PokemonProperties, allowing you to set the moves of a Pokémon in commands and spawn files.
- Added /spawnnpc and /spawnnpcat commands.
### Fixes
- Fixed sync issues with recently learned moves where their PP would not go down until you logout and in.
-24
View File
@@ -1,24 +0,0 @@
### Additions
- Pokémon are now animated when seen in any GUI that isn't the party GUI.
- Quirk animations can now occur for Pokémon that are shoulder mounted.
### Changes
- Improved the performance of display cases that contain Pokémon photos.
### Fixes
- Fixed some Pokémon photos in display cases flashing if the Pokémon has gender differences.
- Fixed Bidoof and Jigglypuff sleep animations stopping after some time, causing them to be 'asleep' while T-posing menacingly.
- Fixed entities not changing poses unless you're looking at them.
- Fixed cries not playing on send-out if the Pokémon is off-screen.
### Developer
- Rebuilt large swaths of the model animation code to simplify it.
- Renamed a bunch of things from %Poseable% to %Posable% because spelling.
- Renamed StatelessAnimation to PoseAnimation.
- Renamed StatefulAnimation to ActiveAnimation.
- Documented the animation system.
### Data Pack & Resource Pack Creators
- Added MoLang compatibility in the isVisible property for transformed parts.
- Added q.has_aspect('some_aspect') function to animations, posers, and entity particle effects.
- Added support for conditional pose animations.
+33 -34
View File
@@ -3,6 +3,8 @@
#### "Now that there's a fishing mechanic, the mod is actually good!"
### Additions
- Added the Poké Dex as a craftable item.
- Poké Dexes can be placed on lecterns.
- Added Pokémon (and item) fishing using modified fishing rods - Poké Rods! You'll need a Poké Rod smithing template, a fishing rod, and some type of Poké Ball. Each Poké Ball makes a differently themed rod. Why not?
- Added Lure Ball functionality, increasing the catch rate of Pokémon that were caught on a fishing rod.
- Added Repeat Ball functionality, increasing the catch rate of Pokémon that are already registered as caught in a player's Pokédex.
@@ -16,7 +18,6 @@
- Moves impacted by the abilities Pixelate, Refrigerate, Aerilate, Galvanize, and Normalize now display as their altered typing.
- Added Polished Tumblestone and Tumblestone Brick block sets from Tumblestone, Black Tumblestone, and Sky Tumblestone.
- Added Metronome, Protective Pads, Punching Glove, Room Service, Scope Lens, Shed Shell, Terrain Extender, Throat Spray, Utility Umbrella, Wide Lens, and Zoom Lens held items
- Added particles for moves: Watersport, Mud Sport, and Psychic.
- Added Fire, Water, Thunder, Leaf, Ice, Sun, Moon, Shiny, Dawn, and Dusk Stone storage blocks.
- Added Eject Pack, Metronome, Protective Pads, Punching Glove, Room Service, Scope Lens, Shed Shell, Terrain Extender, Throat Spray, Utility Umbrella, Wide Lens, and Zoom Lens held items
- Added `stonjourner_henge_ruins`, `luna_henge_ruins`, and `sol_henge_ruins` structures.
@@ -30,12 +31,17 @@
- New config setting `displayEntityNameLabel` and `displayEntityLabelsWhenCrouchingOnly` to control what and when is displayed for the pokemon label
- `/freezepokemon` command to pause a Pokémon's animation at a specific point in time.
- Added `no_ai` and `freeze_frame` options to the `/spawnpokemon` command.
- Added `moves` option to Pokémon properties, allowing you to set the moves of a Pokémon in commands and spawn files using comma-separated move names.
- Added shiny Pokémon effects.
- Added effects for the burn status effect.
- Added effects for the moves: Psychic, Water Sport, and Mud Sport.
- Added a new universal locator called "top".
- Added a new universal locator called `top`.
- Added shiny Pokémon particles with sound effects.
- Added animation for trading.
- Added icons for pending trade, team-up, and battle requests from other players.
- Added `/spawnnpc` and `/spawnnpcat` commands.
- Pokémon are now animated when seen in any GUI that isn't the party GUI.
- Quirk animations can now occur for Pokémon that are shoulder mounted.
### Pokémon Added
#### Gen 1
@@ -54,10 +60,6 @@
- Octillery
- Mantine
- Smeargle
- Remoraid
- Octillery
- Mantine
- Corsola
#### Gen 3
- Slakoth
@@ -75,18 +77,6 @@
- Bagon
- Shelgon
- Salamence
#### Gen 4
- Shellos [East Sea form]
- Gastrodon [East Sea form]
- Finneon
- Lumineon
- Mantyke
- Feebas
- Milotic
- Spheal
- Sealeo
- Walrein
- Kecleon
#### Gen 4
@@ -122,12 +112,6 @@
- Hatenna
- Hattrem
- Hatterene
- Bruxish
- Mareanie
- Toxapex
#### Gen 8
- Cramorant
- Pincurchin
#### Gen 9
@@ -144,6 +128,7 @@
- Wiglett
- Wugtrio
- Flamigo
- Dondozo
#### Unique Forms
- Magikarp Jump Variants
@@ -189,7 +174,7 @@
- Hatenna, Hattrem, Hatterene
- Pincurchin
- Klawf
- Finizen, Palafin [Zero], Palafin [Hero]
- Finizen, Palafin \[Zero\], Palafin \[Hero\]
- Dondozo
### Added shoulder mounts for the following Pokémon
@@ -224,10 +209,10 @@
- Shroodle
- Tatsugiri
- Glimmet
- Gimmighoul [Roaming]
- Gimmighoul \[Roaming\]
### Animation updates for the following Pokémon
- Bellsprout, Weepinbel, Victreebel
- Bellsprout, Weepinbell, Victreebel
- Shellder, Cloyster
- Porygon, Porygon2, Porygon-Z
- Furret
@@ -249,7 +234,7 @@
- Dreepy, Drakloak
- Kleavor
- Scorbunny
- Gimmighoul [Roaming]
- Gimmighoul \[Roaming\]
### Model updates for the following Pokémon
- Bulbasaur, Ivysaur, Venusaur
@@ -332,11 +317,12 @@
- Updated particles for status effects: Paralysis, Poison, and Sleep.
- Updated particles on Gastly.
- Revamped stat buff and de-buff particles.
- Improved the performance of display cases that contain Pokémon photos.
### Fixes
- Scaled down Amaura's fetus model to avoid clipping through the tank while animating.
- Fixed Cubone's cry not having a sound.
- Fixed the sendout sound erronously playing when a wild Pokémon breaks out of a Poké ball.
- Fixed the sendout sound erroneously playing when a wild Pokémon breaks out of a Poké ball.
- Flamethrower is no longer missing sounds.
- Fixed the Seafloor spawning context not being a usable context.
- Fixed Pokemon spawning in non-full blocks like slabs.
@@ -344,11 +330,6 @@
- Fixed Pokémon losing their Hidden Ability through evolution if the middle stage did not have a Hidden Ability.
- Hidden Power no longer plays the water type action effect (It now plays the normal type action effect).
- Fixed Chimchar and Monferno T-posing whenever they sleep.
- Fixed the Magby line not having any placeholder walk animations.
- Fixed Duskull and Dusclops using skylight levels for their nether spawn data. There is no sun in the Nether!
- Fixed Hisuian Zoroark using base Zoroark stats.
- Fixed Bellossom clipping into the player head when shoulder mounted.
- Fixed Shroomish's look range to prevent it from looking higher than it should.
- Fixed Cetitan's cry breaking its walk and sleep animations.
- Fixed Crumbling Arch not blending in with the world.
- Fixed Energy Root applying being usable on a fainted Pokémon.
@@ -405,6 +386,14 @@
- Fixed the Ice Face ability activation being displayed incorrectly in a battle.
- Fixed edge case of F1 and R locking you in battle with no R functionality (requiring ESC to get out).
- Fixed Poké Ball render orientation in battle interface when capturing.
- Fixed sync issues with recently learned moves where their PP would not go down until you logout and in.
- Fixed some Pokémon photos in display cases flashing if the Pokémon has gender differences.
- Fixed Bidoof and Jigglypuff sleep animations stopping after some time, causing them to be 'asleep' while T-posing menacingly.
- Fixed entities not changing poses unless you're looking at them.
- Fixed cries not playing on send-out if the Pokémon is off-screen.
- Fixed text row selection highlight in battle log.
- Fixed top black border rendering in scroll interfaces in summary UI.
- Fixed aspect tracking for Advancements.
### Developer
- `SpawnCause` is now an implementation of `SpawningInfluence`.
@@ -435,6 +424,11 @@
- Pokémon Heal (with context)
- Added HealingSource, an interface applied to all sources of healing from player actions, for easier tracking of healing sources.
- Move Change
- Rebuilt large swaths of the model animation code to simplify it.
- Renamed a bunch of things from %Poseable% to %Posable% because spelling.
- Renamed StatelessAnimation to PoseAnimation.
- Renamed StatefulAnimation to ActiveAnimation.
- Documented the animation system.
### Data Pack & Resource Pack Creators
- Added support for "shedders" similar to Shedinja's evolution logic.
@@ -451,6 +445,11 @@
- Sound events for all blocks now start with "block."
- Gimmighoul chest and item interaction sounds have been moved to where its cry is.
- Unused sound files and sound events have been removed.
- Added MoLang compatibility in the isVisible property for transformed parts.
- Added q.has_aspect('some_aspect') function to animations, posers, and entity particle effects.
- Added support for conditional pose animations.
- Added a new universal locator called "top".
- Added `eggs_collected` and `eggs_hatched` Advancement triggers.
### Localization
- Updated translations for:
@@ -8,7 +8,7 @@
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.client.CobblemonClient;
import com.cobblemon.mod.common.client.render.ClientPlayerIcon;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.client.renderer.MultiBufferSource;
@@ -20,11 +20,9 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(PlayerRenderer.class)
public class PlayerRendererMixin {
@Inject(
method = "render(Lnet/minecraft/client/player/AbstractClientPlayer;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V",
at = @At(value = "TAIL")
)
@Inject(method = "render", at = @At(value = "TAIL"))
public void renderPlayer(AbstractClientPlayer player, float f, float g, PoseStack poseStack, MultiBufferSource multiBufferSource, int i, CallbackInfo ci) {
CobblemonClient.INSTANCE.getRequests().onRenderPlayer(player, g);
ClientPlayerIcon.Companion.onRenderPlayer(player);
}
}
@@ -17,6 +17,7 @@ import com.cobblemon.mod.common.api.drop.DropEntry
import com.cobblemon.mod.common.api.drop.ItemDropEntry
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.CobblemonEvents.DATA_SYNCHRONIZED
import com.cobblemon.mod.common.api.interaction.RequestManager
import com.cobblemon.mod.common.api.permission.PermissionValidator
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CaptureCalculator
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CaptureCalculators
@@ -213,7 +214,7 @@ object Cobblemon {
BattleRegistry.onPlayerDisconnect(it.player)
storage.onPlayerDisconnect(it.player)
playerDataManager.onPlayerDisconnect(it.player)
TradeManager.onLogoff(it.player)
RequestManager.onLogoff(it.player)
}
PlatformEvents.PLAYER_DEATH.subscribe {
PCLinkManager.removeLink(it.player.uuid)
@@ -155,19 +155,19 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
val pokedexes = mutableListOf<PokedexItem>()
@JvmField
val POKEDEX_BLACK = pokedexItem(PokedexTypes.BLACK)
val POKEDEX_RED = pokedexItem(PokedexTypes.RED)
@JvmField
val POKEDEX_BLUE = pokedexItem(PokedexTypes.BLUE)
val POKEDEX_YELLOW = pokedexItem(PokedexTypes.YELLOW)
@JvmField
val POKEDEX_GREEN = pokedexItem(PokedexTypes.GREEN)
@JvmField
val POKEDEX_BLUE = pokedexItem(PokedexTypes.BLUE)
@JvmField
val POKEDEX_PINK = pokedexItem(PokedexTypes.PINK)
@JvmField
val POKEDEX_RED = pokedexItem(PokedexTypes.RED)
val POKEDEX_BLACK = pokedexItem(PokedexTypes.BLACK)
@JvmField
val POKEDEX_WHITE = pokedexItem(PokedexTypes.WHITE)
@JvmField
val POKEDEX_YELLOW = pokedexItem(PokedexTypes.YELLOW)
@JvmField
val VIVICHOKE = compostableItem("vivichoke")
@@ -98,9 +98,6 @@ 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.BattleSelectActionsPacket
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.callback.move.MoveSelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.move.MoveSelectedPacket
import com.cobblemon.mod.common.net.messages.server.callback.party.PartyPokemonSelectedPacket
@@ -129,9 +126,6 @@ import com.cobblemon.mod.common.net.messages.server.storage.pc.ReleasePCPokemonP
import com.cobblemon.mod.common.net.messages.server.storage.pc.SwapPCPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.pc.UnlinkPlayerFromPCPacket
import com.cobblemon.mod.common.net.serverhandling.*
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.callback.move.MoveSelectCancelledHandler
import com.cobblemon.mod.common.net.serverhandling.callback.move.MoveSelectedHandler
import com.cobblemon.mod.common.net.serverhandling.callback.party.PartyPokemonSelectedHandler
@@ -162,6 +156,13 @@ import com.cobblemon.mod.common.net.serverhandling.storage.pc.MovePartyPokemonTo
import com.cobblemon.mod.common.net.serverhandling.storage.pc.ReleasePartyPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.storage.pc.SwapPCPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.storage.pc.UnlinkPlayerFromPCHandler
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.UpdateTradeOfferHandler
import com.cobblemon.mod.common.net.messages.server.battle.*
import com.cobblemon.mod.common.net.serverhandling.battle.*
import com.cobblemon.mod.common.net.messages.client.pokedex.ServerConfirmedRegisterPacket
import com.cobblemon.mod.common.net.messages.client.trade.*
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.FinishScanningPacket
@@ -29,6 +29,8 @@ object CobblemonCriteria : PlatformRegistry<Registry<CriterionTrigger<*>>, Resou
val CATCH_SHINY_POKEMON = this.create("catch_shiny_pokemon", SimpleCriterionTrigger(CountableCriterion.CODEC))
val EGG_COLLECT = this.create("eggs_collected", SimpleCriterionTrigger(CountableCriterion.CODEC))
val EGG_HATCH = this.create("eggs_hatched", SimpleCriterionTrigger(CountableCriterion.CODEC))
val EVOLVE_POKEMON = this.create("pokemon_evolved", SimpleCriterionTrigger(EvolvePokemonCriterion.CODEC))
@@ -58,4 +60,4 @@ object CobblemonCriteria : PlatformRegistry<Registry<CriterionTrigger<*>>, Resou
override val registry = BuiltInRegistries.TRIGGER_TYPES
override val resourceKey = Registries.TRIGGER_TYPE
}
}
@@ -23,7 +23,7 @@ import com.cobblemon.mod.common.pokemon.Species
*/
open class AbilityPool : PrioritizedList<PotentialAbility>() {
fun select(species: Species, aspects: Set<String>): Pair<Ability, Priority> {
for (priority in Priority.values()) {
for (priority in Priority.entries) {
val potentialAbilities = priorityMap[priority]?.filter { it.isSatisfiedBy(aspects) } ?: continue
if (potentialAbilities.isNotEmpty()) {
return potentialAbilities.random().template.create() to priority
@@ -31,7 +31,7 @@ open class AbilityPool : PrioritizedList<PotentialAbility>() {
}
LOGGER.error("Unable to select an ability from the pool for $species and aspects: ${aspects.joinToString()}")
LOGGER.error("Usually this happens when a client is doing logic it shouldn't. Please show this to the Cobblemon developers!")
LOGGER.error("Please show this to the Cobblemon developers!")
Exception().printStackTrace()
return Abilities.first().create() to Priority.LOWEST
}
@@ -11,6 +11,7 @@ package com.cobblemon.mod.common.api.data
import com.cobblemon.mod.common.api.net.Decodable
import com.cobblemon.mod.common.api.net.Encodable
// TODO this is implemented in multiple places but unused. Can we scratch this?
/**
* Represents an object that will possibly be synchronized to the client during datapack synchronization.
*
@@ -11,11 +11,12 @@ package com.cobblemon.mod.common.api.drop
import com.cobblemon.mod.common.api.events.CobblemonEvents.LOOT_DROPPED
import com.cobblemon.mod.common.api.events.drops.LootDroppedEvent
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import kotlin.random.Random
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.phys.Vec3
import kotlin.random.Random
/**
* A table of drops that can produce a list of [DropEntry]. You can produce a drop list from [getDrops] to
@@ -96,4 +97,14 @@ class DropTable {
ifSucceeded = { it.drops.forEach { it.drop(entity, world, pos, player) } }
)
}
fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeInt(this.entries.size)
this.entries.filterIsInstance<ItemDropEntry>().forEach { it.encode(buffer) }
}
fun decode(buffer: RegistryFriendlyByteBuf) {
val entries = buffer.readInt()
repeat(entries) { this.entries.add(ItemDropEntry().decode(buffer)) }
}
}
@@ -17,9 +17,10 @@ import com.cobblemon.mod.common.util.toBlockPos
import net.minecraft.core.component.DataComponentMap
import net.minecraft.core.component.DataComponentPatch
import net.minecraft.core.registries.Registries
import net.minecraft.server.level.ServerPlayer
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.item.ItemEntity
import net.minecraft.world.item.ItemStack
@@ -33,12 +34,12 @@ import net.minecraft.world.phys.Vec3
* @since July 24th, 2022
*/
open class ItemDropEntry : DropEntry {
override val percentage = 100F
override val quantity = 1
open val quantityRange: IntRange? = null
override val maxSelectableTimes = 1
override var percentage = 100F
override var quantity = 1
open var quantityRange: IntRange? = null
override var maxSelectableTimes = 1
open val dropMethod: ItemDropMethod? = null
open val item = ResourceLocation.parse("minecraft:fish")
open var item = ResourceLocation.parse("minecraft:fish")
open val components: DataComponentMap? = null
override fun drop(entity: LivingEntity?, world: ServerLevel, pos: Vec3, player: ServerPlayer?) {
@@ -76,4 +77,19 @@ open class ItemDropEntry : DropEntry {
world.addFreshEntity(ItemEntity(world, pos.x, pos.y, pos.z, stack))
}
}
fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeFloat(this.percentage)
buffer.writeVarInt(this.quantity)
buffer.writeResourceLocation(this.item)
buffer.writeNullable(this.quantityRange) { _, it -> buffer.writeVarInt(it.first); buffer.writeVarInt(it.last) }
}
fun decode(buffer: RegistryFriendlyByteBuf): ItemDropEntry {
this.percentage = buffer.readFloat()
this.quantity = buffer.readVarInt()
this.item = buffer.readResourceLocation()
this.quantityRange = buffer.readNullable { buffer.readVarInt()..buffer.readVarInt() }
return this
}
}
@@ -0,0 +1,106 @@
/*
* 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.interaction
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.battles.TeamManager
import com.cobblemon.mod.common.util.lang
import net.minecraft.server.level.ServerPlayer
import java.util.UUID
/**
* Represents an interaction request between players.
*
* @author Segfault Guy
* @since September 3rd, 2024
*/
interface PlayerActionRequest {
/** The unique ID of this request. */
val requestID: UUID
/** The unique ID of the party that's initiating this request. */
val senderID: UUID
/** The amount of seconds this request is valid for. */
val expiryTime: Int
}
/**
* An outbound [PlayerActionRequest].
*
* @author Segfault Guy
* @since September 29th, 2024
*/
interface ServerPlayerActionRequest : PlayerActionRequest {
/** The subkey identifying lang entries associated with this request. */
val key: String
/** The player initiating this interaction request. */
val sender: ServerPlayer
/** The player that's the target of this interaction request. */
val receiver: ServerPlayer
/** The unique ID of the party that's the target of this request. */
val receiverID: UUID get() = receiver.uuid
override val senderID: UUID get() = sender.uuid
/** Sends [packet] to sending party of this request. */
fun sendToSender(packet: NetworkPacket<*>) = sender.sendPacket(packet)
/** Sends [packet] to receiving party of this request. */
fun sendToReceiver(packet: NetworkPacket<*>) = receiver.sendPacket(packet)
/** Notifies the sending party of this request about [langKey]. */
fun notifySender(error: Boolean, langKey: String, vararg params: Any) = notify(sender, error, "$key.$langKey", *params)
/** Notifies the receiving party of this request about [langKey]. */
fun notifyReceiver(error: Boolean, langKey: String, vararg params: Any) = notify(receiver, error, "$key.$langKey", *params)
/** System message to inform individual [player] about [langKey]. */
fun notify(player: ServerPlayer, error: Boolean, langKey: String, vararg params: Any) {
val lang = lang(langKey, *params).apply { if (error) red() else yellow() }
player.sendSystemMessage(lang, false)
}
}
/**
* An outbound [PlayerActionRequest] made on behalf of a team.
*
* @author Segfault Guy
* @since October 29th, 2024
*/
interface ServerTeamActionRequest : ServerPlayerActionRequest {
/** The team initiating this request. */
val senderTeam: TeamManager.MultiBattleTeam
/** The team that's the target of this request. */
val receiverTeam: TeamManager.MultiBattleTeam
override val senderID: UUID get() = senderTeam.teamID
override val receiverID: UUID get() = receiverTeam.teamID
override fun sendToSender(packet: NetworkPacket<*>) = CobblemonNetwork.sendPacketToPlayers(senderTeam.teamPlayers, packet)
override fun sendToReceiver(packet: NetworkPacket<*>) = CobblemonNetwork.sendPacketToPlayers(receiverTeam.teamPlayers, packet)
override fun notifySender(error: Boolean, langKey: String, vararg params: Any) =
senderTeam.teamPlayers.forEach { this.notify(it, error, "$key.$langKey", *params) }
override fun notifyReceiver(error: Boolean, langKey: String, vararg params: Any) =
receiverTeam.teamPlayers.forEach { this.notify(it, error, "$key.$langKey", *params) }
}
@@ -0,0 +1,230 @@
/*
* 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.interaction
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.scheduling.afterOnServer
import com.cobblemon.mod.common.api.text.aqua
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.battles.TeamManager
import com.cobblemon.mod.common.util.*
import net.minecraft.server.level.ServerPlayer
import java.util.*
/**
* Defines a manager for organizing active interaction requests between parties. Responsible for dispatching requests to clients,
* notifying senders and receivers of requests, and handling their acceptance, expiration, or cancellation.
*
* Implementations can manage either PLAYER-to-PLAYER requests or TEAM-to-TEAM requests.
*
* @param T The type of interaction request this manager tracks.
*
* @author Segfault Guy
* @since October 26th, 2024
*/
abstract class RequestManager<T : ServerPlayerActionRequest> {
/**
* Tracking of the active [ServerPlayerActionRequest]s by their sender.
*
* NOTE: only 1 request can be sent by a sender at a time.
*/
private val outboundRequests = mutableMapOf<UUID, T>()
/**
* Tracking of the active [ServerPlayerActionRequest]s by their recipient.
*
* NOTE: a recipient can receive multiple requests from different senders at the same time.
*/
private val inboundRequests = mutableMapOf<UUID, MutableList<T>>()
// we use the term 'sender' because BattleChallenges can be sent TEAM-to-TEAM instead of PLAYER-to-PLAYER for multi battles
/** Queries the latest pending request that was sent by [senderID]. */
fun getOutboundRequest(senderID: UUID) = outboundRequests.get(senderID)
/** Queries the pending request of [requestID] that was sent by [senderID]. */
fun getOutboundRequest(senderID: UUID, requestID: UUID) = this.getOutboundRequest(senderID)?.takeIf { it.requestID == requestID }
/** Queries the pending request that was sent by [senderID] and received by [receiverID]. */
fun getOutboundRequestByRecipient(senderID: UUID, receiverID: UUID) = this.getOutboundRequest(senderID)?.takeIf { it.receiverID == receiverID }
/** Queries the pending request of [requestID] that was sent by [sender] or their team. */
fun getOutboundRequest(sender: ServerPlayer, requestID: UUID) = TeamManager.getTeam(sender)?.let { this.getOutboundRequest(it.teamID, requestID) }
?: this.getOutboundRequest(sender.uuid, requestID)
/** Queries all pending requests that were received by [receiverID]. */
fun getInboundRequests(receiverID: UUID) = inboundRequests.get(receiverID)
/** Queries the pending request of [requestID] that was received by [receiverID]. */
fun getInboundRequest(receiverID: UUID, requestID: UUID) = this.getInboundRequests(receiverID)?.find { it.requestID == requestID }
/** Queries the pending request that was sent by [senderID] and received by [receiverID]. */
fun getInboundRequestBySender(receiverID: UUID, senderID: UUID) = this.getInboundRequests(receiverID)?.find { it.senderID == senderID }
/** Queries the pending request of [requestID] that was received by [receiver] or their team. */
fun getInboundRequest(receiver: ServerPlayer, requestID: UUID) = TeamManager.getTeam(receiver)?.let { this.getInboundRequest(it.teamID, requestID) }
?: this.getInboundRequest(receiver.uuid, requestID)
/** Determines whether [player] can receive a [T]. */
open fun isBusy(player: ServerPlayer): Boolean = player.isInBattle() || player.isTrading()
/** Determines if the [target] is a valid interaction request target. */
abstract fun isValidInteraction(player: ServerPlayer, target: ServerPlayer): Boolean
/** Determines whether [request] response is valid and can be accepted. If invalid, notifies the sender and receiver of the request why. */
protected abstract fun canAccept(request: T): Boolean
/** Creates a packet for notifying a recipient about a received request. */
protected abstract fun notificationPacket(request: T): NetworkPacket<*>
/** Creates a packet for canceling received requests. */
protected abstract fun expirationPacket(request: T): NetworkPacket<*>
protected fun addRequest(request: T) {
outboundRequests.put(request.senderID, request)
inboundRequests.computeIfAbsent(request.receiverID) { mutableListOf() }.add(request)
request.sendToReceiver(this.notificationPacket(request))
}
protected fun removeRequest(request: T): Boolean {
val inbound = inboundRequests.get(request.receiverID)
val removeOutbound = outboundRequests.remove(request.senderID, request)
val removeInbound = inbound?.remove(request) == true
if (removeInbound && inbound?.size == 0) inboundRequests.remove(request.receiverID, inbound)
if (removeInbound != removeOutbound)
Cobblemon.LOGGER.error("RequestManager: PlayerActionRequest desync") // USE ADDREQUEST AND REMOVEREQUEST
val isRemoved = removeInbound && removeOutbound
if (isRemoved) request.sendToReceiver(this.expirationPacket(request))
return isRemoved
}
/** Notifies and removes an outbound request [T] sent by [ServerPlayerActionRequest.sender] to [ServerPlayerActionRequest.receiver]. */
open fun cancelRequest(request: T, expired: Boolean = false) {
if (!this.removeRequest(request)) return
// canceled by expiration
if (expired) {
request.notifySender(true, "expired.sender", request.receiver.name.copy().aqua())
request.notifyReceiver(true, "expired.receiver", request.sender.name.copy().aqua())
}
// canceled by sender
else {
request.notifySender(true, "canceled.sender", request.receiver.name.copy().aqua())
request.notifyReceiver(true, "canceled.receiver", request.sender.name.copy().aqua())
}
// receivers DECLINE, not cancel
}
/** Hook into [sendRequest] immediately after the [request] is sent and added to the manager. Default behavior is to notify parties. */
protected open fun onSend(request: T) {
request.notifySender(false, "sent", request.receiver.name.copy().aqua())
request.notifyReceiver(false, "received", request.sender.name.copy().aqua())
}
/** Sends an outbound request [T] from [ServerPlayerActionRequest.sender] to [ServerPlayerActionRequest.receiver]. */
fun sendRequest(request: T): Boolean {
val existingRequest = this.getOutboundRequest(request.senderID)
val pendingRequest = this.getInboundRequestBySender(request.senderID, request.receiverID)
// old request
if (existingRequest != null && existingRequest.receiverID != request.receiverID)
this.cancelRequest(existingRequest)
// if sender already has a request awaiting a response from the receiver they're trying to send to.
if (existingRequest != null && existingRequest.receiverID == request.receiverID)
request.notify(request.sender, true, "${request.key}.error.duplicate", request.receiver.name.copy().aqua())
// if sender already has a pending request from the receiver they're trying to send to.
else if (pendingRequest != null)
request.notify(request.sender, true, "${request.key}.error.pending", request.receiver.name.copy().aqua())
// verify sending player can interact with target player
else if (!this.isValidInteraction(request.sender, request.receiver))
request.notify(request.sender, true, "ui.interact.failed")
else if (this.isBusy(request.receiver)) // TODO call to isBusy when setting up the interaction packet
request.notify(request.sender, true, "ui.interact.unavailable")
// new request
else {
this.addRequest(request)
afterOnServer(seconds = request.expiryTime.toFloat()) { this.cancelRequest(request, true) }
this.onSend(request)
return true
}
return false
}
/** Hook into [acceptRequest] immediately after the [request] is accepted and removed from the manager. Default behavior is to notify parties. */
protected open fun onAccept(request: T) {
request.notifySender(false, "accept.sender", request.receiver.name.copy().aqua())
request.notifyReceiver(false, "accept.receiver", request.sender.name.copy().aqua())
}
/** Accepts a pending inbound [requestID] for [player]. */
fun acceptRequest(player: ServerPlayer, requestID: UUID, target: ServerPlayer? = null): Boolean {
var accepted = false
val request = this.getInboundRequest(player, requestID)
// verify request being responded to still valid
if (request == null)
player.sendSystemMessage(lang("ui.interact.request_already_expired").red(), false)
// verify accepting player can respond to sending player
else if (!this.isValidInteraction(player, target ?: request.sender)) // with teams anyone can accept
request.notify(player, true, "ui.interact.failed")
// if sending player is occupied, can't accept response
else if (this.isBusy(request.sender)) // TODO enhancement: allow retries - modify client so we don't remove request when sending acceptance
request.notify(player, true, "ui.interact.unavailable")
// if no condition is blocking acceptance, accept
else if (this.canAccept(request))
accepted = true
request?.let {
this.removeRequest(it)
if (accepted) this.onAccept(request) // order matters
}
return accepted
}
/** Hook into [declineRequest] immediately after the [request] is declined and removed from the manager. Default behavior is to notify parties. */
protected open fun onDecline(request: T) {
request.notifySender(true, "decline.sender", request.receiver.name.copy().aqua())
request.notifyReceiver(false, "decline.receiver", request.sender.name.copy().aqua())
}
/** Declines a pending [request]. */
fun declineRequest(request: T) {
this.removeRequest(request)
this.onDecline(request)
}
/** Declines an inbound request [requestID] for [receiver]. */
fun declineRequest(receiver: ServerPlayer, requestID: UUID) {
val request = this.getInboundRequest(receiver, requestID) ?: return
this.declineRequest(request)
}
/** Cancels pending outbound requests sent from, and pending inbound request sent to, [player]. */
protected open fun onLogoff(player: ServerPlayer) {
// ONLY regarding the player. see TeamManager for how team requests are canceled on team disband.
outboundRequests.get(player.uuid)?.let { request ->
this.cancelRequest(request)
}
inboundRequests.get(player.uuid)?.toMutableList()?.let { requests ->
requests.forEach { request ->
this.declineRequest(player, request.requestID)
}
}
}
companion object {
private val managers = mutableListOf<RequestManager<*>>()
fun register(manager: RequestManager<*>) = managers.add(manager)
fun onLogoff(player: ServerPlayer) = managers.forEach { it.onLogoff(player) }
}
}
@@ -252,6 +252,8 @@ object MoLangFunctions {
val map = hashMapOf<String, java.util.function.Function<MoParams, Any>>()
map.put("class") { StringValue(npc.npc.id.toString()) }
map.put("name") { StringValue(npc.name.string) }
map.put("level") { DoubleValue(npc.level) }
map.put("has_aspect") { params -> DoubleValue(npc.aspects.contains(params.getString(0))) }
map.put("face") { params -> ObjectValue(ReferenceDialogueFaceProvider(npc.id, params.getBooleanOrNull(0) != false)) }
map.put("in_battle") { DoubleValue(npc.isInBattle()) }
map.put("run_script_on_client") { params ->
@@ -297,7 +299,7 @@ object MoLangFunctions {
}
map.put("put_pokemon_in_healer") { params ->
val healer = npc.level().getBlockEntity(params.get<ArrayStruct>(0).asBlockPos(), CobblemonBlockEntities.HEALING_MACHINE).orElse(null) ?: return@put DoubleValue.ZERO
val party = npc.staticParty ?: return@put DoubleValue.ZERO
val party = npc.party ?: return@put DoubleValue.ZERO
if (healer.canHeal(party)) {
healer.activate(npc.uuid, party)
return@put DoubleValue.ONE
@@ -335,7 +337,7 @@ object MoLangFunctions {
val pokemonFunctions = mutableListOf<(Pokemon) -> HashMap<String, java.util.function.Function<MoParams, Any>>>(
{ pokemon ->
val map = hashMapOf<String, java.util.function.Function<MoParams, Any>>()
map.put("is_wild") { DoubleValue(pokemon.isWild()) }
map.put("is_wild") { DoubleValue(pokemon.entity?.let { it.ownerUUID == null } == true) }
map.put("is_shiny") { DoubleValue(pokemon.shiny) }
map.put("form") { StringValue(pokemon.form.name) }
map.put("weight") { DoubleValue(pokemon.species.weight.toDouble()) }
@@ -29,7 +29,10 @@ import net.minecraft.server.level.ServerLevel
class EntityParticlesActionEffectKeyframe : ConditionalActionEffectKeyframe(), EntityConditionalActionEffectKeyframe {
override val entityCondition = "q.entity.is_user".asExpressionLike()
var effect: String? = null
@Deprecated("Use the locators property instead", ReplaceWith("locators"))
var locator: String = "root"
var locators: Set<String> = setOf("root")
val delay: ExpressionLike = "0".asExpressionLike()
val visibilityRange = 200
@@ -45,7 +48,7 @@ class EntityParticlesActionEffectKeyframe : ConditionalActionEffectKeyframe(), E
}?.asIdentifierDefaultingNamespace() ?: return skip()
entities.filter { it is PosableEntity }.forEach { entity ->
val packet = SpawnSnowstormEntityParticlePacket(effectIdentifier, entity.id, listOf(locator))
val packet = SpawnSnowstormEntityParticlePacket(effectIdentifier, entity.id, (locators + locator).toList())
val players = (entity.level() as ServerLevel).getPlayers { it.distanceTo(entity) <= visibilityRange }
packet.sendToPlayers(players)
}
@@ -8,15 +8,11 @@
package com.cobblemon.mod.common.api.npc
import com.cobblemon.mod.common.api.npc.partyproviders.DynamicPartyProvider
import com.cobblemon.mod.common.api.npc.partyproviders.NPCParty
import com.cobblemon.mod.common.api.npc.partyproviders.PoolPartyProvider
import com.cobblemon.mod.common.api.npc.partyproviders.SimplePartyProvider
import com.cobblemon.mod.common.api.storage.party.PartyStore
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.nbt.CompoundTag
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.server.level.ServerPlayer
/**
* A provider of a party for battling the NPC. Completely custom party providers will only display
@@ -29,11 +25,13 @@ interface NPCPartyProvider {
companion object {
val types = mutableMapOf<String, (String) -> NPCPartyProvider>(
SimplePartyProvider.TYPE to { SimplePartyProvider() },
DynamicPartyProvider.TYPE to { DynamicPartyProvider() }
PoolPartyProvider.TYPE to { PoolPartyProvider() }
)
}
val type: String
fun provide(npc: NPCEntity, level: Int): NPCParty
val isStatic: Boolean
fun provide(npc: NPCEntity, level: Int): 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
fun loadFromJSON(json: JsonElement)
}
@@ -1,52 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.npc.partyproviders
import com.cobblemon.mod.common.api.npc.NPCPartyProvider
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.google.gson.JsonElement
import net.minecraft.resources.ResourceLocation
/**
* A provider of a party for battling the NPC. It generates a [StaticNPCParty] but using a more complex process
* that takes an input level and makes a randomized selection of Pokémon and movesets.
*
* @author Hiroku
* @since July 12th, 2024
*/
class DynamicPartyProvider : NPCPartyProvider {
companion object {
val TYPE = "dynamic"
}
class DynamicPool {
val id = ResourceLocation.parse("cobblemon:pool")
val pokemon: MutableList<DynamicPokemon> = mutableListOf()
}
class DynamicPokemon(
val pokemon: PokemonProperties,
val possibleLevels: IntRange,
val selectableTimes: IntRange,
val guaranteed: Boolean = false
)
override val type = "dynamic"
override fun loadFromJSON(json: JsonElement) {
TODO("Not yet implemented")
}
override fun provide(npc: NPCEntity, level: Int): NPCParty {
TODO("Not yet implemented")
}
}
@@ -1,91 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.npc.partyproviders
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
import com.cobblemon.mod.common.api.storage.party.PartyStore
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.net.IntSize
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.readSizedInt
import com.cobblemon.mod.common.util.writeSizedInt
import net.minecraft.core.RegistryAccess
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.server.level.ServerPlayer
/**
* Represents a party of Pokemon that an NPC can have. This isn't directly a party of Pokémon so that
* a dynamically generated party can exist where a party is generated at the time of a challenge.
*
* This interface is sealed. Implementations should be either [StaticNPCParty] or [DynamicNPCParty].
* If you want a custom type then you should implement [DynamicNPCParty] and register it in the
* [DynamicNPCParty.types] map.
*
* @author Hiroku
* @since July 12th, 2024
*/
sealed interface NPCParty {
fun getParty(player: ServerPlayer, npc: NPCEntity): PartyStore?
fun encode(buffer: RegistryFriendlyByteBuf)
fun decode(buffer: RegistryFriendlyByteBuf)
fun saveToNBT(nbt: CompoundTag, registryAccess: RegistryAccess)
fun loadFromNBT(nbt: CompoundTag, registryAccess: RegistryAccess)
}
/**
* A simple wrapping around a [NPCPartyStore]. This is a straightforward party that is always the same.
*
* @author Hiroku
* @since July 12th, 2024
*/
class StaticNPCParty(val party: NPCPartyStore) : NPCParty {
override fun getParty(player: ServerPlayer, npc: NPCEntity) = party
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeSizedInt(IntSize.U_BYTE, party.size())
for (pokemon in party) {
Pokemon.S2C_CODEC.encode(buffer, pokemon)
}
}
override fun decode(buffer: RegistryFriendlyByteBuf) {
repeat(times = buffer.readSizedInt(IntSize.U_BYTE)) {
party.add(Pokemon.S2C_CODEC.decode(buffer))
}
}
override fun saveToNBT(nbt: CompoundTag, registryAccess: RegistryAccess) {
for ((index, pokemon) in party.withIndex()) {
nbt.put(DataKeys.NPC_PARTY_POKEMON + index, pokemon.saveToNBT(registryAccess))
}
}
override fun loadFromNBT(nbt: CompoundTag, registryAccess: RegistryAccess) {
var index = 0
while (nbt.contains(DataKeys.NPC_PARTY_POKEMON + index)) {
this.party.add(Pokemon().loadFromNBT(registryAccess, nbt.getCompound(DataKeys.NPC_PARTY_POKEMON + index)))
index++
}
}
}
/**
* A dynamic party that is generated at the time of a challenge.
*
* @author Hiroku
* @since July 12th, 2024
*/
interface DynamicNPCParty : NPCParty {
companion object {
val types = mutableMapOf<String, Class<out NPCParty>>()
init {}
}
}
@@ -0,0 +1,128 @@
/*
* 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.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.npc.NPCPartyProvider
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.asExpression
import com.cobblemon.mod.common.util.resolveFloat
import com.cobblemon.mod.common.util.resolveInt
import com.cobblemon.mod.common.util.weightedSelection
import com.cobblemon.mod.common.util.withQueryValue
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
/**
* A provider of a party for battling the NPC. It generates a party using a more complex process
* that takes an input level and makes a randomized selection of Pokémon and movesets from a pool.
*
* The party can be static or dynamic based on [isStatic]. If it is dynamic, the pool will be used
* to generate a party whenever the NPC is challenged and AI that depends on a reliable party will
* not run.
*
* Not an actual pool party. That's for v2.0.
*
* @author Hiroku
* @since July 12th, 2024
*/
class PoolPartyProvider : NPCPartyProvider {
companion object {
const val TYPE = "pool"
}
@Transient
override val type = TYPE
override var isStatic: Boolean = false
var minPokemon: Expression = "1".asExpression()
var maxPokemon: Expression = "6".asExpression()
var pool = mutableListOf<DynamicPokemon>()
override fun loadFromJSON(json: JsonElement) {
json as JsonObject
this.minPokemon = json.getAsJsonPrimitive("minPokemon").asString?.asExpression() ?: "1".asExpression()
this.maxPokemon = json.getAsJsonPrimitive("maxPokemon").asString?.asExpression() ?: "6".asExpression()
this.pool = json.getAsJsonArray("pool").map {
if (it is JsonPrimitive) {
return@map DynamicPokemon(
PokemonProperties.parse(it.asString),
0..0,
0..100,
"1".asExpression(),
"1".asExpression()
)
}
it as JsonObject
DynamicPokemon(
PokemonProperties.parse(it.getAsJsonPrimitive("pokemon").asString),
it.getAsJsonPrimitive("levelVariation")?.asString?.split("-")?.let { it[0].toInt()..it[1].toInt() } ?: 0..0,
it.getAsJsonPrimitive("npcLevels")?.asString?.split("-")?.let { it[0].toInt()..it[1].toInt() } ?: 0..100,
it.getAsJsonPrimitive("selectableTimes")?.asString?.asExpression() ?: "1".asExpression(),
it.getAsJsonPrimitive("weight")?.asString?.asExpression() ?: "1".asExpression()
)
}.toMutableList()
isStatic = json.getAsJsonPrimitive("isStatic").asBoolean
}
class DynamicPokemon(
val pokemon: PokemonProperties,
val levelVariation: IntRange,
val npcLevels: IntRange,
val selectableTimes: Expression,
val weight: Expression = "1".asExpression()
) {
fun getWeight(runtime: MoLangRuntime): Float {
return runtime.resolveFloat(weight)
}
fun hasWeight(runtime: MoLangRuntime): Boolean {
return runtime.resolveFloat(weight) != 0F
}
}
fun formulateParty(npc: NPCEntity, level: Int, party: NPCPartyStore) {
val runtime = MoLangRuntime().setup().withQueryValue("npc", npc.struct)
val minPokemon = runtime.resolveInt(this.minPokemon)
val maxPokemon = runtime.resolveInt(this.maxPokemon)
var desiredPokemonCount = (minPokemon..maxPokemon).random()
val workingPool = pool.filter { level in it.npcLevels }.toMutableList()
val useCounts = mutableMapOf<DynamicPokemon, Int>()
while (desiredPokemonCount > 0 && workingPool.filter { it.hasWeight(runtime) }.isNotEmpty()) {
val selected = workingPool.filter { it.getWeight(runtime) == -1F }.randomOrNull()
?: workingPool.weightedSelection { it.getWeight(runtime) }
?: break
useCounts[selected] = useCounts.getOrDefault(selected, 0) + 1
val allowedSelections = runtime.resolveInt(selected.selectableTimes)
if (useCounts[selected]!! >= allowedSelections) {
workingPool.remove(selected)
}
desiredPokemonCount--
// If the Pokémon's props specifies a level then use that, otherwise choose a random level within the range
val randomLevel = (level + selected.levelVariation.random())
val instance = selected.pokemon.copy().also { it.level = it.level ?: randomLevel }.create()
party.add(instance)
}
}
override fun provide(npc: NPCEntity, level: Int): NPCPartyStore {
val party = NPCPartyStore(npc)
formulateParty(npc, level, party)
return party
}
}
@@ -28,6 +28,8 @@ class SimplePartyProvider : NPCPartyProvider {
@Transient
override val type = TYPE
@Transient
override val isStatic = true
val pokemon = mutableListOf<PokemonProperties>()
@@ -47,10 +49,10 @@ class SimplePartyProvider : NPCPartyProvider {
}
}
override fun provide(npc: NPCEntity, level: Int): NPCParty {
override fun provide(npc: NPCEntity, level: Int): NPCPartyStore {
val pokemon = pokemon.map { it.copy().also { it.level = it.level ?: level }.create() }
val party = NPCPartyStore(npc)
pokemon.forEach(party::add)
return StaticNPCParty(party)
return party
}
}
@@ -61,11 +61,15 @@ class FormDexRecord {
@Transient
lateinit var speciesDexRecord: SpeciesDexRecord
@Transient
lateinit var formName: String
@Transient
lateinit var struct: QueryStruct
fun initialize(speciesDexRecord: SpeciesDexRecord) {
fun initialize(speciesDexRecord: SpeciesDexRecord, formName: String) {
this.speciesDexRecord = speciesDexRecord
this.formName = formName
struct = QueryStruct(hashMapOf())
.addFunction("data") { data }
.addFunction("knowledge") { StringValue(knowledge.name) }
@@ -79,7 +79,7 @@ class SpeciesDexRecord {
fun initialize(pokedexManager: AbstractPokedexManager, id: ResourceLocation) {
this.id = id
this.pokedexManager = pokedexManager
this.formRecords.forEach { it.value.initialize(this) }
this.formRecords.forEach { it.value.initialize(this, it.key) }
}
fun onFormRecordUpdated(formDexRecord: FormDexRecord) {
@@ -106,7 +106,7 @@ class SpeciesDexRecord {
fun getOrCreateFormRecord(formName: String): FormDexRecord {
return formRecords.getOrPut(formName.lowercase()) {
val record = FormDexRecord()
record.initialize(this)
record.initialize(this, formName)
onFormRecordUpdated(record)
// Some more stuff eventually
record
@@ -156,12 +156,11 @@ interface Evolution : EvolutionLike {
}
val preEvoName = pokemon.getDisplayName()
val evolveMessage = lang("ui.evolve.into", preEvoName, pokemon.species.translatedName)
val pokemonEntity = pokemon.entity
if (pokemonEntity == null || !useEvolutionEffect) {
pokemon.getOwnerPlayer()?.playNotifySound(CobblemonSounds.EVOLUTION_UI, SoundSource.PLAYERS, 1F, 1F)
evolutionMethod(pokemon)
pokemon.getOwnerPlayer()?.sendSystemMessage(evolveMessage)
pokemon.getOwnerPlayer()?.sendSystemMessage(lang("ui.evolve.into", preEvoName, pokemon.species.translatedName))
} else {
pokemonEntity.busyLocks.add("evolving")
pokemonEntity.navigation.stop()
@@ -174,7 +173,7 @@ interface Evolution : EvolutionLike {
pokemonEntity.after( seconds = 12F ) {
cryAnimation(pokemonEntity)
pokemonEntity.busyLocks.remove("evolving")
pokemon.getOwnerPlayer()?.sendSystemMessage(evolveMessage)
pokemon.getOwnerPlayer()?.sendSystemMessage(lang("ui.evolve.into", preEvoName, pokemon.species.translatedName))
}
}
}
@@ -33,9 +33,9 @@ fun delayedFuture(ticks: Int = 0, seconds: Float = 0F, serverThread: Boolean = f
* being completed after the delay does things like entity removal or other thread-unsafe actions.
*/
@JvmOverloads
fun afterOnServer(ticks: Int = 0, seconds: Float = 0F, action: () -> Unit) = ServerTaskTracker.after(seconds + ticks / 20F, action)
fun afterOnServer(ticks: Int = 0, seconds: Float = 0F, action: () -> Unit) = ServerTaskTracker.after(seconds + ticks / 20F, action) // TODO this won't work with recent tickmanager changes
@JvmOverloads
fun afterOnClient(ticks: Int = 0, seconds: Float, action: () -> Unit) = ClientTaskTracker.after(seconds + ticks / 20F, action)
fun afterOnClient(ticks: Int = 0, seconds: Float, action: () -> Unit) = ClientTaskTracker.after(seconds + ticks / 20F, action) // TODO this won't work with recent tickmanager changes
@Deprecated("Use lerpOnServer or lerpOnClient, side-ambiguity causes problems now")
fun lerp(seconds: Float = 0F, serverThread: Boolean = false, action: (Float) -> Unit) = (if (serverThread) ServerTaskTracker else ClientTaskTracker).lerp(seconds, action = action)
@@ -166,9 +166,9 @@ open class PartyStore(override val uuid: UUID) : PokemonStore<PartyPosition>() {
}
override fun loadFromNBT(nbt: CompoundTag, registryAccess: RegistryAccess): PartyStore {
val slotCount = nbt.getInt(DataKeys.STORE_SLOT_COUNT)
while (slotCount > slots.size) { slots.removeLast() }
while (slotCount < slots.size) { slots.add(null) }
val slotCount = nbt.getInt(DataKeys.STORE_SLOT_COUNT).takeIf { it in 1..6 } ?: 6
while (slotCount < slots.size) { slots.removeLast() }
while (slotCount > slots.size) { slots.add(null) }
for (slot in slots.indices) {
val pokemonNBT = nbt.getCompound(DataKeys.STORE_SLOT + slot)
try {
@@ -8,7 +8,6 @@
package com.cobblemon.mod.common.api.storage.player
import com.cobblemon.mod.common.advancement.criterion.AspectCriterion
import com.cobblemon.mod.common.api.types.ElementalType
import com.cobblemon.mod.common.pokemon.Pokemon
import net.minecraft.server.level.ServerPlayer
@@ -18,6 +17,8 @@ class PlayerAdvancementData {
var totalCaptureCount: Int = 0
private set
var totalEggsCollected: Int = 0
private set
var totalEggsHatched: Int = 0
private set
var totalEvolvedCount: Int = 0
@@ -44,6 +45,10 @@ class PlayerAdvancementData {
totalCaptureCount++
}
fun updateTotalEggsCollected() {
totalEggsCollected++
}
fun updateTotalEggsHatched() {
totalEggsHatched++
}
@@ -102,7 +107,8 @@ class PlayerAdvancementData {
}
fun updateAspectsCollected(player: ServerPlayer, pokemon: Pokemon) {
val aspectConditions = player.advancements.progress.keys
//TODO: take another look at using the Advancement progress
/*val aspectConditions = player.advancements.progress.keys
.flatMap { it.value.criteria.values }
.mapNotNull { it.trigger }
.filterIsInstance<AspectCriterion>()
@@ -114,6 +120,9 @@ class PlayerAdvancementData {
if (trackedAspects.isNotEmpty()) {
val collectedAspects = aspectsCollected.getOrPut(pokemon.species.resourceIdentifier) { mutableSetOf() }
pokemon.aspects.filter(trackedAspects::contains).forEach(collectedAspects::add)
}
}*/
val collectedAspects = aspectsCollected.getOrPut(pokemon.species.resourceIdentifier) { mutableSetOf() }
pokemon.aspects.forEach(collectedAspects::add)
}
}
}
@@ -125,42 +125,44 @@ object BattleBuilder {
@JvmOverloads
fun pvp2v2(
players: List<ServerPlayer> = emptyList(),
leadingPokemon: List<UUID> = emptyList(),
battleFormat: BattleFormat = BattleFormat.GEN_9_MULTI,
cloneParties: Boolean = false,
healFirst: Boolean = false,
partyAccessor: (ServerPlayer) -> PartyStore = { it.party() }
players: List<ServerPlayer> = emptyList(),
leadingPokemon: List<UUID> = emptyList(),
battleFormat: BattleFormat = BattleFormat.GEN_9_MULTI,
cloneParties: Boolean = false,
healFirst: Boolean = false,
partyAccessor: (ServerPlayer) -> PartyStore = { it.party() }
): BattleStartResult {
val adjustLevel = battleFormat.adjustLevel
val teams = players.mapIndexed { index, it ->
partyAccessor(it).toBattleTeam(
clone = cloneParties,
clone = cloneParties || adjustLevel > 0,
healPokemon = healFirst,
leadingPokemon[index]
).sortedBy { it.health <= 0 }
}
val playerActors = teams.mapIndexed { index, team -> PlayerBattleActor(players[index].uuid, team)}.toMutableList()
val adjustLevel = battleFormat.adjustLevel
val battlePartyStores = mutableListOf<PlayerPartyStore>()
if (adjustLevel > 0) {
teams.forEachIndexed { index, battleTeam ->
battleTeam.forEach { battlePokemon ->
teams.forEachIndexed { playerIndex, battleTeam ->
val tempStore = PlayerPartyStore(players[playerIndex].uuid)
battleTeam.forEachIndexed { pokemonIndex, battlePokemon ->
battlePokemon.effectedPokemon.level = adjustLevel
battlePokemon.effectedPokemon.heal()
val tempStore = PlayerPartyStore(players[index].uuid)
battlePartyStores.add(tempStore)
tempStore.set(pokemonIndex, battlePokemon.effectedPokemon)
}
battlePartyStores.add(tempStore)
}
}
val errors = ErroredBattleStart()
if (players.size != BattleRegistry.MAX_TEAM_MEMBER_COUNT * 2) {
if (players.size != TeamManager.MAX_TEAM_MEMBER_COUNT * 2) {
playerActors.forEach {actor ->
errors.participantErrors[actor] += BattleStartError.incorrectActorCount(
requiredCount = BattleRegistry.MAX_TEAM_MEMBER_COUNT * 2,
requiredCount = TeamManager.MAX_TEAM_MEMBER_COUNT * 2,
hadCount = players.size
)
}
@@ -312,7 +314,7 @@ object BattleBuilder {
): BattleStartResult {
val playerTeam = party.toBattleTeam(clone = cloneParties, healPokemon = healFirst, leadingPokemon = leadingPokemon)
val playerActor = PlayerBattleActor(player.uuid, playerTeam)
val npcParty = npcEntity.party?.getParty(player, npcEntity)
val npcParty = npcEntity.getPartyForChallenge(player)
val errors = ErroredBattleStart()
if (playerActor.pokemonList.size < battleFormat.battleType.slotsPerActor) {
@@ -495,6 +497,10 @@ open class ErroredBattleStart(
return this
}
fun sendTo(entities: Collection<Entity>, transformer: (MutableComponent) -> (MutableComponent) = { it }) {
entities.forEach { this.sendTo(it, transformer) }
}
fun sendTo(entity: Entity, transformer: (MutableComponent) -> (MutableComponent) = { it }) {
errors.forEach { entity.sendSystemMessage(transformer(it.getMessageFor(entity))) }
}
@@ -8,8 +8,6 @@
package com.cobblemon.mod.common.battles
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.battles.BattleStartedPostEvent
@@ -19,142 +17,26 @@ import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.pokemon.status.Statuses
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.net.messages.client.battle.BattleChallengeExpiredPacket
import com.cobblemon.mod.common.net.messages.client.battle.TeamMemberRemoveNotificationPacket
import com.cobblemon.mod.common.net.messages.client.battle.TeamRequestExpiredPacket
import com.cobblemon.mod.common.util.getPlayer
import com.cobblemon.mod.common.util.server
import com.cobblemon.mod.common.util.lang
import com.google.gson.GsonBuilder
import java.time.Instant
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import net.minecraft.server.level.ServerPlayer
object BattleRegistry {
const val MAX_TEAM_MEMBER_COUNT = 2
const val MAX_BATTLE_RADIUS = 15.0
class BattleChallenge(
val challengeId: UUID,
val challengedPlayerUUID: UUID,
val selectedPokemonId: UUID,
val battleFormat: BattleFormat,
var expiryTimeSeconds: Int = 60
) {
val challengedTime = Instant.now()
fun isExpired() = Instant.now().isAfter(challengedTime.plusSeconds(expiryTimeSeconds.toLong()))
}
class TeamRequest(
val requestID: UUID,
val requestedPlayerUUID: UUID,
var expiryTimeSeconds: Int = 60
) {
val challengedTime = Instant.now()
fun isExpired() = Instant.now().isAfter(challengedTime.plusSeconds(expiryTimeSeconds.toLong()))
}
class MultiBattleTeam(
val teamID: UUID,
val teamPlayersUUID: MutableList<UUID>,
)
val gson = GsonBuilder()
.disableHtmlEscaping()
.registerTypeAdapter(ShowdownMoveset::class.java, ShowdownMovesetAdapter)
.create()
private val battleMap = ConcurrentHashMap<UUID, PokemonBattle>()
// Challenger to challenge
val pvpChallenges = mutableMapOf<UUID, BattleChallenge>()
// Multi-battle teams
val playerToTeam = mutableMapOf<UUID, MultiBattleTeam>()
val multiBattleTeams = mutableMapOf<UUID, MultiBattleTeam>()
val multiBattleTeamRequests = mutableMapOf<UUID, TeamRequest>()
fun onServerStarted() {
battleMap.clear()
pvpChallenges.clear()
playerToTeam.clear()
}
fun removeChallenge(challengerId: UUID, challengeId: UUID? = null, isMultiBattleTeam: Boolean = false) {
val existing = pvpChallenges[challengerId] ?: return
if (existing.challengeId != challengeId) {
return
}
pvpChallenges.remove(challengerId)
if (isMultiBattleTeam) {
multiBattleTeams[existing.challengedPlayerUUID]?.teamPlayersUUID?.let {
CobblemonNetwork.sendPacketToPlayers(it.mapNotNull { serverPlayer -> serverPlayer.getPlayer() }, BattleChallengeExpiredPacket(existing.challengeId))
}
} else {
existing.challengedPlayerUUID.getPlayer()?.sendPacket(BattleChallengeExpiredPacket(existing.challengeId))
}
}
fun removeTeamUpRequest(requesterId: UUID, requestId: UUID? = null) {
val existing = multiBattleTeamRequests[requesterId] ?: return
if (existing.requestID != requestId) {
return
}
multiBattleTeamRequests.remove(requesterId)
existing.requestedPlayerUUID.getPlayer()?.sendPacket(TeamRequestExpiredPacket(existing.requestID))
}
fun onPlayerDisconnect(player: ServerPlayer) {
// Stop battles
getBattleByParticipatingPlayer(player)?.stop()
// Remove player from any teams they may be a part of
removeTeamMember(player)
}
fun removeTeamMember(player: ServerPlayer) {
val teamEntry = playerToTeam[player.uuid]
if (teamEntry != null) {
teamEntry.teamPlayersUUID.remove(player.uuid)
playerToTeam.remove(player.uuid)
val notificationPacket = TeamMemberRemoveNotificationPacket(player.uuid)
CobblemonNetwork.sendPacketToPlayer(player, notificationPacket)
// Notify remaining members that the player has left the group
server()?.let {
val teamServerPlayers = teamEntry.teamPlayersUUID.mapNotNull { uuid -> it.playerList.getPlayer(uuid) }
CobblemonNetwork.sendPacketToPlayers(teamServerPlayers, notificationPacket)
}
if (teamEntry.teamPlayersUUID.count() == 1) {
val remainingPlayerUUID = teamEntry.teamPlayersUUID.first()
// Remove any outstanding battle requests for the team
// TODO: Find a way to not do a reverse map look up
val battleChallengeEntry = pvpChallenges.entries.firstOrNull { it -> it.value.challengedPlayerUUID == teamEntry.teamID }
if (battleChallengeEntry != null) {
val battleChallenge = battleChallengeEntry.value
removeChallenge(battleChallengeEntry.key, battleChallenge.challengeId, true)
// Notify opposing team that the challenge was cancelled
val opposingTeam = multiBattleTeams[battleChallengeEntry.key]
opposingTeam?.teamPlayersUUID?.map { it.getPlayer() }?.forEach { serverPlayerEntity ->
serverPlayerEntity?.sendSystemMessage(lang("challenge.multi.decline.disband"))
}
}
// Disband the team
playerToTeam.remove(remainingPlayerUUID)
multiBattleTeams.remove(teamEntry.teamID)
// Notify remaining player that the team has been disbanded
server()?.let {
it.playerList.getPlayer(remainingPlayerUUID)?.let { serverPlayerEntity ->
CobblemonNetwork.sendPacketToPlayer(serverPlayerEntity, TeamMemberRemoveNotificationPacket(remainingPlayerUUID))
}
}
}
}
}
/**
@@ -40,7 +40,7 @@ class BattleSide(vararg val actors: BattleActor) {
activePokemon.forEach {
val entity = it.battlePokemon?.entity ?: return@forEach
entity.cry()
if(entity.pokemon.shiny) {
if(entity.pokemon.shiny && entity.pokemon.isWild()) {
SpawnSnowstormEntityParticlePacket(cobblemonResource("shiny_ring"), entity.id, listOf("shiny_particles", "middle"))
.sendToPlayersAround(entity.x, entity.y, entity.z, 64.0, entity.level().dimension())
}
@@ -0,0 +1,194 @@
/*
* 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.battles
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.interaction.RequestManager
import com.cobblemon.mod.common.api.interaction.ServerPlayerActionRequest
import com.cobblemon.mod.common.api.interaction.ServerTeamActionRequest
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.text.aqua
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.battles.TeamManager.MAX_TEAM_MEMBER_COUNT
import com.cobblemon.mod.common.battles.TeamManager.MultiBattleTeam
import com.cobblemon.mod.common.net.messages.client.battle.BattleChallengeExpiredPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleChallengeNotificationPacket
import com.cobblemon.mod.common.util.canInteractWith
import com.cobblemon.mod.common.util.getBattleTeam
import com.cobblemon.mod.common.util.party
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.phys.Vec3
import java.util.*
/**
* Responsible for managing [BattleChallenge]s and initiating PVP [PokemonBattle]s.
*
* @author Segfault Guy, JazzMcNade
* @since October 28th, 2024
*/
object ChallengeManager : RequestManager<ChallengeManager.BattleChallenge>() {
init {
register(this)
}
const val MAX_BATTLE_RADIUS = 15.0
/** Mapping of players to their lead Pokemon. */
private val selectedLead = mutableMapOf<UUID, UUID>()
/**
* Represents an interaction request between players or teams to start a battle.
*
* @param sender The player sending this request.
* @param receiver The player receiving this request.
* @param selectedPokemonId
* @param battleFormat
* @param expiryTime How long (in seconds) this request is active.
*/
abstract class BattleChallenge : ServerPlayerActionRequest {
abstract val selectedPokemonId: UUID
abstract val battleFormat: BattleFormat
override val requestID: UUID = UUID.randomUUID()
}
/** PLAYER-to-PLAYER BattleChallenge */
data class SinglesBattleChallenge(
override val sender: ServerPlayer,
override val receiver: ServerPlayer,
override val selectedPokemonId: UUID,
override val battleFormat: BattleFormat,
override val expiryTime: Int = 20
) : BattleChallenge() {
override val key: String = "challenge"
}
/** TEAM-to-TEAM BattleChallenge */
data class MultiBattleChallenge(
override val sender: ServerPlayer,
override val receiver: ServerPlayer,
override val selectedPokemonId: UUID,
override val battleFormat: BattleFormat, // TODO force this to be multi battletype
override val expiryTime: Int = 20
) : BattleChallenge(), ServerTeamActionRequest {
override val key: String = "challenge.multi"
override val senderTeam: MultiBattleTeam = sender.getBattleTeam() ?: throw IllegalArgumentException("Sending player is not part of a team!")
override val receiverTeam: MultiBattleTeam = receiver.getBattleTeam() ?: throw IllegalArgumentException("Target player is not part of a team!")
}
override fun expirationPacket(request: BattleChallenge): NetworkPacket<*> = BattleChallengeExpiredPacket(request)
override fun notificationPacket(request: BattleChallenge): NetworkPacket<*> = BattleChallengeNotificationPacket(request)
override fun onDecline(request: BattleChallenge) {
request.notifySender(true, "decline.sender", request.receiver.name.copy().aqua(), request.battleFormat.battleType.displayName)
request.notifyReceiver(false, "decline.receiver", request.sender.name.copy().aqua(), request.battleFormat.battleType.displayName)
}
override fun onSend(request: BattleChallenge) {
request.notifySender(false, "sent", request.receiver.name.copy().aqua(), request.battleFormat.battleType.displayName)
request.notifyReceiver(false, "received", request.sender.name.copy().aqua(), request.battleFormat.battleType.displayName)
}
override fun onAccept(request: BattleChallenge) {
if (request is MultiBattleChallenge) {
val players = request.receiverTeam.teamPlayers + request.senderTeam.teamPlayers
val leadPokemon = players.mapNotNull { it.party().first().uuid }
BattleBuilder.pvp2v2(
players,
leadPokemon,
request.battleFormat
).ifErrored {
erredStart -> erredStart.sendTo(players) { it.red() }
}
}
else {
BattleBuilder.pvp1v1(
request.receiver,
request.sender,
selectedLead.get(request.receiver.uuid),
selectedLead.get(request.sender.uuid),
request.battleFormat
).ifErrored {
it.sendTo(request.receiver) { it.red() }
it.sendTo(request.sender) { it.red() }
}
}
}
override fun isValidInteraction(player: ServerPlayer, target: ServerPlayer) = player.canInteractWith(target, Cobblemon.config.BattlePvPMaxDistance)
override fun canAccept(request: BattleChallenge): Boolean {
if (request is MultiBattleChallenge) {
val existingReceiverTeam = TeamManager.getTeam(request.receiverID)
val existingSenderTeam = TeamManager.getTeam(request.senderID)
// validate teams
if (existingReceiverTeam == null || existingSenderTeam == null) {
request.notifySender(true, "error.missing_team")
request.notifyReceiver(true, "error.missing_team")
return false;
}
val players = existingReceiverTeam.teamPlayers + existingSenderTeam.teamPlayers
val leads = players.mapNotNull { it.party().first().uuid }
val farAwayPlayer = this.validateProximity(players)
// validate team sizes
if (players.count() != MAX_TEAM_MEMBER_COUNT * 2) {
request.notifySender(true, "error.invalid_team_size", MAX_TEAM_MEMBER_COUNT)
request.notifyReceiver(true, "error.invalid_team_size", MAX_TEAM_MEMBER_COUNT)
}
// validate parties
else if (leads.count() != players.count()) {
request.notifySender(true, "error.insufficient_pokemon")
request.notifyReceiver(true, "error.insufficient_pokemon")
}
// validate dimension
else if (this.validateDimension(players)) {
request.notifySender(true, "error.player_different_dimension")
request.notifyReceiver(true, "error.player_different_dimension")
}
// validate proximity
else if (farAwayPlayer != null) {
request.notifySender(true, "error.player_distance", farAwayPlayer.name.copy().aqua())
request.notifyReceiver(true, "error.player_distance", farAwayPlayer.name.copy().aqua())
}
else return true
}
else {
// validate parties
if (request.receiver.party().none()) {
request.notifySender(true, "error.insufficient_pokemon.other", request.receiver.name.copy().aqua())
request.notifyReceiver(true, "error.insufficient_pokemon.self")
}
else if (request.sender.party().none()) {
request.notifySender(true, "error.insufficient_pokemon.self")
request.notifyReceiver(true, "error.insufficient_pokemon.other", request.sender.name.copy().aqua())
}
// TODO worth validating size and party health here? BattleBuilder already does it
else return true
}
return false
}
private fun validateDimension(players: Collection<ServerPlayer>): Boolean {
val dimension = players.first().level().dimension()
val playerInWrongDimension = players.firstOrNull { it.level().dimension() != dimension }
return playerInWrongDimension != null
}
private fun validateProximity(players: Collection<ServerPlayer>): ServerPlayer? {
// Check if all players are nearby
var averagePos = Vec3(0.0, 0.0, 0.0)
players.forEach { averagePos = averagePos.add(it.position().multiply(1.0 / players.count(), 0.0, 1.0 / players.count())) }
val farAwayPlayer = players.firstOrNull { it.position().subtract(0.0, it.position().y, 0.0).distanceToSqr(averagePos) > MAX_BATTLE_RADIUS * MAX_BATTLE_RADIUS }
return farAwayPlayer
}
fun setLead(player: ServerPlayer, lead: UUID) = this.selectedLead.put(player.uuid, lead)
}
@@ -403,6 +403,7 @@ class ShowdownPokemon {
lateinit var ability: String
var baseTypes = mutableListOf<String>()
var types = mutableListOf<String>()
var commanding: Boolean = false
var reviving: Boolean = false
val uuid: UUID by lazy { UUID.fromString(details.split(",")[1].trim()) }
@@ -412,6 +413,7 @@ class ShowdownPokemon {
buffer.writeString(condition)
buffer.writeBoolean(active)
buffer.writeBoolean(reviving)
buffer.writeBoolean(commanding)
buffer.writeSizedInt(IntSize.U_BYTE, moves.size)
moves.forEach(buffer::writeString)
buffer.writeString(baseAbility)
@@ -429,6 +431,7 @@ class ShowdownPokemon {
condition = buffer.readString()
active = buffer.readBoolean()
reviving = buffer.readBoolean()
commanding = buffer.readBoolean()
repeat(times = buffer.readSizedInt(IntSize.U_BYTE)) {
moves.add(buffer.readString())
}
@@ -0,0 +1,201 @@
/*
* 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.battles
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.api.interaction.RequestManager
import com.cobblemon.mod.common.api.interaction.ServerPlayerActionRequest
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.text.aqua
import com.cobblemon.mod.common.net.messages.client.battle.*
import com.cobblemon.mod.common.util.*
import net.minecraft.server.level.ServerPlayer
import java.util.*
/**
* Responsible for managing active [MultiBattleTeam]s and the respective [TeamRequest]s that form them.
*
* @author Segfault Guy, JazzMcNade
* @since October 26th, 2024
*/
object TeamManager : RequestManager<TeamManager.TeamRequest>() {
init {
register(this)
}
const val MAX_TEAM_MEMBER_COUNT = 2
/**
* Represents an interaction request between players to form a team.
*
* @param sender The player sending this request.
* @param receiver The player receiving this request.
* @param expiryTime How long (in seconds) this request is active.
*/
data class TeamRequest(
override val sender: ServerPlayer,
override val receiver: ServerPlayer,
override val expiryTime: Int = 60
) : ServerPlayerActionRequest
{
override val key: String = "team"
override val requestID: UUID = UUID.randomUUID()
}
// TODO right now team requests are sent PLAYER-to-PLAYER. May want to redesign once larger teams are implemented? Or have a 'team lead' who is responsible for inviting?
/**
* Represents a team for multi battles.
*
* @param teamPlayers The players that form this team.
*/
data class MultiBattleTeam(val teamPlayers: MutableList<ServerPlayer>)
{
constructor(vararg teamPlayers: ServerPlayer) : this(teamPlayers.toMutableList())
val teamID: UUID = UUID.randomUUID()
val teamPlayersUUID get() = this.teamPlayers.map { it.uuid }
}
// Multi-battle teams
private val playerToTeam = mutableMapOf<UUID, MultiBattleTeam>()
private val multiBattleTeams = mutableMapOf<UUID, MultiBattleTeam>()
fun getTeam(player: ServerPlayer) = playerToTeam.get(player.uuid)
fun getTeam(teamID: UUID) = multiBattleTeams.get(teamID)
override fun notificationPacket(request: TeamRequest): NetworkPacket<*> = TeamRequestNotificationPacket(request)
override fun expirationPacket(request: TeamRequest): NetworkPacket<*> = TeamRequestExpiredPacket(request)
/** Removes the [teamEntry]. */
fun disbandTeam(teamEntry: MultiBattleTeam) {
// Remove any outstanding team-up requests from the team members
val pendingTeamRequests = teamEntry.teamPlayersUUID.mapNotNull { member -> this.getOutboundRequest(member) } // team requests are by player
pendingTeamRequests.forEach { this.cancelRequest(it) }
// Decline any inbound requests for the team
ChallengeManager.getInboundRequests(teamEntry.teamID)?.forEach { ChallengeManager.declineRequest(it) } // multi challenges are by team
// Remove any outstanding multibattle challenges from the team
ChallengeManager.getOutboundRequest(teamEntry.teamID)?.let { ChallengeManager.cancelRequest(it) } // multi challenges are by team
// TODO have a way to get all types of requests targeting a team instead of just querying the ChallengeManger
// Notify remaining players that the team has been disbanded
teamEntry.teamPlayersUUID.forEach { member ->
playerToTeam.remove(member)
member.getPlayer()?.let { CobblemonNetwork.sendPacketToPlayer(it, TeamMemberRemoveNotificationPacket(it.uuid)) }
}
// Disband the team
multiBattleTeams.remove(teamEntry.teamID)
}
/** Removes a [player] from their [MultiBattleTeam]. Will disband the team if the last remaining member. */
fun removeTeamMember(player: ServerPlayer) {
val teamEntry = playerToTeam.get(player.uuid) ?: return
teamEntry.teamPlayers.remove(player)
playerToTeam.remove(player.uuid)
val notificationPacket = TeamMemberRemoveNotificationPacket(player.uuid)
CobblemonNetwork.sendPacketToPlayer(player, notificationPacket)
// Notify remaining members that the player has left the group
val teamServerPlayers = teamEntry.teamPlayersUUID.mapNotNull { it.getPlayer() }
CobblemonNetwork.sendPacketToPlayers(teamServerPlayers, notificationPacket)
if (teamEntry.teamPlayersUUID.count() == 1) this.disbandTeam(teamEntry)
}
/** Adds [player] as a member of existing [team]. */
fun joinTeam(player: ServerPlayer, team: MultiBattleTeam) {
// decline any other incoming requests to team up
this.getInboundRequests(player.uuid)?.forEach { this.cancelRequest(it) } // this is guaranteed to not contain the request currently being accepted
// notify the team
val teamNotifyPacket = TeamMemberAddNotificationPacket(
player.uuid,
player.name.copy()
)
CobblemonNetwork.sendPacketToPlayers(team.teamPlayers, teamNotifyPacket)
team.teamPlayers.add(player)
playerToTeam.put(player.uuid, team)
// notify the joiner
val joinerPacket = TeamJoinNotificationPacket(
team.teamPlayersUUID,
team.teamPlayers.mapNotNull { it.name.plainCopy() }
)
CobblemonNetwork.sendPacketToPlayer(player, joinerPacket)
}
/** Creates and registers a [MultiBattleTeam] from [players]. */
fun createTeam(vararg players: ServerPlayer): MultiBattleTeam {
// decline any other incoming requests to team up
players.forEach {
this.getInboundRequests(it.uuid)?.forEach { this.cancelRequest(it) } // this is guaranteed to not contain the request currently being accepted
}
val team = MultiBattleTeam(*players)
this.multiBattleTeams.put(team.teamID, team)
// Notify the team
val joinerPacket = TeamJoinNotificationPacket(
team.teamPlayersUUID,
players.mapNotNull { it.name.plainCopy() }
)
players.forEach {
this.playerToTeam.put(it.uuid, team)
CobblemonNetwork.sendPacketToPlayer(it, joinerPacket)
}
return team
}
override fun onAccept(request: TeamRequest) {
super.onAccept(request) // notification
val existingTeam = this.getTeam(request.sender)
if (existingTeam == null)
this.createTeam(request.receiver, request.sender)
else
this.joinTeam(request.receiver, existingTeam)
}
override fun canAccept(request: TeamRequest): Boolean {
val existingReceiverTeam = this.getTeam(request.receiverID)
val existingSenderTeam = this.getTeam(request.senderID)
if (request.sender.party().none()) {
request.notifySender(true, "error.no_pokemon")
request.notifyReceiver(true, "error.no_pokemon")
}
else if (existingReceiverTeam != null) {
request.notifySender(true, "error.existing_team.other", request.receiver.name.copy().aqua())
request.notifyReceiver(true, "error.existing_team.self")
}
else if (existingSenderTeam != null && existingSenderTeam.teamPlayersUUID.count() >= MAX_TEAM_MEMBER_COUNT) {
request.notifySender(true, "error.max_team_size.other", request.receiver.name.copy().aqua())
request.notifyReceiver(true, "error.max_team_size.self")
}
else {
return true
}
return false
}
override fun isValidInteraction(player: ServerPlayer, target: ServerPlayer): Boolean = player.canInteractWith(target, Cobblemon.config.tradeMaxDistance)
override fun onLogoff(sender: ServerPlayer) {
super.onLogoff(sender)
// Remove player from any teams they may be a part of
this.removeTeamMember(sender)
}
}
@@ -150,12 +150,12 @@ class DamageInstruction(
if (effect != null) {
val lang = when (effect.id) {
"blacksludge", "stickybarb" -> battleLang("damage.item", pokemonName, effect.typelessData)
"blacksludge", "stickybarb", "jabocaberry", "rowapberry" -> battleLang("damage.item", pokemonName, effect.typelessData)
"brn", "psn", "tox" -> {
val status = Statuses.getStatus(effect.id)?.name?.path ?: return@dispatch GO
lang("status.$status.hurt", pokemonName)
}
"aftermath" -> battleLang("damage.generic", pokemonName)
"aftermath", "innardsout" -> battleLang("damage.generic", pokemonName)
"chloroblast", "steelbeam" -> battleLang("damage.mindblown", pokemonName)
"jumpkick" -> battleLang("damage.highjumpkick", pokemonName)
else -> battleLang("damage.${effect.id}", pokemonName, source?.getName() ?: Component.literal("UNKOWN"))
@@ -209,7 +209,7 @@ class DamageInstruction(
} else if (causedFaint) {
GO
} else {
UntilDispatch {"effects" !in holds}
UntilDispatch { "effects" !in holds}
}
}
}
@@ -33,7 +33,7 @@ class FailInstruction(val message: BattleMessage): InterpreterInstruction {
val of = message.battlePokemonFromOptional(battle)
val lang = when (effectID) {
null, "burnup", "doubleshock" -> battleLang("fail") // Moves that use default fail lang. (Null included for moves that fail with no effect, for example: Baton Pass.)
null, "burnup", "doubleshock", "allyswitch" -> battleLang("fail") // Moves that use default fail lang. (Null included for moves that fail with no effect, for example: Baton Pass.)
"shedtail" -> battleLang("fail.substitute", pokemonName)
"hyperspacefury", "aurawheel" -> battleLang("fail.darkvoid", pokemonName) // Moves that can only be used by one species and fail when any others try
"corrosivegas" -> battleLang("fail.healblock", pokemonName)
@@ -41,6 +41,7 @@ class StatusInstruction( val message: BattleMessage): InterpreterInstruction {
if (status is PersistentStatus) {
pokemon.effectedPokemon.applyStatus(status)
battle.sendUpdate(BattlePersistentStatusPacket(pnx, status))
pokemon.sendUpdate()
}
battle.broadcastChatMessage(status.applyMessage.asTranslated(pokemon.getName()))
@@ -137,7 +137,7 @@ class HealingMachineBlockEntity(
val npc = level
?.getEntities(null, AABB.ofSize(blockPos.toVec3d(), 10.0, 10.0, 10.0)) { it.uuid == currentUser && it is NPCEntity }
?.firstOrNull() as? NPCEntity
val party = npc?.staticParty
val party = npc?.party
if (party != null) {
party.heal()
npc.sendSystemMessage(lang("healingmachine.healed").green()) // An NPC can read text, right?
@@ -1,78 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client
import com.cobblemon.mod.common.client.battle.ClientBattleChallenge
import com.cobblemon.mod.common.client.particle.BedrockParticleOptionsRepository
import com.cobblemon.mod.common.client.particle.ParticleStorm
import com.cobblemon.mod.common.client.render.MatrixWrapper
import com.cobblemon.mod.common.client.trade.ClientTradeOffer
import com.cobblemon.mod.common.util.cobblemonResource
import java.util.UUID
import net.minecraft.client.Minecraft
import net.minecraft.world.entity.player.Player
import net.minecraft.world.phys.Vec3
class ClientPlayerActionRequests {
val battleChallenges = mutableListOf<ClientBattleChallenge>()
val multiBattleTeamRequests = mutableListOf<ClientBattleChallenge>()
val tradeOffers = mutableListOf<ClientTradeOffer>()
val trackedPlayers = mutableMapOf<UUID, MatrixWrapper>()
fun onMultiChallengeExpired(challengeId: UUID) {
val players = multiBattleTeamRequests.filter { it.challengeId == challengeId }.flatMap { it.challengerIds }
players.forEach(trackedPlayers::remove)
multiBattleTeamRequests.removeIf { it.challengeId == challengeId }
}
fun onChallengeExpired(challengeId: UUID) {
val players = battleChallenges.filter { it.challengeId == challengeId }.flatMap { it.challengerIds }
players.forEach(trackedPlayers::remove)
battleChallenges.removeIf { it.challengeId == challengeId }
}
fun onTradeOfferExpired(uuid: UUID) {
val tradeOffers = tradeOffers.filter { it.tradeOfferId == uuid }
val players = tradeOffers.map { it.traderId }
players.forEach(trackedPlayers::remove)
this.tradeOffers.removeAll(tradeOffers)
}
fun onRenderPlayer(player: Player, partialTicks: Float) {
val matrixWrapper = trackedPlayers[player.uuid] ?: return
val position = player.getEyePosition(partialTicks)
matrixWrapper.updatePosition(
Vec3(
position.x,
position.y + 1.1,
position.z
)
)
}
fun addParticleEffect(challenge: ClientBattleChallenge) {
challenge.challengerIds.forEach { playerId ->
val player = Minecraft.getInstance().level?.getPlayerByUUID(playerId) ?: return@forEach
val wrapper = trackedPlayers.getOrPut(player.uuid) { MatrixWrapper() }
wrapper.updatePosition(player.getEyePosition(0F).add(0.0, 1.1, 0.0))
val effect = BedrockParticleOptionsRepository.getEffect(cobblemonResource("particle_exclamation"))
if (effect != null) {
val particleStorm = ParticleStorm(
effect = effect,
matrixWrapper = wrapper,
sourceAlive = { !player.isRemoved && trackedPlayers.containsKey(playerId) },
world = Minecraft.getInstance().level ?: return,
entity = player,
)
particleStorm.spawn()
}
}
}
}
@@ -8,12 +8,16 @@
package com.cobblemon.mod.common.client
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.chat.MutableComponent
import net.minecraft.resources.ResourceLocation
import java.util.UUID
class ClientPlayerTeamData {
var multiBattleTeamMembers = mutableListOf<ClientMultiBattleTeamMember>()
}
class ClientMultiBattleTeamMember(val uuid: UUID, val name: MutableComponent) {
class ClientMultiBattleTeamMember(val uuid: UUID, val name: MutableComponent) : ClientPlayerIcon(null) {
override val texture: ResourceLocation = cobblemonResource("textures/particle/request/icon_partner.png")
}
@@ -19,6 +19,8 @@ import com.cobblemon.mod.common.client.battle.ClientBattle
import com.cobblemon.mod.common.client.gui.PartyOverlay
import com.cobblemon.mod.common.client.gui.battle.BattleOverlay
import com.cobblemon.mod.common.client.particle.BedrockParticleOptionsRepository
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.client.render.DeferredRenderer
import com.cobblemon.mod.common.client.render.block.*
import com.cobblemon.mod.common.client.render.boat.CobblemonBoatRenderer
import com.cobblemon.mod.common.client.render.entity.PokeBobberEntityRenderer
@@ -31,6 +33,7 @@ import com.cobblemon.mod.common.client.render.models.blockbench.repository.*
import com.cobblemon.mod.common.client.render.npc.NPCRenderer
import com.cobblemon.mod.common.client.render.pokeball.PokeBallRenderer
import com.cobblemon.mod.common.client.render.pokemon.PokemonRenderer
import com.cobblemon.mod.common.client.requests.ClientPlayerActionRequests
import com.cobblemon.mod.common.client.sound.BattleMusicController
import com.cobblemon.mod.common.client.sound.EntitySoundTracker
import com.cobblemon.mod.common.client.storage.ClientStorageManager
@@ -104,6 +107,8 @@ object CobblemonClient {
ClientTaskTracker.clear()
checkedStarterScreen = false
CobblemonDataProvider.canReload = true
DeferredRenderer.clearAll()
ClientPlayerIcon.clear()
}
fun initialize(implementation: CobblemonClientImplementation) {
@@ -156,6 +161,7 @@ object CobblemonClient {
it.delegate.spawnShinyParticle(player!!)
}
}
ClientPlayerIcon.onTick()
}
}
@@ -8,11 +8,29 @@
package com.cobblemon.mod.common.client.battle
import com.cobblemon.mod.common.api.interaction.PlayerActionRequest
import com.cobblemon.mod.common.battles.BattleFormat
import java.util.UUID
import com.cobblemon.mod.common.client.requests.ClientPlayerActionRequest
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.resources.ResourceLocation
import java.util.*
class ClientBattleChallenge(
val challengeId: UUID,
val challengerIds: List<UUID>,
val battleFormat: BattleFormat? = null
)
/**
* Clientside representation of a [PlayerActionRequest] for a battle.
*
* @param requestID The unique identifier of this challenge.
* @param senderID The unique identifier of the player (or team) that sent this challenge.
* @param expiryTime The amount of time (in seconds) this challenge is active.
* @param battleFormat The
*
* @author JazzMcNade
* @since July 22nd, 2024
*/
data class ClientBattleChallenge(
override val requestID: UUID,
override val senderID: UUID,
override val expiryTime: Int,
val battleFormat: BattleFormat
) : ClientPlayerActionRequest(expiryTime) {
override val texture: ResourceLocation = cobblemonResource("textures/particle/request/icon_challenge.png")
}
@@ -0,0 +1,22 @@
/*
* 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.battle
import com.cobblemon.mod.common.client.requests.ClientPlayerActionRequest
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.resources.ResourceLocation
import java.util.*
data class ClientTeamRequest(
override val requestID: UUID,
override val senderID: UUID,
override val expiryTime: Int
) : ClientPlayerActionRequest(expiryTime) {
override val texture: ResourceLocation = cobblemonResource("textures/particle/request/icon_team.png")
}
@@ -230,13 +230,6 @@ class PokemonClientDelegate : PosableState(), PokemonSideDelegate {
world
).spawn()
}
currentEntity.after(seconds = 0.1f) {
//This is only for when the player is sending out the Pokemon into the world and not a battle.
if (currentEntity.pokemon.shiny && !currentEntity.isBattling) {
playShinyEffect("cobblemon:shiny_ring")
lastShinyParticle = System.currentTimeMillis()
}
}
}
}
}
@@ -158,7 +158,7 @@ class BattleGUI : Screen(battleLang("gui.title")) {
// Known quirk of Showdown. It'll ask for actions on fainted slots
// Also during a forced switch in doubles/triples it'll ask for actions on non-switching slots
val pokemon = request.side?.pokemon?.firstOrNull { it.uuid == request.activePokemon.battlePokemon?.uuid }
if(pokemon == null || pokemon.condition.contains("fnt") || request.moveSet == null) {
if (pokemon == null || pokemon.condition.contains("fnt") || pokemon.commanding || request.moveSet == null) {
this.selectAction(request, PassActionResponse)
null
} else {
@@ -28,8 +28,8 @@ class BattleMessagePane(
messageQueue: ClientBattleMessageQueue
): ObjectSelectionList<BattleMessagePane.BattleMessageLine>(
Minecraft.getInstance(),
TEXT_BOX_WIDTH, // width
TEXT_BOX_HEIGHT, // height
FRAME_WIDTH, // width
FRAME_HEIGHT, // height
1, // top
LINE_HEIGHT
) {
@@ -62,7 +62,7 @@ class BattleMessagePane(
companion object {
const val LINE_HEIGHT = 10
const val LINE_WIDTH = 142
const val LINE_WIDTH = 146
const val FRAME_WIDTH = 169
const val FRAME_HEIGHT = 55
const val FRAME_EXPANDED_HEIGHT = 101
@@ -72,6 +72,7 @@ class BattleMessagePane(
private val battleMessagePaneFrameResource = cobblemonResource("textures/gui/battle/battle_log.png")
private val battleMessagePaneFrameExpandedResource = cobblemonResource("textures/gui/battle/battle_log_expanded.png")
private val battleMessageHighlight = cobblemonResource("textures/gui/battle/battle_log_row_selected_color.png")
private var expanded = false
}
@@ -79,8 +80,54 @@ class BattleMessagePane(
return super.addEntry(entry)
}
override fun getRowLeft(): Int {
return super.getRowLeft() + 4
}
override fun renderSelection(guiGraphics: GuiGraphics, top: Int, width: Int, height: Int, outerColor: Int, innerColor: Int) {
blitk(
matrixStack = guiGraphics.pose(),
texture = battleMessageHighlight,
x = x + 6,
y = top - 2,
height = 10,
width = 1,
alpha = opacity
)
blitk(
matrixStack = guiGraphics.pose(),
texture = battleMessageHighlight,
x = x + 6,
y = top - 2,
height = 1,
width = LINE_WIDTH,
alpha = opacity
)
blitk(
matrixStack = guiGraphics.pose(),
texture = battleMessageHighlight,
x = x + 6,
y = top + 7,
height = 1,
width = LINE_WIDTH,
alpha = opacity
)
blitk(
matrixStack = guiGraphics.pose(),
texture = battleMessageHighlight,
x = x + 6 + LINE_WIDTH - 1,
y = top - 2,
height = 10,
width = 1,
alpha = opacity
)
}
override fun getRowWidth(): Int {
return 80
return LINE_WIDTH
}
override fun getScrollbarPosition(): Int {
@@ -162,7 +209,7 @@ class BattleMessagePane(
drawScaledText(
context,
line,
rowLeft - 29,
rowLeft,
rowTop - 2,
opacity = pane.opacity
)
@@ -8,15 +8,21 @@
package com.cobblemon.mod.common.client.gui.interact.battleRequest
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.gui.ColourLibrary
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.battles.BattleFormat
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.battle.ClientBattleChallenge
import com.cobblemon.mod.common.client.battle.ClientTeamRequest
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.net.messages.client.PlayerInteractOptionsPacket
import com.cobblemon.mod.common.net.messages.server.*
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.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import net.minecraft.client.Minecraft
@@ -25,12 +31,13 @@ import net.minecraft.client.gui.screens.Screen
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.resources.ResourceLocation
import java.util.*
class BattleConfigureGUI(
private val packet: PlayerInteractOptionsPacket,
private val activeRequest: ClientBattleChallenge? = null,
private val activeTeamRequest: ClientBattleChallenge? = null
) : Screen(lang("challenge.request_battle_title")) {
private val activeTeamRequest: ClientTeamRequest? = null
) : Screen(lang("ui.challenge.challenge_title")) {
companion object {
const val SIZE = 113
private val backgroundResource = cobblemonResource("textures/gui/interact/request/battle_request.png")
@@ -47,11 +54,11 @@ class BattleConfigureGUI(
battleFormat = BattleFormat.GEN_9_SINGLES,
tileTexture = cobblemonResource("textures/gui/interact/request/battle_request_single.png"),
overlayTexture = cobblemonResource("textures/gui/interact/request/battle_request_overlay.png"),
title = lang("challenge.request_battle_title").bold(),
title = lang("ui.challenge.challenge_title").bold(),
subTitle = lang("battle.types.singles").bold(),
buttonText = lang("challenge.challenge").bold(),
buttonText = lang("ui.challenge.challenge").bold(),
onRequest = { packet, battleFormat -> sendBattleRequest(battleFormat, packet) },
onResponse = { packet, accept -> sendBattleResponse(packet, accept) }
onResponse = { packet, requestID, accept -> sendBattleResponse(packet, requestID, accept) }
)
),
Pair(
@@ -61,11 +68,11 @@ class BattleConfigureGUI(
battleFormat = BattleFormat.GEN_9_DOUBLES,
tileTexture = cobblemonResource("textures/gui/interact/request/battle_request_double.png"),
overlayTexture = cobblemonResource("textures/gui/interact/request/battle_request_overlay.png"),
title = lang("challenge.request_battle_title").bold(),
title = lang("ui.challenge.challenge_title").bold(),
subTitle = lang("battle.types.doubles").bold(),
buttonText = lang("challenge.challenge").bold(),
buttonText = lang("ui.challenge.challenge").bold(),
onRequest = { packet, battleFormat -> sendBattleRequest(battleFormat, packet) },
onResponse = { packet, accept -> sendBattleResponse(packet, accept) }
onResponse = { packet, requestID, accept -> sendBattleResponse(packet, requestID, accept) }
),
),
Pair(
@@ -75,11 +82,11 @@ class BattleConfigureGUI(
battleFormat = BattleFormat.GEN_9_TRIPLES,
tileTexture = cobblemonResource("textures/gui/interact/request/battle_request_triple.png"),
overlayTexture = cobblemonResource("textures/gui/interact/request/battle_request_overlay.png"),
title = lang("challenge.request_battle_title").bold(),
title = lang("ui.challenge.challenge_title").bold(),
subTitle = lang("battle.types.triples").bold(),
buttonText = lang("challenge.challenge").bold(),
buttonText = lang("ui.challenge.challenge").bold(),
onRequest = { packet, battleFormat -> sendBattleRequest(battleFormat, packet) },
onResponse = { packet, accept -> sendBattleResponse(packet, accept) }
onResponse = { packet, requestID, accept -> sendBattleResponse(packet, requestID, accept) }
),
),
Pair(
@@ -89,12 +96,12 @@ class BattleConfigureGUI(
battleFormat = BattleFormat.GEN_9_MULTI,
tileTexture = cobblemonResource("textures/gui/interact/request/battle_request_multi.png"),
overlayTexture = cobblemonResource("textures/gui/interact/request/battle_request_multi_overlay_partner.png"),
title = lang("challenge.request_team_join_title").bold(),
title = lang("ui.team.invite_title").bold(),
subTitle = lang("battle.types.multi").bold(),
buttonText = lang("challenge.multi.invite").bold(),
buttonText = lang("ui.team.invite").bold(),
color = ColourLibrary.SIDE_1_ALLY_BATTLE_COLOUR,
onRequest = { packet, battleFormat -> BattleTeamRequestPacket(packet.numericTargetId).sendToServer() },
onResponse = { packet, accept -> BattleTeamResponsePacket(packet.numericTargetId, accept).sendToServer() }
onResponse = { packet, requestID, accept -> sendTeamResponse(packet, requestID, accept) }
),
),
Pair(
@@ -104,12 +111,12 @@ class BattleConfigureGUI(
battleFormat = BattleFormat.GEN_9_MULTI,
tileTexture = cobblemonResource("textures/gui/interact/request/battle_request_multi.png"),
overlayTexture = cobblemonResource("textures/gui/interact/request/battle_request_multi_overlay_partner_leave.png"),
title = lang("challenge.request_team_leave_title").bold(),
title = lang("ui.team.leave_title").bold(),
subTitle = lang("battle.types.multi").bold(),
buttonText = lang("challenge.multi.leave").bold(),
buttonText = lang("ui.team.leave").bold(),
color = ColourLibrary.SIDE_1_ALLY_BATTLE_COLOUR,
onRequest = { _, _ -> BattleTeamLeavePacket().sendToServer() },
onResponse = { _, _ -> Unit }
onResponse = { _, _, _ -> Unit }
),
),
Pair(
@@ -119,11 +126,11 @@ class BattleConfigureGUI(
battleFormat = BattleFormat.GEN_9_MULTI,
tileTexture = cobblemonResource("textures/gui/interact/request/battle_request_multi.png"),
overlayTexture = cobblemonResource("textures/gui/interact/request/battle_request_multi_overlay_opponent.png"),
title = lang("challenge.request_battle_title").bold(),
title = lang("ui.challenge.challenge_title").bold(),
subTitle = lang("battle.types.multi").bold(),
buttonText = lang("challenge.challenge").bold(),
buttonText = lang("ui.challenge.challenge").bold(),
onRequest = { packet, battleFormat -> sendBattleRequest(battleFormat, packet) },
onResponse = { packet, accept -> sendBattleResponse(packet, accept) }
onResponse = { packet, requestID, accept -> sendBattleResponse(packet, requestID, accept) }
),
),
Pair(
@@ -133,38 +140,41 @@ class BattleConfigureGUI(
battleFormat = BattleFormat.GEN_9_ROYAL,
tileTexture = cobblemonResource("textures/gui/interact/request/battle_request_royal.png"),
overlayTexture = cobblemonResource("textures/gui/interact/request/battle_request_royal_overlay.png"),
title = lang("challenge.request_battle_title").bold(),
title = lang("ui.challenge.challenge_title").bold(),
subTitle = lang("battle.types.freeforall").bold(),
buttonText = lang("challenge.challenge").bold(),
buttonText = lang("ui.challenge.challenge").bold(),
onRequest = { packet, battleFormat -> sendBattleRequest(battleFormat, packet) },
onResponse = { packet, accept -> sendBattleResponse(packet, accept) }
onResponse = { packet, requestID, accept -> sendBattleResponse(packet, requestID, accept) }
),
),
)
val levelRulesetOption = mutableListOf(-1, 50, 100, 5)
private fun sendTeamResponse(packet: PlayerInteractOptionsPacket, requestID: UUID, accept: Boolean) {
BattleTeamResponsePacket(packet.numericTargetId, requestID, accept).sendToServer()
}
fun sendBattleRequest(battleFormat: BattleFormat, packet: PlayerInteractOptionsPacket) {
BattleChallengePacket(packet.numericTargetId, packet.selectedPokemonId, battleFormat).sendToServer()
}
private fun sendBattleResponse(packet: PlayerInteractOptionsPacket, accept: Boolean) {
BattleChallengeResponsePacket(packet.numericTargetId, packet.selectedPokemonId, accept).sendToServer()
private fun sendBattleResponse(packet: PlayerInteractOptionsPacket, requestID: UUID, accept: Boolean) {
BattleChallengeResponsePacket(packet.numericTargetId, requestID, packet.selectedPokemonId, accept).sendToServer()
}
private var options: List<PlayerInteractOptionsPacket.Options> = emptyList()
private val blinkRate = 35
}
class BattleTypeTile(
val option: PlayerInteractOptionsPacket.Options,
val battleFormat: BattleFormat,
val tileTexture: ResourceLocation?,
val overlayTexture: ResourceLocation?,
val title: MutableComponent,
val subTitle: MutableComponent,
val buttonText: MutableComponent,
val color: Int = ColourLibrary.SIDE_1_BATTLE_COLOUR,
val onRequest: (packet : PlayerInteractOptionsPacket, battleFormat: BattleFormat) -> Unit,
val onResponse: (packet: PlayerInteractOptionsPacket, accept: Boolean) -> Unit,
val option: PlayerInteractOptionsPacket.Options,
val battleFormat: BattleFormat,
val tileTexture: ResourceLocation?,
val overlayTexture: ResourceLocation?,
val title: MutableComponent,
val subTitle: MutableComponent,
val buttonText: MutableComponent,
val color: Int = ColourLibrary.SIDE_1_BATTLE_COLOUR,
val onRequest: (packet : PlayerInteractOptionsPacket, battleFormat: BattleFormat) -> Unit,
val onResponse: (packet: PlayerInteractOptionsPacket, requestID: UUID, accept: Boolean) -> Unit,
)
private var currentPage = 0
@@ -196,23 +206,20 @@ class BattleConfigureGUI(
override fun init() {
val challenge = activeRequest
targetName = Minecraft.getInstance().player?.level()?.getPlayerByUUID(packet.targetId)?.name?.plainCopy()?.bold() ?: targetName
hasRequest = challenge != null || activeTeamRequest != null
val pendingRequest = activeTeamRequest ?: activeRequest
if (activeTeamRequest != null) {
options = listOf(PlayerInteractOptionsPacket.Options.TEAM_REQUEST)
} else if (challenge != null) {
options = packet.options.keys.filter { packet.options[it] === PlayerInteractOptionsPacket.OptionStatus.AVAILABLE && battleRequestMap[it]?.battleFormat?.battleType?.name == challenge.battleFormat?.battleType?.name }
val level = challenge.battleFormat?.adjustLevel ?: -1
} else if (activeRequest != null) {
options = packet.options.keys.filter { packet.options[it] === PlayerInteractOptionsPacket.OptionStatus.AVAILABLE && battleRequestMap[it]?.battleFormat?.battleType?.name == activeRequest.battleFormat.battleType.name }
val level = activeRequest.battleFormat.adjustLevel
levelRulesetOptionIndex = maxOf(0, levelRulesetOption.indexOf(level))
} else {
options = packet.options.keys.filter { packet.options[it] === PlayerInteractOptionsPacket.OptionStatus.AVAILABLE && battleRequestMap.containsKey(it) }.toList()
}
val (x, y) = getDimensions()
if(hasRequest) {
if(pendingRequest != null) {
// Draw Accept/Decline buttons
this.addRenderableWidget(
BattleResponseButton(
@@ -220,7 +227,7 @@ class BattleConfigureGUI(
y + 99,
true
) {
battleRequestMap[options[currentPage]]?.onResponse?.let { it1 -> it1(packet, true) }
battleRequestMap[options[currentPage]]?.onResponse?.let { it1 -> it1(packet, pendingRequest.requestID, true) }
closeGUI()
}
)
@@ -231,17 +238,18 @@ class BattleConfigureGUI(
y + 99,
false
) {
battleRequestMap[options[currentPage]]?.onResponse?.let { it1 -> it1(packet, false) }
battleRequestMap[options[currentPage]]?.onResponse?.let { it1 -> it1(packet, pendingRequest.requestID, false) }
closeGUI()
}
)
} else {
}
else {
// Draw Challenge button
requestButton = BattleRequestButton(
x + 22,
y + 99,
lang("challenge.challenge"),
lang("ui.challenge.challenge"),
) {
//TODO: add additional battle rules, otherwise this call feels pretty silly
battleRequestMap[options[currentPage]]?.onRequest?.let { it1 ->
@@ -303,7 +311,7 @@ class BattleConfigureGUI(
fun updateRequestButtonText() {
requestButton.let {
if (it != null) {
it.text = battleRequestMap[options[currentPage]]?.buttonText ?: lang("challenge.challenge")
it.text = battleRequestMap[options[currentPage]]?.buttonText ?: lang("ui.challenge.challenge")
}
}
}
@@ -8,11 +8,13 @@
package com.cobblemon.mod.common.client.gui.interact.wheel
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.pokemon.interaction.PokemonInteractionGUICreationEvent
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.gui.interact.battleRequest.BattleConfigureGUI
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.net.messages.client.PlayerInteractOptionsPacket
import com.cobblemon.mod.common.net.messages.server.battle.SpectateBattlePacket
import com.cobblemon.mod.common.net.messages.server.pokemon.interact.InteractPokemonPacket
@@ -57,24 +59,24 @@ fun createPokemonInteractGui(pokemonID: UUID, canMountShoulder: Boolean): Intera
fun createPlayerInteractGui(optionsPacket: PlayerInteractOptionsPacket): InteractWheelGUI {
val trade = InteractWheelOption(
iconResource = cobblemonResource("textures/gui/interact/icon_trade.png"),
secondaryIconResource = if (CobblemonClient.requests.tradeOffers.any { it.traderId == optionsPacket.targetId })
secondaryIconResource = if (CobblemonClient.requests.tradeOffers[optionsPacket.targetId] != null)
cobblemonResource("textures/gui/interact/icon_exclamation.png")
else null,
colour = { null },
tooltipText = "cobblemon.ui.interact.trade",
onPress = {
val tradeOffer = CobblemonClient.requests.tradeOffers.find { it.traderId == optionsPacket.targetId }
val tradeOffer = CobblemonClient.requests.tradeOffers[optionsPacket.targetId]
if (tradeOffer == null) {
CobblemonNetwork.sendToServer(OfferTradePacket(optionsPacket.targetId))
} else {
CobblemonClient.requests.tradeOffers -= tradeOffer
CobblemonNetwork.sendToServer(AcceptTradeRequestPacket(tradeOffer.tradeOfferId))
CobblemonClient.requests.tradeOffers.remove(optionsPacket.targetId)
CobblemonNetwork.sendToServer(AcceptTradeRequestPacket(tradeOffer.requestID))
}
closeGUI()
}
)
val activeBattleRequest = CobblemonClient.requests.battleChallenges.firstOrNull { it.challengerIds.contains(optionsPacket.targetId) }
val activeTeamRequest = CobblemonClient.requests.multiBattleTeamRequests.firstOrNull { it.challengerIds.contains(optionsPacket.targetId) }
val activeBattleRequest = CobblemonClient.requests.battleChallenges[optionsPacket.targetId]
val activeTeamRequest = CobblemonClient.requests.multiBattleTeamRequests[optionsPacket.targetId]
val battle = InteractWheelOption(
iconResource = cobblemonResource("textures/gui/interact/icon_battle.png"),
secondaryIconResource = if(activeBattleRequest != null|| activeTeamRequest != null)
@@ -89,7 +91,7 @@ fun createPlayerInteractGui(optionsPacket: PlayerInteractOptionsPacket): Interac
val spectate = InteractWheelOption(
iconResource = cobblemonResource("textures/gui/interact/icon_spectate_battle.png"),
colour = { if (CobblemonClient.requests.battleChallenges.any { it.challengerIds.contains(optionsPacket.targetId) }) Vector3f(0F, 0.6F, 0F) else null },
colour = { if (CobblemonClient.requests.battleChallenges[optionsPacket.targetId] != null) Vector3f(0F, 0.6F, 0F) else null },
onPress = {
SpectateBattlePacket(optionsPacket.targetId).sendToServer()
closeGUI()
@@ -98,8 +100,8 @@ fun createPlayerInteractGui(optionsPacket: PlayerInteractOptionsPacket): Interac
)
val options: Multimap<Orientation, InteractWheelOption> = ArrayListMultimap.create()
//TODO: hasChallenge and hasTeamRequest get calculated a bunch of times. Might consider having the server just passing it over.
val hasChallenge = CobblemonClient.requests.battleChallenges.any { it.challengerIds.contains(optionsPacket.targetId) }
val hasTeamRequest = CobblemonClient.requests.multiBattleTeamRequests.any { it.challengerIds.contains(optionsPacket.targetId) }
val hasChallenge = CobblemonClient.requests.battleChallenges[optionsPacket.targetId] != null
val hasTeamRequest = CobblemonClient.requests.multiBattleTeamRequests[optionsPacket.targetId] != null
//The way things are positioned should probably be more thought out if more options are added
var addBattleOption = false
optionsPacket.options.forEach {
@@ -448,7 +448,7 @@ class PokedexGUI private constructor(
val form = species.forms.find { it.name.equals(formName, ignoreCase = true) } ?: species.standardForm
when (tabInfoIndex) {
TAB_DESCRIPTION -> {
textToShowInDescription.addAll(species.pokedex)
textToShowInDescription.addAll(form.pokedex)
(tabInfoElement as DescriptionWidget).showPlaceholder = false
}
TAB_ABILITIES -> {
@@ -174,6 +174,7 @@ class PokemonInfoWidget(val pX: Int, val pY: Int, val updateForm: (PokedexForm)
}
val possibleVariationButtonPositions = mutableListOf<Pair<Float, Float>>(
126F to 27F,
114F to 27F,
102F to 27F,
90F to 27F,
@@ -469,7 +470,6 @@ class PokemonInfoWidget(val pX: Int, val pY: Int, val updateForm: (PokedexForm)
)
}
// Cry
blitk(
matrixStack = matrices,
@@ -550,6 +550,12 @@ class PokemonInfoWidget(val pX: Int, val pY: Int, val updateForm: (PokedexForm)
variationButtons.forEach { removeWidget(it.getWidget()) }
variationButtons.clear()
val seenShinyStates = CobblemonClient.clientPokedexData.getSeenShinyStates(pokedexEntry, pokedexForm)
shiny = seenShinyStates.count() == 1 && seenShinyStates.first() == "shiny"
shinyButton.resource = if (shiny) buttonShiny else buttonNone
shinyButton.visible = shiny || seenShinyStates.size > 1
shinyButton.active = seenShinyStates.size > 1
val form = species.forms.find { it.name.equals(pokedexForm.displayForm, ignoreCase = true) } ?: species.standardForm
maleRatio = form.maleRatio
@@ -562,12 +568,13 @@ class PokemonInfoWidget(val pX: Int, val pY: Int, val updateForm: (PokedexForm)
genderButton.visible = true
genderButton.active = seenGenders.size > 1
}
genderButton.buttonX = pX + (if (shinyButton.visible) 114F else 126F)
if (CobblemonClient.clientPokedexData.getHighestKnowledgeFor(pokedexEntry) == PokedexEntryProgress.NONE) {
return
}
if (CobblemonClient.clientPokedexData.getHighestKnowledgeFor(pokedexEntry) == PokedexEntryProgress.NONE) return
var startPosition = if (shinyButton.visible) 1 else 0
startPosition += if (genderButton.visible || (species.maleRatio == -1F)) 1 else 0
val startPosition = if (species.maleRatio == -1F) 0 else 1
pokedexEntry.variations.forEachIndexed { index, variation ->
val pos = possibleVariationButtonPositions[index + startPosition]
val button = VariationButtonWrapper(this, pX + pos.first, pY + pos.second)
@@ -575,11 +582,6 @@ class PokemonInfoWidget(val pX: Int, val pY: Int, val updateForm: (PokedexForm)
addWidget(button.getWidget())
variationButtons.add(button)
}
val seenShinyStates = CobblemonClient.clientPokedexData.getSeenShinyStates(pokedexEntry, pokedexForm)
shiny = seenShinyStates.count() == 1 && seenShinyStates.first() == "shiny"
shinyButton.resource = if (shiny) buttonShiny else buttonNone
shinyButton.active = seenShinyStates.size > 1
}
private fun updateType(species: Species, form: FormData) {
@@ -366,7 +366,7 @@ class Summary private constructor(party: Collection<Pokemon?>, private val edita
if (movesWidget is MovesWidget) {
sideScreen = MoveSwapScreen(
x + 216,
y + 24,
y + 23,
movesWidget = movesWidget,
replacedMove = move
).also { switchPane ->
@@ -387,7 +387,7 @@ class Summary private constructor(party: Collection<Pokemon?>, private val edita
EVOLVE -> {
sideScreen = EvolutionSelectScreen(
x + 216,
y + 22,
y + 23,
pokemon = selectedPokemon
)
}
@@ -592,6 +592,7 @@ class Summary private constructor(party: Collection<Pokemon?>, private val edita
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (sideScreenIndex == MOVE_SWAP || sideScreenIndex == EVOLVE) sideScreen.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
if (mainScreenIndex == MOVES) mainScreen.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
@@ -13,6 +13,7 @@ import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetNicknamePacket
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetNicknameHandler.MAX_NAME_LENGTH
import com.cobblemon.mod.common.pokemon.Pokemon
import com.mojang.blaze3d.platform.InputConstants
import net.minecraft.client.Minecraft
@@ -28,9 +29,6 @@ class NicknameEntryWidget(
Minecraft.getInstance().font,
x, y, width, height, text
) {
companion object {
private const val MAX_NAME_LENGTH = 12
}
var pokemonName = ""
@@ -32,7 +32,7 @@ abstract class SummaryScrollList<T : ObjectSelectionList.Entry<T>>(
) {
companion object {
const val WIDTH = 108
const val HEIGHT = 111
const val HEIGHT = 112
const val SLOT_WIDTH = 91
private val backgroundResource = cobblemonResource("textures/gui/summary/summary_scroll_background.png")
@@ -71,7 +71,7 @@ abstract class SummaryScrollList<T : ObjectSelectionList.Entry<T>>(
)
context.enableScissor(
x,
y - 1,
y,
x + width,
y + height
)
@@ -25,7 +25,6 @@ import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
class InfoWidget(
pX: Int,
pY: Int,
@@ -21,14 +21,11 @@ class MoveDescriptionScrollList(
) : ObjectSelectionList<MoveDescriptionEntry>(
Minecraft.getInstance(),
60, // width
28, // height
30, // height
0, // top
slotHeight
) {
companion object {
const val WIDTH = 108
const val HEIGHT = 111
}
private var scrolling = false
init {
this.y = this.listY
@@ -44,7 +41,7 @@ class MoveDescriptionScrollList(
isHovered = pMouseX >= x && pMouseY >= y && pMouseX < x + width && pMouseY < y + height
context.enableScissor(
x,
y - 1,
y,
x + width,
y + height
)
@@ -52,7 +49,33 @@ class MoveDescriptionScrollList(
context.disableScissor()
}
override fun renderListBackground(guiGraphics: GuiGraphics) {
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
updateScrollingState(mouseX, mouseY)
if (scrolling) isDragging = true
return isDragging
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (scrolling) {
if (mouseY < y) {
scrollAmount = 0.0
} else if (mouseY > bottom) {
scrollAmount = maxScroll.toDouble()
} else {
scrollAmount += deltaY
}
}
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
override fun renderListBackground(guiGraphics: GuiGraphics) {}
private fun updateScrollingState(mouseX: Double, mouseY: Double) {
scrolling = mouseX >= getScrollbarPosition().toDouble()
&& mouseX < (getScrollbarPosition() + 3).toDouble()
&& mouseY >= y
&& mouseY < bottom
}
fun setMoveDescription(moveDescription: MutableComponent) {
@@ -82,4 +105,4 @@ class MoveDescriptionScrollList(
addEntry(MoveDescriptionEntry(Component.literal(part)))
}
}
}
}
@@ -67,7 +67,7 @@ class MovesWidget(
private var descriptionScrollList = MoveDescriptionScrollList(
x + 69,
y + 114,
y + 113,
5
)
@@ -218,10 +218,18 @@ class MovesWidget(
descriptionScrollList.setMoveDescription(selectedMove?.description ?: "".text())
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (descriptionScrollList.isHovered) descriptionScrollList.mouseClicked(mouseX, mouseY, button)
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
if (descriptionScrollList.isHovered) {
descriptionScrollList.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
}
if (descriptionScrollList.isHovered) descriptionScrollList.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 (descriptionScrollList.isHovered) descriptionScrollList.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
}
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.client.keybind.keybinds
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonNetwork.sendToServer
import com.cobblemon.mod.common.battles.BattleFormat
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.gui.battle.BattleGUI
import com.cobblemon.mod.common.client.keybind.CobblemonBlockingKeyBinding
@@ -77,12 +78,12 @@ object PartySendBinding : CobblemonBlockingKeyBinding(
if (CobblemonClient.storage.selectedSlot != -1 && Minecraft.getInstance().screen == null) {
val pokemon = CobblemonClient.storage.myParty.get(CobblemonClient.storage.selectedSlot)
if (pokemon != null && pokemon.currentHealth > 0) {
if (pokemon != null) {
val targetEntity = player.traceFirstEntityCollision(
entityClass = LivingEntity::class.java,
ignoreEntity = player,
maxDistance = Cobblemon.config.battleSpectateMaxDistance,
collideBlock = ClipContext.Fluid.NONE)
entityClass = LivingEntity::class.java,
ignoreEntity = player,
maxDistance = Cobblemon.config.battleSpectateMaxDistance,
collideBlock = ClipContext.Fluid.NONE)
if (targetEntity == null || (targetEntity is PokemonEntity && targetEntity.ownerUUID == player.uuid)) {
sendToServer(SendOutPokemonPacket(CobblemonClient.storage.selectedSlot))
}
@@ -102,7 +103,7 @@ object PartySendBinding : CobblemonBlockingKeyBinding(
}
is PokemonEntity -> {
if (!entity.canBattle(player) || entity.position().distanceToSqr(player.position()) > Cobblemon.config.battleWildMaxDistance.pow(2)) return
sendToServer(BattleChallengePacket(entity.id, pokemon.uuid))
sendToServer(BattleChallengePacket(entity.id, pokemon.uuid, BattleFormat.GEN_9_SINGLES))
}
}
}
@@ -10,11 +10,20 @@ package com.cobblemon.mod.common.client.net.battle
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.net.messages.client.battle.BattleChallengeExpiredPacket
import net.minecraft.client.Minecraft
object BattleChallengeExpiredHandler : ClientNetworkPacketHandler<BattleChallengeExpiredPacket> {
override fun handle(packet: BattleChallengeExpiredPacket, client: Minecraft) {
CobblemonClient.requests.onChallengeExpired(packet.battleChallengeId)
val iter = CobblemonClient.requests.battleChallenges.iterator()
while(iter.hasNext()) {
val entry = iter.next()
val player = entry.key
if (entry.value.requestID == packet.requestID) {
iter.remove()
ClientPlayerIcon.update(player)
}
}
}
}
@@ -9,33 +9,18 @@
package com.cobblemon.mod.common.client.net.battle
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.api.snowstorm.BedrockParticleOptions
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.battle.ClientBattleChallenge
import com.cobblemon.mod.common.client.particle.BedrockParticleOptionsRepository
import com.cobblemon.mod.common.client.particle.ParticleStorm
import com.cobblemon.mod.common.client.render.MatrixWrapper
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.net.messages.client.battle.BattleChallengeNotificationPacket
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.getPlayer
import com.cobblemon.mod.common.util.lang
import com.mojang.authlib.minecraft.client.MinecraftClient
import com.mojang.blaze3d.vertex.PoseStack
import net.minecraft.client.Minecraft
import net.minecraft.client.multiplayer.ClientLevel
object BattleChallengeNotificationHandler : ClientNetworkPacketHandler<BattleChallengeNotificationPacket> {
override fun handle(packet: BattleChallengeNotificationPacket, client: Minecraft) {
val clientBattleChallenge = ClientBattleChallenge(packet.battleChallengeId, packet.challengerIds, packet.battleFormat)
CobblemonClient.requests.battleChallenges.add(clientBattleChallenge)
client.player?.sendSystemMessage(
lang(
"challenge.receiver",
packet.challengerNames.first(),
lang("battle.types.${packet.battleFormat.battleType.name}"),
).yellow()
)
CobblemonClient.requests.addParticleEffect(clientBattleChallenge)
val clientBattleChallenge = ClientBattleChallenge(packet.challengeID, packet.senderID, packet.expiryTime, packet.battleFormat)
packet.challengerIDs.forEach {
CobblemonClient.requests.battleChallenges[it] = clientBattleChallenge
ClientPlayerIcon.update(it)
}
}
}
@@ -11,15 +11,13 @@ package com.cobblemon.mod.common.client.net.battle
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.ClientMultiBattleTeamMember
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.net.messages.client.battle.TeamJoinNotificationPacket
import net.minecraft.client.Minecraft
object TeamJoinNotificationHandler : ClientNetworkPacketHandler<TeamJoinNotificationPacket> {
override fun handle(packet: TeamJoinNotificationPacket, client: Minecraft) {
CobblemonClient.teamData.multiBattleTeamMembers = packet.teamMemberUUIDs.mapIndexed { index, uuid -> ClientMultiBattleTeamMember(uuid, packet.teamMemberNames[index]) }.toMutableList()
// TODO: print something to the client
packet.teamMemberUUIDs.forEach { ClientPlayerIcon.update(it) }
}
}
@@ -9,7 +9,11 @@
package com.cobblemon.mod.common.client.net.battle
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.api.text.aqua
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.net.messages.client.battle.TeamMemberRemoveNotificationPacket
import com.cobblemon.mod.common.util.lang
import net.minecraft.client.Minecraft
@@ -19,27 +23,28 @@ object TeamMemberRemoveNotificationHandler : ClientNetworkPacketHandler<TeamMemb
if(packet.teamMemberUUID == client.player?.uuid) {
// Client removes itself from its team
val memberCount = CobblemonClient.teamData.multiBattleTeamMembers.count()
val members = CobblemonClient.teamData.multiBattleTeamMembers.map { it.uuid }
val memberCount = members.count()
CobblemonClient.teamData.multiBattleTeamMembers.clear()
val langKey = if(memberCount > 1) "challenge.multi.team_remove.sender" else "challenge.multi.team_disband"
members.forEach { ClientPlayerIcon.update(it) }
val langKey = if(memberCount > 1) "team.left.self" else "team.disband"
client.player?.sendSystemMessage(
lang(
langKey,
)
).red()
)
} else {
// Client removes a member from the team
val memberToRemove = CobblemonClient.teamData.multiBattleTeamMembers.find { it.uuid == packet.teamMemberUUID }
val memberToRemove = CobblemonClient.teamData.multiBattleTeamMembers.find { it.uuid == packet.teamMemberUUID } ?: return
CobblemonClient.teamData.multiBattleTeamMembers.remove(memberToRemove)
ClientPlayerIcon.update(memberToRemove.uuid)
if(memberToRemove != null) {
client.player?.sendSystemMessage(
lang(
"challenge.multi.team_remove.receiver",
memberToRemove.name,
)
)
}
client.player?.sendSystemMessage(
lang(
"team.left.other",
memberToRemove.name.copy().aqua(),
).red()
)
}
}
}
@@ -10,11 +10,13 @@ package com.cobblemon.mod.common.client.net.battle
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.net.messages.client.battle.TeamRequestExpiredPacket
import net.minecraft.client.Minecraft
object TeamRequestExpiredHandler : ClientNetworkPacketHandler<TeamRequestExpiredPacket> {
override fun handle(packet: TeamRequestExpiredPacket, client: Minecraft) {
CobblemonClient.requests.onMultiChallengeExpired(packet.teamRequestId)
CobblemonClient.requests.multiBattleTeamRequests.remove(packet.senderID)
ClientPlayerIcon.update(packet.senderID)
}
}
@@ -10,19 +10,14 @@ package com.cobblemon.mod.common.client.net.battle
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.battle.ClientBattleChallenge
import com.cobblemon.mod.common.client.battle.ClientTeamRequest
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.net.messages.client.battle.TeamRequestNotificationPacket
import com.cobblemon.mod.common.util.lang
import net.minecraft.client.Minecraft
object TeamRequestNotificationHandler : ClientNetworkPacketHandler<TeamRequestNotificationPacket> {
override fun handle(packet: TeamRequestNotificationPacket, client: Minecraft) {
CobblemonClient.requests.multiBattleTeamRequests.add(ClientBattleChallenge(packet.teamRequestId, listOf(packet.requesterId)))
client.player?.sendSystemMessage(
lang(
"challenge.multi.team_request.receiver",
packet.requesterName,
)
)
CobblemonClient.requests.multiBattleTeamRequests[packet.senderID] = ClientTeamRequest(packet.requestID, packet.senderID, packet.expiryTime)
ClientPlayerIcon.update(packet.senderID)
}
}
@@ -16,12 +16,9 @@ import com.cobblemon.mod.common.client.particle.BedrockParticleOptionsRepository
import com.cobblemon.mod.common.client.particle.ParticleStorm
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
import com.cobblemon.mod.common.entity.PosableEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormEntityParticlePacket
import com.cobblemon.mod.common.pokemon.Pokemon
import net.minecraft.client.Minecraft
import net.minecraft.world.entity.Entity
import kotlin.text.get
object SpawnSnowstormEntityParticleHandler : ClientNetworkPacketHandler<SpawnSnowstormEntityParticlePacket> {
override fun handle(packet: SpawnSnowstormEntityParticlePacket, client: Minecraft) {
@@ -30,23 +27,24 @@ object SpawnSnowstormEntityParticleHandler : ClientNetworkPacketHandler<SpawnSno
val entity = world.getEntity(packet.entityId) as? PosableEntity ?: return
entity as Entity
val state = entity.delegate as PosableState
val locator = packet.locator.firstOrNull() { state.locatorStates[it] != null } ?: return
val matrixWrapper = state.locatorStates[locator]!!
val locators = packet.locator.firstNotNullOfOrNull { state.getMatchingLocators(it).takeIf { it.isNotEmpty() } } ?: return
val particleRuntime = MoLangRuntime().setup().setupClient()
particleRuntime.environment.query.addFunction("entity") { state.runtime.environment.query }
val matrixWrappers = locators.mapNotNull { state.locatorStates[it] }
matrixWrappers.forEach { matrixWrapper ->
val particleRuntime = MoLangRuntime().setup().setupClient()
particleRuntime.environment.query.addFunction("entity") { state.runtime.environment.query }
val storm = ParticleStorm(
effect = effect,
matrixWrapper = matrixWrapper,
world = world,
runtime = particleRuntime,
sourceVelocity = { entity.deltaMovement },
sourceAlive = { !entity.isRemoved },
sourceVisible = { !entity.isInvisible },
entity = entity
)
val storm = ParticleStorm(
effect = effect,
matrixWrapper = matrixWrapper,
world = world,
runtime = particleRuntime,
sourceVelocity = { entity.deltaMovement },
sourceAlive = { !entity.isRemoved },
sourceVisible = { !entity.isInvisible },
entity = entity
)
storm.spawn()
storm.spawn()
}
}
}
@@ -10,11 +10,13 @@ package com.cobblemon.mod.common.client.net.trade
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.net.messages.client.trade.TradeOfferExpiredPacket
import net.minecraft.client.Minecraft
object TradeOfferExpiredHandler : ClientNetworkPacketHandler<TradeOfferExpiredPacket> {
override fun handle(packet: TradeOfferExpiredPacket, client: Minecraft) {
CobblemonClient.requests.onTradeOfferExpired(packet.tradeOfferId)
CobblemonClient.requests.tradeOffers.remove(packet.senderID)
ClientPlayerIcon.update(packet.senderID)
}
}
@@ -10,19 +10,14 @@ package com.cobblemon.mod.common.client.net.trade
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.keybind.boundKey
import com.cobblemon.mod.common.client.keybind.keybinds.PartySendBinding
import com.cobblemon.mod.common.client.trade.ClientTradeOffer
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.client.trade.ClientTradeRequest
import com.cobblemon.mod.common.net.messages.client.trade.TradeOfferNotificationPacket
import com.cobblemon.mod.common.util.lang
import net.minecraft.client.Minecraft
object TradeOfferNotificationHandler : ClientNetworkPacketHandler<TradeOfferNotificationPacket> {
override fun handle(packet: TradeOfferNotificationPacket, client: Minecraft) {
CobblemonClient.requests.tradeOffers.add(ClientTradeOffer(packet.tradeOfferId, packet.traderId))
client.player?.displayClientMessage(
lang("trade.offer", packet.traderName, PartySendBinding.boundKey().displayName),
true
)
CobblemonClient.requests.tradeOffers[packet.senderID] = ClientTradeRequest(packet.requestID, packet.senderID, packet.expiryTime)
ClientPlayerIcon.update(packet.senderID)
}
}
@@ -8,9 +8,11 @@
package com.cobblemon.mod.common.client.net.trade
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.gui.trade.TradeGUI
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.client.trade.ClientTrade
import com.cobblemon.mod.common.net.messages.client.trade.TradeStartedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeStartedPacket.TradeablePokemon
@@ -18,6 +20,7 @@ import net.minecraft.client.Minecraft
object TradeStartedHandler : ClientNetworkPacketHandler<TradeStartedPacket> {
override fun handle(packet: TradeStartedPacket, client: Minecraft) {
Cobblemon.LOGGER.error("handle trade start from: " + packet.traderId)
val trade = ClientTrade()
CobblemonClient.trade = trade
Minecraft.getInstance().setScreen(
@@ -29,5 +32,7 @@ object TradeStartedHandler : ClientNetworkPacketHandler<TradeStartedPacket> {
CobblemonClient.storage.myParty.map { it?.let(::TradeablePokemon) }.toMutableList()
)
)
CobblemonClient.requests.tradeOffers.remove(packet.traderId)
ClientPlayerIcon.update(packet.traderId)
}
}
@@ -0,0 +1,164 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.requests.ClientPlayerActionRequest
import com.cobblemon.mod.common.client.ClientMultiBattleTeamMember
import com.cobblemon.mod.common.platform.events.RenderEvent
import com.cobblemon.mod.common.util.cobblemonResource
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.*
import com.mojang.math.Axis
import net.minecraft.client.Camera
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.GameRenderer
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.Mth
import net.minecraft.world.entity.EntityAttachment
import net.minecraft.world.entity.player.Player
import org.joml.Matrix4f
import java.util.*
/**
* Visual notification rendered above a player's nametag for [ClientMultiBattleTeamMember]s and unanswered [ClientPlayerActionRequest]s.
*
* @param expiryTime The amount of time (in seconds) this icon is valid.
*
* @author Segfault Guy
* @since September 1st, 2024
*/
abstract class ClientPlayerIcon(expiryTime: Int? = null) {
/** The texture to use for the icon. */
open val texture: ResourceLocation = cobblemonResource("textures/particle/request/icon_exclamation.png")
/** The offset of the icon from the player's head. */
open val Y_OFFSET = 1.5F
open val SCALE = 0.75F
private val FADE_RATIO = 0.25F
private val tickCount get() = Minecraft.getInstance().player?.tickCount ?: 0 // icon is only ticked when tracked, so derive lifetime from player tick - startTick
private val startTick = Minecraft.getInstance().player?.tickCount ?: 0
private val tickRateManager = Minecraft.getInstance().level!!.tickRateManager()
/** The tick at which the icon begins fading. */
private val fadeTickStamp = expiryTime?.let { it * tickRateManager.tickrate() * (1 - FADE_RATIO) + startTick}?.toInt() ?: Int.MAX_VALUE // TODO is server expiration based on epoc going to cause a problem?
/** Duration (in ticks) of the fade transition. */
open val fadeDuration = 30
/** How long (in ticks) the icon has been fading. */
private var fadeCount = 0
private val fading get() = tickCount >= fadeTickStamp
private var fadeOut = true
set(value) {
fadeCount = 0
field = value
}
private val MAX_ALPHA = 1F
private val MIN_ALPHA = 0.15F // MC fragment shaders won't draw textures with < 0.1 alpha and the sudden cutoff from 0.1 to 0 and vice versa is jarring
private var alpha = 1F
private val startAlpha get() = if (fadeOut) MAX_ALPHA else MIN_ALPHA
private val finalAlpha get() = if (fadeOut) MIN_ALPHA else MAX_ALPHA
fun onTick() {
if (!fading) return else fadeCount++
if (fadeCount == fadeDuration) fadeOut = !fadeOut // toggle transition
// alpha changed outside of render deltaticks so fade does not freeze off screen
alpha = Mth.lerp(fadeCount.toFloat()/fadeDuration, startAlpha, finalAlpha)
}
fun render(player: Player) {
if (CobblemonClient.battle != null) return
DeferredRenderer.enqueue(RenderEvent.Stage.TRANSLUCENT) { event ->
val poseStack = event.poseStack
val partialTicks = event.tickCounter.getGameTimeDeltaPartialTick(!tickRateManager.isEntityFrozen(player))
val camera = event.camera
poseStack.pushPose()
positionTag(player, poseStack, camera, partialTicks)
drawTag(poseStack)
poseStack.popPose()
}
}
/** Offsets and billboards the quad above the player. */
protected open fun positionTag(player: Player, poseStack: PoseStack, camera: Camera, partialTicks: Float) {
// position
val vec3 = player.attachments.getNullable(EntityAttachment.NAME_TAG, 0, player.getViewYRot(partialTicks))
poseStack.translate(player.getPosition(partialTicks).x, player.getPosition(partialTicks).y, player.getPosition(partialTicks).z)
poseStack.translate(vec3!!.x, vec3.y + Y_OFFSET, vec3.z)
poseStack.translate(-camera.position.x, -camera.position.y, -camera.position.z) // event posestack does not have PlayerRenderer context
// billboard
val entityRenderDispatcher = Minecraft.getInstance().entityRenderDispatcher
poseStack.mulPose(Axis.YP.rotationDegrees((180f - entityRenderDispatcher.camera.yRot)))
poseStack.scale(SCALE, -SCALE, SCALE)
}
/** Constructs the vertices for the rendered quad. */
protected open fun buildTag(model: Matrix4f, vertices: VertexConsumer) {
vertices.addVertex(model, -0.5f, 0f, 0f).setUv(0f, 0f).setColor(1f, 1f, 1f, 1f)
vertices.addVertex(model, -0.5f, 1f, 0f).setUv(0f, 1f).setColor(1f, 1f, 1f, 1f)
vertices.addVertex(model, 0.5f, 1f, 0f).setUv(1f, 1f).setColor(1f, 1f, 1f, 1f)
vertices.addVertex(model, 0.5f, 0f, 0f).setUv(1f, 0f).setColor(1f, 1f, 1f, 1f)
}
/** Sets the shading and gl states of the quad then queues for drawing. */
private fun drawTag(poseStack: PoseStack) {
// texture
RenderSystem.setShader { GameRenderer.getPositionTexColorShader() }
RenderSystem.setShaderTexture(0, texture)
RenderSystem.setShaderColor(1f, 1f, 1f, alpha)
// gl states
RenderSystem.enableBlend()
RenderSystem.defaultBlendFunc()
RenderSystem.enableDepthTest()
// TODO worth making a custom rendertype and getting buffer from multiBufferSource?
val model = poseStack.last().pose()
val buffer = Tesselator.getInstance().begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX_COLOR)
buildTag(model, buffer)
BufferUploader.drawWithShader(buffer.buildOrThrow())
// undo
RenderSystem.disableBlend()
RenderSystem.disableDepthTest()
RenderSystem.setShaderColor(1f, 1f, 1f, 1f)
}
companion object {
private val trackedIcons = mutableMapOf<UUID, ClientPlayerIcon>()
fun onTick() = trackedIcons.values.forEach { it.onTick() }
/** Updates the icon that is rendered above the player. */
fun update(player: UUID) {
val teamIcon = CobblemonClient.teamData.multiBattleTeamMembers.find { it.uuid == player }
val requests = CobblemonClient.requests.getRequestsFrom(player).filterIsInstance<ClientPlayerIcon>()
if (requests.isNotEmpty()) {
val icon = requests.minBy { it.fadeTickStamp } // icon that expires the soonest has priority
trackedIcons[player] = icon
}
else if (teamIcon != null) {
trackedIcons[player] = teamIcon
}
else {
trackedIcons.remove(player)
}
}
// TODO make some api so this is expandable? i cant be assed right now
fun clear() = trackedIcons.clear()
fun onRenderPlayer(player: Player) = trackedIcons[player.uuid]?.render(player)
}
}
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.platform.events.RenderEvent
/**
* Maintains a queue of [RenderEventConsumer]s for renders that need to be deferred until the specified [RenderEvent.Stage]
* occurs in the level renderer.
*
* Useful for changing the order of a render.
*
* @author Segfault Guy
* @since September 11th, 2024
*/
object DeferredRenderer {
private val renderQueues = HashMap<RenderEvent.Stage, ArrayDeque<RenderEventConsumer>>()
fun enqueue(stage: RenderEvent.Stage, consumer: RenderEventConsumer) = renderQueues.computeIfAbsent(stage) { ArrayDeque() }.add(consumer)
fun clear(stage: RenderEvent.Stage) = renderQueues[stage]?.clear()
fun clearAll() = renderQueues.clear()
init {
PlatformEvents.RENDER.subscribe { event ->
val queue = renderQueues[event.stage] ?: return@subscribe
while (queue.isNotEmpty()) {
queue.first().invoke(event)
queue.removeFirst()
}
}
}
}
typealias RenderEventConsumer = (RenderEvent) -> Unit
@@ -221,6 +221,11 @@ class PokeBobberEntityRenderer(context: EntityRendererProvider.Context?) : Entit
matrixStack.popPose() // close main rendering transforms
// Iris Shader Compatibility: Keeps Iris from connecting the fishing lines of multiple players' Pokerods.
// Iris performs a mixin into FishingHookRenderer to achieve the same result.
// So we mimic their fix here.
vertexConsumer2.addVertex( 0f, 0f, 0f).setColor(0, 0, 0, 255).setNormal( 0f, 0f, 0f);
super.render(fishingBobberEntity, elapsedPartialTicks, tickDelta, matrixStack, vertexConsumerProvider, light)
}
@@ -27,7 +27,7 @@ class LocatorAccess(
val children: List<LocatorAccess> = listOf()
) {
companion object {
const val PREFIX = "locator_"
const val PREFIX = "internal_locator__"
fun resolve(part: Bone): LocatorAccess? {
val (
@@ -236,6 +236,10 @@ abstract class PosableState : Schedulable {
}
}
fun getMatchingLocators(locator: String): List<String> {
return locatorStates.keys.filter { it.matches("${locator}[0-9]*".toRegex()) }
}
/** Decides how an update to partial ticks should be applied to the state. See [FloatingState] for how it could happen. */
abstract fun updatePartialTicks(partialTicks: Float)
@@ -70,7 +70,7 @@ class DragonairModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
standing = registerPose(
poseName = "standing",
poseTypes = STATIONARY_POSES + UI_POSES - PoseType.FLOAT - PoseType.HOVER,
poseTypes = setOf(PoseType.STAND) + UI_POSES,
quirks = arrayOf(blink),
condition = { !it.isBattling && !it.isInWater},
transformTicks = 10,
@@ -70,9 +70,9 @@ class SquirtleModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
standing = registerPose(
poseName = "standing",
poseTypes = STATIONARY_POSES - PoseType.HOVER + UI_POSES,
poseTypes = setOf(PoseType.STAND) + UI_POSES,
quirks = arrayOf(blink, quirkidle),
condition = { !it.isBattling && !it.isInWater && !it.isUnderWater},
condition = { !it.isBattling && !it.isInWater },
namedAnimations = mutableMapOf("faint" to faint),
animations = arrayOf(
singleBoneLook(),
@@ -94,9 +94,9 @@ class SquirtleModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
walk = registerPose(
poseName = "walk",
poseTypes = PoseType.MOVING_POSES - PoseType.FLY,
poseTypes = setOf(PoseType.WALK),
quirks = arrayOf(blink),
condition = { !it.isInWater && !it.isUnderWater},
condition = { !it.isInWater },
namedAnimations = mutableMapOf("faint" to faint),
animations = arrayOf(
singleBoneLook(),
@@ -108,7 +108,6 @@ class SquirtleModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
poseName = "floating",
transformTicks = 10,
poseType = PoseType.FLOAT,
condition = { it.isUnderWater },
namedAnimations = mutableMapOf("faint" to faint),
quirks = arrayOf(blink, quirkwater),
animations = arrayOf(
@@ -119,10 +118,9 @@ class SquirtleModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
swimming = registerPose(
poseName = "swimming",
transformTicks = 10,
condition = { it.isUnderWater },
namedAnimations = mutableMapOf("faint" to faint),
poseType = PoseType.SWIM,
transformTicks = 10,
namedAnimations = mutableMapOf("faint" to faint),
quirks = arrayOf(blink),
animations = arrayOf(
singleBoneLook(),
@@ -132,10 +130,10 @@ class SquirtleModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
water_surface_idle = registerPose(
poseName = "surface_idle",
poseTypes = PoseType.STATIONARY_POSES,
poseType = PoseType.STAND,
namedAnimations = mutableMapOf("faint" to faint),
quirks = arrayOf(blink),
condition = { !it.isUnderWater && it.isInWater },
condition = { it.isInWater },
animations = arrayOf(
singleBoneLook(),
bedrock("squirtle", "surfacewater_idle"),
@@ -147,9 +145,9 @@ class SquirtleModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
water_surface_swim = registerPose(
poseName = "surface_swim",
poseTypes = PoseType.MOVING_POSES,
poseType = PoseType.WALK,
quirks = arrayOf(blink),
condition = { !it.isUnderWater && it.isInWater },
condition = { it.isInWater },
namedAnimations = mutableMapOf("faint" to faint),
animations = arrayOf(
singleBoneLook(),
@@ -38,7 +38,7 @@ class VaporeonModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
override fun registerPoses() {
standing = registerPose(
poseName = "standing",
poseTypes = STATIONARY_POSES + UI_POSES,
poseTypes = STATIONARY_POSES + UI_POSES - PoseType.FLOAT,
transformTicks = 10,
animations = arrayOf(
singleBoneLook(),
@@ -67,9 +67,9 @@ class WartortleModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
standing = registerPose(
poseName = "standing",
poseTypes = PoseType.STATIONARY_POSES - PoseType.HOVER + UI_POSES,
poseTypes = setOf(PoseType.STAND) + UI_POSES,
quirks = arrayOf(blink, quirkidle),
condition = { !it.isBattling && !it.isInWater && !it.isUnderWater},
condition = { !it.isBattling && !it.isUnderWater},
namedAnimations = mutableMapOf("faint" to faint),
animations = arrayOf(
singleBoneLook(),
@@ -91,9 +91,8 @@ class WartortleModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
walk = registerPose(
poseName = "walk",
poseTypes = PoseType.MOVING_POSES - PoseType.FLY,
poseTypes = setOf(PoseType.WALK),
quirks = arrayOf(blink),
condition = { !it.isInWater && !it.isUnderWater},
namedAnimations = mutableMapOf("faint" to faint),
animations = arrayOf(
singleBoneLook(),
@@ -105,7 +104,6 @@ class WartortleModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
poseName = "floating",
transformTicks = 10,
poseType = PoseType.FLOAT,
condition = { it.isUnderWater },
namedAnimations = mutableMapOf("faint" to faint),
quirks = arrayOf(blink),
animations = arrayOf(
@@ -117,7 +115,6 @@ class WartortleModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
swimming = registerPose(
poseName = "swimming",
transformTicks = 10,
condition = { it.isUnderWater },
namedAnimations = mutableMapOf("faint" to faint),
poseType = PoseType.SWIM,
quirks = arrayOf(blink),
@@ -129,10 +126,10 @@ class WartortleModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
water_surface_idle = registerPose(
poseName = "surface_idle",
poseTypes = PoseType.STATIONARY_POSES,
poseType = PoseType.STAND,
namedAnimations = mutableMapOf("faint" to faint),
quirks = arrayOf(blink),
condition = { !it.isUnderWater && it.isInWater },
condition = { it.isInWater },
animations = arrayOf(
singleBoneLook(),
bedrock("wartortle", "surfacewater_idle"),
@@ -144,9 +141,9 @@ class WartortleModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
water_surface_swim = registerPose(
poseName = "surface_swim",
poseTypes = PoseType.MOVING_POSES,
poseType = PoseType.WALK,
quirks = arrayOf(blink),
condition = { !it.isUnderWater && it.isInWater },
condition = { it.isInWater },
namedAnimations = mutableMapOf("faint" to faint),
animations = arrayOf(
singleBoneLook(),
@@ -19,6 +19,7 @@ import com.cobblemon.mod.common.client.render.models.blockbench.*
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockAnimationRepository
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Bone
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository.MixinCompatibilityExclusionStrategy
import com.cobblemon.mod.common.client.util.exists
import com.cobblemon.mod.common.util.adapters.ExpressionAdapter
import com.cobblemon.mod.common.util.adapters.ExpressionLikeAdapter
@@ -26,6 +27,8 @@ import com.cobblemon.mod.common.util.adapters.Vec3dAdapter
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.endsWith
import com.cobblemon.mod.common.util.fromJson
import com.google.gson.ExclusionStrategy
import com.google.gson.FieldAttributes
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
@@ -36,6 +39,7 @@ import net.minecraft.server.packs.resources.ResourceManager
import net.minecraft.world.phys.Vec3
import java.io.File
import java.nio.charset.StandardCharsets
import java.util.Optional
import java.util.function.BiFunction
import java.util.function.Function
@@ -71,6 +75,7 @@ abstract class VaryingModelRepository<T : PosableModel> {
.registerTypeAdapter(Vec3::class.java, Vec3dAdapter)
.registerTypeAdapter(Expression::class.java, ExpressionAdapter)
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
.addDeserializationExclusionStrategy(MixinCompatibilityExclusionStrategy)
.also { configureGson(it) }
.create()
}
@@ -96,6 +101,31 @@ abstract class VaryingModelRepository<T : PosableModel> {
return JsonModelAdapter { poserClass.getConstructor(Bone::class.java).newInstance(it) }
}
//Some mods will inject extra properties into the model part, which we use GSON for (through the Bone interface)
//If through a 3rd party mixin fields get injected that cant be deserialized by default (e.g. optional), we crash
//this strategy aims to skip the known 3rd party libraries that do this to avoid crashing
object MixinCompatibilityExclusionStrategy : ExclusionStrategy {
private var known3rdPartyMixins = listOf("embeddium")
private var knownUnusedClasses = listOf(Optional::class.java)
override fun shouldSkipField(field: FieldAttributes?): Boolean {
if (known3rdPartyMixins.any { field?.name?.contains(it) == true }) {
Cobblemon.LOGGER.debug("Skipping non-vanilla field encountered during model deserialization ${field?.name}")
return true
}
return false
}
override fun shouldSkipClass(p0: Class<*>?): Boolean {
if (p0 in knownUnusedClasses) {
Cobblemon.LOGGER.debug("Skipping non-vanilla class encountered during model deserialization: ${p0?.name}")
return true
}
return false
}
}
open fun configureGson(gsonBuilder: GsonBuilder) {
adapter = createAdapter()
gsonBuilder.setupGsonForJsonPosableModels(adapter) { json -> conditionParser(json) }
@@ -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.client.requests
import com.cobblemon.mod.common.api.interaction.PlayerActionRequest
import com.cobblemon.mod.common.api.text.aqua
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.util.lang
import net.minecraft.client.Minecraft
import net.minecraft.network.chat.Component
import java.util.UUID
/**
* An inbound [PlayerActionRequest].
*
* @author Segfault Guy
* @since September 21st, 2024
*/
abstract class ClientPlayerActionRequest(expiryTime: Int) : ClientPlayerIcon(expiryTime), PlayerActionRequest {
companion object {
/** Client message to inform the player about a [langKey] request from [senderID]. */
fun notify(langKey: String, senderID: UUID, vararg params: Any) {
val sender = Minecraft.getInstance().level?.players()?.find { it.uuid == senderID }
val senderName = sender?.name?.copy()?.aqua() ?: Component.literal("NULL").red()
val lang = lang(langKey, senderName, *params).yellow()
Minecraft.getInstance().player!!.displayClientMessage(lang, false)
}
}
}
@@ -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.client.requests
import com.cobblemon.mod.common.client.battle.ClientBattleChallenge
import com.cobblemon.mod.common.client.battle.ClientTeamRequest
import com.cobblemon.mod.common.client.trade.ClientTradeRequest
import java.util.*
class ClientPlayerActionRequests {
val battleChallenges = mutableMapOf<UUID, ClientBattleChallenge>() // Player to challenge
val multiBattleTeamRequests = mutableMapOf<UUID, ClientTeamRequest>() // Player to team up invite
val tradeOffers = mutableMapOf<UUID, ClientTradeRequest>() // Player to trade offer
fun getRequestsFrom(player: UUID): List<ClientPlayerActionRequest> = buildList {
battleChallenges[player]?.let { add(it) }
multiBattleTeamRequests[player]?.let { add(it) }
tradeOffers[player]?.let { add(it) }
}
}
@@ -1,13 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.trade
import java.util.UUID
class ClientTradeOffer(val tradeOfferId: UUID, val traderId: UUID)
@@ -0,0 +1,22 @@
/*
* 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.trade
import com.cobblemon.mod.common.client.requests.ClientPlayerActionRequest
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.resources.ResourceLocation
import java.util.UUID
data class ClientTradeRequest(
override val requestID: UUID,
override val senderID: UUID,
override val expiryTime: Int
) : ClientPlayerActionRequest(expiryTime) {
override val texture: ResourceLocation = cobblemonResource("textures/particle/request/icon_trade.png")
}
@@ -10,7 +10,7 @@ package com.cobblemon.mod.common.command
import com.cobblemon.mod.common.api.permission.CobblemonPermissions
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.TeamManager
import com.cobblemon.mod.common.util.alias
import com.cobblemon.mod.common.util.permission
import com.mojang.brigadier.Command.SINGLE_SUCCESS
@@ -36,7 +36,7 @@ object AbandonMultiTeam {
val player = context.source.player
if (player != null) {
BattleRegistry.removeTeamMember(player)
TeamManager.removeTeamMember(player)
}
return SINGLE_SUCCESS
}
@@ -18,6 +18,7 @@ import com.cobblemon.mod.common.util.permission
import com.cobblemon.mod.common.util.toBlockPos
import com.mojang.brigadier.Command
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.context.CommandContext
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType
import net.minecraft.commands.CommandSourceStack
@@ -32,6 +33,7 @@ object SpawnNPCCommand {
private const val NAME = "spawnnpc"
private const val CLASS = "class"
private const val POSITION = "pos"
private const val LEVEL = "level"
private const val ALIAS = "npcspawn"
private const val AT_NAME = "${NAME}at"
private const val AT_ALIAS = "${ALIAS}at"
@@ -43,7 +45,11 @@ object SpawnNPCCommand {
val contextPositionCommand = dispatcher.register(literal(NAME)
.permission(CobblemonPermissions.SPAWN_NPC)
.then(argument(CLASS, NPCClassArgumentType.npcClass())
.executes{ context -> execute(context, context.source.position) }
.then(
argument(LEVEL, IntegerArgumentType.integer(1))
.executes { context -> execute(context, context.source.position, IntegerArgumentType.getInteger(context, LEVEL)) }
)
.executes { context -> execute(context, context.source.position, 1) }
)
)
dispatcher.register(contextPositionCommand.alias(ALIAS))
@@ -51,14 +57,18 @@ object SpawnNPCCommand {
.permission(CobblemonPermissions.SPAWN_NPC)
.then(argument(POSITION, Vec3Argument.vec3())
.then(argument(CLASS, NPCClassArgumentType.npcClass())
.executes { context -> execute(context, Vec3Argument.getVec3(context, POSITION)) }
.then(
argument(LEVEL, IntegerArgumentType.integer(1))
.executes { context -> execute(context, Vec3Argument.getVec3(context, POSITION), IntegerArgumentType.getInteger(context, LEVEL)) }
)
.executes { context -> execute(context, Vec3Argument.getVec3(context, POSITION), 1) }
)
)
)
dispatcher.register(argumentPositionCommand.alias(AT_ALIAS))
}
private fun execute(context: CommandContext<CommandSourceStack>, pos: Vec3): Int {
private fun execute(context: CommandContext<CommandSourceStack>, pos: Vec3, level: Int): Int {
val world = context.source.level
val blockPos = pos.toBlockPos()
if (!Level.isInSpawnableBounds(blockPos)) {
@@ -68,7 +78,7 @@ object SpawnNPCCommand {
val npc = NPCEntity(world)
npc.moveTo(pos.x, pos.y, pos.z, npc.yRot, npc.xRot)
npc.npc = npcClass
npc.initialize(1)
npc.initialize(level)
if (world.addFreshEntity(npc)) {
return Command.SINGLE_SUCCESS
}
@@ -40,7 +40,7 @@ class FeatureFix(output: Schema) : PokemonFix(output) {
feature.saveToNBT(tag)
featureTag += tag
}
if(featureTag.isEmpty()){
if (featureTag.isEmpty()){
return dynamic // no features to migrate
}
rootTag.put(FEATURES, Codec.list<CompoundTag>(CompoundTag.CODEC)
@@ -408,7 +408,7 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
val owner = this.owner
if (this.state == State.FLYING && owner != null) { // starts casting sound when instantiated on client
val rand = this.level().random
val toPlay = EntityBoundSoundInstance(this.castingSound, SoundSource.PLAYERS, 1.0F, 1.0f / (rand.nextFloat() * 0.4f + 0.8f), owner, rand.nextLong())
val toPlay = EntityBoundSoundInstance(this.castingSound, SoundSource.PLAYERS, 1.0F, 1.0f, owner, rand.nextLong())
EntitySoundTracker.play(owner.id, toPlay)
}
}
@@ -31,9 +31,6 @@ import com.cobblemon.mod.common.api.npc.NPCClasses
import com.cobblemon.mod.common.api.npc.configuration.NPCBattleConfiguration
import com.cobblemon.mod.common.api.npc.configuration.NPCBehaviourConfiguration
import com.cobblemon.mod.common.api.npc.configuration.NPCInteractConfiguration
import com.cobblemon.mod.common.api.npc.partyproviders.DynamicNPCParty
import com.cobblemon.mod.common.api.npc.partyproviders.NPCParty
import com.cobblemon.mod.common.api.npc.partyproviders.StaticNPCParty
import com.cobblemon.mod.common.api.scheduling.Schedulable
import com.cobblemon.mod.common.api.scheduling.SchedulingTracker
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
@@ -61,6 +58,7 @@ import java.util.UUID
import java.util.concurrent.CompletableFuture
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.IntTag
import net.minecraft.nbt.ListTag
import net.minecraft.nbt.StringTag
import net.minecraft.nbt.Tag
@@ -69,6 +67,7 @@ import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket
import net.minecraft.network.protocol.game.ClientGamePacketListener
import net.minecraft.network.protocol.game.ClientboundAddEntityPacket
import net.minecraft.network.syncher.EntityDataAccessor
import net.minecraft.network.syncher.EntityDataSerializers
import net.minecraft.network.syncher.SynchedEntityData
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerEntity
@@ -111,11 +110,22 @@ class NPCEntity(world: Level) : AgeableMob(CobblemonEntities.NPC, world), Npc, P
}
}
val level: Int
get() = entityData.get(LEVEL)
var skill: Int? = npc.skill ?: 0 // range from 0 - 5
var party: NPCParty? = null
val staticParty: NPCPartyStore?
get() = (party as? StaticNPCParty)?.party
var party: NPCPartyStore? = null
fun getPartyForChallenge(player: ServerPlayer): NPCPartyStore? {
return if (party != null) {
party
} else if (npc.party?.isStatic == false) {
npc.party?.provide(this, level)
} else {
null
}
}
val appliedAspects = mutableSetOf<String>()
override val delegate = if (world.isClientSide) {
@@ -180,6 +190,7 @@ class NPCEntity(world: Level) : AgeableMob(CobblemonEntities.NPC, world), Npc, P
val POSE_TYPE = SynchedEntityData.defineId(NPCEntity::class.java, PoseTypeDataSerializer)
val BATTLE_IDS = SynchedEntityData.defineId(NPCEntity::class.java, UUIDSetDataSerializer)
val NPC_PLAYER_TEXTURE = SynchedEntityData.defineId(NPCEntity::class.java, NPCPlayerTextureSerializer)
val LEVEL = SynchedEntityData.defineId(NPCEntity::class.java, EntityDataSerializers.INT)
// val BATTLING = Activity.register("npc_battling")
@@ -228,6 +239,7 @@ class NPCEntity(world: Level) : AgeableMob(CobblemonEntities.NPC, world), Npc, P
builder.define(POSE_TYPE, PoseType.STAND)
builder.define(BATTLE_IDS, setOf())
builder.define(NPC_PLAYER_TEXTURE, NPCPlayerTexture(ByteArray(1), NPCPlayerModelType.NONE))
builder.define(LEVEL, 1)
}
override fun getAddEntityPacket(serverEntity: ServerEntity) = ClientboundCustomPayloadPacket(
@@ -283,6 +295,7 @@ class NPCEntity(world: Level) : AgeableMob(CobblemonEntities.NPC, world), Npc, P
super.saveWithoutId(nbt)
nbt.put(DataKeys.NPC_DATA, MoLangFunctions.writeMoValueToNBT(data))
nbt.put(DataKeys.NPC_CONFIG, MoLangFunctions.writeMoValueToNBT(config))
nbt.put(DataKeys.NPC_LEVEL, IntTag.valueOf(level))
nbt.putString(DataKeys.NPC_CLASS, npc.id.toString())
nbt.put(DataKeys.NPC_ASPECTS, ListTag().also { list -> appliedAspects.forEach { list.add(StringTag.valueOf(it)) } })
interaction?.let {
@@ -301,7 +314,6 @@ class NPCEntity(world: Level) : AgeableMob(CobblemonEntities.NPC, world), Npc, P
val party = party
if (party != null) {
val partyNBT = CompoundTag()
partyNBT.putBoolean(DataKeys.NPC_PARTY_IS_STATIC, party is StaticNPCParty)
party.saveToNBT(partyNBT, registryAccess())
nbt.put(DataKeys.NPC_PARTY, partyNBT)
}
@@ -320,6 +332,7 @@ class NPCEntity(world: Level) : AgeableMob(CobblemonEntities.NPC, world), Npc, P
override fun load(nbt: CompoundTag) {
npc = NPCClasses.getByIdentifier(ResourceLocation.parse(nbt.getString(DataKeys.NPC_CLASS))) ?: NPCClasses.classes.first()
entityData.set(LEVEL, nbt.getInt(DataKeys.NPC_LEVEL).takeIf { it != 0 } ?: 1)
super.load(nbt)
data = MoLangFunctions.readMoValueFromNBT(nbt.getCompound(DataKeys.NPC_DATA)) as VariableStruct
config = if (nbt.contains(DataKeys.NPC_CONFIG)) MoLangFunctions.readMoValueFromNBT(nbt.getCompound(DataKeys.NPC_CONFIG)) as VariableStruct else VariableStruct()
@@ -336,21 +349,7 @@ class NPCEntity(world: Level) : AgeableMob(CobblemonEntities.NPC, world), Npc, P
this.skill = npc.skill
val partyNBT = nbt.getCompound(DataKeys.NPC_PARTY)
if (!partyNBT.isEmpty) {
val isStatic = partyNBT.getBoolean(DataKeys.NPC_PARTY_IS_STATIC)
this.party = if (isStatic) {
StaticNPCParty(NPCPartyStore(this)).also { it.loadFromNBT(partyNBT, registryAccess()) }
} else {
val type = partyNBT.getString(DataKeys.NPC_PARTY_TYPE)
val clazz = DynamicNPCParty.types[type]
if (clazz == null) {
Cobblemon.LOGGER.error("Tried deserializing NPC entity with unknown party type: $type. I am at $x $y $z. Setting party to null.")
null
} else {
val party = clazz.getConstructor().newInstance()
party.loadFromNBT(partyNBT, registryAccess())
party
}
}
party = NPCPartyStore(this).also { it.loadFromNBT(partyNBT, registryAccess()) }
}
if (nbt.contains(DataKeys.NPC_PLAYER_TEXTURE)) {
val textureNBT = nbt.getCompound(DataKeys.NPC_PLAYER_TEXTURE)
@@ -393,9 +392,12 @@ class NPCEntity(world: Level) : AgeableMob(CobblemonEntities.NPC, world), Npc, P
fun initialize(level: Int) {
appliedAspects.clear()
entityData.set(LEVEL, level)
npc.config.forEach { it.applyDefault(this) }
npc.variations.values.forEach { this.appliedAspects.addAll(it.provideAspects(this)) }
party = npc.party?.provide(this, level)
if (party == null || npc.party != null) {
party = npc.party?.takeIf { it.isStatic }?.provide(this, level)
}
updateAspects()
}
@@ -53,7 +53,7 @@ object GoToHealingMachineTask {
val speedMultiplier = runtime.resolveFloat(speedMultiplier)
val completionRange = runtime.resolveInt(completionRange)
if ((entity.staticParty?.getHealingRemainderPercent() ?: 0F) > 0F) {
if ((entity.party?.getHealingRemainderPercent() ?: 0F) > 0F) {
val npcPos = entity.blockPosition()
val nearestFreeHealer = world
.getNearbyBlockEntities(
@@ -53,7 +53,7 @@ class HealUsingHealingMachineTask(
return false
}
val partyNeedsHealing = (entity.staticParty?.getHealingRemainderPercent() ?: 0.0F) > 0.0F
val partyNeedsHealing = (entity.party?.getHealingRemainderPercent() ?: 0.0F) > 0.0F
if (!partyNeedsHealing) {
return false
}
@@ -77,7 +77,7 @@ class HealUsingHealingMachineTask(
override fun start(world: ServerLevel, entity: LivingEntity, l: Long) {
entity as NPCEntity
nearestHealer?.activate(entity.uuid, entity.staticParty ?: return)
nearestHealer?.activate(entity.uuid, entity.party ?: return)
entity.playAnimation("punch_right")
}
}
@@ -252,6 +252,9 @@ open class PokemonEntity(
/** The form exposed to the client and used for calculating hitbox and height. */
val exposedForm: FormData get() = this.effects.mockEffect?.exposedForm ?: this.pokemon.form
/** The aspects exposed to the client */
val exposedAspects: Set<String> get() = this.effects.mockEffect?.exposedForm?.aspects?.toSet() ?: this.pokemon.aspects
override val struct: QueryStruct = QueryStruct(hashMapOf())
.addStandardFunctions()
.addEntityFunctions(this)
@@ -13,8 +13,10 @@ import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.PokemonPropertyExtractor
import com.cobblemon.mod.common.api.scheduling.afterOnServer
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormEntityParticlePacket
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.core.HolderLookup
import net.minecraft.nbt.CompoundTag
import java.util.concurrent.CompletableFuture
@@ -45,6 +47,7 @@ class IllusionEffect(
entity.effects.mockEffect = null
afterOnServer(seconds = 1.0F) {
entity.cry()
if (entity.pokemon.shiny) SpawnSnowstormEntityParticlePacket(cobblemonResource("shiny_ring"), entity.id, listOf("shiny_particles", "middle")).sendToPlayersAround(entity.x, entity.y, entity.z, 64.0, entity.level().dimension())
future.complete(entity)
}
}
@@ -28,6 +28,7 @@ import java.util.concurrent.CompletableFuture
* @since March 5th, 2024
*/
class TransformEffect(
// This really should include aspects separately
override var mock: PokemonProperties = PokemonProperties(),
override var scale: Float = 1.0F,
val doCry: Boolean = true
@@ -13,21 +13,17 @@ import com.cobblemon.mod.common.advancement.CobblemonCriteria
import com.cobblemon.mod.common.advancement.criterion.*
import com.cobblemon.mod.common.api.Priority
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
import com.cobblemon.mod.common.api.events.CobblemonEvents.BATTLE_STARTED_POST
import com.cobblemon.mod.common.api.events.CobblemonEvents.BATTLE_VICTORY
import com.cobblemon.mod.common.api.events.CobblemonEvents.EVOLUTION_COMPLETE
import com.cobblemon.mod.common.api.events.CobblemonEvents.LEVEL_UP_EVENT
import com.cobblemon.mod.common.api.events.CobblemonEvents.POKEMON_CAPTURED
import com.cobblemon.mod.common.api.events.CobblemonEvents.TRADE_COMPLETED
import com.cobblemon.mod.common.api.events.battles.BattleVictoryEvent
import com.cobblemon.mod.common.api.events.pokemon.LevelUpEvent
import com.cobblemon.mod.common.api.events.pokemon.PokemonCapturedEvent
import com.cobblemon.mod.common.api.events.pokemon.TradeCompletedEvent
import com.cobblemon.mod.common.api.events.pokemon.*
import com.cobblemon.mod.common.api.events.pokemon.evolution.EvolutionCompleteEvent
import com.cobblemon.mod.common.block.TumblestoneBlock
import com.cobblemon.mod.common.item.TumblestoneItem
import com.cobblemon.mod.common.platform.events.ServerPlayerEvent
import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreType
import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.util.effectiveName
@@ -62,13 +58,23 @@ object AdvancementHandler : EventHandler {
Cobblemon.playerDataManager.saveSingle(playerData, PlayerInstancedDataStoreTypes.GENERAL)
}
// fun onHatch(event: HatchEggEvent) {
// val playerData = Cobblemon.playerData.get(event.player)
// val advancementData = playerData.advancementData
// advancementData.updateTotalEggsHatched()
// Cobblemon.playerData.saveSingle(playerData)
// CobblemonCriteria.EGG_HATCH.trigger(event.player, advancementData.totalEggsHatched)
// }
fun onEggCollect(event: CollectEggEvent) {
if (!event.isCanceled) {
val playerData = Cobblemon.playerDataManager.getGenericData(event.player)
val advancementData = playerData.advancementData
advancementData.updateTotalEggsCollected()
Cobblemon.playerDataManager.saveSingle(playerData, PlayerInstancedDataStoreTypes.GENERAL)
CobblemonCriteria.EGG_COLLECT.trigger(event.player, CountableContext(advancementData.totalEggsCollected))
}
}
fun onHatch(event: HatchEggEvent.Post) {
val playerData = Cobblemon.playerDataManager.getGenericData(event.player)
val advancementData = playerData.advancementData
advancementData.updateTotalEggsHatched()
Cobblemon.playerDataManager.saveSingle(playerData, PlayerInstancedDataStoreTypes.GENERAL)
CobblemonCriteria.EGG_HATCH.trigger(event.player, CountableContext(advancementData.totalEggsHatched))
}
fun onEvolve(event: EvolutionCompleteEvent) {
val player = event.pokemon.getOwnerPlayer()
@@ -174,4 +180,4 @@ object AdvancementHandler : EventHandler {
CobblemonCriteria.PLANT_TUMBLESTONE.trigger(event.player, PlantTumblestoneContext(event.pos, block))
}
}
}
}
@@ -198,7 +198,7 @@ class PokerodItem(val pokeRodId: ResourceLocation, settings: Properties) : Fishi
}
world.playSound(null as Player?, user.x, user.y, user.z, CobblemonSounds.FISHING_ROD_REEL_IN, SoundSource.PLAYERS, 1.0f, 1.0f / (world.getRandom().nextFloat() * 0.4f + 0.8f))
world.playSound(null as Player?, user.x, user.y, user.z, CobblemonSounds.FISHING_ROD_REEL_IN, SoundSource.PLAYERS, 1.0f, 1.0f)
user.gameEvent(GameEvent.ITEM_INTERACT_FINISH)
} else { // if the bobber is not out yet
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.net.messages.client.battle
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.battles.ChallengeManager
import com.cobblemon.mod.common.util.cobblemonResource
import java.util.UUID
import net.minecraft.network.RegistryFriendlyByteBuf
@@ -16,17 +17,23 @@ import net.minecraft.network.RegistryFriendlyByteBuf
/**
* Packet fired to tell the client that a battle challenge expired.
*
* Handled by [com.cobblemon.mod.common.client.net.battle.BattleChallengeExpiredHandler].
*
* @param requestID The unique identifier of the challenge.
*
* @author Hiroku
* @since March 11th, 2023
*/
class BattleChallengeExpiredPacket(val battleChallengeId: UUID) : NetworkPacket<BattleChallengeExpiredPacket> {
class BattleChallengeExpiredPacket(val requestID: UUID) : NetworkPacket<BattleChallengeExpiredPacket> {
companion object {
val ID = cobblemonResource("battle_challenge_expired")
val ID = cobblemonResource("battle_challenge_canceled")
fun decode(buffer: RegistryFriendlyByteBuf) = BattleChallengeExpiredPacket(buffer.readUUID())
}
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeUUID(battleChallengeId)
buffer.writeUUID(requestID)
}
constructor(challenge: ChallengeManager.BattleChallenge) : this(challenge.requestID)
}
@@ -10,41 +10,68 @@ package com.cobblemon.mod.common.net.messages.client.battle
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.battles.BattleFormat
import com.cobblemon.mod.common.battles.ChallengeManager
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.network.chat.ComponentSerialization
import net.minecraft.network.chat.MutableComponent
import java.util.*
/**
* Packet send when a player has challenged to battle. The responsibility
* of this packet currently is to send a battle challenge message that includes
* the keybind to challenge them back. In future this is likely to include information
* about the battle.
* the keybind to challenge them back.
*
* Handled by [com.cobblemon.mod.common.client.net.battle.BattleChallengeNotificationHandler].
*
* @param challengeID The unique identifier of the challenge.
* @param senderID The unique identifier of the party that sent the challenge.
* @param challengerIDs In the case of a multi battle, the unique identifiers of all the players on the challenging team.
* Otherwise, the unique identifier of the challenging player (same as [senderID]).
* @param battleFormat The showdown format of the challenge.
* @param expiryTime How long (in seconds) the challenge is active.
*
* @author Hiroku
* @since August 5th, 2022
*/
class BattleChallengeNotificationPacket(
val battleChallengeId: UUID,
val challengerIds: List<UUID>,
val challengerNames: List<MutableComponent>,
val battleFormat: BattleFormat
val challengeID: UUID,
val senderID: UUID,
val challengerIDs: List<UUID>,
val battleFormat: BattleFormat,
val expiryTime: Int
): NetworkPacket<BattleChallengeNotificationPacket> {
override val id = ID
constructor(battleChallengeId: UUID, challengerId: UUID, challengerName: MutableComponent, battleFormat: BattleFormat) : this(battleChallengeId, listOf(challengerId), listOf(challengerName), battleFormat)
constructor(battleChallengeId: UUID, challengerId: UUID, battleFormat: BattleFormat, expiryTime: Int) :
this(battleChallengeId, challengerId, listOf(challengerId), battleFormat, expiryTime)
constructor(challenge: ChallengeManager.BattleChallenge) : this(
challengeID = challenge.requestID,
senderID = challenge.senderID,
challengerIDs =
if (challenge is ChallengeManager.MultiBattleChallenge)
challenge.senderTeam.teamPlayersUUID
else
listOf(challenge.senderID),
battleFormat = challenge.battleFormat,
expiryTime = challenge.expiryTime
)
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeUUID(battleChallengeId)
buffer.writeCollection(challengerIds) { _, value -> buffer.writeUUID(value) }
buffer.writeCollection(challengerNames) { _, value -> ComponentSerialization.TRUSTED_CONTEXT_FREE_STREAM_CODEC.encode(buffer, value) }
buffer.writeUUID(challengeID)
buffer.writeUUID(senderID)
buffer.writeCollection(challengerIDs) { _, value -> buffer.writeUUID(value) }
battleFormat.saveToBuffer(buffer)
buffer.writeInt(expiryTime)
}
companion object {
val ID = cobblemonResource("battle_challenge_notification")
fun decode(buffer: RegistryFriendlyByteBuf) = BattleChallengeNotificationPacket(buffer.readUUID(), buffer.readList { it.readUUID() }, buffer.readList { ComponentSerialization.TRUSTED_CONTEXT_FREE_STREAM_CODEC.decode(it).copy() } , BattleFormat.loadFromBuffer(buffer))
fun decode(buffer: RegistryFriendlyByteBuf) = BattleChallengeNotificationPacket(
buffer.readUUID(),
buffer.readUUID(),
buffer.readList { it.readUUID() },
BattleFormat.loadFromBuffer(buffer),
buffer.readInt()
)
}
}
@@ -8,14 +8,10 @@
package com.cobblemon.mod.common.net.messages.client.battle
import com.cobblemon.mod.common.api.callback.PartySelectPokemonDTO
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyCallbackPacket
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readText
import com.cobblemon.mod.common.util.writeBox
import com.cobblemon.mod.common.util.writeText
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.RegistryFriendlyByteBuf
import java.util.UUID
import net.minecraft.network.chat.MutableComponent
@@ -32,8 +28,8 @@ import net.minecraft.network.chat.MutableComponent
* @since April 9th, 2024
*/
class TeamJoinNotificationPacket(
val teamMemberUUIDs: List<UUID>,
val teamMemberNames: List<MutableComponent>,
val teamMemberUUIDs: List<UUID>,
val teamMemberNames: List<MutableComponent>,
): NetworkPacket<TeamJoinNotificationPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
@@ -18,7 +18,7 @@ import java.util.UUID
/**
* Packet sent when a player joins or leaves a team that the client is currently a member of.
*
* Handled by [com.cobblemon.mod.common.client.net.battle.TeamRequestNotificationHandler].
* Handled by [com.cobblemon.mod.common.client.net.battle.TeamMemberRemoveNotificationHandler].
*
* @author JazzMcNade
* @since April 9th, 2024
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.net.messages.client.battle
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.battles.TeamManager
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
import java.util.UUID
@@ -16,10 +17,15 @@ import java.util.UUID
/**
* Packet fired to tell the client that a team request expired.
*
* Handled by [com.cobblemon.mod.common.client.net.battle.TeamRequestExpiredHandler]
*
* @param senderID The unique identifier of the party that sent the request.
* @param expired Whether this cancellation is due to expiration.
*
* @author JazzMcNade
* @since April 4th, 2024
*/
class TeamRequestExpiredPacket(val teamRequestId: UUID) : NetworkPacket<TeamRequestExpiredPacket> {
class TeamRequestExpiredPacket(val senderID: UUID) : NetworkPacket<TeamRequestExpiredPacket> {
companion object {
val ID = cobblemonResource("team_request_expired")
fun decode(buffer: RegistryFriendlyByteBuf) = TeamRequestExpiredPacket(buffer.readUUID())
@@ -27,6 +33,8 @@ class TeamRequestExpiredPacket(val teamRequestId: UUID) : NetworkPacket<TeamRequ
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeUUID(teamRequestId)
buffer.writeUUID(senderID)
}
constructor(request: TeamManager.TeamRequest) : this(request.senderID)
}
@@ -9,36 +9,47 @@
package com.cobblemon.mod.common.net.messages.client.battle
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.battles.TeamManager
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readText
import com.cobblemon.mod.common.util.writeText
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.network.chat.MutableComponent
import java.util.UUID
/**
* Packet send when a player has requested to join a team. The responsibility
* of this packet currently is to send a battle challenge message that includes
* the keybind to challenge them back. In future this is likely to include information
* about the battle.
* Packet sent when a player has requested to form or join a team.
*
* Handled by [com.cobblemon.mod.common.client.net.battle.TeamRequestNotificationHandler].
*
* @param requestID The unique identifier of the request.
* @param senderID The unique identifier of the party that sent the request.
* @param expiryTime How long (in seconds) this request is active.
*
* @author JazzMcNade
* @since April 15th, 2024
*/
class TeamRequestNotificationPacket(
val teamRequestId: UUID,
val requesterId: UUID,
val requesterName: MutableComponent,
val requestID: UUID,
val senderID: UUID,
val expiryTime: Int
): NetworkPacket<TeamRequestNotificationPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeUUID(teamRequestId)
buffer.writeUUID(requesterId)
buffer.writeText(requesterName)
buffer.writeUUID(requestID)
buffer.writeUUID(senderID)
buffer.writeInt(expiryTime)
}
companion object {
val ID = cobblemonResource("team_request_notification")
fun decode(buffer: RegistryFriendlyByteBuf) = TeamRequestNotificationPacket(buffer.readUUID(), buffer.readUUID(), buffer.readText().copy())
fun decode(buffer: RegistryFriendlyByteBuf) = TeamRequestNotificationPacket(
buffer.readUUID(),
buffer.readUUID(),
buffer.readInt()
)
}
constructor(request: TeamManager.TeamRequest) : this(
requestID = request.requestID,
senderID = request.senderID,
expiryTime = request.expiryTime
)
}
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.net.messages.client.effect
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.client.net.effect.SpawnSnowstormEntityParticleHandler
import com.cobblemon.mod.common.util.*
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
@@ -16,6 +17,8 @@ import net.minecraft.resources.ResourceLocation
/**
* Packet that spawns a snowstorm particle effect on a specified entity and specified locator.
*
* Handled by [SpawnSnowstormEntityParticleHandler]
*
* @author Hiroku
* @since January 21st, 2024
*/
Loaded 100 of 1303 files, more files were not shown because too many files have changed in this diff. Show more