Compare commits

...
162 Commits
Author SHA1 Message Date
Plastered_Crab a1dcede488 Fix bug with spawn files 2024-11-14 12:43:10 -08:00
Plastered_Crab aaec9ea397 Merge remote-tracking branch 'origin/1.6-spawns' into 1.6-Release-Testing 2024-11-14 09:31:33 -08:00
Veraxiel 5011607415 Updated submerged and fishing spawns to work under overhangs. 2024-11-14 06:29:10 -05:00
Veraxiel 580b0f2ccf Updated spawn files. Edited ocean_monument preset. Edited is_lush, is_ocean, and is_shrubland tags. Fixed revival_herbs feature tag. Updated spawn writer. 2024-11-14 05:15:14 -05:00
Hiroku fcd3951815 Merge branch 'More-structure-adjustments' into 'main'
Making more changes to structures

See merge request cable-mc/cobblemon!1291
2024-11-14 09:02:25 +00:00
Hiroku 5caa6d5950 Merge branch 'ihatebugtypes' into 'main'
faint (kill) all bug types

Closes #1153 and #1157

See merge request cable-mc/cobblemon!1276
2024-11-14 08:57:42 +00:00
Segfault GuyandHiroku 352ac4e15d faint (kill) all bug types 2024-11-14 08:57:42 +00:00
Hiroku e6d35e709e Merge branch 'Myslippy-main-patch-53217' into 'main'
Update CHANGELOG.md

See merge request cable-mc/cobblemon!1292
2024-11-14 08:56:04 +00:00
Myslippy 83cfcdce43 Update CHANGELOG.md 2024-11-14 05:25:44 +00:00
Slippy 96ae28ea91 Making more changes to structures 2024-11-13 23:14:12 -06:00
Landon 062fd2451a Merge branch 'Cynthia-Test-NPC-Change' into 'main'
Cynthia NPC team update

See merge request cable-mc/cobblemon!1289
2024-11-14 02:27:44 +00:00
Landon 235de329ee Merge branch 'main' into 'main'
Switch tooltip rendering to check Hide Additional instead

See merge request cable-mc/cobblemon!1279
2024-11-14 02:27:03 +00:00
Landon aede012933 Merge branch 'Fishing_Bait_Changes' into 'main'
New Bait Effects + Karp jump aspect name fixes

See merge request cable-mc/cobblemon!1287
2024-11-14 02:24:19 +00:00
Landon 1613cd281d Merge branch 'Swappable_Rod_Bobbers' into 'main'
Pokerods can Swap bobbers now

See merge request cable-mc/cobblemon!1288
2024-11-14 02:22:20 +00:00
Landon 706fcc8ca4 Merge branch 'init-events' into 'main'
Add Initialisation Events for Sidemod Devs

See merge request cable-mc/cobblemon!1290
2024-11-14 02:21:18 +00:00
Xander Forrest 5568bb6a95 add two new initialisation events for sidemod devs to hook into
- pokemon properties initialised event
so people can register their CustomPokemonProperty(s) at the right time so they can take effect in things like NPCs - previously not an issue because PokemonProperties were only really parsed post server start

- cobblemon initialised event
generic event because we got rid of the Cobblemon server started event. nice for sidemods to hook into so they know when they can init stuff that depends on cobblemon stuff being initialised (for instance: grab species/load mons from config files)
2024-11-14 00:17:48 +00:00
Plastered_Crab d5f22e7394 Test Cynthia NPC has lvl 100 mons now with updated Spiritomb 2024-11-13 15:37:57 -08:00
Test 0dc8456623 fix symbols 2024-11-13 23:30:39 +00:00
Plastered_Crab f5228db99f - Fix Tooltips for bait
- Add lang-able Egg Group tooltips
- Updated Egg Group bait logic
2024-11-13 15:19:33 -08:00
Plastered_Crab de8ebc9fcb - Fix Tooltips for bait
- Add lang-able Egg Group tooltips
- Updated Egg Group bait logic
2024-11-13 11:06:54 -08:00
Apion 093e0f2811 Merge branch 'more-dfu-sad' into 'main'
Fixes for JSON Pokemon store

See merge request cable-mc/cobblemon!1285
2024-11-13 16:07:10 +00:00
Apion 1a782e4185 Fixes for JSON Pokemon store 2024-11-13 16:07:10 +00:00
Apion 2e054dd5e1 Merge branch 'lang-updates' into 'main'
Weblate translations

See merge request cable-mc/cobblemon!1286
2024-11-13 16:07:02 +00:00
Apion 5e47102c10 Weblate translations 2024-11-13 16:07:02 +00:00
Apion e56a7d5325 Merge branch 'move-effects' into 'main'
Added particles for some moves.

See merge request cable-mc/cobblemon!1283
2024-11-13 16:00:39 +00:00
TyzillionandApion 9b8261d23b Added particles for some moves. 2024-11-13 16:00:39 +00:00
Apion e1705056f2 Merge branch 'shiny-shimmering-splendid' into 'main'
🍋‍🟩 API Feature: Shiny chance event and player context

See merge request cable-mc/cobblemon!1274
2024-11-13 15:48:16 +00:00
BriarandApion 76db87d9a5 🍋‍🟩 API Feature: Shiny chance event and player context 2024-11-13 15:48:15 +00:00
plastered_crab 780d9ece59 - Add bait for luring ElementalType and Egg Group
- Fix names for Jump Karps
- Alter langs and tooltips for new bait
2024-11-13 01:14:14 -08:00
Stampede 4b55783785 Switch tooltip rendering to check Hide Additional instead 2024-11-11 12:15:16 -06:00
Paul e3fca67d52 Fixed NPCs and other stuff not rendering 2024-11-11 11:09:16 +10:00
Hiroku 932cdaaad3 Merge branch 'updated-tags' into 'main'
More Tag Changes, but this time with Edits!

See merge request cable-mc/cobblemon!1273
2024-11-10 23:41:15 +00:00
Paul e3f9937e78 Merge remote-tracking branch 'origin/main' 2024-11-11 09:40:49 +10:00
Paul 5b46d04c27 Reduce impact of repeatedly searching for a pokemon in a PC 2024-11-11 09:40:39 +10:00
dhi_holo c23aeaba78 Merge branch 'main' into updated-tags 2024-11-11 09:16:01 +10:00
Hiroku ecff621084 Merge branch 'DexScannerXrayVisionBegone' into 'main'
DexScannerXrayVisionBegone

See merge request cable-mc/cobblemon!1275
2024-11-10 22:08:53 +00:00
JazzMcNade 83ce63325d Dex scanner now checks for block collision. 2024-11-10 13:34:30 -06:00
dhi_holo 4f33612bbb Merge branch 'main' into updated-tags 2024-11-10 20:49:23 +10:00
dhi_holo 4624faf63e More Tag Changes, but this time with Edits!
Noticed a couple structures weren't in their tags yet, very fast fixing (don't you love it when you get a branch merged to immediately notice something else that could've been in the MR? I sure do)
2024-11-10 20:46:31 +10:00
Hiroku 187674afb6 Merge branch 'implement_pokemon' into 'main'
Latest implement fixes/changes/additions

Closes #1137

See merge request cable-mc/cobblemon!1271
2024-11-10 10:39:29 +00:00
CarmendarandHiroku fe5cf5a7d8 Latest implement fixes/changes/additions 2024-11-10 10:39:28 +00:00
Hiroku 3b3219ba69 Merge branch 'one-tag' into 'main'
Create fishing_boat.json

See merge request cable-mc/cobblemon!1272
2024-11-10 10:38:44 +00:00
dhi_holo 0e98b07b7b Create fishing_boat.json
I spent too damn long messing up the structure ids for this
2024-11-10 20:19:33 +10:00
Apion 57d380e18f Merge branch 'HiddenAbilityEvolutionFix' into 'main'
Hidden Ability Evolution Fix

Closes #1149

See merge request cable-mc/cobblemon!1270
2024-11-10 04:21:33 +00:00
JazzMcNadeandApion 086cdcdbd1 Hidden Ability Evolution Fix 2024-11-10 04:21:32 +00:00
Apion 2b796f7e4c Merge branch 'npc-fixo' into 'main'
Allow simple NPC parties to be static, added autoHealParty property to NPC...

See merge request cable-mc/cobblemon!1266
2024-11-10 03:58:09 +00:00
HirokuandApion b7d1f9bb2d Allow simple NPC parties to be static, added autoHealParty property to NPC... 2024-11-10 03:58:09 +00:00
Hiroku ba0f0657b2 Merge branch 'particle-hotfix' into 'main'
Fix default behavior of EntityParticlesActionEffectKeyframe to prioritize the...

See merge request cable-mc/cobblemon!1263
2024-11-10 02:48:22 +00:00
apion 8f59ec871f Fixed compile issue 2024-11-09 21:21:40 -05:00
apion b3d4306165 Remove backward compat with old locator field in EntityParticlesActionEffectKeyframe 2024-11-09 21:17:46 -05:00
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
PolymetaandHiroku 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
PolymetaandHiroku 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
TestandHiroku 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
SterrezoandHiroku 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
CarmendarandHiroku 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
CarmendarandHiroku 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
CarmendarandHiroku 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
CarmendarandHiroku 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
2016 changed files with 41651 additions and 18503 deletions
-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.
+40 -32
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.
@@ -34,8 +36,19 @@
- 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 `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: Seismic Toss, Withdraw, Bite, Crunch, Super Fang, Hyper Fang, Pursuit, Mist, Haze, Lick, Kinesis, Psychic, Water Sport, and Mud Sport.
- 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.
- Added `fishing boat` structures that contain a map to a shipwreck cove and a pokerod smithing template.
- Added `submerged_shipwreck_cove` and `lush_shipwreck_cove` structures.
### Pokémon Added
#### Gen 1
@@ -54,10 +67,6 @@
- Octillery
- Mantine
- Smeargle
- Remoraid
- Octillery
- Mantine
- Corsola
#### Gen 3
- Slakoth
@@ -75,18 +84,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 +119,6 @@
- Hatenna
- Hattrem
- Hatterene
- Bruxish
- Mareanie
- Toxapex
#### Gen 8
- Cramorant
- Pincurchin
#### Gen 9
@@ -144,6 +135,7 @@
- Wiglett
- Wugtrio
- Flamigo
- Dondozo
#### Unique Forms
- Magikarp Jump Variants
@@ -189,7 +181,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 +216,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 +241,7 @@
- Dreepy, Drakloak
- Kleavor
- Scorbunny
- Gimmighoul [Roaming]
- Gimmighoul \[Roaming\]
### Model updates for the following Pokémon
- Bulbasaur, Ivysaur, Venusaur
@@ -332,11 +324,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 +337,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 +393,15 @@
- 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.
- Fixed illusion not copying aspects or caught ball.
### Developer
- `SpawnCause` is now an implementation of `SpawningInfluence`.
@@ -435,6 +432,12 @@
- 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
- Shiny Chance Calculation Event (with player context)
- 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 +454,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)
@@ -285,6 +286,8 @@ object Cobblemon {
CustomPokemonProperty.register(FreezeFrameProperty)
CustomPokemonProperty.register(NoAIProperty)
CobblemonEvents.POKEMON_PROPERTY_INITIALISED.emit()
ifDedicatedServer {
isDedicatedServer = true
}
@@ -385,6 +388,9 @@ object Cobblemon {
}
registerEventHandlers()
CobblemonEvents.COBBLEMON_INITIALISED.emit()
}
fun registerEventHandlers() {
@@ -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
}
}
@@ -26,7 +26,7 @@ import net.minecraft.nbt.NbtOps
* @author Qu
* @since January 9th, 2022
*/
open class Ability internal constructor(var template: AbilityTemplate, forced: Boolean) {
open class Ability internal constructor(var template: AbilityTemplate, forced: Boolean, priority: Priority) {
val name: String
get() = template.name
@@ -57,7 +57,7 @@ open class Ability internal constructor(var template: AbilityTemplate, forced: B
*
* @see [Pokemon.updateAbility].
*/
var priority = Priority.LOWEST
var priority = priority
internal set
@Deprecated("Please use the Codec instead", ReplaceWith("Ability.CODEC"))
@@ -111,7 +111,7 @@ open class Ability internal constructor(var template: AbilityTemplate, forced: B
Codec.BOOL.optionalFieldOf(DataKeys.POKEMON_ABILITY_FORCED, false).forGetter(Ability::forced),
Codec.INT.optionalFieldOf(DataKeys.POKEMON_ABILITY_INDEX, -1).forGetter(Ability::index),
Priority.CODEC.optionalFieldOf(DataKeys.POKEMON_ABILITY_PRIORITY, Priority.LOWEST).forGetter(Ability::priority)
).apply(it) { template, forced, index, priority -> Ability(template, forced).apply {
).apply(it) { template, forced, index, priority -> Ability(template, forced, priority).apply {
this.index = index
this.priority = priority
} }
@@ -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
}
@@ -8,6 +8,7 @@
package com.cobblemon.mod.common.api.abilities
import com.cobblemon.mod.common.api.Priority
import com.cobblemon.mod.common.util.codec.CodecUtils
import com.google.gson.JsonObject
import com.mojang.serialization.Codec
@@ -21,14 +22,14 @@ import net.minecraft.nbt.CompoundTag
*/
class AbilityTemplate(
val name: String = "",
var builder: (AbilityTemplate, forced: Boolean) -> Ability = { template, forced -> Ability(template, forced) },
var builder: (AbilityTemplate, forced: Boolean, priority: Priority) -> Ability = { template, forced, priority -> Ability(template, forced, priority) },
val displayName: String = "cobblemon.ability.$name",
val description: String = "cobblemon.ability.$name.desc"
) {
/**
* Returns the Ability or if applicable the extension connected to this template
*/
fun create(forced: Boolean = false) = builder(this, forced)
fun create(forced: Boolean = false, priority: Priority = Priority.LOWEST) = builder(this, forced, priority)
/**
* Returns the Ability and loads the given NBT Tag into it.
@@ -0,0 +1,19 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.battles.interpreter
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
/**
* An exception thrown when properties from a [BattleMessage] cannot be interpreted for an [InterpreterInstruction].
*
* @author Segfault Guy
* @since November 10th, 2024
*/
class InvalidInstructionException(battleMessage: BattleMessage) : Exception("Failed to interpret ${battleMessage.rawMessage}")
@@ -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
}
}
@@ -8,11 +8,13 @@
package com.cobblemon.mod.common.api.entity.pokemon
import com.cobblemon.mod.common.api.pokeball.PokeBalls
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.effects.IllusionEffect
import com.cobblemon.mod.common.entity.pokemon.effects.TransformEffect
import com.cobblemon.mod.common.pokeball.PokeBall
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.util.DataKeys
@@ -95,4 +97,7 @@ interface MocKEffect : PhysicalEffect {
get() = this.mock.form?.let {
formID -> this.exposedSpecies?.forms?.firstOrNull { it.formOnlyShowdownId().equals(formID, true) } }
?: this.exposedSpecies?.standardForm
val exposedBall: PokeBall?
get() = this.mock.pokeball?.let { PokeBalls.getPokeBall(it.asIdentifierDefaultingNamespace()) }
}
@@ -54,6 +54,10 @@ import net.minecraft.server.level.ServerPlayer
@Suppress("unused")
object CobblemonEvents {
@JvmField
val POKEMON_PROPERTY_INITIALISED = SimpleObservable<Unit>()
@JvmField
val COBBLEMON_INITIALISED = SimpleObservable<Unit>()
@JvmField
val DATA_SYNCHRONIZED = SimpleObservable<ServerPlayer>()
@JvmField
@@ -129,6 +133,8 @@ object CobblemonEvents {
val POKEMON_ENTITY_SAVE_TO_WORLD = CancelableObservable<PokemonEntitySaveToWorldEvent>()
@JvmField
val ENTITY_SPAWN = CancelableObservable<SpawnEvent<*>>()
@JvmField
val SHINY_CHANCE_CALCULATION = EventObservable<ShinyChanceCalculationEvent>()
@JvmField
val POKEMON_ENTITY_SPAWN = ENTITY_SPAWN
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.pokemon
import com.cobblemon.mod.common.pokemon.Pokemon
import net.minecraft.server.level.ServerPlayer
/**
* An event to modify the shiny chance of a Pokémon.
*/
class ShinyChanceCalculationEvent(
val baseChance: Float,
val pokemon: Pokemon
) {
val chance: Float = baseChance
var isShiny: Boolean = false
private val modifiers = mutableListOf<Float>()
private val modificationFunctions = mutableListOf<(Float, ServerPlayer?, Pokemon) -> Float>()
/**
* Adds a modifier to the shiny chance.
*/
fun addModifier(modifier: Float) {
modifiers.add(modifier)
}
/**
* Adds a function to modify the shiny chance.
*/
fun addModificationFunction(function: (Float, ServerPlayer?, Pokemon) -> Float) {
modificationFunctions.add(function)
}
/**
* Calculates the shiny chance of a Pokémon.
*/
fun calculate(player: ServerPlayer?): Float {
var result = baseChance
for (modifier in modifiers) {
result += modifier
}
for (function in modificationFunctions) {
result = function(result, player, pokemon)
}
return result
}
fun isShiny(player: ServerPlayer?): Boolean {
return calculate(player) >= chance || isShiny
}
}
@@ -16,6 +16,7 @@ import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.core.Registry
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.ListTag
import net.minecraft.nbt.StringTag
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.entity.Entity
import net.minecraft.world.item.Item
@@ -87,7 +88,8 @@ data class FishingBait(
val BITE_TIME = cobblemonResource("bite_time")
val GENDER_CHANCE = cobblemonResource("gender_chance")
val LEVEL_RAISE = cobblemonResource("level_raise")
val TERA = cobblemonResource("tera")
val TYPING = cobblemonResource("typing")
val EGG_GROUP = cobblemonResource("egg_group")
val SHINY_REROLL = cobblemonResource("shiny_reroll")
val HIDDEN_ABILITY_CHANCE = cobblemonResource("ha_chance")
val POKEMON_CHANCE = cobblemonResource("pokemon_chance")
@@ -108,7 +110,6 @@ data class FishingBait(
EFFECT_FUNCTIONS[SHINY_REROLL] = { entity, effect -> FishingSpawnCause.shinyReroll(entity, effect) }
EFFECT_FUNCTIONS[GENDER_CHANCE] = { entity, effect -> FishingSpawnCause.alterGenderAttempt(entity, effect) }
EFFECT_FUNCTIONS[LEVEL_RAISE] = { entity, effect -> FishingSpawnCause.alterLevelAttempt(entity, effect) }
EFFECT_FUNCTIONS[TERA] = { entity, effect -> FishingSpawnCause.alterTeraAttempt(entity, effect) }
EFFECT_FUNCTIONS[HIDDEN_ABILITY_CHANCE] = { entity, _ -> FishingSpawnCause.alterHAAttempt(entity) }
EFFECT_FUNCTIONS[FRIENDSHIP] = { entity, effect -> FishingSpawnCause.alterFriendshipAttempt(entity, effect) }
CobblemonEvents.BAIT_EFFECT_REGISTRATION.post(BaitEffectFunctionRegistryEvent()) { event ->
@@ -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()) }
@@ -11,12 +11,14 @@ package com.cobblemon.mod.common.api.moves.animations.keyframes
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.EntityProvider
import com.cobblemon.mod.common.api.moves.animations.TargetsProvider
import com.cobblemon.mod.common.api.moves.animations.UsersProvider
import com.cobblemon.mod.common.api.scheduling.delayedFuture
import com.cobblemon.mod.common.entity.PosableEntity
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormEntityParticlePacket
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import net.minecraft.commands.arguments.EntityArgument.getPlayers
import java.util.concurrent.CompletableFuture
import net.minecraft.server.level.ServerLevel
@@ -29,7 +31,8 @@ import net.minecraft.server.level.ServerLevel
class EntityParticlesActionEffectKeyframe : ConditionalActionEffectKeyframe(), EntityConditionalActionEffectKeyframe {
override val entityCondition = "q.entity.is_user".asExpressionLike()
var effect: String? = null
var locator: String = "root"
var locators: List<String> = listOf("target")
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).toList())
val players = (entity.level() as ServerLevel).getPlayers { it.distanceTo(entity) <= visibilityRange }
packet.sendToPlayers(players)
}
@@ -47,6 +47,7 @@ class NPCClass {
var variables = mutableMapOf<String, MoValue>() // Questionable whether this should be here.
var party: NPCPartyProvider? = null
var skill: Int = 0
var autoHealParty: Boolean = false
var battleTheme: ResourceLocation? = null
var ai: MutableList<BrainConfig> = mutableListOf()
@@ -77,6 +78,7 @@ class NPCClass {
buffer.writeString(v.defaultValue)
}
buffer.writeInt(skill)
buffer.writeBoolean(autoHealParty)
buffer.writeMapK(size = IntSize.U_BYTE, map = variables) { (key, value) ->
buffer.writeString(key)
buffer.writeString(value.asString())
@@ -116,6 +118,7 @@ class NPCClass {
NPCConfigVariable(variableName, displayName, description, type, defaultValue)
}.toMutableList()
skill = buffer.readInt()
autoHealParty = buffer.readBoolean()
buffer.readMapK(size = IntSize.U_BYTE, map = variables) {
val key = buffer.readString()
val value = buffer.readString()
@@ -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)
}
@@ -29,6 +29,7 @@ class NPCPreset {
var hitbox: EntityDimensions? = null
var ai: List<BrainConfig>? = null
var skill: Int? = null
var autoHealParty: Boolean? = null
var battleTheme: ResourceLocation? = null
fun applyTo(npcClass: NPCClass) {
@@ -47,6 +48,7 @@ class NPCPreset {
names?.let { npcClass.names.addAll(it) }
hitbox?.let { npcClass.hitbox = it }
skill?.let { npcClass.skill = it }
autoHealParty?.let { npcClass.autoHealParty = it }
battleTheme?.let { npcClass.battleTheme = it }
ai?.let { npcClass.ai.addAll(it) }
}
@@ -8,15 +8,15 @@
package com.cobblemon.mod.common.api.npc.configuration
import com.cobblemon.mod.common.api.npc.NPCPartyProvider
import com.cobblemon.mod.common.util.DataKeys
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.RegistryFriendlyByteBuf
class NPCBattleConfiguration {
var canChallenge = false
var party: NPCPartyProvider? = null
@Deprecated("This is not likely to get used")
var simultaneousBattles = false
@Deprecated("This is not likely to get used")
var healAfterwards = true
fun encode(buffer: RegistryFriendlyByteBuf) {
@@ -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
}
}
@@ -16,7 +16,7 @@ import com.cobblemon.mod.common.util.toProperties
import com.google.gson.JsonElement
/**
* A basic party provider that just produces a [StaticNPCParty] based on a list of [PokemonProperties].
* A basic party provider that just produces a [NPCPartyStore] based on a simple list of [PokemonProperties].
*
* @author Hiroku
* @since August 19th, 2023
@@ -28,10 +28,15 @@ class SimplePartyProvider : NPCPartyProvider {
@Transient
override val type = TYPE
override var isStatic = true
val pokemon = mutableListOf<PokemonProperties>()
override fun loadFromJSON(json: JsonElement) {
val isStatic = json.asJsonObject.get("isStatic")?.asBoolean
if (isStatic != null) {
this.isStatic = isStatic
}
val pokemonList = json.asJsonObject.getAsJsonArray("pokemon")
pokemonList.forEach {
val pokemon = if (it.isJsonPrimitive) {
@@ -47,10 +52,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
@@ -14,6 +14,8 @@ import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.abilities.Abilities
import com.cobblemon.mod.common.api.abilities.Ability
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.pokemon.ShinyChanceCalculationEvent
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.pokeball.PokeBalls
import com.cobblemon.mod.common.api.pokemon.aspect.AspectProvider
@@ -56,6 +58,8 @@ import net.minecraft.nbt.Tag
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.Level
@@ -573,18 +577,24 @@ open class PokemonProperties {
return true
}
fun create(): Pokemon {
@JvmOverloads
fun create(player: ServerPlayer? = null): Pokemon {
val pokemon = Pokemon()
apply(pokemon)
pokemon.initialize()
roll(pokemon)
roll(pokemon, player)
return pokemon
}
// TEST YOUR LUCK!
fun roll(pokemon: Pokemon) {
@JvmOverloads
fun roll(pokemon: Pokemon, player: ServerPlayer? = null) {
val baseTypes = pokemon.form.types.toList()
if (this.shiny == null) pokemon.shiny = Cobblemon.config.shinyRate.checkRate()
var shinyRate = Cobblemon.config.shinyRate
CobblemonEvents.SHINY_CHANCE_CALCULATION.post(ShinyChanceCalculationEvent(shinyRate, pokemon)) { event ->
shinyRate = event.calculate(player)
}
if (this.shiny == null) pokemon.shiny = shinyRate.checkRate()
if (this.teraType == null) pokemon.teraType =
if (Cobblemon.config.teraTypeRate.checkRate()) {
var picked = TeraTypes.random(true)
@@ -596,8 +606,9 @@ open class PokemonProperties {
else TeraTypes.forElementalType(baseTypes.random())
}
fun createEntity(world: Level): PokemonEntity {
return PokemonEntity(world, create()).also { applyCustomProperties(it) }
@JvmOverloads
fun createEntity(world: Level, player: ServerPlayer? = null): PokemonEntity {
return PokemonEntity(world, create(player)).also { applyCustomProperties(it) }
}
// TODO Codecs at some point
@@ -783,7 +794,7 @@ open class PokemonProperties {
}
// If the config value is at least 1, then do 1/x and use that as the property chance
private fun Float.checkRate(): Boolean = this >= 1 && (Random.Default.nextFloat() < 1 / this)
private fun Float.checkRate(): Boolean = if (this >= 1) (Random.Default.nextFloat() < 1 / this) else Random.Default.nextFloat() < this
/**
* Attempts to find an ability by ID and resolve if it should be forced or if it's legal for the given form.
@@ -53,7 +53,7 @@ fun interface PokemonPropertyExtractor {
val EVS = add { pokemon, properties -> properties.evs = pokemon.evs }
@JvmField
val ILLUSION = mutableListOf(SPECIES, FORM, ASPECTS, GENDER, NICKNAME, SHINY)
val ILLUSION = mutableListOf(SPECIES, FORM, ASPECTS, GENDER, NICKNAME, SHINY, POKEBALL)
@JvmField
val TRANSFORM = mutableListOf(SPECIES, FORM, ASPECTS, GENDER)
@@ -15,7 +15,7 @@ package com.cobblemon.mod.common.api.pokemon.egg
*
* @property showdownID used for data synchronization with Showdown data format.
*/
enum class EggGroup(internal val showdownID: String) {
enum class EggGroup(val showdownID: String) {
MONSTER("Monster"),
WATER_1("Water 1"),
@@ -31,6 +31,16 @@ enum class EggGroup(internal val showdownID: String) {
WATER_2("Water 2"),
DITTO("Ditto"),
DRAGON("Dragon"),
UNDISCOVERED("Undiscovered")
UNDISCOVERED("Undiscovered");
companion object {
private val identifierMap: Map<String, EggGroup> = entries.associateBy {
it.showdownID.lowercase().replace(" ", "_").replace("-", "_")
}
fun fromIdentifier(identifier: String): EggGroup? {
val key = identifier.lowercase().replace(" ", "_").replace("-", "_")
return identifierMap[key]
}
}
}
@@ -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))
}
}
}
@@ -10,6 +10,8 @@ package com.cobblemon.mod.common.api.scheduling
import java.util.concurrent.CompletableFuture
import com.cobblemon.mod.common.util.runOnServer
import net.minecraft.world.TickRateManager
import net.minecraft.world.level.Level
@Deprecated("Use afterOnServer or afterOnClient; ambiguous side is not good for your health")
@JvmOverloads
@@ -33,9 +35,13 @@ 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(seconds: Float, action: () -> Unit) = ServerTaskTracker.after(seconds, action)
@JvmOverloads
fun afterOnClient(ticks: Int = 0, seconds: Float, action: () -> Unit) = ClientTaskTracker.after(seconds + ticks / 20F, action)
fun afterOnServer(ticks: Int, level: Level, action: () -> Unit) = ServerTaskTracker.after(ticks / level.tickRateManager().tickrate(), action)
@JvmOverloads
fun afterOnClient(seconds: Float, action: () -> Unit) = ClientTaskTracker.after(seconds, action)
@JvmOverloads
fun afterOnClient(ticks: Int, level: Level, action: () -> Unit) = ClientTaskTracker.after(ticks / level.tickRateManager().tickrate(), action)
@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)
@@ -15,6 +15,8 @@ import com.cobblemon.mod.common.api.spawning.context.SpawningContext
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.feature.SeasonFeatureHandler
import com.cobblemon.mod.common.util.weightedSelection
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.player.Player
/**
* A [SpawnAction] that will spawn a single [PokemonEntity].
@@ -44,7 +46,8 @@ class PokemonSpawnAction(
} else {
null
}?.createStack(ctx)
val entity = props.createEntity(ctx.world)
val sourcePlayer = ctx.cause.entity as? ServerPlayer
val entity = props.createEntity(ctx.world, sourcePlayer)
entity.spawnCause = ctx.cause
SeasonFeatureHandler.updateSeason(entity.pokemon, Cobblemon.seasonResolver(ctx.world, ctx.position))
if (heldItem != null) {
@@ -17,6 +17,8 @@ import com.cobblemon.mod.common.api.events.fishing.BobberSpawnPokemonEvent
import com.cobblemon.mod.common.api.fishing.FishingBait
import com.cobblemon.mod.common.api.pokemon.Natures
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.pokemon.egg.EggGroup
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import com.cobblemon.mod.common.api.spawning.SpawnCause
@@ -24,6 +26,7 @@ import com.cobblemon.mod.common.api.spawning.context.SpawningContext
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.api.types.tera.TeraTypes
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.item.interactive.PokerodItem
@@ -101,13 +104,6 @@ class FishingSpawnCause(
pokemonEntity.pokemon.level = level
}
fun alterTeraAttempt(pokemonEntity: PokemonEntity, effect: FishingBait.Effect) {
if (pokemonEntity.pokemon.teraType == effect.subcategory?.let { TeraTypes.get(it.path) } ||
TeraTypes.get(effect.subcategory!!.path) == null) return
pokemonEntity.pokemon.teraType = TeraTypes.get(effect.subcategory.path)!!
}
fun alterHAAttempt(pokemonEntity: PokemonEntity) {
//val species = pokemonEntity.pokemon.species.let { PokemonSpecies.getByName(it.name) } ?: return
//val ability = species.abilities.mapping[Priority.LOW]?.first()?.template?.name ?: return
@@ -122,7 +118,7 @@ class FishingSpawnCause(
.randomOrNull ()
?.let { ability ->
// No need to force, this is legal
pokemonEntity.pokemon.ability = ability.template.create(false)
pokemonEntity.pokemon.ability = ability.template.create(false, ability.priority)
return
}
}
@@ -179,6 +175,49 @@ class FishingSpawnCause(
}
}
}
// if bait exists and any effects are related to Typing
if (bait != null && bait.effects.any{ it.type == FishingBait.Effects.TYPING }){
if (detail is PokemonSpawnDetail) {
val detailSpecies = detail.pokemon.species?.let { PokemonSpecies.getByName(it) }
val baitEffect = bait.effects.firstOrNull { it.type == FishingBait.Effects.TYPING }
val baitTypingEffect = baitEffect?.subcategory?.path?.let { ElementalTypes.get(it) }
if (detailSpecies != null && baitTypingEffect != null) {
val isMatchingType = detailSpecies.types.contains(baitTypingEffect)
return when {
isMatchingType -> super.affectWeight(detail, ctx, weight * baitEffect.value.toFloat()) // multiply weight by multiplier of bait effect if typing is found
else -> super.affectWeight(detail, ctx, weight) // use base spawn weight if typing is not same as bait
}
}
}
}
// if bait exists and any effects are related to Egg Groups
if (bait != null && bait.effects.any { it.type == FishingBait.Effects.EGG_GROUP }) {
if (detail is PokemonSpawnDetail) {
val detailSpecies = detail.pokemon.species?.let { PokemonSpecies.getByName(it) }
if (detailSpecies != null) {
// Collect all the egg group effects
val eggGroupEffects = bait.effects.filter { it.type == FishingBait.Effects.EGG_GROUP }
// Check if any of the egg group effects match the species' egg groups
val matchingEffect = eggGroupEffects.firstOrNull { effect ->
val effectEggGroupKey = effect.subcategory?.path ?: return@firstOrNull false
val eggGroup = EggGroup.fromIdentifier(effectEggGroupKey)
if (eggGroup == null) {
LOGGER.warn("Unknown egg group identifier: $effectEggGroupKey")
return@firstOrNull false
}
detailSpecies.eggGroups.contains(eggGroup)
}
if (matchingEffect != null) {
val multiplier = matchingEffect.value
return super.affectWeight(detail, ctx, (weight * multiplier).toFloat())
}
}
}
}
return super.affectWeight(detail, ctx, weight)
}
@@ -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)
}
}
}
@@ -134,12 +134,12 @@ class Toast(
}
/**
* Invokes [expire] after the specified [ticks].
* Invokes [expire] after the specified [seconds].
*
* @param ticks The number of ticks before the lifecycle of this toast ends.
* @param seconds The number of seconds before the lifecycle of this toast ends.
*/
fun expireAfter(ticks: Int) {
afterOnServer(ticks = ticks) {
fun expireAfter(seconds: Float) {
afterOnServer(seconds = seconds) {
this.expire()
}
}
@@ -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) {
@@ -332,12 +334,12 @@ object BattleBuilder {
return errors
}
val npcActor = NPCBattleActor(npcEntity, npcParty, npcEntity.skill ?: 0)
val npcActor = NPCBattleActor(npcEntity, npcParty, npcEntity.skill ?: npcEntity.npc.skill)
// if (npcEntity.battleIds.get().isPresent) {
// errors.participantErrors[npcActor] += BattleStartError.alreadyInBattle(npcActor)
// }
if (npcActor.pokemonList.size < battleFormat.battleType.slotsPerActor) {
if (npcActor.pokemonList.filter { it.health > 0 }.size < battleFormat.battleType.slotsPerActor) {
errors.participantErrors[npcActor] += BattleStartError.insufficientPokemon(
actorEntity = npcEntity,
requiredCount = 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())
}
@@ -9,14 +9,11 @@
package com.cobblemon.mod.common.battles
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.api.battles.interpreter.BasicContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.interpreter.Effect
import com.cobblemon.mod.common.api.battles.interpreter.MissingContext
import com.cobblemon.mod.common.api.battles.interpreter.*
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.api.battles.model.actor.EntityBackedBattleActor
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.battles.dispatch.InstructionSet
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
@@ -63,12 +60,12 @@ object ShowdownInterpreter {
updateInstructionParser["-ability"] = { _, instructionSet, message, _ -> AbilityInstruction(instructionSet, message) }
updateInstructionParser["-activate"] = { _, instructionSet, message, _ -> ActivateInstruction(instructionSet, message) }
updateInstructionParser["bagitem"] = { _, _, message, _ -> BagItemInstruction(message) }
updateInstructionParser["-boost"] = { _, instructionSet, message, remainingLines -> BoostInstruction(instructionSet, message, remainingLines, true) }
updateInstructionParser["-boost"] = { battle, _, message, _ -> BoostInstruction(battle, message, true) }
updateInstructionParser["-block"] = { _, _, message, _ -> BlockInstruction(message) }
updateInstructionParser["cant"] = { _, _, message, _ -> CantInstruction(message) }
updateInstructionParser["-clearallboost"] = { _, _, message, _ -> ClearAllBoostInstruction(message) }
updateInstructionParser["-clearnegativeboost"] = { _, _, message, _ -> ClearNegativeBoostInstruction(message) }
updateInstructionParser["-clearboost"] = { _, _, message, _ -> ClearBoostInstruction(message) }
updateInstructionParser["-clearboost"] = { _, _, message, _ -> ClearBoostInstruction(message) }
updateInstructionParser["-copyboost"] = { _, _, message, _ -> CopyBoostInstruction(message) }
updateInstructionParser["-crit"] = { _, instructionSet, message, _ -> CritInstruction(message, instructionSet) }
updateInstructionParser["-curestatus"] = { _, _, message, _ -> CureStatusInstruction(message) }
@@ -108,7 +105,7 @@ object ShowdownInterpreter {
updateInstructionParser["-terastallize"] = { _, _, message, _ -> TerastallizeInstruction(message) }
updateInstructionParser["-transform"] = { battle, _, message, _ -> TransformInstruction(battle, message) }
updateInstructionParser["turn"] = { _, _, message, _ -> TurnInstruction(message) }
updateInstructionParser["-unboost"] = { _, instructionSet, message, remainingLines -> BoostInstruction(instructionSet, message, remainingLines, false) }
updateInstructionParser["-unboost"] = { battle, _, message, _ -> BoostInstruction(battle, message, false) }
updateInstructionParser["upkeep"] = { _, _, _, _ -> UpkeepInstruction() }
updateInstructionParser["-weather"] = { _, _, message, _ -> WeatherInstruction(message) }
updateInstructionParser["win"] = { _, _, message, _ -> WinInstruction(message) }
@@ -279,8 +276,16 @@ object ShowdownInterpreter {
}
instructionSet.execute(battle)
}
catch (e: InvalidInstructionException) {
e.message?.let {
battle.broadcastChatMessage(it.red())
LOGGER.error(it)
}
}
catch (e: Exception) {
LOGGER.error("Caught exception interpreting {}", e)
battle.broadcastChatMessage("A fatal error occurred. Please report to developers.".red())
LOGGER.error("Caught exception interpreting battle instructions.", e)
}
}
@@ -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)
}
}
@@ -11,6 +11,7 @@ package com.cobblemon.mod.common.battles.interpreter.instructions
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.interpreter.InvalidInstructionException
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
@@ -19,7 +20,6 @@ import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.ActionEffectInstruction
import com.cobblemon.mod.common.battles.dispatch.GO
import com.cobblemon.mod.common.battles.dispatch.InstructionSet
import com.cobblemon.mod.common.battles.dispatch.UntilDispatch
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.cobblemonResource
@@ -33,19 +33,25 @@ import java.util.concurrent.CompletableFuture
* @author Hiroku
* @since August 20th, 2022
*/
class BoostInstruction(val instructionSet: InstructionSet, val message: BattleMessage, val remainingLines: Iterator<BattleMessage>, val isBoost: Boolean = true): ActionEffectInstruction {
class BoostInstruction(battle: PokemonBattle, val message: BattleMessage, val isBoost: Boolean = true): ActionEffectInstruction {
override var future: CompletableFuture<*> = CompletableFuture.completedFuture(Unit)
override var holds = mutableSetOf<String>()
override val id = cobblemonResource("boost")
val pokemon = message.battlePokemon(0, battle) ?: throw InvalidInstructionException(message)
val statKey = message.argumentAt(1) ?: throw InvalidInstructionException(message)
val stages = message.argumentAt(2)?.toInt() ?: throw InvalidInstructionException(message)
val stat = Stats.getStat(statKey).displayName
override fun preActionEffect(battle: PokemonBattle) {
}
override fun runActionEffect(battle: PokemonBattle, runtime: MoLangRuntime) {
if (stages == 0) return // only play effect if there was a stat change
battle.dispatch {
val actionEffect = if (isBoost) BOOST_EFFECT else UNBOOST_EFFECT
val providers = mutableListOf<Any>(battle)
val pokemon = message.battlePokemon(0, battle) ?: return@dispatch GO
pokemon.effectedPokemon.entity?.let { UsersProvider(it) }?.let(providers::add)
val context = ActionEffectContext(
actionEffect = actionEffect,
@@ -60,10 +66,6 @@ class BoostInstruction(val instructionSet: InstructionSet, val message: BattleMe
}
override fun postActionEffect(battle: PokemonBattle) {
val pokemon = message.battlePokemon(0, battle) ?: return
val statKey = message.argumentAt(1) ?: return
val stages = message.argumentAt(2)?.toInt() ?: return
val stat = Stats.getStat(statKey).displayName
val severity = Stats.getSeverity(stages)
val rootKey = if (isBoost) "boost" else "unboost"
@@ -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()))
@@ -164,7 +164,7 @@ class GildedChestBlock(settings: Properties, val type: Type = Type.RED) : BaseEn
world.addFreshEntity(entity)
world.removeBlock(pos, false)
afterOnServer(ticks = 2) {
afterOnServer(seconds = 0.1F) {
if (player !in player.level().players()) {
return@afterOnServer
}
@@ -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?
@@ -126,7 +126,7 @@ class FossilMultiblockStructure (
stack?.shrink(1)
}
val pokemon = this.resultingFossil?.result?.create()
val pokemon = this.resultingFossil?.result?.create(player)
if(pokemon != null) {
pokemon.caughtBall = ballType
@@ -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")
}
@@ -15,6 +15,7 @@ import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.entity.PokemonSideDelegate
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunctions
import com.cobblemon.mod.common.api.pokeball.PokeBalls
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.scheduling.ScheduledTask
import com.cobblemon.mod.common.api.scheduling.SchedulingTracker
@@ -31,10 +32,7 @@ import com.cobblemon.mod.common.client.render.models.blockbench.repository.Pokem
import com.cobblemon.mod.common.client.render.pokemon.PokemonRenderer.Companion.ease
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.MovingSoundInstance
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.resolve
import com.cobblemon.mod.common.util.*
import com.mojang.blaze3d.vertex.PoseStack
import net.minecraft.client.Minecraft
import net.minecraft.network.syncher.EntityDataAccessor
@@ -106,6 +104,10 @@ class PokemonClientDelegate : PosableState(), PokemonSideDelegate {
} else if (data == PokemonEntity.ASPECTS) {
currentAspects = currentEntity.entityData.get(PokemonEntity.ASPECTS)
currentEntity.pokemon.shiny = currentAspects.contains("shiny")
} else if (data == PokemonEntity.CAUGHT_BALL) {
PokeBalls.getPokeBall(currentEntity.entityData.get(PokemonEntity.CAUGHT_BALL).asIdentifierDefaultingNamespace())?.let {
currentEntity.pokemon.caughtBall = it
}
} else if (data == PokemonEntity.DYING_EFFECTS_STARTED) {
val isDying = currentEntity.entityData.get(PokemonEntity.DYING_EFFECTS_STARTED)
if (isDying) {
@@ -230,13 +232,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(),
@@ -1,87 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen4
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BipedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
import com.cobblemon.mod.common.client.render.models.blockbench.pose.CobblemonPose
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.PoseType.Companion.MOVING_POSES
import com.cobblemon.mod.common.entity.PoseType.Companion.STATIONARY_POSES
import com.cobblemon.mod.common.entity.PoseType.Companion.UI_POSES
import com.cobblemon.mod.common.util.isBattling
import net.minecraft.client.model.geom.ModelPart
import net.minecraft.world.phys.Vec3
class HappinyModel(root: ModelPart) : PokemonPosableModel(root), BipedFrame {
override val rootPart = root.registerChildWithAllChildren("happiny")
override val leftLeg = getPart("left_foot")
override val rightLeg = getPart("right_foot")
override var portraitScale = 2.09F
override var portraitTranslation = Vec3(-0.1, -0.8, 0.0)
override var profileScale = 0.76F
override var profileTranslation = Vec3(0.0, 0.6, 0.0)
lateinit var standing: CobblemonPose
lateinit var walk: CobblemonPose
lateinit var sleep: CobblemonPose
lateinit var battle_idle: CobblemonPose
override val cryAnimation = CryProvider { bedrockStateful("happiny", "cry") }
override fun registerPoses() {
val blink = quirk { bedrockStateful("happiny", "blink") }
val hairQuirk = quirk { bedrockStateful("happiny", "hairshake_quirk") }
val happy = quirk { bedrockStateful("happiny", "happy_quirk") }
sleep = registerPose(
poseName = "sleep",
poseType = PoseType.SLEEP,
animations = arrayOf(bedrock("happiny", "sleep"))
)
standing = registerPose(
poseName = "standing",
poseTypes = STATIONARY_POSES + UI_POSES,
condition = { !it.isBattling },
quirks = arrayOf(blink, hairQuirk, happy),
transformTicks = 10,
animations = arrayOf(
bedrock("happiny", "ground_idle")
)
)
walk = registerPose(
poseName = "walk",
poseTypes = MOVING_POSES,
quirks = arrayOf(blink),
transformTicks = 10,
animations = arrayOf(
bedrock("happiny", "ground_walk")
)
)
battle_idle = registerPose(
poseName = "battle_idle",
poseTypes = PoseType.STATIONARY_POSES,
quirks = arrayOf(blink, hairQuirk, happy),
transformTicks = 10,
condition = { it.isBattling },
animations = arrayOf(
bedrock("happiny", "battle_idle")
)
)
}
override fun getFaintAnimation(state: PosableState) = if (state.isPosedIn(standing, walk, sleep, battle_idle)) bedrockStateful("happiny", "faint") else null
}
@@ -169,7 +169,6 @@ object PokemonModelRepository : VaryingModelRepository<PokemonPosableModel>() {
inbuilt("hitmontop", ::HitmontopModel)
inbuilt("electivire", ::ElectivireModel)
inbuilt("glaceon", ::GlaceonModel)
inbuilt("happiny", ::HappinyModel)
inbuilt("leafeon", ::LeafeonModel)
inbuilt("lickilicky", ::LickilickyModel)
inbuilt("mimejr", ::MimejrModel)
@@ -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) }
@@ -106,7 +136,7 @@ abstract class VaryingModelRepository<T : PosableModel> {
val jsonObject = gson.fromJson(json, JsonObject::class.java)
return {
var boneName = jsonObject.getAsJsonPrimitive("rootBone")
adapter.modelPart = if (boneName != null && it.children[boneName.asString] != null) it.children[boneName.asString] else it.children[fileName] ?: it.children.values.first()
adapter.modelPart = if (boneName != null && it.children[boneName.asString] != null) it.children[boneName.asString] else it.children[fileName] ?: it.children.entries.filter { LocatorAccess.PREFIX !in it.key }.first().value
gson.fromJson(jsonObject, poserClass).also {
it.poses.forEach { (poseName, pose) -> pose.poseName = poseName }
}
@@ -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)
}
}
}

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