Compare commits

..
Author SHA1 Message Date
jackowes ea99b339ea smoothening camera 2025-11-08 22:49:47 -06:00
Landon 5b23b0cfbe Merge branch 'pondering-my-orb-and-not-crashing' into 'main'
Fix a crash with Ponder and likely others in CameraMixin

See merge request cable-mc/cobblemon!1933
2025-11-05 13:52:17 +00:00
Nadja Reitzenstein 778ceff183 Fix a crash with Ponder and likely others in CameraMixin
Our setRotation mixin assumes the camera to be attached to an entity. It's not always, and it's not for Ponder Scenes specifically.
2025-11-05 13:57:36 +01:00
Paul f32050c8b5 Added setter to player spawner mixin and opened PlayerSpawner 2025-11-05 21:40:09 +10:00
Hiroku 3522bac72a Merge branch 'smeargle-paint-me-like-one-of-your-french-mooshtanks' into 'main'
Smeargle paint me like one of your french mooshtanks

See merge request cable-mc/cobblemon!1746
2025-11-05 11:24:27 +00:00
Paul ecf037666f Merge remote-tracking branch 'weblate/lang-updates' 2025-11-05 21:13:23 +10:00
Watuneni iteite 618d062ca9 Translated using Weblate (Japanese)
Currently translated at 93.5% (8334 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-11-05 11:13:04 +00:00
Paul 49841b40ec Merge remote-tracking branch 'weblate/lang-updates' 2025-11-05 21:10:49 +10:00
Paul 87141e1137 Variable naming cleanup 2025-11-05 21:09:18 +10:00
Paul 1f962d664f Removed inaccurate comment 2025-11-05 21:08:50 +10:00
Paul 09797f25b1 Added characteristic_rainbow molang function to pokemon behaviour object 2025-11-05 21:08:31 +10:00
Paul 3911413c69 Removed unused lang function 2025-11-05 21:08:09 +10:00
Paul 12f3e7b78b Fix party renders not respecting aspects 2025-11-05 19:01:24 +10:00
Landon fdb37ba5e8 Merge branch 'dolphin-sub-noPuke' into 'main'
submarine, dolphin rework, projectile support, view vector fix, and motion sick cam

See merge request cable-mc/cobblemon!1929
2025-11-05 06:56:07 +00:00
Jackson Bowes 7a99845d0f submarine, dolphin rework, projectile support, view vector fix, and motion sick cam 2025-11-05 06:56:06 +00:00
Landon 247052ee67 Merge branch 'moonrise-compat' into 'main'
Retain moonrise compatibility for Cobblemon 1.7

See merge request cable-mc/cobblemon!1926
2025-11-05 03:11:05 +00:00
Veraxiel 29f6524c49 Fix to formatting in TimeRange.kt 2025-11-04 17:50:24 -05:00
Veraxiel 1e617a684b Minor edit to a time range in TimeRange.kt 2025-11-04 17:49:42 -05:00
Paul c9a0352a7c Nerfed aprijuice stat point gain, tweak saccharine tree top leaf gen 2025-11-05 06:40:51 +10:00
Paul d157536e1e party pokemon walk again woops 2025-11-04 18:54:29 +10:00
Paul fb90ccb86e Fixed dismount not working for non-owner passengers if they have no Pokemon 2025-11-03 22:08:12 +10:00
Paul c112f9615c Merge remote-tracking branch 'origin/main' 2025-11-03 21:42:57 +10:00
Paul fc882190fe /ride no longer works on seatless pokemon, closes #1750 2025-11-03 21:39:48 +10:00
Paul 15d7cd815d Only handle mount transition camera logic for the actual client player. Probably doesn't fix anything? 2025-11-03 21:23:35 +10:00
Hiroku fefdfd877b Merge branch 'let-me-/ride' into 'main'
Fix /ride

Closes #1751

See merge request cable-mc/cobblemon!1927
2025-11-03 02:35:18 +00:00
Landon Wills 35fb170225 Fix /ride 2025-11-02 20:50:39 -05:00
Paul d2e405131f Fixed speculative cast in pokedex FOV multiplier. Closes #1756 2025-11-03 11:37:00 +10:00
Paul b6ec0205ef Fixed NPC gibber sounds playing even when text was skipped. Closes #1755 2025-11-03 11:36:20 +10:00
Paul e57a42e755 Fixed immobile pokemon walking to follow the player. Closes #1757 2025-11-03 11:25:46 +10:00
Landon Wills 5119617f00 Check for moonrise and disable conflicting mixin if loaded 2025-11-02 09:39:33 -05:00
Paul 6e84efb53c No Packed Up backpacks either. Your move, Monocle 2025-11-02 22:41:11 +10:00
Paul de3191b768 Some english improvements on new NPC Behaviours 2025-11-02 22:06:27 +10:00
Paul 76ec4b4d5a Merge remote-tracking branch 'origin/main' 2025-11-02 21:34:42 +10:00
Hiroku 25702ade61 Merge branch 'main' into 'main'
dialogue selection interaction and name tag display - done properly this time

See merge request cable-mc/cobblemon!1925
2025-11-02 11:34:19 +00:00
Paul c6da88d0b2 Improvements to GUI pokemon animation config options, pokebait effects, setup some berry gen stuff for vera 2025-11-02 21:32:18 +10:00
Jolihan b22a31909d Update file en_us.json 2025-11-02 08:09:19 +00:00
Jolihan cbbbacc215 Edit name_tag_display.json 2025-11-02 07:01:40 +00:00
Jolihan566 d22d81209d dialogue selection interaction and name tag display - done properly this time 2025-11-02 16:59:21 +10:00
Hiroku 8571d78894 Merge branch 'FixGreedyHoneyEaters' into 'main'
Fixed Greedy Honey Eaters

Closes #1725

See merge request cable-mc/cobblemon!1920
2025-11-02 05:54:22 +00:00
Paul 65c5e95ddf Fixed possible crash from bees putting honey in saccarine leaves 2025-11-02 11:56:27 +10:00
Nadja Reitzenstein 0fae95aabc Reorder smeargle's tail colours to allow handing them cosmetic dyes
Our aspect system has the limitation that you can not easily have two separate systems add mutually exclusive aspects correctly. This means that for cosmetics to work without much duplication, the characteristic rainbow has to set a different aspect with some (but much more limited) duplication in the assignment of textures.
2025-11-01 19:55:55 +01:00
Nadja Reitzenstein bbfd864b33 Clean up Rainbow Aspect provider 2025-11-01 18:43:31 +01:00
Nadja Reitzenstein b107d76762 Cosmetic Item dye affects smeargle tails now 2025-11-01 17:37:04 +01:00
Nadja Reitzenstein 8d4f10349c Use the easier to understand maxBy call 2025-11-01 17:25:23 +01:00
Nadja Reitzenstein f36787e67f Clean up Characteristic calculation
Also ensures characteristics are recalculated on IV updates.
2025-11-01 16:42:59 +01:00
Nadja Reitzenstein fc198c599e Update CHANGELOG.md 2025-11-01 16:23:12 +01:00
Nadja Reitzenstein 26f753880c Implement rotating a feature on lightning hit more sensibly 2025-11-01 16:23:12 +01:00
Nadja Reitzenstein a9ebb0b6f2 Merge thunderHit method for Pokemon entity 2025-11-01 16:23:12 +01:00
Nadja Reitzenstein a7158ee57a Rainbow aspected Smeargle 2025-11-01 16:23:12 +01:00
Nadja Reitzenstein 06c3fd3f09 Make lightning damage only not apply if there was an actual aspect change. 2025-11-01 16:23:12 +01:00
Nadja Reitzenstein 4e196e8a29 Generalise switching aspect behaviour on lightning hit 2025-11-01 16:23:12 +01:00
Nadja Reitzenstein 871f33f442 Make mooshtank switch colour on lightning hit 2025-11-01 16:23:12 +01:00
Polymeta 21c1f912a7 Merge branch 'typo' into 'main'
Update crops.json

See merge request cable-mc/cobblemon!1924
2025-11-01 10:16:37 +00:00
dhi_holo 776fb5413f Update crops.json
Apparently missed this file while updating stuff? I swear I didn't but it must not've commited
2025-11-01 14:36:45 +10:00
Paul f645fd9b60 Added support for species-specific move action effects 2025-11-01 11:44:05 +10:00
Landon ac161ae5b4 Merge branch 'orientation-refactor' into 'main'
Refactor orientation to better allow for passengers on rolling rides.

See merge request cable-mc/cobblemon!1910
2025-11-01 01:19:29 +00:00
Jackson Bowes fc3542e3eb Refactor orientation to better allow for passengers on rolling rides. 2025-11-01 01:19:28 +00:00
Hiroku 88932c8c41 Merge branch 'poke-snack-spawner' into 'main'
Makes Poké Snacks act as spawners

See merge request cable-mc/cobblemon!1915
2025-10-31 12:10:36 +00:00
lion 411f98d0ae Makes Poké Snacks act as spawners 2025-10-31 12:10:36 +00:00
Polymeta 02a1929ede Merge branch 'fix-pick-block' into 'main'
Fix picking blocks while riding mounts

Closes #1698

See merge request cable-mc/cobblemon!1922
2025-10-31 12:00:14 +00:00
Jordan ff90059eec Fix picking blocks while riding mounts 2025-10-31 12:00:13 +00:00
Polymeta 4a30bb3b04 Merge branch 'seasoning-filters' into 'main'
Cooking Pot Recipes now have filtered

See merge request cable-mc/cobblemon!1919
2025-10-29 07:45:59 +00:00
Polymeta fac8c5f510 Merge branch 'party-overlay-config-fix' into 'main'
Party overlay config fix

See merge request cable-mc/cobblemon!1921
2025-10-28 14:41:22 +00:00
lion 2d077c607a Party overlay config fix 2025-10-28 14:41:21 +00:00
Nico Merenq f75e3d11da Translated using Weblate (Russian)
Currently translated at 63.6% (5669 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ru/
2025-10-28 08:50:20 +00:00
Gertimoshka f46968f878 Remove Unnecesary Comments 2025-10-28 07:20:54 +02:00
Nico Merenq 3439aeff1e Translated using Weblate (Russian)
Currently translated at 63.5% (5662 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ru/
2025-10-27 21:50:57 +00:00
Nico Merenq 2ada7e0b68 Translated using Weblate (Russian)
Currently translated at 63.5% (5659 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ru/
2025-10-26 06:57:00 +00:00
Polymeta b8f73f277d Fix PokeRodFishingBobberEntity baitId detection for ReelInPokemonCriterionCondition
Fixes #1744

Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-10-25 21:40:17 +01:00
JazzMcNade 7f6197ab8b Honey slathered logs no longer activated multiple time on the same pokemon, and ignore pokemon that already have their hidden ability. 2025-10-25 14:52:51 -05:00
Polymeta 6df2b996e2 Merge branch 'smarter-scalable-brains' into 'main'
New tools for brain activity management

See merge request cable-mc/cobblemon!1916
2025-10-25 19:39:15 +00:00
Polymeta ddc4bd358d Merge branch 'niche-tags' into 'main'
Berry Tag Updates

See merge request cable-mc/cobblemon!1918
2025-10-25 19:34:59 +00:00
Gertimoshka e197287775 Fixed preview item still using seasoning items even if its invalid by tag 2025-10-25 22:04:47 +03:00
dhi_holo be019c79df Update poke_cake.json
Sadly, Poke Cakes do not get tinted by non-flavour colours, so despite my attempt to allow it, it just doesn't work
2025-10-25 20:45:47 +10:00
dhi_holo 73fe851c58 Merge branch 'seasoning-filters' of https://gitlab.com/cable-mc/cobblemon into seasoning-filters 2025-10-25 20:20:09 +10:00
dhi_holo 8544744530 Tags in Recipes 2025-10-25 20:19:37 +10:00
Gertimoshka 32d7bfcef3 Recipes no longer need to define seasoning tag and will assume it as cobblemon:empty 2025-10-25 12:53:25 +03:00
Gertimoshka 27263cd057 JEI is now filtering seasoning slots additionally based berries 2025-10-25 12:22:08 +03:00
Gertimoshka 55f0962205 Recipes will now not consume seasoning items that are not in seasoning filter of recipe and slots will highlight red if seasoning item does not apply to current recipe 2025-10-25 12:02:22 +03:00
dhi_holo 40c5df46ce Berry Tag Updates
Reformatted how the tag is handled to be a bit cleaner, and allow for more specific berry querying via searching, mod compat, etc
2025-10-25 17:08:06 +10:00
dhi_holo e753c75c31 Merge branch 'seasoning-filters' of https://gitlab.com/cable-mc/cobblemon into seasoning-filters 2025-10-25 15:52:35 +10:00
dhi_holo 4e96222a4d Tags
Renamed a tag and fixed errors it caused, as well as adding the current 6 Filter tags requested for system upgrades
2025-10-25 15:52:09 +10:00
Paul 4ee90d4cfb Removed unneeded comment 2025-10-25 15:22:29 +10:00
Paul 540cd612c1 Added seasoning tag filter to cooking pot recipes 2025-10-25 14:22:47 +10:00
Hiroku 1b1d22fdf3 Merge branch 'social-distancing' into 'main'
Adds random offset to follow herd leader task

See merge request cable-mc/cobblemon!1917
2025-10-25 01:49:48 +00:00
lion 1ee1001774 Adds random offset to follow herd leader task 2025-10-25 01:49:48 +00:00
Paul 533272ef09 u got a loicense for that error? 2025-10-24 20:27:31 +10:00
Nico Merenq 4b8196685a Translated using Weblate (Russian)
Currently translated at 63.4% (5653 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ru/
2025-10-24 04:12:32 +00:00
Landon 91300e4d03 Merge branch '1.7-spawns' into 'main'
Some JSON tweaks

See merge request cable-mc/cobblemon!1913
2025-10-21 23:38:34 +00:00
Polymeta fc135faf57 revert pokemon number range 2025-10-21 22:48:59 +00:00
Polymeta cdd72c664e fix some typos
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-10-21 23:23:43 +01:00
Polymeta 5f025d0f68 Merge branch 'moomoo-milk' into 'main'
Adds clear stat changes effect to Moomoo Milk

See merge request cable-mc/cobblemon!1911
2025-10-21 21:50:59 +00:00
lion 6af2c807ab Adds clear stat changes effect to Moomoo Milk 2025-10-21 21:50:58 +00:00
Polymeta c45c90c6c7 Merge branch 'main' into 1.7-spawns
# Conflicts:
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0027_sandshrew.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0028_sandslash.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0079_slowpoke.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0080_slowbro.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0194_wooper.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0199_slowking.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0688_binacle.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0689_barbaracle.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0751_dewpider.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0752_araquanid.json
#	common/src/main/resources/data/cobblemon/spawn_pool_world/0845_cramorant.json
#	common/src/main/resources/data/cobblemon/tags/worldgen/biome/is_overworld.json
#	utilityscripts/cobblemon_spawn_csv_to_json.py
2025-10-21 22:38:33 +01:00
Polymeta 1ec97c4c90 Merge branch 'riding-style-molang' into 'main'
Add molang function to get the current riding style of a pokemon

See merge request cable-mc/cobblemon!1912
2025-10-21 06:42:54 +00:00
Landon Wills f0bcd38ac7 Add molang function to get the current riding style of a pokemon 2025-10-21 01:55:19 -04:00
Test f4ba446c79 Merge branch 'restoration-tank-fixes' into 'main'
Small fixes

Closes #1735 and #1741

See merge request cable-mc/cobblemon!1908
2025-10-21 03:49:09 +00:00
Guilherme Pereira 60f98ebfce refactor poke snack 2025-10-21 00:04:20 -03:00
Teggy 08c2a0d07b Translated using Weblate (Spanish)
Currently translated at 85.0% (7578 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/es/
2025-10-20 20:24:26 +00:00
Nico Merenq 1f68fdac8c Translated using Weblate (Russian)
Currently translated at 63.4% (5649 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ru/
2025-10-20 17:26:47 +00:00
Teggy e8d4f0fcf4 Translated using Weblate (Spanish)
Currently translated at 84.7% (7546 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/es/
2025-10-20 17:26:45 +00:00
Guilherme Pereira 36a93bda5a refactor poke snack 2025-10-20 03:10:09 -03:00
阿鱼 5d522bd608 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (8908 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/zh_Hans/
2025-10-20 05:04:02 +00:00
Paul 8d60b07c54 Merge remote-tracking branch 'origin/main' 2025-10-20 14:45:26 +10:00
Paul 5f9cc86044 Added rouseChance and drowsyChance to RestBehaviour so pokemon don't insta sleep in the wild and may spontaneously wake up 2025-10-20 14:45:06 +10:00
Landon a6b7e07cf5 Merge branch 'held-item-fix' into 'main'
Remove pvp check for held item logic

See merge request cable-mc/cobblemon!1907
2025-10-20 03:18:36 +00:00
Landon Wills 0ba6215b8d Elaborate on comment 2025-10-19 23:05:16 -04:00
Guilherme Pereira e2f6fae689 change willDefendSelf combat behaviour default to false 2025-10-19 23:36:52 -03:00
Guilherme Pereira 296aa01d0f remove println 2025-10-19 18:41:38 -03:00
Guilherme Pereira 12ab3f2d07 fix dolphin crash on servers 2025-10-19 18:18:31 -03:00
Guilherme Pereira 5a7a374bc9 Merge branch 'main' of gitlab.com:cable-mc/cobblemon into restoration-tank-fixes 2025-10-19 17:35:50 -03:00
Guilherme Pereira ceece45ad4 make poke snack spawn 4 mons per bite 2025-10-19 17:24:01 -03:00
Landon Wills b344ad66c5 Remove pvp check 2025-10-19 14:44:08 -04:00
Paul dbc7af4ad4 Don't need to override the start conditions function for activity aware behavior 2025-10-18 21:40:43 +10:00
Paul 5f5bf01cb5 Added ActivityAwareBehavior and tagging for memories and activities 2025-10-18 21:30:47 +10:00
Guilherme Pereira 3f03afc7eb smooth campfire pot spinning animation 2025-10-18 06:53:20 -03:00
Watuneni iteite 3894a99bc4 Translated using Weblate (Japanese)
Currently translated at 93.5% (8334 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-10-17 18:40:49 +00:00
Guilherme Pereira 93011f084b make riding fov effects proportional to minecraft fov effect scale setting 2025-10-15 19:23:51 -03:00
Guilherme Pereira 7c90a8425e fix restoration tank concurrent modification crash #1735 2025-10-15 15:01:41 -03:00
阿鱼 9036a2f317 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (8908 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/zh_Hans/
2025-10-13 12:31:47 +00:00
阿鱼 c75ab28002 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.3% (8853 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/zh_Hans/
2025-10-13 06:00:42 +00:00
Nico Merenq 62a8320068 Translated using Weblate (Russian)
Currently translated at 63.2% (5635 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ru/
2025-10-11 20:53:16 +00:00
Watuneni iteite f21b38f66a Translated using Weblate (Japanese)
Currently translated at 93.1% (8300 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-10-11 20:53:15 +00:00
阿鱼 596765bb52 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 98.6% (8791 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/zh_Hans/
2025-10-11 06:00:06 +00:00
Watuneni iteite 297aafba45 Translated using Weblate (Japanese)
Currently translated at 92.9% (8282 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-10-11 06:00:04 +00:00
Watuneni iteite a0a682e224 Translated using Weblate (Japanese)
Currently translated at 92.3% (8227 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-10-06 12:15:07 +00:00
Nico Merenq d161b11892 Translated using Weblate (Russian)
Currently translated at 63.1% (5627 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ru/
2025-10-04 22:39:35 +00:00
Yasito d70bb243b2 Translated using Weblate (French)
Currently translated at 100.0% (8908 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2025-10-02 18:49:12 +00:00
Yasito 301a3efb7c Translated using Weblate (French (Canada))
Currently translated at 100.0% (8908 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr_CA/
2025-10-02 18:49:11 +00:00
Yasito 05b7cbe20a Translated using Weblate (French)
Currently translated at 100.0% (8908 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2025-10-02 18:38:58 +00:00
Yasito 48cbbc1bef Translated using Weblate (French (Canada))
Currently translated at 100.0% (8908 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr_CA/
2025-10-02 18:38:57 +00:00
Yasito f8f7c28274 Translated using Weblate (French)
Currently translated at 99.8% (8895 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2025-10-02 17:13:57 +00:00
Yasito 0ebb53888a Translated using Weblate (French (Canada))
Currently translated at 99.8% (8891 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr_CA/
2025-10-02 17:13:56 +00:00
Yasito c0071c1525 Translated using Weblate (French)
Currently translated at 99.5% (8868 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2025-10-02 14:53:46 +00:00
Yasito 71f7b3b3d8 Translated using Weblate (French (Canada))
Currently translated at 99.2% (8841 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr_CA/
2025-10-02 14:53:45 +00:00
Watuneni iteite bd6d7b7149 Translated using Weblate (Japanese)
Currently translated at 91.1% (8116 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-10-01 00:25:05 +00:00
Alexis Salgueiro 63de8fe44a Translated using Weblate (French)
Currently translated at 99.5% (8868 of 8908 strings)

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/fr/
2025-09-29 18:00:45 +00:00
Veraxiel 17c71efb17 Added spawns for 1.7 Pokémon. Updated spawns. Updated spawn scraper. 2025-09-08 02:04:58 -04:00
Veraxiel a5b6a954fd Added "Mansion Dining" preset tag. Updated is_overworld tag. 2025-09-07 20:42:53 -04:00
463 changed files with 13788 additions and 2270 deletions

No files matched your search

+2 -1
View File
@@ -12,7 +12,8 @@ out
*.ipr
*.iws
*.iml
.idea
.idea/*
!.idea/icon.png
# gradle
build
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

+9 -1
View File
@@ -12,6 +12,14 @@
- SpawningContext is now SpawnablePosition
- WorldSlice is SpawningZone
- SpawningProspector is now SpawningZoneGenerator
- Majorly refactored the hierarchy of Spawner
- The base Spawner interface provides more functions to allow single-point and area spawning given appropriate inputs.
- TickingSpawner is removed in favour of outside code handling ticking logic.
- AreaSpawner is removed.
- BasicSpawner is the first implementation of Spawner which can be used for any purpose.
- PlayerSpawners are now mixin'd into ServerPlayer and ticked from ServerPlayer#tick.
- A hierarchy diagram can be found at `./docs/spawner-hierarchy.png` in the mod repository.
- The SpawnerManager class has been removed as its functionality is all now handled elsewhere.
- Renamed things in Spawn Rules to go with the other renames:
- contextSelector is now spawnablePositionSelector
- contextSelector is now spawnablePositionSelector
- context is now spawnable_position
+28 -2
View File
@@ -54,6 +54,8 @@
- `PokemonRecallEvent`
- `TradeEvent`
- Added a new gamerule, 'healerHealsPC', when set to true a successful use of a healer will also heal all of the Pokemon in that player's PC.
- Mooshtanks will switch between their red and brown variant when hit by lightning
- Smeargle spawns with differing tail colour depending on its [Characteristic](https://bulbapedia.bulbagarden.net/wiki/Characteristic) stat
### Pokémon Added
@@ -421,6 +423,8 @@
- Updated some item tags to better integrate behaviours between Cobblemon, Vanilla Minecraft, and other mods
- Removed Cooked Meat, Raw Meat, Protein Ingredients, and Zinc Ingredients, the first two are now using `c` namespace tags, the latter have better integrated use of tags within their recipes which removes need for custom tags.
- Added our seeds tag into `#c:seeds`, which is now made use of for the Miracle Seed recipe.
- Updated the `#cobblemon:berries` tag to use subtags located in `data/cobblemon/tags/item/berries`, such as `#cobblemon:berries/hp_recovery` for sorting purposes
- Moved the tag `#cobblemon:mutated_berries` to `#cobblemon:berries/mutation_result`
- Added herbs and snowballs to the consumable in PvE and Wild battle tags
- Tweaked the Natural Materials Vanilla file to fit with the changes to tags
- Substantially optimised spawning checks mainly by front-loading biome filtering.
@@ -472,7 +476,8 @@
- Updated `HatchEggEvent.Post` to include the Pokemon that hatched.
- Roseli Berry Trees now naturally generate in their preferred biomes.
- Refactored dynamic lights compat to be more future proof. The dynamic light support is only tested with LambDynamicLights, on NeoForged use that mod in combination with Sinytra Connector
- Shulker Boxes and Traveler's Backpacks can no longer be held by Pokémon. Thanks Monocle ;) You could re-enable this with a datapack but you'd be crazy.
- Shulker Boxes and Traveler's Backpacks AND Packed Up backpacks can no longer be held by Pokémon. Thanks Monocle ;) You could re-enable this with our datapack (but you'd be crazy).
- Moomoo Milk now clears Pokémon stat changes when used in battle.
### Fixes
- Fixed game crashing when removing national pokedex using datapacks
@@ -557,6 +562,8 @@
- Fixed singular Pokémon corruption causing entire storage corruption. Storages will now skip corrupted Pokémon and print an error to console.
- Fixed species additions not being able to properly mark a species as implemented.
- Fixed Pokémon item models not showing a glint when enchanted.
- Fixed some specific bag items not being dropped when used in battle.
- Fixed the "use all berry bait" achievement not being progressed
### Developer
- A finished battle now has winners and losers set inside of `PokemonBattle` instead of them always being empty.
@@ -594,10 +601,25 @@
- Added a `hoverText` option to PartySelectCallback, to display a tooltip on hovering over a Pokémon in the selection screen.
- `PokemonEntity` instances spawned into the world now appropriately finalize the spawn for mod compatibility.
- Added PokedexManager.obtain as a replacement for .catch which is not a friendly function name in Java.
- Added `Pokemon#hyperTrainIV()` and `IVs#setHyperTrainedIV(Stat, Int)`
- `ElementalType` now implements `ShowdownIdentifiable` to ensure the communcation with showdown stays consistent (also in regards to TeraTypes)
- Pokemon no longer have a change observable
- Pokémon can now have a behaviour changing the value of a species feature on lightning hit:
```JSON
{
"behaviour": {
"lightningHit": {
"rotateFeatures": [
{
"key": "mooshtank",
"chain": ["red", "brown"]
}
]
}
}
}
```
This will change the value of `mooshtank` from `red` to `brown` and vice versa when a lightning strikes this Pokémon. Only chains of which a value is the current value will be considered, so multiple entries for the same key with different chains can be added.
### MoLang & Datapacks
- The following usages for item predicates can now use item conditions like advancements do, you can learn about them in the [Minecraft wiki](https://minecraft.wiki/w/Advancement_definition#minecraft:filled_bucket)
@@ -701,6 +723,10 @@
- Mechanics
- Unlockable Wallpapers
- Starter Data
- Fixed `cobblemon:reel_in_pokemon` criteria not working when used together with a `baitId`
- Also changed the default from `cobblemon:empty_bait` to `any`
- The previous default is still available by using the above as baitId
- Added support for species-specific move action effects, using the format `{move_id}_{species}.json`.
### Particles
Added new/updated particles for the following moves:
@@ -0,0 +1,23 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common;
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner;
/**
* Used for mixin shenanigans. ServerPlayers now store their Cobblemon spawner
* inside, so this interface helps execute that.
*
* @author Hiroku
* @since October 27th, 2025
*/
public interface PlayerSpawnerAccessor {
PlayerSpawner getPlayerSpawner();
void setPlayerSpawner(PlayerSpawner spawner);
}
@@ -1,3 +1,11 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.OrientationControllable;
@@ -7,6 +7,7 @@
*/
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.duck.ChannelDuck;
import com.mojang.blaze3d.audio.Channel;
import org.lwjgl.openal.AL10;
@@ -53,12 +53,6 @@ public abstract class LivingEntityMixin extends Entity implements OrientationCon
return cobblemon$orientationController;
}
@Inject(method = "baseTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/ClientPacketListener;send(Lnet/minecraft/network/protocol/Packet;)V"))
private void cobblemon$updateRotationMatrix(CallbackInfo ci) {
if (!cobblemon$orientationController.isActive() || cobblemon$orientationController.getOrientation() == null) return;
CobblemonNetwork.INSTANCE.sendToServer(new ServerboundUpdateOrientationPacket(this.getId(), cobblemon$orientationController.getOrientation()));
}
@Inject(method = "onEffectRemoved", at = @At(value = "TAIL"))
private void cobblemon$onEffectRemoved(MobEffectInstance effect, CallbackInfo ci) {
final LivingEntity entity = (LivingEntity) (Object) this;
@@ -111,7 +105,7 @@ public abstract class LivingEntityMixin extends Entity implements OrientationCon
@Inject(method = "tick", at = @At("HEAD"))
private void cobblemon$updateRenderOrientation(CallbackInfo ci) {
this.cobblemon$orientationController.tick();
//this.cobblemon$orientationController.tick();
}
@Override
@@ -8,21 +8,56 @@
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.Cobblemon;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.PlayerSpawnerAccessor;
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner;
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ServerPlayer.class)
public abstract class ServerPlayerMixin {
public abstract class ServerPlayerMixin implements PlayerSpawnerAccessor {
@Shadow
public abstract ServerLevel serverLevel();
@Unique
public PlayerSpawner cobblemon$spawner;
@Override
public PlayerSpawner getPlayerSpawner() {
ServerPlayer player = (ServerPlayer)(Object)this;
if (cobblemon$spawner == null) {
cobblemon$spawner = PlayerSpawnerFactory.INSTANCE.create(player);
}
return cobblemon$spawner;
}
@Override
public void setPlayerSpawner(PlayerSpawner spawner) {
this.cobblemon$spawner = spawner;
}
@Inject(method = "tick", at = @At("TAIL"))
private void cobblemon$tickSpawner(CallbackInfo ci) {
if (!Cobblemon.config.getEnableSpawning()) {
return;
} else if (!serverLevel().getGameRules().getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING)) {
return;
}
getPlayerSpawner().tick();
}
@Inject(method = "rideTick", at = @At("HEAD"))
private void cobblemon$updateOrientationControllerRideTick(CallbackInfo ci) {
@@ -54,5 +89,4 @@ public abstract class ServerPlayerMixin {
private boolean cobblemon$cancelSavingPokemonMounts(boolean original, @Local(ordinal=0) Entity entity) {
return original && !(entity instanceof PokemonEntity);
}
}
@@ -36,9 +36,6 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import static net.minecraft.util.Mth.abs;
import static net.minecraft.util.Mth.atan2;
@Mixin(Camera.class)
public abstract class CameraMixin {
@Shadow private Entity entity;
@@ -65,13 +62,14 @@ public abstract class CameraMixin {
@Unique private double cobblemon$frameTime = 0.0;
@Inject(method = "setRotation", at = @At("HEAD"), cancellable = true)
public void cobblemon$setRotation(float f, float g, CallbackInfo ci) {
// Calculate the current frametime
double d = Blaze3D.getTime();
this.cobblemon$frameTime = (this.cobblemon$lastHandledRotationTime != Double.MIN_VALUE) ? d - this.cobblemon$lastHandledRotationTime : 0.0;
this.cobblemon$lastHandledRotationTime = d;
// Don't assume the camera to be attached to an entity. Ponder Scenes from create et.al. for example aren't.
if (this.entity == null) return;
var vehicle = this.entity.getVehicle();
if (!(vehicle instanceof OrientationControllable controllableVehicle)) return;
var instance = (Camera)(Object)this;
@@ -163,17 +161,17 @@ public abstract class CameraMixin {
var lerpRateMod = Mth.cos((float)Math.toRadians(cutoffPitch));
// Apply smoothing from the cameras current yaw to the yaw of the velocity vector of the ride.
var k = 2.0f; // Some smoothing constant k
var k = 5.0f; // Some smoothing constant k
var smoothingFactor = lerpRateMod * cobblemon$frameTime * k;
var newYaw = currAngs.y() + (Math.toRadians(degDiff) * smoothingFactor);
newRotation.set(new Quaternionf().rotateYXZ((float)newYaw, eulerAngs.x(), 0.0f));
}
var k = 0;
var rotationOffset = 0;
if (Minecraft.getInstance().options.getCameraType().isMirrored()) {
newRotation.rotateY((float)Math.toRadians(180));
k = 180;
rotationOffset = 180;
}
// Get the drivers local look angle in relation to the ride and apply it.
@@ -194,7 +192,7 @@ public abstract class CameraMixin {
this.yRot = eulerAngs.y() * Mth.RAD_TO_DEG;
// Set the drivers rotations to match
var driverEulerAngs = new Quaternionf(this.rotation).getEulerAnglesYXZ(new Vector3f());
driver.setXRot(-driverEulerAngs.x() * Mth.RAD_TO_DEG + k);
driver.setXRot(-driverEulerAngs.x() * Mth.RAD_TO_DEG + rotationOffset);
driver.setYRot(180.0f - (driverEulerAngs.y() * Mth.RAD_TO_DEG));
OrientationController.Companion.getFORWARDS().rotate(this.rotation, this.forwards);
@@ -9,15 +9,21 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.api.scheduling.ClientTaskTracker;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.function.Predicate;
@Mixin(GameRenderer.class)
public class GameRendererMixin {
@Unique
@@ -42,4 +48,16 @@ public class GameRendererMixin {
lastTime = newTime;
}
@ModifyArg(
method = "pick(Lnet/minecraft/world/entity/Entity;DDF)Lnet/minecraft/world/phys/HitResult;",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/projectile/ProjectileUtil;getEntityHitResult(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/AABB;Ljava/util/function/Predicate;D)Lnet/minecraft/world/phys/EntityHitResult;"
),
index = 4
)
private Predicate<Entity> filterPokemonVehicle(Predicate<Entity> original, @Local(ordinal = 0, argsOnly = true) Entity entity) {
return (entityX) -> !(entity.getVehicle() == entityX && entityX instanceof PokemonEntity) && original.test(entityX);
}
}
@@ -55,6 +55,10 @@ public abstract class LocalPlayerMixin extends AbstractClientPlayer implements O
@Unique Matrix3f cobblemon$lastOrientation;
@Unique float cobblemon$lastRideXRot;
@Unique float cobblemon$lastRideYRot;
@Unique Vec3 cobblemon$lastRideEyePos;
@Unique boolean cobblemon$isDoubleJumping = false;
@Shadow
@@ -72,15 +76,6 @@ public abstract class LocalPlayerMixin extends AbstractClientPlayer implements O
super(clientLevel, gameProfile);
}
// @Inject(method = "sendPosition", at = @At("TAIL"))
// private void cobblemon$updateRotationMatrix(CallbackInfo ci) {
// if (!(this instanceof OrientationControllable controllable)) return;
// var controller = controllable.getOrientationController();
// if (!controller.isActive() || controller.getOrientation() == cobblemon$lastOrientation) return;
// cobblemon$lastOrientation = controller.getOrientation() != null ? new Matrix3f(controller.getOrientation()) : null;
// CobblemonNetwork.INSTANCE.sendToServer(new ServerboundUpdateOrientationPacket(controller.getOrientation()));
// }
@Inject(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/ClientPacketListener;send(Lnet/minecraft/network/protocol/Packet;)V"))
private void cobblemon$updateRotationMatrixPassenger(CallbackInfo ci) {
if (!(this.getVehicle() instanceof OrientationControllable controllableVehicle)) return;
@@ -146,8 +141,21 @@ public abstract class LocalPlayerMixin extends AbstractClientPlayer implements O
var rideYRot = playerRotater.cobblemon$getRideYRot();
var rideEyePos = playerRotater.cobblemon$getRideEyePos();
// Send new info to server. Currently not checking to see if this has change substantially
CobblemonNetwork.INSTANCE.sendToServer(new ServerboundUpdateRiderRotationPacket(rideXRot, rideYRot, rideEyePos));
// Check for a change in the values before sending them.
if (cobblemon$lastRideXRot != rideXRot ||
cobblemon$lastRideYRot != rideYRot ||
cobblemon$lastRideEyePos != rideEyePos
) {
// Update 'last' values
cobblemon$lastRideXRot = rideXRot;
cobblemon$lastRideYRot = rideYRot;
cobblemon$lastRideEyePos = rideEyePos;
CobblemonNetwork.INSTANCE.sendToServer(new ServerboundUpdateRiderRotationPacket(rideXRot, rideYRot, rideEyePos));
}
}
@Unique
@@ -123,8 +123,6 @@ public abstract class MouseHandlerMixin {
@Local(argsOnly = true) double d,
@Local(argsOnly = true) double movementTime
) {
boolean returnValue = true;
PokedexUsageContext usageContext = CobblemonClient.INSTANCE.getPokedexUsageContext();
if (usageContext.getScanningGuiOpen()) {
this.smoothTurnY.reset();
@@ -135,7 +133,6 @@ public abstract class MouseHandlerMixin {
var sensitivity = lerp(usageContext.getFovMultiplier(), spyglassSensitivity, lookSensitivity);
var yRotationFlip = this.minecraft.options.invertYMouse().get() ? -1 : 1;
player.turn(this.accumulatedDX * sensitivity, (this.accumulatedDY * sensitivity * yRotationFlip));
returnValue = false;
}
var vehicle = player.getVehicle();
@@ -153,7 +150,7 @@ public abstract class MouseHandlerMixin {
pitchSmoother.reset();
rollSmoother.reset();
yawSmoother.reset();
return returnValue;
return true;
}
var vehicleController = controllableVehicle.getOrientationController();
@@ -83,6 +83,7 @@ import com.cobblemon.mod.common.net.messages.client.settings.ServerSettingsPacke
import com.cobblemon.mod.common.permission.LaxPermissionValidator
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.aspects.CHARACTERISTIC_RAINBOW_ASPECT
import com.cobblemon.mod.common.pokemon.aspects.COSMETIC_SLOT_ASPECT
import com.cobblemon.mod.common.pokemon.aspects.GENDER_ASPECT
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
@@ -293,6 +294,7 @@ object Cobblemon {
SHINY_ASPECT.register()
GENDER_ASPECT.register()
COSMETIC_SLOT_ASPECT.register()
CHARACTERISTIC_RAINBOW_ASPECT.register()
SpeciesFeatures.types["choice"] = ChoiceSpeciesFeatureProvider::class.java
SpeciesFeatures.types["flag"] = FlagSpeciesFeatureProvider::class.java
@@ -696,7 +696,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val SUPERB_REMEDY = compostableItem("superb_remedy", RemedyItem(RemedyItem.SUPERB), 0.85f)
@JvmField
val MOOMOO_MILK = create("moomoo_milk", CobblemonItem(Item.Properties().craftRemainder(Items.GLASS_BOTTLE).stacksTo((16))))
val MOOMOO_MILK = create("moomoo_milk", MoomooMilk(Item.Properties().craftRemainder(Items.GLASS_BOTTLE).stacksTo((16))))
@JvmField
val POTION = create("potion", PotionItem(PotionType.POTION))
@@ -33,7 +33,7 @@ import com.cobblemon.mod.common.client.net.gui.PokedexUIPacketHandler
import com.cobblemon.mod.common.client.net.gui.SummaryUIPacketHandler
import com.cobblemon.mod.common.client.net.npc.CloseNPCEditorHandler
import com.cobblemon.mod.common.client.net.npc.OpenNPCEditorHandler
import com.cobblemon.mod.common.client.net.orientation.S2CUpdateOrientationHandler
import com.cobblemon.mod.common.client.net.orientation.ClientboundUpdateOrientationHandler
import com.cobblemon.mod.common.client.net.pasture.ClosePastureHandler
import com.cobblemon.mod.common.client.net.pasture.OpenPastureHandler
import com.cobblemon.mod.common.client.net.pasture.PokemonPasturedHandler
@@ -217,7 +217,7 @@ import com.cobblemon.mod.common.net.serverhandling.dialogue.EscapeDialogueHandle
import com.cobblemon.mod.common.net.serverhandling.dialogue.InputToDialogueHandler
import com.cobblemon.mod.common.net.serverhandling.evolution.AcceptEvolutionHandler
import com.cobblemon.mod.common.net.serverhandling.npc.SaveNPCHandler
import com.cobblemon.mod.common.net.serverhandling.orientation.OrientationPacketHandler
import com.cobblemon.mod.common.net.serverhandling.orientation.ServerboundUpdateOrientationHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.PasturePokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.SetPastureConflictHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.UnpastureAllPokemonHandler
@@ -444,7 +444,7 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(UpdatePastureConflictFlagPacket.ID, UpdatePastureConflictFlagPacket::decode, UpdatePastureConflictFlagHandler))
// Orientation
list.add(PacketRegisterInfo(ClientboundUpdateOrientationPacket.ID, ClientboundUpdateOrientationPacket::decode, S2CUpdateOrientationHandler))
list.add(PacketRegisterInfo(ClientboundUpdateOrientationPacket.ID, ClientboundUpdateOrientationPacket::decode, ClientboundUpdateOrientationHandler))
// Behaviours
list.add(PacketRegisterInfo(PlayPosableAnimationPacket.ID, PlayPosableAnimationPacket::decode, PlayPosableAnimationHandler))
@@ -579,7 +579,7 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(SaveNPCPacket.ID, SaveNPCPacket::decode, SaveNPCHandler))
// Riding packet(s)
list.add(PacketRegisterInfo(ServerboundUpdateOrientationPacket.ID, ServerboundUpdateOrientationPacket::decode, OrientationPacketHandler))
list.add(PacketRegisterInfo(ServerboundUpdateOrientationPacket.ID, ServerboundUpdateOrientationPacket::decode, ServerboundUpdateOrientationHandler))
list.add(PacketRegisterInfo(ServerboundUpdateRidingStatePacket.ID, ServerboundUpdateRidingStatePacket::decode, ServerboundUpdateRidingStateHandler))
list.add(PacketRegisterInfo(ServerboundUpdateRidingStatsPacket.ID, ServerboundUpdateRidingStatsPacket::decode, ServerboundUpdateRidingStatsHandler))
list.add(PacketRegisterInfo(ServerboundUpdateRidingStatRangePacket.ID, ServerboundUpdateRidingStatRangePacket::decode, ServerboundUpdateRidingStatRangeHandler))
@@ -26,8 +26,6 @@ object CobblemonPoiTypes: PlatformRegistry<Registry<PoiType>, ResourceKey<Regist
@JvmField
val NURSE_KEY = createKey("nurse", CobblemonBlocks.HEALING_MACHINE, 1, 1)
@JvmField
val POKE_SNACK_KEY = createKey("poke_snack", CobblemonBlocks.POKE_SNACK, 0, 1)
@JvmField
val SACCHARINE_LOG_SLATHERED_KEY = createKey("saccharine_log_slathered", CobblemonBlocks.SACCHARINE_LOG_SLATHERED, 0, 1)
// TODO after 1.7
// @JvmField
@@ -22,18 +22,19 @@ class ReelInPokemonContext(val species : ResourceLocation, val baitId: ResourceL
class ReelInPokemonCriterionCondition(
playerCtx: Optional<ContextAwarePredicate>,
val species: String,
val baitId: String
val baitId: String // also allows "cobblemon:empty_bait" to indicate that no bait is allowed for this criterion to match
) : SimpleCriterionCondition<ReelInPokemonContext>(playerCtx) {
companion object {
val CODEC: Codec<ReelInPokemonCriterionCondition> = RecordCodecBuilder.create { it.group(
EntityPredicate.ADVANCEMENT_CODEC.optionalFieldOf("player").forGetter { it.playerCtx },
Codec.STRING.optionalFieldOf("species", "any").forGetter { it.species },
Codec.STRING.optionalFieldOf("baitId", "empty_bait").forGetter { it.baitId }
).apply(it, { playerCtx, pokemonId, baitId -> ReelInPokemonCriterionCondition(playerCtx, pokemonId, baitId.ifEmpty { "empty_bait" }) }) }
Codec.STRING.optionalFieldOf("baitId", "any").forGetter { it.baitId }
).apply(it, { playerCtx, pokemonId, baitId -> ReelInPokemonCriterionCondition(playerCtx, pokemonId, baitId) }) }
}
override fun matches(player: ServerPlayer, context: ReelInPokemonContext): Boolean {
return (context.species == this.species.asIdentifierDefaultingNamespace() || this.species == "any") && (context.baitId == this.baitId.asIdentifierDefaultingNamespace() || this.baitId == "empty_bait")
return (context.species == this.species.asIdentifierDefaultingNamespace() || this.species == "any")
&& (context.baitId == this.baitId.asIdentifierDefaultingNamespace() || this.baitId == "any")
}
}
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.Behavior
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.MemoryStatus
import net.minecraft.world.entity.schedule.Activity
/**
* A [Behavior] subclass that represents tasks that check that the entity is still in one
* of the activities that this task is part of. If the entity is no longer doing any of those
* activities, the task will be interrupted and you can handle any cleanup in [onInterrupted].
*
* @author Hiroku
* @since October 17th, 2025
*/
abstract class ActivityAwareBehavior<E : LivingEntity> : Behavior<E> {
constructor(entryCondition: Map<MemoryModuleType<*>, MemoryStatus>) : super(entryCondition)
constructor(entryCondition: Map<MemoryModuleType<*>, MemoryStatus>, duration: Int) : super(entryCondition, duration)
constructor(entryCondition: Map<MemoryModuleType<*>, MemoryStatus>, minDuration: Int, maxDuration: Int) : super(entryCondition, minDuration, maxDuration)
val activities: MutableSet<Activity> = mutableSetOf()
fun isStillDoingActivity(entity: E): Boolean {
return activities.any(entity.brain::isActive)
}
override fun canStillUse(level: ServerLevel, entity: E, gameTime: Long) = isStillDoingActivity(entity)
open fun onInterrupted(level: ServerLevel, entity: E, gameTime: Long) {
// No-op
}
override fun stop(level: ServerLevel, entity: E, gameTime: Long) {
super.stop(level, entity, gameTime)
if (!isStillDoingActivity(entity)) {
onInterrupted(level, entity, gameTime)
}
}
}
@@ -11,7 +11,6 @@ package com.cobblemon.mod.common.api.ai
import com.google.common.collect.ImmutableList
import com.mojang.datafixers.util.Pair
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.Mob
import net.minecraft.world.entity.ai.behavior.BehaviorControl
import net.minecraft.world.entity.schedule.Activity
@@ -23,6 +22,13 @@ class ActivityConfigurationContext(val activity: Activity) {
}
fun apply(entity: LivingEntity) {
tasks.forEach {
val activityAwareBehavior = it.second as? ActivityAwareBehavior<*>
?: (it.second as? WrapperLivingEntityTask<*>)?.task as? ActivityAwareBehavior
?: return@forEach
activityAwareBehavior.activities += activity
}
entity.brain.addActivity(activity, ImmutableList.copyOf(tasks))
}
}
@@ -23,7 +23,7 @@ import net.minecraft.world.entity.schedule.Activity
class ActivityChangeTaskConfig : SingleTaskConfig {
val activity: Activity = Activity.IDLE
val condition: ExpressionLike = "false".asExpressionLike()
val condition: ExpressionLike = "true".asExpressionLike()
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext): List<MoLangConfigVariable> = emptyList()
@@ -38,7 +38,7 @@ class MoveToOwnerTaskConfig : SingleTaskConfig {
override fun createTask(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): BehaviorControl<in LivingEntity>? {
): BehaviorControl<in LivingEntity> {
behaviourConfigurationContext.addMemories(
MemoryModuleType.WALK_TARGET,
MemoryModuleType.LOOK_TARGET
@@ -110,7 +110,7 @@ class PlaceHoneyInSaccLeavesTaskConfig : SingleTaskConfig {
override fun stop(level: ServerLevel, entity: LivingEntity, gameTime: Long) {
if (successfulPollinationTicks > REQUIRED_SUCCESSFUL_POLLINATION_TICKS) {
val blockPos = entity.brain.getMemorySafely(CobblemonMemories.NEARBY_SACC_LEAVES).orElse(null)
blockPos.let {
blockPos?.let {
val blockState = level.getBlockState(blockPos)
if (blockState.block == CobblemonBlocks.SACCHARINE_LEAVES) {
val saccAge = blockState.getValue( SaccharineLeafBlock.AGE)
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.berry.spawncondition
import com.cobblemon.mod.common.api.berry.Berry
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.core.Holder
import net.minecraft.tags.TagKey
import net.minecraft.util.RandomSource
import net.minecraft.world.level.biome.Biome
class SpecificBiomeCondition : BerrySpawnCondition {
lateinit var biome: TagKey<Biome>
val minGroveSize: Int = 1
val maxGroveSize: Int = 1
override fun canSpawn(berry: Berry, biome: Holder<Biome>) = biome.`is`(this.biome)
override fun getGroveSize(random: RandomSource) = random.nextIntBetweenInclusive(minGroveSize, maxGroveSize)
companion object {
val ID = cobblemonResource("specific_biome")
}
}
@@ -22,6 +22,7 @@ interface PokemonSideDelegate : EntitySideDelegate<PokemonEntity> {
fun handleStatus(status: Byte) {}
fun spawnShinyParticle(player: Player) {}
fun spawnAspectParticle() {}
fun applyRenderRotation(partialTick: Float) {}
fun positionRider(passenger: Entity, positionUpdater: MoveFunction)
fun updateAge(age: Int) {}
}
@@ -17,18 +17,11 @@ object SpawnBaitUtils {
return grouped.map { (key, groupEffects) ->
val (type, subcategory) = key
val count = groupEffects.size
val totalChance = groupEffects.sumOf { it.chance }
val totalValue = groupEffects.sumOf { it.value }
val multiplier = when (count) {
2 -> 0.8
3 -> 0.6
else -> 1.0
}
val adjustedChance = (totalChance * multiplier).coerceAtMost(1.0)
val adjustedValue = ceil(totalValue * multiplier)
val adjustedChance = totalChance.coerceAtMost(1.0)
val adjustedValue = ceil(totalValue)
SpawnBait.Effect(type, subcategory, adjustedChance, adjustedValue)
}
@@ -12,7 +12,6 @@ import com.bedrockk.molang.runtime.MoLangEnvironment
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.MoParams
import com.bedrockk.molang.runtime.struct.ArrayStruct
import com.bedrockk.molang.runtime.struct.MoStruct
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.struct.VariableStruct
import com.bedrockk.molang.runtime.value.DoubleValue
@@ -40,7 +39,6 @@ import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
import com.cobblemon.mod.common.api.moves.animations.NPCProvider
import com.cobblemon.mod.common.api.moves.animations.TargetsProvider
import com.cobblemon.mod.common.api.npc.NPCClass
import com.cobblemon.mod.common.api.npc.NPCClasses
import com.cobblemon.mod.common.api.npc.configuration.interaction.DialogueNPCInteractionConfiguration
import com.cobblemon.mod.common.api.npc.configuration.interaction.ScriptNPCInteractionConfiguration
@@ -57,7 +55,6 @@ import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
import com.cobblemon.mod.common.api.pokemon.experience.SidemodExperienceSource
import com.cobblemon.mod.common.api.pokemon.moves.LearnsetQuery
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.riding.Rideable
import com.cobblemon.mod.common.api.riding.stats.RidingStat
import com.cobblemon.mod.common.api.scheduling.ClientTaskTracker
import com.cobblemon.mod.common.api.scheduling.Schedulable
@@ -137,7 +134,6 @@ import net.minecraft.world.entity.LightningBolt
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.Mob
import net.minecraft.world.entity.PathfinderMob
import net.minecraft.world.entity.PlayerRideable
import net.minecraft.world.entity.TamableAnimal
import net.minecraft.world.entity.ai.behavior.BlockPosTracker
import net.minecraft.world.entity.ai.memory.MemoryModuleType
@@ -145,7 +141,6 @@ import net.minecraft.world.entity.ai.memory.MemoryStatus
import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.entity.animal.Animal
import net.minecraft.world.entity.item.ItemEntity
import net.minecraft.world.entity.monster.Enemy
import net.minecraft.world.entity.monster.Monster
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
@@ -160,9 +155,6 @@ import net.minecraft.world.level.levelgen.Heightmap
import net.minecraft.world.level.pathfinder.PathType
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import kotlin.text.get
import kotlin.text.toInt
import kotlin.toString
/**
* Holds a bunch of useful MoLang trickery that can be used or extended in API
@@ -1185,16 +1177,28 @@ object MoLangFunctions {
return@put DoubleValue.ONE
}
map.put("has_memory_value") { params ->
val memoryTypes = params.params.map {
it.asString().asIdentifierDefaultingNamespace()
}.map(BuiltInRegistries.MEMORY_MODULE_TYPE::get)
return@put DoubleValue(memoryTypes.all { entity.brain.checkMemory(it, MemoryStatus.VALUE_PRESENT) })
for (param in params.params) {
if (!entity.hasMemoryFromString(param.asString())) {
return@put DoubleValue.ZERO
}
}
return@put DoubleValue.ONE
}
map.put("lacks_memory_value") { params ->
val memoryTypes = params.params.map {
it.asString().asIdentifierDefaultingNamespace()
}.map(BuiltInRegistries.MEMORY_MODULE_TYPE::get)
return@put DoubleValue(memoryTypes.none { entity.brain.checkMemory(it, MemoryStatus.VALUE_PRESENT) })
for (param in params.params) {
if (entity.hasMemoryFromString(param.asString())) {
return@put DoubleValue.ZERO
}
}
return@put DoubleValue.ONE
}
map.put("is_doing_activity") { params ->
for (param in params.params) {
if (entity.isDoingActivityFromString(param.asString())) {
return@put DoubleValue.ONE
}
}
return@put DoubleValue.ZERO
}
map.put("can_see") { params ->
val target = params.get<MoValue>(0)
@@ -1252,15 +1256,6 @@ object MoLangFunctions {
map.put("in_battle") { DoubleValue(npc.isInBattle()) }
map.put("battles") { ArrayStruct(npc.battleIds.mapNotNull { BattleRegistry.getBattle(it)?.struct }.mapIndexed { index, value -> "$index" to value }.toMap()) }
map.put("stop_battles") { _ -> npc.battleIds.forEach { BattleRegistry.getBattle(it)?.stop() } }
map.put("is_doing_activity") { params ->
val identifiers = params.params.map { it.asString().asIdentifierDefaultingNamespace(namespace = "minecraft") }
val activities = identifiers.mapNotNull { identifier -> BuiltInRegistries.ACTIVITY.get(identifier) }
if (activities.isNotEmpty()) {
return@put DoubleValue(activities.any { activity -> npc.brain.isActive(activity) })
} else {
return@put DoubleValue.ZERO
}
}
map.put("run_script_on_client") { params ->
val world = npc.level()
if (world is ServerLevel) {
@@ -1905,6 +1900,9 @@ object MoLangFunctions {
map.put("is_holding_item") { DoubleValue(!pokemonEntity.entityData.get(PokemonEntity.SHOWN_HELD_ITEM).let {
it.isEmpty || it.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) || it.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS)
}) }
map.put("riding_style") {
StringValue(pokemonEntity.ifRidingAvailableSupply("") { behaviour, settings, state -> behaviour.getRidingStyle(settings, state).name })
}
map.put("is_wearing_hat") { DoubleValue(pokemonEntity.entityData.get(PokemonEntity.SHOWN_HELD_ITEM).`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS)) }
map.put("is_wearing_face") { DoubleValue(pokemonEntity.entityData.get(PokemonEntity.SHOWN_HELD_ITEM).`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS)) }
map.put("is_pastured") {
@@ -53,8 +53,7 @@ open class MoveTemplate(
val pp: Int,
val priority: Int,
val critRatio: Double,
val effectChances: Array<Double>,
val actionEffect: ActionEffectTimeline?
val effectChances: Array<Double>
) {
val struct: MoStruct by lazy {
QueryStruct(hashMapOf())
@@ -87,8 +86,7 @@ open class MoveTemplate(
pp = 5,
priority = 0,
critRatio = 0.0,
effectChances = emptyArray(),
actionEffect = null
effectChances = emptyArray()
)
companion object {
@@ -92,18 +92,7 @@ object Moves : DataRegistry {
effectChances += secondaryMember.get("chance").asDouble
}
}
val actionEffect = ActionEffects.actionEffects[id.asIdentifierDefaultingNamespace()]
?: run {
ActionEffects.actionEffects["generic_move".asIdentifierDefaultingNamespace()]
// if (damageCategory == DamageCategories.STATUS) {
// ActionEffects.actionEffects[cobblemonResource("status")]
// } else {
// val type = elementalType.name.lowercase()
// val category = damageCategory.name.lowercase()
// ActionEffects.actionEffects["${category}_$type".asIdentifierDefaultingNamespace()]
// }
}
val move = MoveTemplate(id, num, elementalType, damageCategory, power, target, accuracy, pp, priority, critRatio, effectChances.toTypedArray(), actionEffect)
val move = MoveTemplate(id, num, elementalType, damageCategory, power, target, accuracy, pp, priority, critRatio, effectChances.toTypedArray())
this.register(move)
} catch (e: Exception) {
Cobblemon.LOGGER.error("Caught exception trying to resolve the move '{}'", id, e)
@@ -14,6 +14,7 @@ import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.animations.keyframes.*
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.pokemon.adapters.CobblemonStatTypeAdapter
import com.cobblemon.mod.common.util.adapters.*
import com.cobblemon.mod.common.util.cobblemonResource
@@ -91,5 +92,11 @@ object ActionEffects : JsonDataRegistry<ActionEffectTimeline> {
observable.emit(this)
}
fun getEffectWithBattleContext(id: ResourceLocation, pokemon: BattlePokemon): ActionEffectTimeline? {
val species = pokemon.entity?.exposedSpecies ?: pokemon.effectedPokemon.species
val contextedEffect = actionEffects[ResourceLocation.fromNamespaceAndPath(id.namespace, id.path + "_" + species)]
return contextedEffect ?: actionEffects[id]
}
override fun sync(player: ServerPlayer) {}
}
@@ -10,13 +10,17 @@ package com.cobblemon.mod.common.api.orientation
import com.cobblemon.mod.common.util.math.geometry.toDegrees
import com.cobblemon.mod.common.util.math.geometry.toRadians
import com.mojang.blaze3d.Blaze3D
import net.minecraft.util.Mth
import net.minecraft.world.entity.LivingEntity
import org.joml.AxisAngle4f
import org.joml.Matrix3f
import org.joml.Quaternionf
import org.joml.Vector3f
import kotlin.math.abs
import kotlin.math.asin
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sign
open class OrientationController(val entity: LivingEntity) {
@@ -35,6 +39,11 @@ open class OrientationController(val entity: LivingEntity) {
private var renderOrientationO: Matrix3f? = null
private var renderOrientation: Matrix3f? = null
private var angularVelocity: Vector3f = Vector3f(0.0f, 0.0f, 0.0f)
private var lastHandledRotationTime = Double.MIN_VALUE
private var frameTime = 0.0
/** Adding this simply because it irritates me seeing `getActive` when most of this is Java. */
fun isActive() = active
@@ -63,7 +72,7 @@ open class OrientationController(val entity: LivingEntity) {
}
fun getRenderOrientation(delta: Float): Quaternionf {
val old = renderOrientationO ?: renderOrientation ?: orientation
val old = renderOrientationO ?: renderOrientation ?: orientation ?: Matrix3f()
val new = renderOrientation ?: old
val oldQuat = Quaternionf().setFromUnnormalized(old)
val newQuat = Quaternionf().setFromUnnormalized(new)
@@ -72,18 +81,93 @@ open class OrientationController(val entity: LivingEntity) {
}
fun tick() {
if (!active || orientation == null) return
renderOrientationO = renderOrientation
val current = orientation
val renderMatrix = this.renderOrientation ?: current
val renderQuat = Quaternionf().setFromUnnormalized(renderMatrix)
val targetQuat = Quaternionf().setFromUnnormalized(current)
val dampingFactor = 0.66f // We can change this factor for faster transitions
renderQuat.slerp(targetQuat, dampingFactor)
// val d = Blaze3D.getTime();
// this.frameTime =
// if (this.lastHandledRotationTime != Double.MIN_VALUE) d - this.lastHandledRotationTime else 0.0;
// this.lastHandledRotationTime = d;
//
// if (!active || orientation == null) return
//
// // Find the desired angular velocity
// // lerp the current ang vel towards the desired at a set rate (does this need to take into account frametime?
// val current = orientation
// val renderMatrix = this.renderOrientation ?: Matrix3f()
// val renderQuat = Quaternionf().setFromUnnormalized(renderMatrix)
// val targetQuat = Quaternionf().setFromUnnormalized(current)
//
// // Max rate of angular change in radians per second
// val maxAngVel = Math.toRadians(180.0).toFloat()
//
// // Find the difference between the current render orientation and the target orientation
// val qDiff: Quaternionf = targetQuat.mul(renderQuat.invert())
//
// // Construct the target rate of change from the difference quat
// val axAng = qDiff.get(AxisAngle4f())
// val capdRate = axAng.angle.coerceIn(-maxAngVel, maxAngVel)
// val targetAngVel = Vector3f(
// axAng.x,
// axAng.y,
// axAng.z
// ).mul(capdRate)
//
// // lerp the current angular velocity towards the target
// val k = 10.0f // Some smoothing constant k
// val smoothingFactor = Math.min(1.0f, this.frameTime.toFloat() * k)
// angularVelocity = angularVelocity.lerp(targetAngVel, smoothingFactor)
//
// // Use the angular velocity to slerp the renderQuat toward the targetQuat
// val frameDisplacement =
// angularVelocity.mul(this.frameTime.toFloat()) // Angular displacement happening on this frame
//
// val fAng = frameDisplacement.length() // Angle of displacement for this given pass/frame
// if (fAng > 0.0001f) { // Ensure that the angle is significant enough that its worth normalizing and carrying out rotation
// val fAx = frameDisplacement.normalize()
// // Construct a rotation quat from the
// val rotQuat = Quaternionf().fromAxisAngleRad(fAx, fAng)
//
// // Set the new orientation post frame stepped rotation.
// renderOrientation = Matrix3f().set(renderQuat.mul(rotQuat))
// }
//
//// this.renderOrientation = orientation
//
// // apply the new current angular velocity to the current render orientation
// // construct a quat out of the angular velocity
// // slerp the quat with a value of frametime
val newRenderOrientation = Matrix3f()
renderQuat.get(newRenderOrientation)
renderOrientation = newRenderOrientation
val d = Blaze3D.getTime();
this.frameTime = if (this.lastHandledRotationTime != Double.MIN_VALUE) d - this.lastHandledRotationTime else 0.0;
this.lastHandledRotationTime = d;
if (!active || orientation == null) return
val current = orientation
val renderMatrix = this.renderOrientation ?: Matrix3f()
// Create mutable copies for manipulation
val renderQuat = Quaternionf().setFromUnnormalized(renderMatrix)
var targetQuat = Quaternionf().setFromUnnormalized(current)
// --- Shortest Path Check (Still mandatory for Slerp) ---
if (renderQuat.dot(targetQuat) < 0) {
targetQuat = targetQuat.invert()
}
// --------------------------------------------------------
// K is the speed (rad/s) we want to close the gap at.
val k_rate = 10.0f // E.g., target 10 units/second closing rate.
// Calculate the frame-rate independent alpha (t) for Slerp.
// This makes the smoothing constant 'k_rate' a rate of change, not an arbitrary alpha value.
val slerpAlpha = 1.0f - Math.exp(-(k_rate * this.frameTime)).toFloat()
// Use Slerp to interpolate the renderQuat towards the targetQuat
val newRenderQuat = renderQuat.slerp(targetQuat, slerpAlpha, Quaternionf())
// Set the new orientation
renderOrientation = Matrix3f().set(newRenderQuat)
// Angular velocity is now redundant for rendering, but could be used for particle effects, etc.
}
val forwardVector: Vector3f
@@ -170,11 +170,8 @@ object Natures {
* Utility method to get a nature by string
* @return a nature type or null
*/
fun getNature(identifier: String): Nature? {
val nature = getNature(cobblemonResource(identifier))
if(nature != null) return nature
return getNature(ResourceLocation.parse(identifier))
}
fun getNature(identifier: String) = getNature(cobblemonResource(identifier))
?: getNature(ResourceLocation.parse(identifier))
/**
* Helper function for a random Nature
@@ -806,8 +806,8 @@ open class PokemonProperties {
return PokemonProperties().loadFromJSON(this.saveToJSON())
}
// If the config value is at least 1, then do 1/x and use that as the property chance
private fun Float.checkRate(): Boolean = if (this >= 1) (Random.Default.nextFloat() < 1 / this) else Random.Default.nextFloat() < this
// If the config value is at least 0, then do 1/x and use that as the property chance
private fun Float.checkRate(): Boolean = this > 0 && (Random.nextFloat() < 1 / this)
/**
* Attempts to find an ability by ID and resolve if it should be forced or if it's legal for the given form.
@@ -11,7 +11,7 @@ package com.cobblemon.mod.common.api.pokemon.aspect
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.api.ai.config.task.PollinateFlowerTaskConfig
import com.cobblemon.mod.common.api.spawning.influence.SaccharineLogSlatheredInfluence
import com.cobblemon.mod.common.api.spawning.prospecting.LurePokeSnackDetector
import com.cobblemon.mod.common.block.entity.PokeSnackBlockEntity
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
import net.minecraft.core.particles.BlockParticleOption
import net.minecraft.core.particles.ParticleOptions
@@ -23,7 +23,7 @@ import net.minecraft.resources.ResourceLocation
*/
val aspectParticleMap: Map<String, ParticleData> = mapOf(
SaccharineLogSlatheredInfluence.HONEY_DRENCHED_ASPECT to ParticleData.MinecraftParticle(ParticleTypes.FALLING_HONEY, 0.075, 1),
LurePokeSnackDetector.POKE_SNACK_CRUMBED_ASPECT to ParticleData.MinecraftParticle(
PokeSnackBlockEntity.POKE_SNACK_CRUMBED_ASPECT to ParticleData.MinecraftParticle(
BlockParticleOption(
ParticleTypes.BLOCK,
CobblemonBlocks.POKE_SNACK.defaultBlockState()
@@ -59,7 +59,7 @@ class RidingController(
val newKey = behaviourSettings.key
for (passenger in entity.passengers) {
if (passenger is Player && passenger !is ServerPlayer) {
MountedCameraTypeHandler.handleTransition(previousKey ?: continue, newKey)
MountedCameraTypeHandler.handleTransition(passenger, previousKey ?: continue, newKey)
}
}
@@ -295,32 +295,6 @@ class JetBehaviour : RidingBehaviour<JetSettings, JetState> {
mouseXSmoother: SmoothDouble,
sensitivity: Double,
deltaTime: Double
): Vec3 {
return rollRotation(
settings,
state,
vehicle,
driver,
mouseY,
mouseX,
mouseYSmoother,
mouseXSmoother,
sensitivity,
deltaTime
)
}
fun rollRotation(
settings: JetSettings,
state: JetState,
vehicle: PokemonEntity,
driver: Player,
mouseY: Double,
mouseX: Double,
mouseYSmoother: SmoothDouble,
mouseXSmoother: SmoothDouble,
sensitivity: Double,
deltaTime: Double
): Vec3 {
if (vehicle !is OrientationControllable) return Vec3.ZERO
@@ -358,32 +358,7 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
} else {
state.noInputTime.set(0.0)
}
return rollRotation(
settings,
state,
vehicle,
driver,
mouseY,
mouseX,
mouseYSmoother,
mouseXSmoother,
sensitivity,
deltaTime
)
}
fun rollRotation(
settings: DolphinSettings,
state: DolphinState,
vehicle: PokemonEntity,
driver: Player,
mouseY: Double,
mouseX: Double,
mouseYSmoother: SmoothDouble,
mouseXSmoother: SmoothDouble,
sensitivity: Double,
deltaTime: Double
): Vec3 {
if (vehicle !is OrientationControllable) return Vec3.ZERO
val controller = (vehicle as OrientationControllable).orientationController
val topSpeed = vehicle.runtime.resolveDouble(settings.speedExpr ?: globalDolphin.speedExpr!!) / 20.0
@@ -57,9 +57,9 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
}
val poseProvider = PoseProvider<SubmarineSettings, SubmarineState>(PoseType.FLOAT)
.with(PoseOption(PoseType.SWIM) { _, state, entity -> state.rideVelocity.get().z > 0.05 && !state.onSurface.get()})
.with(PoseOption(PoseType.STAND) { _, state, entity -> state.rideVelocity.get().z <= 0.05 && state.onSurface.get()})
.with(PoseOption(PoseType.STAND) { _, state, entity -> state.rideVelocity.get().z > 0.05 && state.onSurface.get()})
.with(PoseOption(PoseType.SWIM) { _, state, entity -> abs(state.rideVelocity.get().z) > 0.05 && !state.onSurface.get()})
.with(PoseOption(PoseType.STAND) { _, state, entity -> abs(state.rideVelocity.get().z) <= 0.05 && state.onSurface.get()})
.with(PoseOption(PoseType.STAND) { _, state, entity -> abs(state.rideVelocity.get().z) > 0.05 && state.onSurface.get()})
override fun isActive(settings: SubmarineSettings, state: SubmarineState, vehicle: PokemonEntity): Boolean {
return vehicle.isInWater || !vehicle.onGround()
@@ -123,15 +123,13 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
return
}
// if it is then check half the bbheight above and below the center point.
val hitboxBreachPercentage = vehicle.runtime.resolveDouble(settings.hitboxBreachPercentage ?: globalSubmarine.hitboxBreachPercentage!!)
// If the point above is air and the point below is water then you know to trend towards on surface.
// Grab the center position of the vehicle (vehicle.position.add(bbheight/2)
val vehicleCenter = vehicle.position().add(0.0, vehicle.bbHeight/2.0, 0.0)
// Grab the position of the vehicle that when out of the water is considered breached
val vehicleBreachPoint = vehicle.position().add(0.0, vehicle.bbHeight * hitboxBreachPercentage, 0.0)
// If the block above the center is not fluid but this one is then move the center
val centerBlockPos = BlockPos.containing(vehicleCenter)
val centerBlockPos = BlockPos.containing(vehicleBreachPoint)
val centerInWater = !vehicle.level().getFluidState(centerBlockPos).isEmpty
val abovePos = centerBlockPos.above()
@@ -561,6 +559,10 @@ class SubmarineSettings : RidingBehaviourSettings {
var staminaRegenTime: Expression? = null
private set
// Percentage (0.0->1.0) of ride needing to be breached out of the water to be considered surfaced.
var hitboxBreachPercentage: Expression? = null
private set
// Tope strafe speed in Bl/s
var jumpExpr: Expression? = null
// Yaw rate in degrees per second
@@ -579,6 +581,7 @@ class SubmarineSettings : RidingBehaviourSettings {
rideSounds.encode(buffer)
buffer.writeNullableExpression(infiniteStamina)
buffer.writeNullableExpression(staminaRegenTime)
buffer.writeNullableExpression(hitboxBreachPercentage)
buffer.writeNullableExpression(jumpExpr)
buffer.writeNullableExpression(handlingExpr)
buffer.writeNullableExpression(speedExpr)
@@ -591,6 +594,7 @@ class SubmarineSettings : RidingBehaviourSettings {
rideSounds = RideSoundSettingsList.decode(buffer)
infiniteStamina = buffer.readNullableExpression()
staminaRegenTime = buffer.readNullableExpression()
hitboxBreachPercentage = buffer.readNullableExpression()
jumpExpr = buffer.readNullableExpression()
handlingExpr = buffer.readNullableExpression()
speedExpr = buffer.readNullableExpression()
@@ -18,18 +18,11 @@ import com.cobblemon.mod.common.api.spawning.condition.SeafloorSpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.SpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.SubmergedSpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.SurfaceSpawningCondition
import com.cobblemon.mod.common.api.spawning.position.calculators.GroundedSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.LavafloorSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SeafloorSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SubmergedSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SurfaceSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.detail.NPCSpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.PokemonHerdSpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.fishing.FishingSpawner
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
@@ -39,16 +32,21 @@ import com.cobblemon.mod.common.api.spawning.position.SeafloorSpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.SubmergedSpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.SurfaceSpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.calculators.GroundedSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.LavafloorSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SeafloorSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SubmergedSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SurfaceSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.preset.BasicSpawnDetailPreset
import com.cobblemon.mod.common.api.spawning.preset.BestSpawnerConfig
import com.cobblemon.mod.common.api.spawning.preset.PokemonSpawnDetailPreset
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import com.cobblemon.mod.common.api.spawning.spawner.AreaSpawner
import com.cobblemon.mod.common.api.spawning.spawner.BasicSpawner
import com.cobblemon.mod.common.api.spawning.spawner.FixedAreaSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.TickingSpawner
import com.cobblemon.mod.common.entity.pokemon.CobblemonAgingDespawner
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import net.minecraft.server.MinecraftServer
@@ -66,14 +64,13 @@ import net.minecraft.server.MinecraftServer
* In the case of more specialized use, the creation of a [SpawnablePosition] that motivates most of the spawn
* process can be created manually, skipping the first two steps.
*
* An individually spawnable entity is defined as a [SpawnDetail]. A processor handling this process is a [Spawner].
* Various subclasses exist for more specialized cases. A spawner that is constantly ticking and will spawn things
* without prompts is a [TickingSpawner], and one of those which occurs within a defined area is a [AreaSpawner]. If
* An individually spawnable thing is defined as a [SpawnDetail]. A processor handling this process is a [Spawner].
* Some subclasses exist for more specialized cases. If you are spawning in a fixed area and the spawner controlling
* that area is unmoving then it is a [FixedAreaSpawner] whereas if it is actively following the player it is a
* [PlayerSpawner].
* [PlayerSpawner]. You can also use a [BasicSpawner] for more general-purpose spawning without any issue.
*
* Spawning is coordinated and ticked using a [SpawnerManager], and all the current managers are accessible from
* [BestSpawner.spawnerManagers].
* PlayerSpawners are stored inside ServerPlayers using a mixin and tick at the end of a player's tick. The [PlayerSpawnerFactory]
* is used to build those. An extension function exists for getting the spawner for a player.
*
* Spawners and spawnable positions are often put under the effects of [SpawningInfluence]s which can be used to make
* temporary or lasting changes to spawning for whatever component they are attached to (whether that is a spawner or a
@@ -87,9 +84,9 @@ import net.minecraft.server.MinecraftServer
*/
object BestSpawner {
var config = BestSpawnerConfig()
val spawnerManagers = mutableListOf<SpawnerManager>(CobblemonWorldSpawnerManager)
lateinit var defaultPokemonDespawner: Despawner<PokemonEntity>
lateinit var fishingSpawner: FishingSpawner
lateinit var fishingSpawner: BasicSpawner
fun init() {
LOGGER.info("Starting the Best Spawner...")
@@ -137,12 +134,13 @@ object BestSpawner {
fun reloadConfig() {
loadConfig()
spawnerManagers.forEach(SpawnerManager::onConfigReload)
}
fun onServerStarted(server: MinecraftServer) {
CobblemonSpawnPools.onServerLoad(server)
spawnerManagers.forEach(SpawnerManager::onServerStarted)
fishingSpawner = FishingSpawner()
fishingSpawner = BasicSpawner(
name = "fishing",
spawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL
)
}
}
@@ -40,7 +40,6 @@ object CobblemonSpawnPools {
fun onServerLoad(server: MinecraftServer) {
Cobblemon.LOGGER.info("Optimizing spawn pools...")
WORLD_SPAWN_POOL.forEach { it.onServerLoad(server) }
WORLD_SPAWN_POOL.precalculate()
WORLD_SPAWN_POOL.onServerLoad(server)
}
}
@@ -1,76 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
import net.minecraft.server.level.ServerLevel
import java.util.UUID
import net.minecraft.server.level.ServerPlayer
/**
* The [SpawnerManager] that handles Cobblemon's world spawner, which is made up
* of [PlayerSpawner] instances. This manager listens for login and logout and
* cleans up where relevant.
*
* @author Hiroku
* @since February 14th, 2022
*/
object CobblemonWorldSpawnerManager : SpawnerManager() {
var spawnersForPlayers = mutableMapOf<UUID, PlayerSpawner>()
init {
PlatformEvents.SERVER_PLAYER_LOGIN.subscribe { this.onPlayerLogin(it.player) }
PlatformEvents.SERVER_PLAYER_LOGOUT.subscribe { this.onPlayerLogout(it.player) }
PlatformEvents.CHANGE_DIMENSION.subscribe { this.onDimensionChange(it.player, it.origin, it.destination) }
}
fun onPlayerLogin(player: ServerPlayer) {
// Disables spawning
if (!Cobblemon.config.enableSpawning || player.level().gameRules.getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING) == false) {
return;
}
registerPlayerSpawner(player)
}
fun onPlayerLogout(player: ServerPlayer) {
unregisterPlayerSpawner(player)
}
fun onDimensionChange(player: ServerPlayer, origin: ServerLevel?, destination: ServerLevel?) {
fun getSpawnRule(level: ServerLevel?) = level?.gameRules?.getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING)
if (!Cobblemon.config.enableSpawning || getSpawnRule(origin) == getSpawnRule(destination)) {
return
}
val spawnerAction = if (destination?.gameRules?.getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING) == false) {
::unregisterPlayerSpawner
} else {
::registerPlayerSpawner
}
spawnerAction(player)
}
private fun unregisterPlayerSpawner(player: ServerPlayer) {
spawnersForPlayers.remove(player.uuid)?.let { unregisterSpawner(it) }
}
private fun registerPlayerSpawner(player: ServerPlayer) {
PlayerSpawnerFactory.create(this, player).also {
spawnersForPlayers[player.uuid] = it
registerSpawner(it)
}
}
}
@@ -17,16 +17,21 @@ import net.minecraft.world.entity.EntityType
open class SpawnCause(
val spawner: Spawner,
entity: Entity? = null
): SpawningInfluence {
) : SpawningInfluence {
val entityWorldId = entity?.level()?.dimension()
val entityId = entity?.id
val entityUUID = entity?.uuid
val entityType = entity?.type
val entity: Entity?
get() = if (entityType == EntityType.PLAYER) {
server()?.playerList?.getPlayer(entityUUID!!)
} else {
server()?.getLevel(entityWorldId!!)?.getEntity(entityId!!)
}
get() =
if (entityType == EntityType.PLAYER) {
entityUUID?.let {
server()?.playerList?.getPlayer(it)
}
} else if (entityWorldId != null && entityId != null) {
server()?.getLevel(entityWorldId)?.getEntity(entityId)
} else {
null
}
}
@@ -1,85 +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.spawning
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.TickingSpawner
import com.cobblemon.mod.common.util.server
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules.DO_POKEMON_SPAWNING
/**
* A manager of various spawners. This is a class in which you should register
* ticking spawners so that they can be automatically ticked by the server.
* This also supports registering [SpawningInfluence]s to apply to each spawner
* that is registered.
*
* @author Hiroku
* @since February 5th, 2022
*/
open class SpawnerManager {
val spawners = mutableListOf<Spawner>()
get() {
if (server()?.isSameThread == false) {
Cobblemon.LOGGER.error("Illegal access to spawners list from non-server thread!")
Exception().printStackTrace()
}
return field
}
val influences = mutableListOf<SpawningInfluence>()
get() {
if (server()?.isSameThread == false) {
Cobblemon.LOGGER.error("Illegal access to influences from non-server thread!")
Exception().printStackTrace()
}
return field
}
inline fun <reified T : Spawner> getSpawnersOfType() = spawners.filterIsInstance<T>()
open fun getSpawnerByName(name: String) = spawners.find { it.name == name }
open fun registerSpawner(spawner: Spawner) {
spawners.add(spawner)
if (spawner !is TickingSpawner) {
spawner.influences.addAll(influences)
}
}
open fun unregisterSpawner(spawner: Spawner) {
spawners.remove(spawner)
if (spawner !is TickingSpawner) {
spawner.influences.removeAll(influences)
}
}
open fun onConfigReload() {
spawners.filterIsInstance<PlayerSpawner>().forEach {
it.ticksBetweenSpawns = Cobblemon.config.ticksBetweenSpawnAttempts
}
}
open fun onServerStarted() {
spawners.clear()
}
open fun onServerTick() {
// Disables spawning
if (!Cobblemon.config.enableSpawning) {
return
}
influences.removeIf { it.isExpired() }
getSpawnersOfType<TickingSpawner>().forEach {
if (it.getCauseEntity()?.level()?.gameRules?.getBoolean(DO_POKEMON_SPAWNING) == false) {
return@forEach
}
it.tick()
}
}
}
@@ -33,15 +33,15 @@ class TimeRange : IntRanges {
"any" to TimeRange(0..23999),
"day" to TimeRange(23460..23999, 0..12541),
"night" to TimeRange(12542..23459),
"noon" to TimeRange(5000..6999),
"midnight" to TimeRange(17000..18999),
"dawn" to TimeRange(22300..23999, 0..999),
"dusk" to TimeRange(11834..13701),
"twilight" to TimeRange(11834..13701, 22300..23999, 0..166),
"morning" to TimeRange(23000..23999, 0..4999),
"noon" to TimeRange(5000..6999),
"afternoon" to TimeRange(7000..12999),
"predawn" to TimeRange(19000..22999),
"evening" to TimeRange(13000..16999)
"evening" to TimeRange(13000..16999),
"midnight" to TimeRange(17000..18999),
"predawn" to TimeRange(19000..22999),
"dawn" to TimeRange(22300..23999, 0..166),
"dusk" to TimeRange(11834..13701),
"twilight" to TimeRange(11834..13701, 22300..23999, 0..166)
)
}
@@ -39,9 +39,13 @@ import net.minecraft.server.MinecraftServer
*/
abstract class SpawnDetail : ModDependant {
companion object {
val pokemonTypes = mutableListOf<String>()
val spawnDetailTypes = mutableMapOf<String, RegisteredSpawnDetail<*>>()
fun <T : SpawnDetail> registerSpawnType(name: String, detailClass: Class<T>) {
spawnDetailTypes[name] = RegisteredSpawnDetail(detailClass)
if (detailClass == PokemonSpawnDetail::class.java || detailClass == PokemonHerdSpawnDetail::class.java) {
pokemonTypes.add(name)
}
}
}
@@ -24,6 +24,7 @@ import com.cobblemon.mod.common.util.server
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
@@ -72,6 +73,13 @@ class SpawnPool(val name: String) : JsonDataRegistry<SpawnSet>, Iterable<SpawnDe
return this
}
fun onServerLoad(server: MinecraftServer) {
for (detail in details) {
detail.onServerLoad(server)
}
precalculate()
}
/**
* Precalculates spawns into hash mappings using the [precalculators] included
* in this pool as well as the range of spawnable positions mentioned in the pool.
@@ -12,16 +12,15 @@ import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.fishing.BobberSpawnPokemonEvent
import com.cobblemon.mod.common.api.fishing.SpawnBaitEffects
import com.cobblemon.mod.common.api.fishing.SpawnBait
import com.cobblemon.mod.common.api.fishing.SpawnBaitEffects
import com.cobblemon.mod.common.api.pokemon.Natures
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
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.influence.SpawnBaitInfluence
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.item.interactive.PokerodItem
@@ -1,38 +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.spawning.fishing
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.spawner.TriggerSpawner
import net.minecraft.server.level.ServerLevel
import net.minecraft.core.BlockPos
/**
* A spawner that takes in a [FishingSpawnCause] and spawns things.
*
* @author Hiroku
* @since February 3rd, 2024
*/
open class FishingSpawner(
override val name: String = "fishing",
pool: SpawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL
) : TriggerSpawner<FishingSpawnCause>(name, pool) {
override fun parseSpawnablePosition(cause: FishingSpawnCause, world: ServerLevel, pos: BlockPos): FishingSpawnablePosition? {
// Maybe confirm that it's water we're fishing in?
val spawnablePosition = FishingSpawnablePosition(
cause = cause,
world = world,
pos = pos,
influences = influences
)
return spawnablePosition
}
}
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.influence
import com.cobblemon.mod.common.api.spawning.SpawnBucket
/**
* A [SpawningInfluence] that multiplies bucket weights by the provided values
* and then normalizes the resulting weights so that their total adds up to 100.
*/
class BucketMultiplyingInfluence(
val multipliers: Map<String, Float>,
) : SpawningInfluence {
override fun affectBucketWeights(bucketWeights: MutableMap<SpawnBucket, Float>) {
val updates = mutableMapOf<SpawnBucket, Float>()
for ((bucket, weight) in bucketWeights) {
val multiplier = multipliers[bucket.name]
if (multiplier != null) {
updates[bucket] = weight * multiplier
}
}
for ((bucket, newWeight) in updates) {
bucketWeights[bucket] = newWeight
}
normalizeBucketWeights(bucketWeights)
}
}
@@ -44,11 +44,6 @@ open class BucketNormalizingInfluence(
bucketWeights[it] = weight.toDouble().pow(1 / normalizationFactor).toFloat()
}
val sum = bucketWeights.values.sum()
val to100 = 100F / sum
bucketWeights.keys.toList().forEach {
val weight = bucketWeights[it] ?: return@forEach
bucketWeights[it] = weight * to100
}
normalizeBucketWeights(bucketWeights)
}
}
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.api.spawning.influence
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.api.Priority
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.fishing.FishingSpawnCause
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
@@ -43,6 +44,10 @@ class SaccharineLogSlatheredInfluence(val pos: BlockPos? = null) : SpawningInflu
override fun affectSpawn(action: SpawnAction<*>, entity: Entity) {
if (entity is PokemonEntity) {
// avoid double application or applying to a pokemon that already has a hidden ability
if (entity.pokemon.forcedAspects.contains(HONEY_DRENCHED_ASPECT)
|| entity.pokemon.ability.priority !== Priority.LOWEST) return
if (Math.random() <= HIDDEN_ABILITY_CHANCE) {
FishingSpawnCause.alterHAAttempt(entity)
}
@@ -40,6 +40,15 @@ interface SpawningInfluence {
fun affectSpawn(action: SpawnAction<*>, entity: Entity) {}
/** Applies some influence over the weight of spawn buckets. */
fun affectBucketWeights(bucketWeights: MutableMap<SpawnBucket, Float>) {}
/** Normalizes all bucket weights to add up to 100 */
fun normalizeBucketWeights(bucketWeights: MutableMap<SpawnBucket, Float>) {
val sum = bucketWeights.values.sum()
val to100 = 100F / sum
bucketWeights.keys.toList().forEach {
val weight = bucketWeights[it] ?: return@forEach
bucketWeights[it] = weight * to100
}
}
/** Filters positions that could be usable for a spawnable position calculator */
fun isAllowedPosition(world: ServerLevel, pos: BlockPos, spawnablePositionCalculator: SpawnablePositionCalculator<*, *>): Boolean = true
/**
@@ -9,16 +9,15 @@
package com.cobblemon.mod.common.api.spawning.influence.detector
import com.cobblemon.mod.common.api.spawning.SpawningZone
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.influence.SpawningZoneInfluence
import com.cobblemon.mod.common.api.spawning.prospecting.IncenseSweetDetector
import com.cobblemon.mod.common.api.spawning.prospecting.SaccharineLogSlatheredDetector
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.level.block.state.BlockState
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.prospecting.IncenseSweetDetector
import com.cobblemon.mod.common.api.spawning.prospecting.LurePokeSnackDetector
import com.cobblemon.mod.common.api.spawning.prospecting.SaccharineLogSlatheredDetector
/**
* Prospects for [SpawningZoneInfluence] in a world at a given position. Occurs as part of a
@@ -31,7 +30,6 @@ interface SpawningInfluenceDetector {
companion object {
@JvmStatic
val detectors = mutableSetOf<SpawningInfluenceDetector>(
LurePokeSnackDetector,
SaccharineLogSlatheredDetector,
IncenseSweetDetector
)
@@ -14,13 +14,15 @@ import net.minecraft.server.level.ServerLevel
import net.minecraft.core.BlockPos
/**
* A spawnable position relating to triggered spawning. Uses a single-point structure cache which is
* the only contribution of this implementation.
* A spawnable position relating to single-point spawning. Uses a single-point structure cache which is
* the only contribution of this implementation really. By contrast, the area spawnable position provides
* a structure cache that's shared across the entire zone that was used for spawning, which is great for
* performance. That kind of optimization shouldn't be necessary for single-point spawning.
*
* @author Hiroku
* @since February 3rd, 2024
*/
open class TriggerSpawnablePosition(
open class BasicSpawnablePosition(
override val cause: SpawnCause,
override val world: ServerLevel,
override val position: BlockPos,
@@ -29,6 +31,6 @@ open class TriggerSpawnablePosition(
override val canSeeSky: Boolean,
override val influences: MutableList<SpawningInfluence>
) : SpawnablePosition() {
val triggerStructureCache = StructureChunkCache()
override fun getStructureCache(pos: BlockPos) = triggerStructureCache
val singlePointStructureCache = StructureChunkCache()
override fun getStructureCache(pos: BlockPos) = singlePointStructureCache
}
@@ -28,7 +28,7 @@ class FishingSpawnablePosition(
world: ServerLevel,
pos: BlockPos,
influences: MutableList<SpawningInfluence>
) : TriggerSpawnablePosition(
) : BasicSpawnablePosition(
cause = cause,
world = world,
position = pos,
@@ -36,6 +36,7 @@ interface SpawnablePositionCalculator<I : SpawnablePositionInput, O : SpawnableP
val isLavaCondition: (BlockState) -> Boolean = { it.fluidState.`is`(FluidTags.LAVA) && it.fluidState.isSource }
private val calculators = PrioritizedList<SpawnablePositionCalculator<*, *>>()
val prioritizedAreaCalculators: List<AreaSpawnablePositionCalculator<*>>
get() = calculators.filterIsInstance<AreaSpawnablePositionCalculator<*>>()
@@ -1,189 +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.spawning.prospecting
import com.cobblemon.mod.common.CobblemonPoiTypes
import com.cobblemon.mod.common.api.fishing.SpawnBait
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpatialSpawningZoneInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawnBaitInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawningZoneInfluence
import com.cobblemon.mod.common.api.spawning.influence.UnconditionalSpawningZoneInfluence
import com.cobblemon.mod.common.api.spawning.influence.detector.SpawningInfluenceDetector
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput
import com.cobblemon.mod.common.block.PokeSnackBlock
import com.cobblemon.mod.common.block.entity.PokeSnackBlockEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.effect.PokeSnackBlockParticlesPacket
import com.cobblemon.mod.common.util.math.pow
import com.cobblemon.mod.common.util.toBlockPos
import kotlin.math.ceil
import kotlin.math.sqrt
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.core.Holder
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.ai.village.poi.PoiManager
import net.minecraft.world.entity.ai.village.poi.PoiType
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.phys.Vec3
import kotlin.math.abs
object LurePokeSnackDetector : SpawningInfluenceDetector {
@JvmField
val RANGE: Int = 48
@JvmField
val POKE_SNACK_CRUMBED_ASPECT = "poke_snack_crumbed"
override fun detectFromInput(spawner: Spawner, input: SpawningZoneInput): MutableList<SpawningZoneInfluence> {
val world = input.world
val listOfInfluences = mutableListOf<SpawningZoneInfluence>()
val searchRange = RANGE + ceil(sqrt(((input.length pow 2) + (input.width pow 2)).toDouble())).toInt()
val lurePokeSnackPositions = world.poiManager.findAll(
{ holder: Holder<PoiType> -> holder.`is`(CobblemonPoiTypes.POKE_SNACK_KEY) },
{ true },
input.getCenter().toBlockPos(),
searchRange,
PoiManager.Occupancy.ANY
).toList()
var highestLureTier = 0
for (lurePokeSnackPos in lurePokeSnackPositions) {
val blockState = world.getBlockState(lurePokeSnackPos)
if (blockState.block !is PokeSnackBlock) continue
val blockEntity = world.getBlockEntity(lurePokeSnackPos) as? PokeSnackBlockEntity ?: continue
val baitEffects = blockEntity.getBaitEffects()
highestLureTier = maxOf(highestLureTier, baitEffects.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }.maxOfOrNull { it.value }?.toInt() ?: 0)
listOfInfluences.add(
SpatialSpawningZoneInfluence(
pos = lurePokeSnackPos,
radius = RANGE.toFloat(),
influence = SpawnBaitInfluence(
effects = baitEffects,
onUsed = { usedAmount, entity ->
entity?.let {
// Prevent triggering twice on same entity
if (usedAmount % 2 == 0) {
tryBite(world, lurePokeSnackPos)
moveAndApplyAspect(world, lurePokeSnackPos, entity)
}
}
}
)
)
)
}
if (highestLureTier > 0) {
listOfInfluences.add(UnconditionalSpawningZoneInfluence(influence = BucketNormalizingInfluence(tier = highestLureTier)))
}
return listOfInfluences
}
private fun tryBite(level: ServerLevel, blockPos: BlockPos) {
if (level.isLoaded(blockPos)) {
val blockState = level.getBlockState(blockPos)
val block = blockState.block as? PokeSnackBlock ?: return
block.eat(level, blockPos, blockState, null)
}
}
private fun moveAndApplyAspect(level: ServerLevel, blockPos: BlockPos, entity: PokemonEntity?) {
entity?.let {
entity.pokemon.forcedAspects += POKE_SNACK_CRUMBED_ASPECT
attemptSafeMove(level, blockPos, entity)
}
}
fun attemptSafeMove(level: ServerLevel, startPos: BlockPos, entity: PokemonEntity) {
val inWater = entity.isInWater
val maxDistance = 10 // maximum search radius
val maxPositions = 10 // maximum number of positions at the closest distance
var foundPositions: List<BlockPos> = emptyList()
// Search layer by layer, starting from the closest
for (distance in 1..maxDistance) {
val positionsAtThisDistance = mutableListOf<BlockPos>()
for (dx in -distance..distance) {
for (dy in -distance..distance) {
for (dz in -distance..distance) {
// Consider only positions on the surface of the cube at current distance
if (listOf(dx, dy, dz).count { abs(it) != 0 } == distance) {
val candidatePos = startPos.offset(dx, dy, dz)
if (!candidatePos.equals(startPos.relative(Direction.UP))) {
// Check collision
val currentBox = entity.boundingBox.move(
candidatePos.x - entity.x,
candidatePos.y - entity.y,
candidatePos.z - entity.z
)
if (level.noCollision(entity, currentBox)) {
val blockBelowPos = candidatePos.below()
val blockState = level.getBlockState(blockBelowPos)
val isWater = blockState.block == Blocks.WATER
if (inWater) {
if (isWater) {
positionsAtThisDistance.add(candidatePos)
}
} else {
if (!blockState.isAir) {
positionsAtThisDistance.add(candidatePos)
}
}
}
}
}
}
}
}
if (positionsAtThisDistance.isNotEmpty()) {
// Limit to 10 nearest positions (all at this distance)
foundPositions = if (positionsAtThisDistance.size > maxPositions) {
positionsAtThisDistance.shuffled().take(maxPositions)
} else {
positionsAtThisDistance
}
break // Found the nearest layer with safe positions, stop expanding
}
}
if (foundPositions.isNotEmpty()) {
val chosenPos = foundPositions.random()
val centerX = chosenPos.x + 0.5
val centerZ = chosenPos.z + 0.5
entity.moveTo(Vec3(centerX, chosenPos.y.toDouble(), centerZ))
PokeSnackBlockParticlesPacket(startPos, chosenPos).sendToPlayersAround(
startPos.x.toDouble(),
startPos.y.toDouble(),
startPos.z.toDouble(),
64.0,
level.dimension()
)
}
}
override fun detectFromBlock(level: ServerLevel, pos: BlockPos, blockState: BlockState) = emptyList<SpawningZoneInfluence>()
}
@@ -24,6 +24,11 @@ import com.cobblemon.mod.common.api.spawning.spawner.Spawner
* @since January 31st, 2022
*/
interface SpawningSelector<T : SpawnSelectionData> {
companion object {
@JvmField
val DEFAULT = FlatSpawnablePositionWeightedSelector()
}
fun getSelectionData(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>): T
fun selectSpawnAction(
@@ -32,12 +37,12 @@ interface SpawningSelector<T : SpawnSelectionData> {
selectionData: T
): SpawnAction<*>?
fun select(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>, max: Int): List<SpawnAction<*>> {
fun select(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>, maxSpawns: Int): List<SpawnAction<*>> {
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
val spawnActions = selectionData.spawnActions
while (spawnActions.size < max) {
while (spawnActions.size < maxSpawns) {
val spawnAction = selectSpawnAction(
spawner = spawner,
bucket = bucket,
@@ -1,180 +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.spawning.spawner
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
import com.cobblemon.mod.common.api.spawning.position.calculators.AreaSpawnablePositionCalculator
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator.Companion.prioritizedAreaCalculators
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.isBoxLoaded
import com.cobblemon.mod.common.util.squeezeWithinBounds
import com.cobblemon.mod.common.util.toVec3f
import net.minecraft.core.BlockPos
import net.minecraft.core.SectionPos
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.level.Level
import net.minecraft.world.level.chunk.ChunkAccess
import net.minecraft.world.level.chunk.status.ChunkStatus
import net.minecraft.world.level.pathfinder.PathComputationType
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
/**
* A type of [TickingSpawner] that operates within some area. When this spawner type
* is told to do a spawning action, the subclass can provide a [SpawningZoneInput] to use.
* If a non-null value is returned, the [spawningZoneGenerator] and [spawnablePositionResolver] will be used to
* select a spawn and action it.
*
* Subclasses must implement the function retrieving what area to do the spawning in,
* but otherwise this class is feature-complete.
*
* @author Hiroku
* @since February 5th, 2022
*/
abstract class AreaSpawner(
name: String,
spawns: SpawnPool,
manager: SpawnerManager
) : TickingSpawner(name, spawns, manager) {
abstract fun getZoneInput(cause: SpawnCause): SpawningZoneInput?
companion object {
const val CHUNK_REACH = 3
}
var spawningZoneGenerator: SpawningZoneGenerator = Cobblemon.spawningZoneGenerator
var spawnablePositionResolver: AreaSpawnablePositionResolver = Cobblemon.areaSpawnablePositionResolver
var spawnablePositionCalculators: List<AreaSpawnablePositionCalculator<*>> = prioritizedAreaCalculators
var spawnsPerPass: Int? = null
override fun run(cause: SpawnCause): List<SpawnAction<*>> {
val areaInput = getZoneInput(cause)
val constrainedArea = if (areaInput != null) constrainArea(areaInput) else null
if (constrainedArea != null) {
val areaBox = AABB.ofSize(
Vec3(constrainedArea.getCenter().toVec3f()),
CHUNK_REACH * 16.0 * 2,
1000.0,
CHUNK_REACH * 16.0 * 2
)
if (!constrainedArea.world.isBoxLoaded(areaBox)) {
return emptyList()
}
val numberNearby = constrainedArea.world.getEntitiesOfClass(
PokemonEntity::class.java,
areaBox,
PokemonEntity::countsTowardsSpawnCap
).size
val chunksCovered = CHUNK_REACH * CHUNK_REACH
if (numberNearby.toFloat() / chunksCovered >= Cobblemon.config.pokemonPerChunk) {
return emptyList()
}
//val prospectStart = System.currentTimeMillis()
val zone = spawningZoneGenerator.generate(this, constrainedArea)
//val prospectEnd = System.currentTimeMillis()
val spawnablePositions = spawnablePositionResolver.resolve(this, spawnablePositionCalculators, zone)
val influences = this.getAllInfluences() + zone.unconditionalInfluences
val bucket = chooseBucket(cause, influences)
//val resolveEnd = System.currentTimeMillis()
//val prospectDuration = prospectEnd - prospectStart
//val resolveDuration = resolveEnd - prospectEnd
//println("Prospecting took: $prospectDuration ms. Resolution took: $resolveDuration ms")
// Takes about 3ms on my laptop to prospect, similar to spawnable position resolve - not very good, needs some thought
return getSpawningSelector().select(spawner = this, bucket = bucket, spawnablePositions = spawnablePositions, max = spawnsPerPass ?: Cobblemon.config.maximumSpawnsPerPass)
}
return emptyList()
}
fun isValidStartPoint(world: Level, chunk: ChunkAccess, startPos: BlockPos.MutableBlockPos): Boolean {
val y = startPos.y
if (!world.isLoaded(startPos) || !world.isLoaded(startPos.setY(y + 1))) {
return false
}
val mid = chunk.getBlockState(startPos.setY(y))
val above = chunk.getBlockState(startPos.setY(y + 1))
// Above must be non-solid
if (!above.isPathfindable(PathComputationType.AIR)) {
return false
}
// Position must be non-air
if (mid.isAir) {
return false
}
return true
}
fun constrainArea(area: SpawningZoneInput): SpawningZoneInput? {
val basePos = BlockPos.MutableBlockPos(area.baseX, area.baseY, area.baseZ)
val originalY = area.baseY
val (chunkX, chunkZ) = Pair(SectionPos.blockToSectionCoord(area.baseX), SectionPos.blockToSectionCoord(area.baseZ))
// if the chunk isn't loaded, we don't want to go further & we don't want the getChunk function below to load/create the chunk.
if (!area.world.areEntitiesLoaded(ChunkPos.asLong(chunkX, chunkZ))) return null
val chunk = area.world.getChunk(chunkX, chunkZ, ChunkStatus.FULL) ?: return null
var valid = isValidStartPoint(area.world, chunk, basePos)
if (!valid) {
var offset = 1
do {
if (isValidStartPoint(area.world, chunk, basePos.setY(originalY + offset))) {
valid = true
basePos.y = originalY + offset
break
} else if (isValidStartPoint(area.world, chunk, basePos.setY(originalY - offset))) {
valid = true
basePos.y = originalY - offset
break
}
offset++
} while (offset <= Cobblemon.config.maxVerticalCorrectionBlocks)
}
if (valid) {
val min = area.world.squeezeWithinBounds(basePos)
val max = area.world.squeezeWithinBounds(basePos.move(area.length, area.height, area.width))
if (area.world.isLoaded(min) && area.world.isLoaded(max) &&
min.x < max.x && min.y < max.y && min.z < max.z
) {
return SpawningZoneInput(
cause = area.cause,
world = area.world,
baseX = min.x,
baseY = min.y,
baseZ = min.z,
length = max.x - min.x,
height = max.y - min.y,
width = max.z - min.z
)
}
}
return null
}
}
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
/**
* A basic spawner implementation that uses Cobblemon's default algorithms for zone generation,
* position resolving, and spawn selection.
*
* @author Hiroku
* @since October 27th, 2025
*/
open class BasicSpawner(
override val name: String,
override var spawnPool: SpawnPool,
override var maxPokemonPerChunk: Float = Cobblemon.config.pokemonPerChunk
) : Spawner {
override var selector: SpawningSelector<*> = SpawningSelector.DEFAULT
override var generator: SpawningZoneGenerator = Cobblemon.spawningZoneGenerator
override var resolver: AreaSpawnablePositionResolver = Cobblemon.areaSpawnablePositionResolver
override val influences = mutableListOf<SpawningInfluence>()
}
@@ -8,33 +8,37 @@
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import net.minecraft.server.level.ServerLevel
import net.minecraft.core.BlockPos
/**
* A spawner that works within a fixed area, and ticks on its own. The location
* and size of the area is necessary for construction, but after that this spawner
* can be registered in a [SpawnerManager] and left to its own devices.
* A spawner that works within a fixed area. Instances provide the center point and the radii and then the spawner
* can be told to [run] and it will spawn Pokémon within that area.
*
* @author Hiroku
* @since February 5th, 2022
*/
open class FixedAreaSpawner(
name: String,
spawns: SpawnPool,
manager: SpawnerManager,
spawnPool: SpawnPool,
val world: ServerLevel,
val position: BlockPos,
val horizontalRadius: Int,
val verticalRadius: Int,
override var ticksBetweenSpawns: Float = 20F
) : AreaSpawner(name, spawns, manager) {
override fun getZoneInput(cause: SpawnCause): SpawningZoneInput? {
val basePos = position.offset(-horizontalRadius, -verticalRadius, -horizontalRadius)
maxPokemonPerChunk: Float = Cobblemon.config.pokemonPerChunk
) : BasicSpawner(name, spawnPool, maxPokemonPerChunk) {
fun run(cause: SpawnCause, maxSpawns: Int? = null): List<Any> {
return runForArea(
zoneInput = getZoneInput(cause),
maxSpawns = maxSpawns
)
}
fun getZoneInput(cause: SpawnCause): SpawningZoneInput {
val basePos = position.offset(-horizontalRadius, -verticalRadius, -horizontalRadius)
return SpawningZoneInput(
cause = cause,
world = world,
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.Cobblemon.config
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.util.getPlayer
import com.cobblemon.mod.common.util.nextBetween
@@ -31,11 +30,16 @@ import kotlin.random.Random
* @author Hiroku
* @since February 14th, 2022
*/
class PlayerSpawner(player: ServerPlayer, spawns: SpawnPool, manager: SpawnerManager) : AreaSpawner(player.name.string, spawns, manager) {
open class PlayerSpawner(
player: ServerPlayer,
spawnPool: SpawnPool
) : BasicSpawner(
name = player.name.string,
spawnPool = spawnPool
) {
val uuid: UUID = player.uuid
override var ticksBetweenSpawns = config.ticksBetweenSpawnAttempts
override fun getCauseEntity() = uuid.getPlayer()
override fun getZoneInput(cause: SpawnCause): SpawningZoneInput? {
fun getZoneInput(cause: SpawnCause): SpawningZoneInput? {
val player = uuid.getPlayer() ?: return null
val zoneDiameter = config.spawningZoneDiameter
val zoneHeight = config.spawningZoneHeight
@@ -53,6 +57,7 @@ class PlayerSpawner(player: ServerPlayer, spawns: SpawnPool, manager: SpawnerMan
} else {
thetatemp
}
val x = center.x + r * cos(theta)
val z = center.z + r * sin(theta)
@@ -67,4 +72,22 @@ class PlayerSpawner(player: ServerPlayer, spawns: SpawnPool, manager: SpawnerMan
width = zoneDiameter
)
}
var active = true
var ticksUntilNextSpawn = 100F
var ticksBetweenSpawns: Float = config.ticksBetweenSpawnAttempts
var tickTimerMultiplier = 1F
fun tick() {
if (!active) {
return
}
ticksUntilNextSpawn -= tickTimerMultiplier
if (ticksUntilNextSpawn <= 0) {
val zoneInput = getZoneInput(cause = SpawnCause(spawner = this, entity = uuid.getPlayer()))
zoneInput?.let(::runForArea)
ticksUntilNextSpawn = ticksBetweenSpawns
}
}
}
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnRules
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence.Companion.TYPICAL_VARIATION
@@ -28,7 +27,7 @@ import net.minecraft.server.level.ServerPlayer
* @since February 14th, 2022
*/
object PlayerSpawnerFactory {
var spawns: SpawnPool by mutableLazy {
var spawnPool: SpawnPool by mutableLazy {
CobblemonSpawnPools.WORLD_SPAWN_POOL
}
@@ -46,9 +45,9 @@ object PlayerSpawnerFactory {
)
})
fun create(spawnerManager: SpawnerManager, player: ServerPlayer): PlayerSpawner {
fun create(player: ServerPlayer): PlayerSpawner {
val influences = influenceBuilders.mapNotNull { it(player) }
return PlayerSpawner(player, spawns, spawnerManager).also {
return PlayerSpawner(player, spawnPool).also {
it.influences.addAll(influences)
it.influences.addAll(
CobblemonSpawnRules.rules.values.filter(SpawnRule::enabled).flatMap(SpawnRule::components)
@@ -14,33 +14,67 @@ import com.cobblemon.mod.common.api.events.entity.SpawnBucketChosenEvent
import com.cobblemon.mod.common.api.spawning.BestSpawner
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.detail.EntitySpawnResult
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator.Companion.prioritizedAreaCalculators
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.isBoxLoaded
import com.cobblemon.mod.common.util.squeezeWithinBounds
import com.cobblemon.mod.common.util.toVec3f
import com.cobblemon.mod.common.util.weightedSelection
import kotlin.Any
import kotlin.Boolean
import kotlin.Float
import kotlin.Int
import kotlin.Pair
import kotlin.String
import kotlin.collections.plus
import kotlin.math.max
import net.minecraft.core.BlockPos
import net.minecraft.core.SectionPos
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.level.Level
import net.minecraft.world.level.chunk.ChunkAccess
import net.minecraft.world.level.chunk.status.ChunkStatus
import net.minecraft.world.level.pathfinder.PathComputationType
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
/**
* Interface representing something that performs the action of spawning. Various functions
* exist to streamline the process of using the [BestSpawner].
*
* The simplest implementation is a [BasicSpawner]
*
* @author Hiroku
* @since January 24th, 2022
*/
interface Spawner {
val name: String
val influences: MutableList<SpawningInfluence>
fun getSpawningSelector(): SpawningSelector<*>
fun setSpawningSelector(selector: SpawningSelector<*>)
fun getSpawnPool(): SpawnPool
fun setSpawnPool(spawnPool: SpawnPool)
var spawnPool: SpawnPool
var selector: SpawningSelector<*>
var generator: SpawningZoneGenerator
var resolver: AreaSpawnablePositionResolver
var maxPokemonPerChunk: Float
fun <R> afterSpawn(action: SpawnAction<R>, result: R) {}
companion object {
/** The chunk range radius over which spawning will roughly keep the entity density under control. */
const val ENTITY_LIMIT_CHUNK_RANGE = 3
}
fun getMatchingSpawns(bucket: SpawnBucket, spawnablePosition: SpawnablePosition): List<SpawnDetail> {
val spawns = mutableListOf<SpawnDetail>()
spawns.addAll(getSpawnPool().retrieve(bucket, spawnablePosition).filter { it.isSatisfiedBy(spawnablePosition) })
spawns.addAll(spawnPool.retrieve(bucket, spawnablePosition).filter { it.isSatisfiedBy(spawnablePosition) })
spawnablePosition.influences.forEach { influence ->
val influencedSpawns = influence.injectSpawns(bucket, spawnablePosition)
if (influencedSpawns != null) {
@@ -50,6 +84,101 @@ interface Spawner {
return spawns
}
/**
* Runs the spawner for a specific [SpawnablePosition] and calculates a [SpawnAction] that could spawn there.
* This does not trigger the resulting spawn. You can trigger the spawn in the same action by running
* [runForPosition] instead or manually complete the [SpawnAction] it returns using [SpawnAction.complete].
*/
fun calculateSpawnActionForPosition(
cause: SpawnCause,
spawnablePosition: SpawnablePosition,
): SpawnAction<*>? {
influences.removeIf { it.isExpired() }
spawnablePosition.influences.addAll(influences)
val bucket = chooseBucket(cause, spawnablePosition.influences)
return selector.select(
spawner = this,
bucket = bucket,
spawnablePositions = listOf(spawnablePosition),
maxSpawns = 1
).firstOrNull()
}
fun calculateSpawnActionsForArea(
zoneInput: SpawningZoneInput,
maxSpawns: Int?
): List<SpawnAction<*>> {
val maxSpawns = maxSpawns ?: Cobblemon.config.maximumSpawnsPerPass
influences.removeIf { it.isExpired() }
val constrainedArea = constrainArea(zoneInput)
?: return emptyList()
val areaBox = AABB.ofSize(
Vec3(constrainedArea.getCenter().toVec3f()),
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2,
1000.0,
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2
)
if (!constrainedArea.world.isBoxLoaded(areaBox)) {
return emptyList()
}
val numberNearby = constrainedArea.world.getEntitiesOfClass(
PokemonEntity::class.java,
areaBox,
PokemonEntity::countsTowardsSpawnCap
).size
val chunksCovered = ENTITY_LIMIT_CHUNK_RANGE * ENTITY_LIMIT_CHUNK_RANGE
val maxPokemonPerChunk = max(Cobblemon.config.pokemonPerChunk, zoneInput.cause.spawner.maxPokemonPerChunk)
if (numberNearby.toFloat() / chunksCovered >= maxPokemonPerChunk) {
return emptyList()
}
val zone = generator.generate(this, constrainedArea)
val spawnablePositions = resolver.resolve(this, prioritizedAreaCalculators, zone)
val influences = influences + zone.unconditionalInfluences
val bucket = chooseBucket(zoneInput.cause, influences)
return selector.select(
spawner = this,
bucket = bucket,
spawnablePositions = spawnablePositions,
maxSpawns = maxSpawns
)
}
/**
* Runs the spawner at the given [SpawnablePosition] and returns whatever the calculated [SpawnAction] produces,
* if any spawn occurred. For most implementations of [SpawnAction], the resulting type is
* [EntitySpawnResult]. Technically you can make spawns for anything though.
*/
fun runForPosition(
cause: SpawnCause,
spawnablePosition: SpawnablePosition
): Any? {
val action = calculateSpawnActionForPosition(
cause = cause,
spawnablePosition = spawnablePosition
) ?: return null
return action.complete()
}
fun runForArea(
zoneInput: SpawningZoneInput,
maxSpawns: Int? = null
): List<Any> {
val spawnActions = calculateSpawnActionsForArea(zoneInput = zoneInput, maxSpawns = maxSpawns)
val results = mutableListOf<Any>()
for (spawnAction in spawnActions) {
spawnAction.complete()?.let(results::add)
}
return results
}
fun copyInfluences() = influences.filter { !it.isExpired() }.toMutableList()
fun chooseBucket(cause: SpawnCause, influences: List<SpawningInfluence>): SpawnBucket {
@@ -66,4 +195,77 @@ interface Spawner {
CobblemonEvents.SPAWN_BUCKET_CHOSEN.post(event)
return event.bucket
}
fun isValidStartPoint(world: Level, chunk: ChunkAccess, startPos: BlockPos.MutableBlockPos): Boolean {
val y = startPos.y
if (!world.isLoaded(startPos) || !world.isLoaded(startPos.setY(y + 1))) {
return false
}
val mid = chunk.getBlockState(startPos.setY(y))
val above = chunk.getBlockState(startPos.setY(y + 1))
// Above must be non-solid
if (!above.isPathfindable(PathComputationType.AIR)) {
return false
}
// Position must be non-air
if (mid.isAir) {
return false
}
return true
}
fun constrainArea(area: SpawningZoneInput): SpawningZoneInput? {
val basePos = BlockPos.MutableBlockPos(area.baseX, area.baseY, area.baseZ)
val originalY = area.baseY
val (chunkX, chunkZ) = Pair(SectionPos.blockToSectionCoord(area.baseX), SectionPos.blockToSectionCoord(area.baseZ))
// if the chunk isn't loaded, we don't want to go further & we don't want the getChunk function below to load/create the chunk.
if (!area.world.areEntitiesLoaded(ChunkPos.asLong(chunkX, chunkZ))) return null
val chunk = area.world.getChunk(chunkX, chunkZ, ChunkStatus.FULL) ?: return null
var valid = isValidStartPoint(area.world, chunk, basePos)
if (!valid) {
var offset = 1
do {
if (isValidStartPoint(area.world, chunk, basePos.setY(originalY + offset))) {
valid = true
basePos.y = originalY + offset
break
} else if (isValidStartPoint(area.world, chunk, basePos.setY(originalY - offset))) {
valid = true
basePos.y = originalY - offset
break
}
offset++
} while (offset <= Cobblemon.config.maxVerticalCorrectionBlocks)
}
if (valid) {
val min = area.world.squeezeWithinBounds(basePos)
val max = area.world.squeezeWithinBounds(basePos.move(area.length, area.height, area.width))
if (area.world.isLoaded(min) && area.world.isLoaded(max) &&
min.x < max.x && min.y < max.y && min.z < max.z
) {
return SpawningZoneInput(
cause = area.cause,
world = area.world,
baseX = min.x,
baseY = min.y,
baseZ = min.z,
length = max.x - min.x,
height = max.y - min.y,
width = max.z - min.z
)
}
}
return null
}
}
@@ -1,89 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.detail.EntitySpawnResult
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.selection.FlatSpawnablePositionWeightedSelector
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import net.minecraft.world.entity.Entity
/**
* A spawner that regularly attempts spawning entities. It has timing utilities,
* and subclasses must provide the logic for generating a spawn which is called
* periodically by the server.
*
* @author Hiroku
* @since February 5th, 2022
*/
abstract class TickingSpawner(
override val name: String,
var spawns: SpawnPool,
val manager: SpawnerManager
) : Spawner {
private var selector: SpawningSelector<*> = FlatSpawnablePositionWeightedSelector()
override val influences = mutableListOf<SpawningInfluence>()
override fun getSpawningSelector() = selector
override fun setSpawningSelector(selector: SpawningSelector<*>) { this.selector = selector }
override fun getSpawnPool() = spawns
override fun setSpawnPool(spawnPool: SpawnPool) { spawns = spawnPool }
abstract fun run(cause: SpawnCause): List<SpawnAction<*>>
var active = true
val spawnedEntities = mutableListOf<Entity>()
var lastSpawnTime = 0L
var ticksUntilNextSpawn = 100F
abstract var ticksBetweenSpawns: Float
var tickTimerMultiplier = 1F
var removalCheckTicks = 0
open fun tick() {
removalCheckTicks++
influences.removeIf { it.isExpired() }
if (removalCheckTicks == 60) {
spawnedEntities.removeIf { it.isRemoved }
removalCheckTicks = 0
}
if (!active) {
return
}
ticksUntilNextSpawn -= tickTimerMultiplier
if (ticksUntilNextSpawn <= 0) {
val spawnActions = run(cause = SpawnCause(spawner = this, entity = getCauseEntity()))
ticksUntilNextSpawn = ticksBetweenSpawns
for (spawnAction in spawnActions) {
spawnAction.complete()
}
}
}
override fun <R> afterSpawn(action: SpawnAction<R>, result: R) {
super.afterSpawn(action, result)
if (result is EntitySpawnResult) {
spawnedEntities.addAll(result.entities)
}
lastSpawnTime = System.currentTimeMillis()
}
open fun getCauseEntity(): Entity? = null
fun getAllInfluences() = this.influences + manager.influences
override fun copyInfluences() = this.getAllInfluences().toMutableList()
}
@@ -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.spawning.spawner
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.selection.FlatSpawnablePositionWeightedSelector
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
/**
* A type of spawner that occurs at a single point and provides its own means of generating a
* spawnable position from a particular cause and position. The trigger spawner must be directly called to
* spawn things (almost as if it was being triggered!!!) as opposed to being scheduled on a ticker.
*
* @author Hiroku
* @since February 3rd, 2024
*/
abstract class TriggerSpawner<T : SpawnCause>(override val name: String, var spawns: SpawnPool) : Spawner {
private var selector: SpawningSelector<*> = FlatSpawnablePositionWeightedSelector()
override val influences = mutableListOf<SpawningInfluence>()
open fun getSpawnAction(
cause: T,
world: ServerLevel,
pos: BlockPos,
influences: List<SpawningInfluence> = emptyList()
): SpawnAction<*>? {
val spawnablePosition = parseSpawnablePosition(cause, world, pos) ?: return null
spawnablePosition.influences.addAll(influences)
val bucket = chooseBucket(cause, spawnablePosition.influences)
return selector.select(this, bucket, listOf(spawnablePosition), max = 1).firstOrNull()
}
/** Parses a spawnable position, if possible, from the given cause and position. */
abstract fun parseSpawnablePosition(cause: T, world: ServerLevel, pos: BlockPos): SpawnablePosition?
override fun getSpawningSelector() = selector
override fun setSpawningSelector(selector: SpawningSelector<*>) { this.selector = selector }
override fun getSpawnPool() = spawns
override fun setSpawnPool(spawnPool: SpawnPool) { spawns = spawnPool }
}
@@ -137,6 +137,9 @@ object CobblemonItemTags {
@JvmField val WEARABLE_HAT_ITEMS = create("held/visibility/hat")
@JvmField val HIDDEN_ITEMS = create("held/visibility/hidden")
//Empty Tag
@JvmField val EMPTY = create("empty")
private fun create(path: String) = TagKey.create(Registries.ITEM, cobblemonResource(path))
@@ -20,6 +20,8 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.addStandardFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangValue
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.ActionEffectTimeline
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
import com.cobblemon.mod.common.api.moves.animations.TargetsProvider
import com.cobblemon.mod.common.api.moves.animations.UsersProvider
import com.cobblemon.mod.common.battles.ShowdownInterpreter
@@ -31,6 +33,7 @@ import com.cobblemon.mod.common.battles.dispatch.UntilDispatch
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.pokemon.evolution.progress.UseMoveEvolutionProgress
import com.cobblemon.mod.common.util.asArrayValue
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.cobblemonResource
import java.util.concurrent.CompletableFuture
@@ -49,7 +52,7 @@ class MoveInstruction(
) : InterpreterInstruction, CauserInstruction {
val effect = message.effectAt(1) ?: Effect.pure("", "")
val move = Moves.getByNameOrDummy(effect.id)
val actionEffect = move.actionEffect
val actionEffect: ActionEffectTimeline? by lazy { ActionEffects.getEffectWithBattleContext(cobblemonResource(move.name), userPokemon) }
val spreadTargets = message.optionalArgument("spread")?.split(",") ?: emptyList()
var future = CompletableFuture.completedFuture(Unit)
@@ -61,6 +64,8 @@ class MoveInstruction(
override fun invoke(battle: PokemonBattle) {
userPokemon = message.battlePokemon(0, battle)!!
targetPokemon = message.battlePokemon(2, battle)
val actionEffect = actionEffect ?: ActionEffects.actionEffects["generic_move".asIdentifierDefaultingNamespace()]
val targetPokemon = targetPokemon // So smart non-null casts can happen
val optionalEffect = message.effect()
@@ -18,6 +18,7 @@ import net.minecraft.core.BlockPos
import net.minecraft.core.particles.BlockParticleOption
import net.minecraft.core.particles.ColorParticleOption
import net.minecraft.core.particles.ParticleTypes
import net.minecraft.server.level.ServerLevel
import net.minecraft.sounds.SoundEvents
import net.minecraft.sounds.SoundSource
import net.minecraft.stats.Stats
@@ -51,12 +52,13 @@ import net.minecraft.world.phys.Vec3
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
import kotlin.math.floor
import kotlin.random.Random
class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock(settings) {
companion object {
val MAX_BITES = 8
val CAKE_HEIGHT = 0.4375
const val MAX_BITES = 8
const val CAKE_HEIGHT = 0.4375
val CANDLE_PARTICLE_POSITION = Vec3(0.5, CAKE_HEIGHT + 0.5, 0.5)
@@ -135,16 +137,18 @@ class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock
if (hasCandle(state)) {
return Shapes.or(SHAPES[state.getValue(BITES)], CANDLE_SHAPE)
}
return SHAPES[state.getValue(BITES)]
}
override fun getRenderShape(blockState: BlockState?) = RenderShape.MODEL
override fun getRenderShape(blockState: BlockState) = RenderShape.MODEL
override fun setPlacedBy(level: Level, pos: BlockPos, state: BlockState, placer: LivingEntity?, stack: ItemStack) {
super.setPlacedBy(level, pos, state, placer, stack)
val blockEntity = level.getBlockEntity(pos) as? PokeSnackBlockEntity
blockEntity?.initializeFromItemStack(stack)
val blockEntity = level.getBlockEntity(pos) as? PokeSnackBlockEntity ?: return
blockEntity.initializeFromItemStack(stack)
blockEntity.placedBy = placer?.uuid
}
override fun getCloneItemStack(level: LevelReader, pos: BlockPos, state: BlockState): ItemStack {
@@ -294,15 +298,28 @@ class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock
fun eat(level: Level, pos: BlockPos, state: BlockState, player: Player?) {
val bites = state.getValue(BITES) as Int
if (bites < MAX_BITES) {
level.setBlock(pos, state.setValue(BITES, bites + 1) as BlockState, UPDATE_ALL)
} else {
val newBites =
if (isLure) {
val pokeSnackBlockEntity = level.getBlockEntity(pos) as PokeSnackBlockEntity? ?: return
pokeSnackBlockEntity.amountSpawned += 1
floor(pokeSnackBlockEntity.amountSpawned / PokeSnackBlockEntity.SPAWNS_PER_BITE.toDouble()).toInt()
} else {
bites + 1
}
if (newBites > MAX_BITES) {
dropCandle(level, pos, state, player)
level.removeBlock(pos, false)
level.removeBlockEntity(pos)
} else {
level.setBlock(pos, state.setValue(BITES, newBites) as BlockState, UPDATE_ALL)
}
level.playSound(null, player?.blockPosition() ?: pos, SoundEvents.GENERIC_EAT, if (player != null) SoundSource.PLAYERS else SoundSource.NEUTRAL)
spawnEatParticles(level, pos)
player?.let {
level.gameEvent(it, GameEvent.BLOCK_DESTROY, pos)
}
@@ -352,8 +369,19 @@ class PokeSnackBlock(settings: Properties, val isLure: Boolean): BaseEntityBlock
0.0,
0.0,
0.0
)
}
}
override fun isRandomlyTicking(state: BlockState) = isLure
override fun randomTick(
state: BlockState,
level: ServerLevel,
pos: BlockPos,
random: RandomSource
) {
if (level.isClientSide) return
(level.getBlockEntity(pos) as? PokeSnackBlockEntity)?.randomTick()
}
}
@@ -52,8 +52,8 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
constructor(containerId: Int, playerInventory: Inventory) : super(CobblemonMenuType.COOKING_POT, containerId) {
this.playerInventory = playerInventory
this.container = CookingPotContainer(this,
CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH,
CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
CampfireBlockEntity.CRAFTING_GRID_WIDTH,
CampfireBlockEntity.CRAFTING_GRID_WIDTH
)
this.resultContainer = ResultContainer()
this.containerData = SimpleContainerData(4)
@@ -82,25 +82,25 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
val resultSlotX = 128
val resultSlotY = 55
addSlot(CookingPotResultSlot(this.container, CampfireBlockEntity.Companion.RESULT_SLOT, resultSlotX, resultSlotY))
addSlot(CookingPotResultSlot(this.container, CampfireBlockEntity.RESULT_SLOT, resultSlotX, resultSlotY))
for ((index, slotIndex) in CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.withIndex()) {
val i = index / CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
val j = index % CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
for ((index, slotIndex) in CampfireBlockEntity.CRAFTING_GRID_SLOTS.withIndex()) {
val i = index / CampfireBlockEntity.CRAFTING_GRID_WIDTH
val j = index % CampfireBlockEntity.CRAFTING_GRID_WIDTH
addSlot(Slot(this.container, slotIndex, 33 + j * 18, 18 + i * 18))
}
for ((index, slotIndex) in CampfireBlockEntity.Companion.SEASONING_SLOTS.withIndex()) {
addSlot(SeasoningSlot(this.container, slotIndex, 110 + index * 18, 18))
for ((index, slotIndex) in CampfireBlockEntity.SEASONING_SLOTS.withIndex()) {
addSlot(SeasoningSlot(this, this.container, slotIndex, 110 + index * 18, 18))
}
for ((index, _) in CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS.withIndex()) {
val i = index / CampfireBlockEntity.Companion.PLAYER_INVENTORY_WIDTH
val j = index % CampfireBlockEntity.Companion.PLAYER_INVENTORY_WIDTH
for ((index, _) in CampfireBlockEntity.PLAYER_INVENTORY_SLOTS.withIndex()) {
val i = index / CampfireBlockEntity.PLAYER_INVENTORY_WIDTH
val j = index % CampfireBlockEntity.PLAYER_INVENTORY_WIDTH
addSlot(Slot(playerInventory, index + 9, 8 + j * 18, 84 + i * 18))
}
for ((index, _) in CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS.withIndex()) {
for ((index, _) in CampfireBlockEntity.PLAYER_HOTBAR_SLOTS.withIndex()) {
addSlot(Slot(playerInventory, index, 8 + index * 18, 142))
}
}
@@ -173,29 +173,31 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
currentActiveRecipe = recipe
if (recipe != null) {
previewItem = recipe.value.assemble(craftInput, level.registryAccess())
// Apply seasoning to the preview item
recipe.value.applySeasoning(
previewItem,
container.items.subList(CampfireBlockEntity.SEASONING_SLOTS.first,
CampfireBlockEntity.SEASONING_SLOTS.last + 1)
.filterNotNull()
.filter { !it.isEmpty })
.filter { !it.isEmpty && it.`is`(recipe.value.seasoningTag) })
} else previewItem = ItemStack.EMPTY
}
override fun getResultSlotIndex(): Int {
return CampfireBlockEntity.Companion.RESULT_SLOT
return CampfireBlockEntity.RESULT_SLOT
}
override fun getGridWidth(): Int {
return CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
return CampfireBlockEntity.CRAFTING_GRID_WIDTH
}
override fun getGridHeight(): Int {
return CampfireBlockEntity.Companion.CRAFTING_GRID_WIDTH
return CampfireBlockEntity.CRAFTING_GRID_WIDTH
}
override fun getSize(): Int {
return CampfireBlockEntity.Companion.ITEMS_SIZE
return CampfireBlockEntity.ITEMS_SIZE
}
fun getBurnProgress(): Float {
@@ -227,24 +229,24 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
val slotItemStack = slot.item
itemStack = slotItemStack.copy()
if (index == CampfireBlockEntity.Companion.RESULT_SLOT) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
if (index == CampfireBlockEntity.RESULT_SLOT) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
return ItemStack.EMPTY
}
slot.onQuickCraft(slotItemStack, itemStack);
} else if (index in CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS || index in CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS) {
} else if (index in CampfireBlockEntity.PLAYER_INVENTORY_SLOTS || index in CampfireBlockEntity.PLAYER_HOTBAR_SLOTS) {
if (Seasonings.isSeasoning(slotItemStack)) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.last + 1, false) &&
!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.SEASONING_SLOTS.first, CampfireBlockEntity.Companion.SEASONING_SLOTS.last + 1, false)
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.CRAFTING_GRID_SLOTS.last + 1, false) &&
!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.SEASONING_SLOTS.first, CampfireBlockEntity.SEASONING_SLOTS.last + 1, false)
) {
return ItemStack.EMPTY
}
} else if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS.last + 1, false)) {
} else if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.CRAFTING_GRID_SLOTS.first, CampfireBlockEntity.CRAFTING_GRID_SLOTS.last + 1, false)) {
return ItemStack.EMPTY
}
} else if (index in CampfireBlockEntity.Companion.CRAFTING_GRID_SLOTS || index in CampfireBlockEntity.Companion.SEASONING_SLOTS) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.Companion.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.Companion.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
} else if (index in CampfireBlockEntity.CRAFTING_GRID_SLOTS || index in CampfireBlockEntity.SEASONING_SLOTS) {
if (!this.moveItemStackTo(slotItemStack, CampfireBlockEntity.PLAYER_INVENTORY_SLOTS.first, CampfireBlockEntity.PLAYER_HOTBAR_SLOTS.last + 1, false)) {
return ItemStack.EMPTY
}
}
@@ -8,13 +8,13 @@
package com.cobblemon.mod.common.block.campfirepot
import com.cobblemon.mod.common.api.cooking.Seasonings
import net.minecraft.world.Container
import net.minecraft.world.inventory.Slot
import net.minecraft.world.item.ItemStack
class SeasoningSlot(container: Container, slot: Int, x: Int, y: Int) : Slot(container, slot, x, y) {
class SeasoningSlot(val menu: CookingPotMenu, container: Container, slot: Int, x: Int, y: Int) : Slot(container, slot, x, y) {
override fun mayPlace(stack: ItemStack): Boolean {
return Seasonings.isSeasoning(stack) && super.mayPlace(stack)
val recipe = menu.currentActiveRecipe?.value() ?: return false
return stack.`is`(recipe.seasoningTag) && super.mayPlace(stack)
}
}
@@ -134,6 +134,8 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
campfireBlockEntity.particleCooldown = 20
}
campfireBlockEntity.time++
}
fun serverTick(level: Level, pos: BlockPos, state: BlockState, campfireBlockEntity: CampfireBlockEntity) {
@@ -166,7 +168,7 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
val cookedItem = recipe.assemble(craftingInput, level.registryAccess())
val resultSlotItem = campfireBlockEntity.getItem(0)
recipe.applySeasoning(cookedItem, campfireBlockEntity.getSeasonings())
recipe.applySeasoning(cookedItem, campfireBlockEntity.getSeasonings().filter { it.`is`(recipe.seasoningTag) })
if (!campfireBlockEntity.blockState.getValue(CampfireBlock.LID)) {
campfireBlockEntity.cookingProgress = 0
@@ -237,6 +239,7 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
private var particleCooldown: Int = 0
var brothColor: Int = BASE_BROTH_COLOR
var bubbleColor: Int = BASE_BROTH_BUBBLE_COLOR
var time: Int = 0
var dataAccess: ContainerData = object : ContainerData {
override fun get(index: Int): Int {
@@ -311,7 +314,7 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
consumeItem(i)
}
for (i in SEASONING_SLOTS.first..SEASONING_SLOTS.last) {
if (recipe.seasoningProcessors.any { it.consumesItem(getItem(i)) }) consumeItem(i)
if (getItem(i).`is`(recipe.seasoningTag) && recipe.seasoningProcessors.any { it.consumesItem(getItem(i)) }) consumeItem(i)
}
val direction = state.getValue(CampfireBlock.ITEM_DIRECTION)
@@ -8,29 +8,121 @@
package com.cobblemon.mod.common.block.entity
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.CobblemonItemComponents
import com.cobblemon.mod.common.api.cooking.getColourMixFromFlavours
import com.cobblemon.mod.common.api.fishing.SpawnBait
import com.cobblemon.mod.common.api.fishing.SpawnBait.Effect
import com.cobblemon.mod.common.api.fishing.SpawnBaitEffects
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.detail.PokemonHerdSpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.influence.BucketMultiplyingInfluence
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawnBaitInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.spawner.FixedAreaSpawner
import com.cobblemon.mod.common.block.PokeSnackBlock
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.item.components.BaitEffectsComponent
import com.cobblemon.mod.common.item.components.FlavourComponent
import com.cobblemon.mod.common.item.components.IngredientComponent
import com.cobblemon.mod.common.net.messages.client.effect.PokeSnackBlockParticlesPacket
import com.cobblemon.mod.common.util.DataKeys
import java.util.UUID
import net.minecraft.core.BlockPos
import net.minecraft.core.HolderLookup
import net.minecraft.nbt.*
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtOps
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.block.state.BlockState
import kotlin.collections.orEmpty
open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEntity(CobblemonBlockEntities.POKE_SNACK, pos, state) {
open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
TintBlockEntity(CobblemonBlockEntities.POKE_SNACK, pos, state),
SpawningInfluence {
companion object {
const val SPAWNS_PER_BITE = 1
const val RADIUS = 8
const val RANDOM_TICKS_BETWEEN_SPAWNS = 4
const val POKE_SNACK_CRUMBED_ASPECT = "poke_snack_crumbed"
}
val spawner: FixedAreaSpawner by lazy {
FixedAreaSpawner(
name = "poke_snack_spawner_${level?.dimension()?.location()}}_$blockPos",
spawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL,
world = level as ServerLevel,
position = blockPos,
horizontalRadius = RADIUS,
verticalRadius = RADIUS,
maxPokemonPerChunk = Cobblemon.config.pokeSnackPokemonPerChunk
).also {
it.influences.add(this)
val baitEffects = getBaitEffects()
val stackedLureTier = baitEffects
.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }
.sumOf { it.value }
.toInt()
if (stackedLureTier > 0) {
it.influences += BucketNormalizingInfluence(
tier = stackedLureTier,
gradient = 0.2F,
firstTier = 1.2F
)
}
it.influences += BucketMultiplyingInfluence(
mapOf(
"uncommon" to 2.25f,
"rare" to 5.5f,
"ultra-rare" to 5.5f,
)
)
it.influences += SpawnBaitInfluence(effects = baitEffects)
}
}
// Only non-herd Pokémon can be spawned from a Poké Snack
override fun affectSpawnable(detail: SpawnDetail, spawnablePosition: SpawnablePosition) = detail.type in SpawnDetail.pokemonTypes && detail !is PokemonHerdSpawnDetail
// When a Pokémon spawns, apply the crumbed aspect and play the eating effects. Eat part of the cake, too.
override fun affectSpawn(action: SpawnAction<*>, entity: Entity) {
if (entity is PokemonEntity) {
entity.pokemon.forcedAspects += POKE_SNACK_CRUMBED_ASPECT
val block = blockState.block as? PokeSnackBlock ?: return
val level = level ?: return
val entityPos = entity.blockPosition()
PokeSnackBlockParticlesPacket(blockPos, entityPos).sendToPlayersAround(
blockPos.x.toDouble(),
blockPos.y.toDouble(),
blockPos.z.toDouble(),
64.0,
level.dimension(),
)
block.eat(level, blockPos, blockState, action.spawnablePosition.cause.entity as? Player)
}
}
var placedBy: UUID? = null
var amountSpawned: Int = 0
var flavourComponent: FlavourComponent? = null
var baitEffectsComponent: BaitEffectsComponent? = null
var ingredientComponent: IngredientComponent? = null
var randomTicksUntilNextSpawn: Float = getRandomTicksBetweenSpawns()
fun initializeFromItemStack(itemStack: ItemStack) {
flavourComponent = itemStack.get(CobblemonItemComponents.FLAVOUR)
@@ -38,9 +130,7 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEnt
if (isLure()) baitEffectsComponent = itemStack.get(CobblemonItemComponents.BAIT_EFFECTS)
flavourComponent?.let {
getColourMixFromFlavours(it.getDominantFlavours())?.let {
setTint(it)
}
getColourMixFromFlavours(it.getDominantFlavours())?.let(::setTint)
}
}
@@ -52,14 +142,60 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEnt
return false
}
fun randomTick() {
randomTicksUntilNextSpawn--
if (randomTicksUntilNextSpawn <= 0) {
val nearestPlayer = level?.getNearestPlayer(
blockPos.x.toDouble(),
blockPos.y.toDouble(),
blockPos.z.toDouble(),
Cobblemon.config.maximumSpawningZoneDistanceFromPlayer.toDouble(),
false, // true here makes it so creative players aren't considered
)
// High simulation distances may cause the player to be null here, we don't want to spawn without a player nearby.
if (nearestPlayer != null) {
spawner.run(
cause = SpawnCause(spawner = spawner, entity = nearestPlayer),
maxSpawns = 1
)
}
randomTicksUntilNextSpawn = getRandomTicksBetweenSpawns()
}
}
fun getRandomTicksBetweenSpawns(): Float {
val biteTimeMultiplier = getBiteTimeMultiplier()
return (RANDOM_TICKS_BETWEEN_SPAWNS * biteTimeMultiplier).coerceAtLeast(1F)
}
fun getBiteTimeMultiplier(): Float {
val baitEffects = getBaitEffects()
val biteTimeEffects = baitEffects.filter { it.type == SpawnBait.Effects.BITE_TIME }
if (biteTimeEffects.isEmpty()) return 1F
val biteTimeEffect = biteTimeEffects.random()
if (Math.random() > biteTimeEffect.chance) {
return 1F
}
return 1F - biteTimeEffect.value.toFloat()
}
fun toItemStack(): ItemStack {
val stack = ItemStack(this.blockState.block)
if (isLure() && baitEffectsComponent != null) {
stack.set(CobblemonItemComponents.BAIT_EFFECTS, baitEffectsComponent)
}
if (flavourComponent != null) {
stack.set(CobblemonItemComponents.FLAVOUR, flavourComponent)
}
if (ingredientComponent != null) {
stack.set(CobblemonItemComponents.INGREDIENT, ingredientComponent)
}
return stack
}
@@ -67,12 +203,18 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEnt
* Combine all the [SpawnBait.Effect] values from the [baitEffectsComponent] data.
*/
fun getBaitEffects(): List<Effect> {
return baitEffectsComponent?.effects?.mapNotNull(SpawnBaitEffects::getFromIdentifier)?.flatMap { it.effects }.orEmpty()
return baitEffectsComponent
?.effects
?.mapNotNull(SpawnBaitEffects::getFromIdentifier)
?.flatMap { it.effects }
.orEmpty()
}
override fun saveAdditional(tag: CompoundTag, registries: HolderLookup.Provider) {
super.saveAdditional(tag, registries)
tag.putInt(DataKeys.AMOUNT_SPAWNED, amountSpawned)
flavourComponent?.let { component ->
CobblemonItemComponents.FLAVOUR.codec()!!
.encodeStart(NbtOps.INSTANCE, component)
@@ -99,11 +241,19 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEnt
tag.put(DataKeys.INGREDIENTS, encodedTag)
}
}
tag.putFloat(DataKeys.TICKS_UNTIL_NEXT_SPAWN, randomTicksUntilNextSpawn)
placedBy?.let {
tag.putUUID(DataKeys.PLACED_BY, it)
}
}
override fun loadAdditional(tag: CompoundTag, registries: HolderLookup.Provider) {
super.loadAdditional(tag, registries)
amountSpawned = tag.getInt(DataKeys.AMOUNT_SPAWNED)
if (tag.contains(DataKeys.FLAVOUR)) {
CobblemonItemComponents.FLAVOUR.codec()
?.parse(NbtOps.INSTANCE, tag.get(DataKeys.FLAVOUR))
@@ -130,5 +280,13 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) : TintBlockEnt
ingredientComponent = component
}
}
if (tag.contains(DataKeys.TICKS_UNTIL_NEXT_SPAWN)) {
randomTicksUntilNextSpawn = tag.getFloat(DataKeys.TICKS_UNTIL_NEXT_SPAWN)
}
if (tag.contains(DataKeys.PLACED_BY)) {
placedBy = tag.getUUID(DataKeys.PLACED_BY)
}
}
}
@@ -56,8 +56,11 @@ object MountedCameraTypeHandler {
data.updatePerspective(key, currentCameraType)
}
fun handleTransition(fromKey: ResourceLocation, toKey: ResourceLocation) {
fun handleTransition(passenger: Entity, fromKey: ResourceLocation, toKey: ResourceLocation) {
val mc = Minecraft.getInstance()
if (passenger == mc.player) {
return
}
val currentCameraType = mc.options.cameraType
val data = ClientPersistedData.ridingPerspectives
data.updatePerspective(fromKey, currentCameraType)
@@ -491,6 +491,12 @@ class PokemonClientDelegate : PosableState(), PokemonSideDelegate {
cryAnimation = animation
}
override fun applyRenderRotation(partialTick: Float) {
currentEntity.ifRidingAvailable { behaviour, settings, state ->
behaviour.applyRenderRotation(settings, state, currentEntity, partialTick)
}
}
fun getSeatLocator(passenger: Entity): String {
val seatIndex = this.getEntity().occupiedSeats.indexOf(passenger)
if (seatIndex == -1) throw IllegalArgumentException("Entity is not currently riding a seat")
@@ -11,26 +11,25 @@ package com.cobblemon.mod.common.client.gui
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.gui.drawPosablePortrait
import com.cobblemon.mod.common.api.text.darkGray
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.battle.BattleGUI
import com.cobblemon.mod.common.client.gui.portrait.FullyAnimatedPortraitDrawer
import com.cobblemon.mod.common.client.gui.portrait.SelectedAnimatedPortraitDrawer
import com.cobblemon.mod.common.client.gui.portrait.InanimatePortraitDrawer
import com.cobblemon.mod.common.client.gui.portrait.PortraitStyle
import com.cobblemon.mod.common.client.gui.toast.CobblemonToast
import com.cobblemon.mod.common.client.keybind.boundKey
import com.cobblemon.mod.common.client.keybind.keybinds.HidePartyBinding
import com.cobblemon.mod.common.client.keybind.keybinds.SummaryBinding
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.getDepletableRedGreen
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.renderScaledGuiItemIcon
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import java.util.UUID
import net.minecraft.client.DeltaTracker
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.Gui
@@ -58,12 +57,10 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
private val genderIconMale = cobblemonResource("textures/gui/party/party_gender_male.png")
private val genderIconFemale = cobblemonResource("textures/gui/party/party_gender_female.png")
private val portraitBackground = cobblemonResource("textures/gui/party/party_slot_portrait_background.png")
private val inanimatePortraitDrawer = InanimatePortraitDrawer()
private val selectedAnimatedPortraitDrawer = SelectedAnimatedPortraitDrawer()
private val fullyAnimatedPortraitDrawer = FullyAnimatedPortraitDrawer()
}
private val stateAtIndex = hashMapOf<Int, Pair<UUID, FloatingState>>()
private val screenExemptions: List<Class<out Screen>> = listOf(
ChatScreen::class.java,
BattleGUI::class.java
@@ -88,6 +85,16 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
attachedToast = false
}
fun getPokemonState(index: Int, pokemonUUID: UUID): FloatingState {
val state = this.stateAtIndex.getOrPut(index) { pokemonUUID to FloatingState() }
if (state.first != pokemonUUID) {
val newState = FloatingState()
this.stateAtIndex[index] = pokemonUUID to newState
return newState
}
return state.second
}
override fun render(context: GuiGraphics, tickCounter: DeltaTracker) {
val partialDeltaTicks = tickCounter.realtimeDeltaTicks
@@ -160,13 +167,20 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
0.0
)
val portraitDrawer = when (Cobblemon.config.partyPortraitAnimations) {
PortraitStyle.NEVER_ANIMATE -> inanimatePortraitDrawer
PortraitStyle.ANIMATE_SELECTED -> selectedAnimatedPortraitDrawer
PortraitStyle.ALWAYS_ANIMATE -> fullyAnimatedPortraitDrawer
val shouldAnimate = when (Cobblemon.config.partyPortraitAnimations) {
PokemonGUIAnimationStyle.NEVER_ANIMATE -> false
PokemonGUIAnimationStyle.ANIMATE_SELECTED -> selectedSlot == index
PokemonGUIAnimationStyle.ALWAYS_ANIMATE -> true
}
portraitDrawer.draw(pokemon, matrices, partialDeltaTicks, selectedSlot == index, index)
drawPosablePortrait(
identifier = pokemon.species.resourceIdentifier,
matrixStack = matrices,
doQuirks = false,
partialTicks = if (shouldAnimate) partialDeltaTicks else 0F,
contextScale = pokemon.form.baseScale,
state = getPokemonState(index = index, pokemonUUID = pokemon.uuid).also { it.currentAspects = pokemon.aspects }
)
matrices.popPose()
context.disableScissor()
@@ -6,24 +6,20 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.portrait
package com.cobblemon.mod.common.client.gui
import com.mojang.serialization.Codec
import net.minecraft.util.StringRepresentable
enum class PortraitStyle : StringRepresentable {
enum class PokemonGUIAnimationStyle : StringRepresentable {
NEVER_ANIMATE,
ANIMATE_SELECTED,
ALWAYS_ANIMATE;
override fun getSerializedName(): String = this.name
override fun getSerializedName() = this.name
companion object {
@JvmStatic
val CODEC: Codec<PortraitStyle> = StringRepresentable.fromEnum(PortraitStyle::values)
val CODEC: Codec<PokemonGUIAnimationStyle> = StringRepresentable.fromEnum(PokemonGUIAnimationStyle::values)
}
}
@@ -8,6 +8,7 @@
package com.cobblemon.mod.common.client.gui.battle
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.gui.drawPosablePortrait
import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
@@ -322,7 +323,7 @@ class BattleOverlay : Gui(Minecraft.getInstance()), Schedulable {
reversed = reversed,
doQuirks = false,
state = state,
partialTicks = partialTicks
partialTicks = if (Cobblemon.config.animateBattleTiles) partialTicks else 0F
)
}
matrixStack.popPose()
@@ -13,7 +13,6 @@ import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.battles.*
import com.cobblemon.mod.common.client.gui.battle.subscreen.BattleGimmickButton.GimmickTile
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.toRGB
import com.mojang.blaze3d.vertex.PoseStack
@@ -139,8 +138,7 @@ abstract class BattleGimmickButton(gimmick: ShowdownMoveset.Gimmick, val x: Floa
pp = gimmickTemplate?.pp ?: moveTemplate.pp,
priority = gimmickTemplate?.priority ?: moveTemplate.priority,
critRatio = gimmickTemplate?.critRatio ?: moveTemplate.critRatio,
effectChances = gimmickTemplate?.effectChances ?: moveTemplate.effectChances,
actionEffect = null
effectChances = gimmickTemplate?.effectChances ?: moveTemplate.effectChances
)
}
}
@@ -201,7 +201,7 @@ class CookingPotScreen(
}
override fun renderSlot(guiGraphics: GuiGraphics, slot: Slot) {
if (slot is SeasoningSlot && !menu.carried.isEmpty && !slot.mayPlace(menu.carried)) {
if (slot is SeasoningSlot && ((!menu.carried.isEmpty && !slot.mayPlace(menu.carried)) || (slot.hasItem() && !slot.mayPlace(slot.item))) ) {
val x = slot.x
val y = slot.y
guiGraphics.fill(x,y,x+16,y+16,822018048)
@@ -299,7 +299,7 @@ class DialogueScreen(var dialogueDTO: DialogueDTO) : Screen("gui.dialogue".asTra
fun playGibberSpeak(delta: Float, text: String, gibber: DialogueGibberDTO) {
timeElapsedSinceGibber += delta / 20
if (timeElapsedSinceGibber < gibber.interval) {
if (timeElapsedSinceGibber < gibber.interval || gibberDone) {
return
}
@@ -8,10 +8,12 @@
package com.cobblemon.mod.common.client.gui.pc
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.pasture.PasturePCGUIConfiguration
import com.cobblemon.mod.common.client.gui.PokemonGUIAnimationStyle
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.renderScaledGuiItemIcon
@@ -72,12 +74,19 @@ open class StorageSlot(
matrices.translate(posX + (SIZE / 2.0), posY + 1.0, 0.0)
matrices.scale(2.5F, 2.5F, 1F)
val animationConfig = if (this is PartyStorageSlot) Cobblemon.config.summaryProfileAnimations else Cobblemon.config.pcProfileAnimations
val shouldAnimate = when (animationConfig) {
PokemonGUIAnimationStyle.ALWAYS_ANIMATE -> true
PokemonGUIAnimationStyle.NEVER_ANIMATE -> false
PokemonGUIAnimationStyle.ANIMATE_SELECTED -> isHoveredOrFocused
}
drawProfilePokemon(
renderablePokemon = pokemon.asRenderablePokemon(),
matrixStack = matrices,
rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, 35F, 0F)),
state = state,
partialTicks = partialTicks,
partialTicks = if (shouldAnimate) partialTicks else 0F,
scale = 4.5F
)
matrices.popPose()
@@ -1,63 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.portrait
import com.cobblemon.mod.common.api.gui.drawPosablePortrait
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.pokemon.Pokemon
import com.mojang.blaze3d.vertex.PoseStack
import java.util.UUID
/**
* Base logic for a [PortraitDrawer] that animates a portrait based on conditions.
*/
abstract class AnimatedPortraitDrawer : PortraitDrawer {
private val state = FloatingState()
private val stateAtIndex = hashMapOf<Int, Pair<UUID, FloatingState>>()
override fun draw(pokemon: Pokemon, poseStack: PoseStack, partialTicks: Float, isSelected: Boolean, index: Int) {
drawPosablePortrait(
identifier = pokemon.species.resourceIdentifier,
matrixStack = poseStack,
partialTicks = this.ticksFor(pokemon, isSelected, partialTicks),
contextScale = pokemon.form.baseScale,
state = this.stateFor(pokemon, isSelected, index)
)
}
protected abstract fun shouldAnimate(pokemon: Pokemon, isSelected: Boolean): Boolean
protected open fun ticksFor(pokemon: Pokemon, isSelected: Boolean, partialTicks: Float): Float {
return if (this.shouldAnimate(pokemon, isSelected)) partialTicks else 0F
}
protected open fun stateFor(pokemon: Pokemon, isSelected: Boolean, index: Int): FloatingState {
if (!this.shouldAnimate(pokemon, isSelected)) {
return this.state
}
val state = this.stateAtIndex.getOrPut(index) { pokemon.uuid to this.createState(pokemon) }
if (state.first != pokemon.uuid) {
val newState = this.createState(pokemon)
this.stateAtIndex[index] = pokemon.uuid to newState
return newState
}
return state.second
}
protected open fun createState(pokemon: Pokemon): FloatingState {
val state = FloatingState()
state.currentAspects = pokemon.aspects
return state
}
}
@@ -1,25 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.portrait
import com.cobblemon.mod.common.pokemon.Pokemon
import net.minecraft.client.GraphicsStatus
/**
* Draws portraits if the Pokemon is not fainted.
*
* Meant for client with [GraphicsStatus.FABULOUS].
*/
class FullyAnimatedPortraitDrawer : AnimatedPortraitDrawer() {
override fun shouldAnimate(pokemon: Pokemon, isSelected: Boolean): Boolean {
return !pokemon.isFainted()
}
}
@@ -1,37 +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.gui.portrait
import com.cobblemon.mod.common.api.gui.drawPosablePortrait
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.pokemon.Pokemon
import com.mojang.blaze3d.vertex.PoseStack
import net.minecraft.client.GraphicsStatus
/**
* The simplest implementation, the Pokemon is drawn staying still.
*
* Meant for client with [GraphicsStatus.FAST].
*/
class InanimatePortraitDrawer : PortraitDrawer {
private val state = FloatingState()
override fun draw(pokemon: Pokemon, poseStack: PoseStack, partialTicks: Float, isSelected: Boolean, index: Int) {
state.currentAspects = pokemon.aspects
drawPosablePortrait(
identifier = pokemon.species.resourceIdentifier,
matrixStack = poseStack,
partialTicks = 0F,
contextScale = pokemon.form.baseScale,
state = this.state
)
}
}
@@ -1,38 +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.gui.portrait
import com.cobblemon.mod.common.pokemon.Pokemon
import com.mojang.blaze3d.vertex.PoseStack
/**
* Responsible for drawing a Pokemon portrait.
*
* Keep in mind any scaling and handling of the [PoseStack] must be done on the implementations using this system.
*/
fun interface PortraitDrawer {
/**
* Draws the portrait.
*
* @param pokemon The [Pokemon] being drawn.
* @param poseStack The [PoseStack] being used.
* @param partialTicks The partial ticks of the screen using this.
* @param isSelected If the [pokemon] is considered in a selected state for example hovered or the current party member.
* @param index The index of the [pokemon] in the screen using this.
*/
fun draw(
pokemon: Pokemon,
poseStack: PoseStack,
partialTicks: Float,
isSelected: Boolean,
index: Int
)
}
@@ -1,25 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.portrait
import com.cobblemon.mod.common.pokemon.Pokemon
import net.minecraft.client.GraphicsStatus
/**
* Draws portraits if the Pokemon is not fainted and selected.
*
* Meant for client with [GraphicsStatus.FANCY].
*/
class SelectedAnimatedPortraitDrawer : AnimatedPortraitDrawer() {
override fun shouldAnimate(pokemon: Pokemon, isSelected: Boolean): Boolean {
return !pokemon.isFainted() && isSelected
}
}
@@ -8,9 +8,11 @@
package com.cobblemon.mod.common.client.gui.summary.widgets
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.PokemonGUIAnimationStyle
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.summary.Summary
import com.cobblemon.mod.common.client.render.drawScaledText
@@ -154,13 +156,20 @@ class PartySlotWidget(
matrices.pushPose()
matrices.translate(x + (PORTRAIT_DIAMETER / 2.0), y - 3.0, 0.0)
matrices.scale(2.5F, 2.5F, 1F)
val shouldAnimate = when (Cobblemon.config.summaryProfileAnimations) {
PokemonGUIAnimationStyle.ALWAYS_ANIMATE -> true
PokemonGUIAnimationStyle.NEVER_ANIMATE -> false
PokemonGUIAnimationStyle.ANIMATE_SELECTED -> isHovered || isSelected
}
drawProfilePokemon(
species = slotPokemon.species.resourceIdentifier,
matrixStack = matrices,
rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, 35F, 0F)),
state = state,
scale = 4.5F,
partialTicks = delta
partialTicks = if (shouldAnimate) delta else 0F
)
matrices.popPose()
@@ -78,14 +78,17 @@ object PartySendBinding : CobblemonBlockingKeyBinding(
return
}
if (CobblemonClient.storage.selectedSlot == -1) return
if (Minecraft.getInstance().screen != null) return
val selectedPartyPokemon = CobblemonClient.storage.party.get(CobblemonClient.storage.selectedSlot) ?: return
val selectedPartyPokemon = if (CobblemonClient.storage.selectedSlot >= 0) {
CobblemonClient.storage.party.get(CobblemonClient.storage.selectedSlot)
} else {
null
}
if (isRidingPokemon(player) && canAttemptDismount(player, selectedPartyPokemon)) {
sendToServer(DismountPokemonPacket())
}
else if (!isRidingSelectedPokemon(player, selectedPartyPokemon)){
} else if (selectedPartyPokemon != null && !isRidingSelectedPokemon(player, selectedPartyPokemon)){
checkForTargetInteractions(player, selectedPartyPokemon)
}
}
@@ -105,8 +108,7 @@ object PartySendBinding : CobblemonBlockingKeyBinding(
collideBlock = ClipContext.Fluid.NONE)
if (canSendOutPokemon(player, targetEntity)) {
sendToServer(SendOutPokemonPacket(CobblemonClient.storage.selectedSlot))
}
else {
} else {
processEntityTarget(player, selectedPartyPokemon, targetEntity)
}
}
@@ -140,7 +142,7 @@ object PartySendBinding : CobblemonBlockingKeyBinding(
}
}
private fun canAttemptDismount(player: LocalPlayer, selectedPartyPokemon: Pokemon): Boolean {
private fun canAttemptDismount(player: LocalPlayer, selectedPartyPokemon: Pokemon?): Boolean {
if (player.vehicle !is PokemonEntity) return false
val vehicle = player.vehicle as PokemonEntity
if (player != vehicle.controllingPassenger) {
@@ -148,11 +150,10 @@ object PartySendBinding : CobblemonBlockingKeyBinding(
}
val isAirRide = vehicle.ridingController?.context?.style == RidingStyle.AIR
val hasLandRide = vehicle.rideProp.behaviours?.get(RidingStyle.LAND) != null
if (isAirRide && hasLandRide) {
return false
}
else {
return vehicle.pokemon.uuid == selectedPartyPokemon.uuid
return if (isAirRide && hasLandRide) {
false
} else {
vehicle.pokemon.uuid == selectedPartyPokemon?.uuid
}
}
@@ -15,7 +15,7 @@ import com.cobblemon.mod.common.net.messages.client.orientation.ClientboundUpdat
import net.minecraft.client.Minecraft
import net.minecraft.util.Mth
object S2CUpdateOrientationHandler : ClientNetworkPacketHandler<ClientboundUpdateOrientationPacket> {
object ClientboundUpdateOrientationHandler : ClientNetworkPacketHandler<ClientboundUpdateOrientationPacket> {
override fun handle(packet: ClientboundUpdateOrientationPacket, client: Minecraft) {
client.executeIfPossible {
val level = client.level ?: return@executeIfPossible
@@ -125,7 +125,7 @@ class CampfireBlockEntityRenderer(ctx: BlockEntityRendererProvider.Context) : Bl
) {
val seasonings = blockEntity.getSeasonings()
val gameTime = (blockEntity.level?.gameTime ?: 0) + tickDelta
val gameTime = blockEntity.time + tickDelta
val rotationAngle = (gameTime * ROTATION_SPEED) % 360
seasonings.forEachIndexed { index, seasoning ->
@@ -8,6 +8,7 @@
package com.cobblemon.mod.common.client.render.entity
import com.cobblemon.mod.common.OrientationControllable
import com.cobblemon.mod.common.api.riding.Rideable
import com.cobblemon.mod.common.client.MountedPokemonAnimationRenderController
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
@@ -30,11 +31,20 @@ object MountedLivingEntityRenderer {
partialTicks: Float,
i: Float
) {
val vehicle = entity.vehicle
if (entity.vehicle !is Rideable) return
val matrix = stack.last().pose()
val delegate = pokemon.delegate as PokemonClientDelegate
val locator = delegate.locatorStates[delegate.getSeatLocator(entity)]
if (vehicle is OrientationControllable) {
val vehicleController = vehicle.orientationController
if (vehicleController.isActive() && vehicleController.orientation != null) {
vehicleController.tick()
}
}
//Positions entity
if (locator != null) {
MountedPokemonAnimationRenderController.setup(pokemon, partialTicks)
@@ -187,7 +187,7 @@ class PokemonRenderer(
if (!DISABLE_ROLLING_DEBUG && controller.active) {
// Allow the ride controller to modify its rotations using partialTick
entity.applyRenderRotation(partialTicks)
entity.delegate.applyRenderRotation(partialTicks)
val matrix = poseMatrix.last().pose()
val yaw = Mth.rotLerp(partialTicks, entity.yBodyRotO, entity.yBodyRot)
@@ -8,10 +8,11 @@
package com.cobblemon.mod.common.command
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.config
import com.cobblemon.mod.common.api.permission.CobblemonPermissions
import com.cobblemon.mod.common.api.spawning.CobblemonWorldSpawnerManager
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator.Companion.prioritizedAreaCalculators
import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput
import com.cobblemon.mod.common.api.text.add
import com.cobblemon.mod.common.api.text.green
@@ -24,6 +25,7 @@ import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.command.argument.SpawnBucketArgumentType
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.permission
import com.cobblemon.mod.common.util.spawner
import com.mojang.brigadier.Command
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.context.CommandContext
@@ -56,11 +58,11 @@ object CheckSpawnsCommand {
return 0
}
val spawner = CobblemonWorldSpawnerManager.spawnersForPlayers[player.uuid] ?: return Command.SINGLE_SUCCESS
val spawner = player.spawner
val bucket = SpawnBucketArgumentType.getSpawnBucket(context, name = "bucket")
val cause = SpawnCause(spawner, player)
val slice = spawner.spawningZoneGenerator.generate(
val slice = Cobblemon.spawningZoneGenerator.generate(
spawner = spawner,
input = SpawningZoneInput(
cause = cause,
@@ -74,9 +76,9 @@ object CheckSpawnsCommand {
)
)
val contexts = spawner.spawnablePositionResolver.resolve(spawner, spawner.spawnablePositionCalculators, slice)
val contexts = Cobblemon.areaSpawnablePositionResolver.resolve(spawner, prioritizedAreaCalculators, slice)
val spawnProbabilities = spawner.getSpawningSelector().getProbabilities(spawner, bucket, contexts)
val spawnProbabilities = spawner.selector.getProbabilities(spawner, bucket, contexts)
val spawnNames = mutableMapOf<String, MutableComponent>()
val namedProbabilities = mutableMapOf<MutableComponent, Float>()
@@ -9,7 +9,6 @@
package com.cobblemon.mod.common.command
import com.cobblemon.mod.common.api.permission.CobblemonPermissions
import com.cobblemon.mod.common.api.spawning.CobblemonWorldSpawnerManager
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.detail.EntitySpawnResult
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePosition
@@ -19,6 +18,7 @@ import com.cobblemon.mod.common.util.alias
import com.cobblemon.mod.common.util.commandLang
import com.cobblemon.mod.common.util.effectiveName
import com.cobblemon.mod.common.util.permission
import com.cobblemon.mod.common.util.spawner
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.context.CommandContext
@@ -31,7 +31,7 @@ import net.minecraft.commands.Commands.literal
*
* `/spawnpokemonfrompool [amount]` or the alias `/forcespawn [amount]`
*
* This command can fail if the randomly selection spawn region has no possible [AreaSpawnablePosition]. For example if
* This command can fail if the randomly selected spawn region has no possible [AreaSpawnablePosition]. For example if
* you are flying in the air
*/
object SpawnPokemonFromPool {
@@ -54,47 +54,26 @@ object SpawnPokemonFromPool {
private fun execute(context: CommandContext<CommandSourceStack>, amount: Int): Int {
val player = context.source.playerOrException
val spawner = CobblemonWorldSpawnerManager.spawnersForPlayers.getValue(player.uuid)
val spawner = player.spawner
var spawnsTriggered = 0
// This could instead directly use a [Spawner] method if refactored, as it is currently it has
// entity counting coupled to the generation of an entity to spawn. Might be a good future change?
for (i in 1..amount) {
val spawnCause = SpawnCause(spawner = spawner, entity = spawner.getCauseEntity())
val zoneInput = spawner.getZoneInput(spawnCause) ?: continue
val zone = spawner.spawningZoneGenerator.generate(spawner, zoneInput)
val contexts = spawner.spawnablePositionResolver.resolve(spawner, spawner.spawnablePositionCalculators, zone)
val influences = spawner.getAllInfluences() + zone.unconditionalInfluences
// This has a chance to fail, if you get a spawning zone that has no associated contexts.
// but as it was selected at random by the Spawning Zone Generator, it could just be a miss which
// means two attempts to spawn in the same location can have differing results (which is expected for
// randomness).
if (contexts.isEmpty()) {
repeat(times = amount) {
val spawnCause = SpawnCause(spawner = spawner, entity = player)
val results = spawner.runForArea(spawner.getZoneInput(spawnCause) ?: return@repeat, maxSpawns = 1)
if (results.isEmpty()) {
player.sendSystemMessage(UNABLE_TO_SPAWN.red())
continue
return@repeat
}
val bucket = spawner.chooseBucket(spawnCause, influences)
val spawnAction = spawner.getSpawningSelector().select(spawner, bucket, contexts, max = 1).firstOrNull() // one at a time
if (spawnAction == null) {
player.sendSystemMessage(UNABLE_TO_SPAWN.red())
continue
}
spawnAction.future.thenApply {
results.forEach {
if (it is EntitySpawnResult) {
for (entity in it.entities) {
player.sendSystemMessage(commandLang("spawnpokemonfrompool.success", entity.effectiveName()).green())
spawnsTriggered++
player.sendSystemMessage(commandLang("spawnpokemonfrompool", entity.effectiveName()).green())
}
}
}
spawnAction.complete()
spawnsTriggered++
}
return spawnsTriggered
@@ -11,7 +11,7 @@ package com.cobblemon.mod.common.config
import com.cobblemon.mod.common.api.drop.ItemDropMethod
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CaptureCalculator
import com.cobblemon.mod.common.api.pokemon.status.Statuses
import com.cobblemon.mod.common.client.gui.portrait.PortraitStyle
import com.cobblemon.mod.common.client.gui.PokemonGUIAnimationStyle
import com.cobblemon.mod.common.config.CobblemonConfigField.CobblemonConfigSide.CLIENT
import com.cobblemon.mod.common.config.CobblemonConfigField.CobblemonConfigSide.SERVER
import com.cobblemon.mod.common.config.constraint.IntConstraint
@@ -155,6 +155,9 @@ class CobblemonConfig {
@CobblemonConfigField(Category.Spawning, lang = "pokemon_per_chunk", SERVER)
var pokemonPerChunk = 1F
@CobblemonConfigField(Category.Spawning, lang = "poke_snack_pokemon_per_chunk", SERVER)
var pokeSnackPokemonPerChunk = 2F
@CobblemonConfigField(Category.PassiveStatus, lang = "passive_statuses", SERVER)
var passiveStatuses = mutableMapOf(
Statuses.POISON.configEntry(),
@@ -273,7 +276,16 @@ class CobblemonConfig {
var summaryPokemonFollowCursor = true
@CobblemonConfigField(Category.Interface, lang = "party_portrait_animations", CLIENT)
var partyPortraitAnimations = PortraitStyle.NEVER_ANIMATE
var partyPortraitAnimations = PokemonGUIAnimationStyle.NEVER_ANIMATE
@CobblemonConfigField(Category.Interface, lang = "pc_profile_animations", CLIENT)
var pcProfileAnimations = PokemonGUIAnimationStyle.ANIMATE_SELECTED
@CobblemonConfigField(Category.Interface, lang = "summary_profile_animations", CLIENT)
var summaryProfileAnimations = PokemonGUIAnimationStyle.ANIMATE_SELECTED
@CobblemonConfigField(Category.Interface, lang = "animate_battle_tiles", CLIENT)
var animateBattleTiles = false
@CobblemonConfigField(Category.Riding, lang = "third_person_view_bobbing", CLIENT)
var thirdPersonViewBobbing = true
@@ -11,16 +11,17 @@ package com.cobblemon.mod.common.entity.ai
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.util.toBlockPos
import com.google.common.collect.ImmutableMap
import java.util.UUID
import kotlin.math.pow
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.MoverType
import net.minecraft.util.Mth
import net.minecraft.world.entity.ai.behavior.Behavior
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.MemoryStatus
import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.phys.Vec3
import java.util.*
import kotlin.math.cos
import kotlin.math.sin
class FollowHerdLeaderTask : Behavior<PokemonEntity>(
ImmutableMap.of(
@@ -34,7 +35,7 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
var tooFar = 8F
var closeEnough = 4F
/** Chance per tick of following the herd leader. */
val chance = 1 / 20F
val chance = 1 / 60F
override fun checkExtraStartConditions(level: ServerLevel, owner: PokemonEntity): Boolean {
leader = level.getEntity(owner.brain.getMemory(CobblemonMemories.HERD_LEADER).map(UUID::fromString).orElse(null) ?: return false) as? PokemonEntity
@@ -50,16 +51,19 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
override fun tick(level: ServerLevel, entity: PokemonEntity, gameTime: Long) {
val leader = leader ?: return
if (entity.brain.hasMemoryValue(MemoryModuleType.WALK_TARGET)) {
return // We're busy goin' n' shit
}
val moveControl = entity.moveControl as? PokemonMoveControl ?: return
val leaderMoveControl = leader.moveControl as? PokemonMoveControl ?: return
if (leader.distanceTo(entity) > tooFar) {
entity.brain.setMemory(
MemoryModuleType.WALK_TARGET,
CobblemonWalkTarget(
pos = leader.blockPosition(),
pos = leader.position().add(getRandomOffset(entity, level)).toBlockPos(),
speedModifier = 0.4F,
completionRange = closeEnough.toInt()
)
@@ -69,10 +73,11 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
entity.brain.eraseMemory(CobblemonMemories.HERD_LEADER) // We can't go where this guy's going
return
}
entity.brain.setMemory(
MemoryModuleType.WALK_TARGET,
CobblemonWalkTarget(
pos = leader.blockPosition(),
pos = leader.position().add(getRandomOffset(entity, level)).toBlockPos(),
speedModifier = 0.3F,
completionRange = closeEnough.toInt()
)
@@ -83,7 +88,7 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
entity.brain.setMemory(
MemoryModuleType.WALK_TARGET,
CobblemonWalkTarget(
pos = leader.blockPosition(),
pos = leader.position().add(getRandomOffset(entity, level)).toBlockPos(),
speedModifier = 0.4F,
completionRange = 1
)
@@ -105,14 +110,15 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
// Go to the leader's walk target, roughly.
val walkTarget = leader.brain.getMemory(MemoryModuleType.WALK_TARGET).orElse(null) ?: return
if (entity.distanceToSqr(walkTarget.target.currentPosition()) < leader.distanceToSqr(walkTarget.target.currentPosition())) {
return // We're already closer to the walk target than the leader is, so no need to move, if anything we should get out of the way
// We're already closer to the walk target than the leader is, so no need to move, if anything we should get out of the way
if (entity.distanceToSqr(walkTarget.target.currentPosition()) + closeEnough < leader.distanceToSqr(walkTarget.target.currentPosition())) {
return
}
entity.brain.setMemory(
MemoryModuleType.WALK_TARGET,
CobblemonWalkTarget(
pos = walkTarget.target.currentBlockPosition(),
pos = walkTarget.target.currentPosition().add(getRandomOffset(entity, level)).toBlockPos(),
speedModifier = walkTarget.speedModifier,
nodeTypeFilter = (walkTarget as? CobblemonWalkTarget)?.nodeTypeFilter ?: { true },
completionRange = closeEnough.toInt()
@@ -120,4 +126,23 @@ class FollowHerdLeaderTask : Behavior<PokemonEntity>(
)
}
}
fun getRandomOffset(entity: PokemonEntity, level: ServerLevel): Vec3 {
val leader = leader ?: return Vec3.ZERO
val minRange = closeEnough.toDouble()
val maxRange = (tooFar - closeEnough / 2).toDouble()
val randomDistance = Mth.nextDouble(level.random, minRange, maxRange)
val randomAngle = Mth.nextDouble(level.random, 0.0, 2 * Math.PI)
var offset = Vec3(cos(randomAngle) * randomDistance, 0.0, sin(randomAngle) * randomDistance)
if ((entity.isInLiquid && leader.isInLiquid) || (entity.isFlying() && leader.isFlying())) {
val verticalRange = randomDistance * 0.5
val randomY = Mth.nextDouble(level.random, -verticalRange, verticalRange)
offset = offset.add(0.0, randomY, 0.0)
}
return offset
}
}
@@ -26,6 +26,7 @@ import com.cobblemon.mod.common.api.spawning.fishing.FishingSpawnCause
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence.Companion.TYPICAL_VARIATION
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.client.sound.EntitySoundTracker
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
@@ -372,11 +373,11 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
if (Mth.nextInt(random, 0, 100) < getPokemonSpawnChance(rodStack)) {
val player = playerOwner as? ServerPlayer ?: return
this.typeCaught = TypeCaught.POKEMON
val lureBoost = SpawnBaitEffects.getEffectsFromItemStack(bobberBait)
val stackedLureTier = SpawnBaitEffects.getEffectsFromItemStack(bobberBait)
.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }
.maxOfOrNull { it.value.toInt() }
?: 0
planSpawn(player, rodStack, lureBoost)
.sumOf { it.value }
.toInt()
planSpawn(player, rodStack, stackedLureTier)
val selectedWeight = plannedSpawnAction?.bucket?.weight ?: 50F
val reactionMinMax = calculateMinMaxCountdown(selectedWeight)
@@ -771,11 +772,16 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
lureLevel = lureLevel,
)
val result = spawner.getSpawnAction(
val spawnablePosition = FishingSpawnablePosition(
cause = spawnCause,
world = level() as ServerLevel,
pos = position().toBlockPos(),
influences = listOf(PlayerLevelRangeInfluence(player, TYPICAL_VARIATION), bucketInfluence)
influences = mutableListOf(PlayerLevelRangeInfluence(player, TYPICAL_VARIATION), bucketInfluence)
)
val result = spawner.calculateSpawnActionForPosition(
cause = spawnCause,
spawnablePosition = spawnablePosition
)
this.plannedSpawnAction = result
@@ -811,8 +817,11 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
// create accessory splash particle when you fish something up
particleEntityHandler(this, ResourceLocation.fromNamespaceAndPath("cobblemon", "accessory_fish_splash"))
var baitId = PokerodItem.getBaitStackOnRod(this.bobberBait).takeUnless { it.isEmpty }?.itemHolder?.unwrapKey()
?.orElse(null)?.location() ?: "empty_bait".asIdentifierDefaultingNamespace()
val baitId =
if (bobberBait.isEmpty)
"empty_bait".asIdentifierDefaultingNamespace()
else
bobberBait.itemHolder.unwrapKey().map { it.location() }.orElse("empty_bait".asIdentifierDefaultingNamespace())
val pokemonId = spawnedPokemon.pokemon.species.resourceIdentifier
CobblemonCriteria.REEL_IN_POKEMON.trigger(player, ReelInPokemonContext(pokemonId, baitId))
@@ -850,7 +859,7 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
BobberSpawnPokemonEvent.Post(
this,
spawnAction,
rodItemStack,
bobberBait,
entity
)
)
@@ -121,6 +121,7 @@ import com.cobblemon.mod.common.util.math.geometry.toRadians
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
import com.mojang.serialization.Codec
import com.mojang.serialization.Dynamic
import net.minecraft.client.Minecraft
import java.util.Optional
import java.util.UUID
import java.util.concurrent.CompletableFuture
@@ -300,6 +301,8 @@ open class PokemonEntity(
get() = entityData.get(SHOWN_HELD_ITEM)
set(value) = entityData.set(SHOWN_HELD_ITEM, value)
var lastLightningBoltUUID: UUID? = null
var drops: DropTable? = null
var tethering: PokemonPastureBlockEntity.Tethering? = null
@@ -554,22 +557,55 @@ open class PokemonEntity(
}
override fun thunderHit(level: ServerLevel, lightning: LightningBolt) {
// Ground types shouldn't take lightning damage
val isTypeImmune = ElementalTypes.GROUND in pokemon.types
// Deals with special cases in which Pokemon should either be immune or buffed by lightning strikes.
when (pokemon.ability.name) {
val isAbilityImmune = when (pokemon.ability.name) {
"lightningrod" -> {
this.addEffect(MobEffectInstance(MobEffects.DAMAGE_BOOST, 1200, 1))
true
}
"motordrive" -> {
this.addEffect(MobEffectInstance(MobEffects.MOVEMENT_SPEED, 1200, 0))
true
}
"voltabsorb" -> {
this.addEffect(MobEffectInstance(MobEffects.HEAL, 1, 1))
true
}
// Ground types shouldn't take lightning damage
else -> if (this.pokemon.types.none { it == ElementalTypes.GROUND }) super.thunderHit(level, lightning)
else -> false
}
// Lightning hits entities multiple times, if this Pokémon changed a feature because of this lighting strike it should be immune to all remaining hits of this specific lightning strike as well.
var rotated = this.lastLightningBoltUUID == lightning.uuid
if (!rotated && pokemon.form.behaviour.lightningHit.isSpecial()) {
for (rotateFeature in pokemon.form.behaviour.lightningHit.rotateFeatures) {
val feature: StringSpeciesFeature = pokemon.getFeature(rotateFeature.key) ?: continue
val index = rotateFeature.chain.indexOf(feature.value)
// index is -1 if the element wasn't found, i.e. here if the feature's value is not part of the chain
if (index < 0) continue
val next = index.inc()
val nextIndex = if (next < rotateFeature.chain.size) next else 0
feature.value = rotateFeature.chain[nextIndex]
pokemon.markFeatureDirty(feature)
rotated = true
}
if (rotated) {
this.lastLightningBoltUUID = lightning.uuid
this.playSound(SoundEvents.MOOSHROOM_CONVERT, 2.0F, 1.0F)
pokemon.updateAspects()
}
}
if (!isTypeImmune && !isAbilityImmune && !rotated) {
super.thunderHit(level, lightning)
}
}
override fun tick() {
/* Addresses watchdog hanging that is completely bloody inexplicable. */
yBodyRot = Mth.wrapDegrees(yBodyRot)
@@ -590,8 +626,7 @@ open class PokemonEntity(
if (passengers.isNotEmpty() && level().isClientSide) {
rideSoundManager.tick()
ridingAnimationData.update()
} else if (!passengers.isNotEmpty() && level().isClientSide)
{
} else if (!passengers.isNotEmpty() && level().isClientSide) {
rideSoundManager.stop()
}
@@ -2080,7 +2115,7 @@ open class PokemonEntity(
private fun createSidedPokemon(): Pokemon = Pokemon().apply { isClient = this@PokemonEntity.level().isClientSide }
override fun canRide(entity: Entity): Boolean {
return platform == PlatformType.NONE && super.canRide(entity)
return platform == PlatformType.NONE && super.canRide(entity) && seats.isNotEmpty()
}
// Takes in a requested stat type with a base minimum and base maximum and returns the interpolated
@@ -2096,6 +2131,10 @@ open class PokemonEntity(
return max(statVal, 1e-6)
}
override fun couldAcceptPassenger(): Boolean {
return seats.isNotEmpty() && super.couldAcceptPassenger()
}
fun getRawRideStat(stat: RidingStat, style: RidingStyle): Double {
if (rideStatOverrides[style] != null && rideStatOverrides[style]!![stat] != null) {
return rideStatOverrides[style]!![stat]!!
@@ -2321,12 +2360,6 @@ open class PokemonEntity(
return loc.compareTo(resourceLocation) == 0
}
fun applyRenderRotation(partialTicks: Float) {
ifRidingAvailable { behaviour, settings, state ->
behaviour.applyRenderRotation(settings, state, this, partialTicks)
}
}
var jumpInputStrength: Int = 0 // move this
override fun onPlayerJump(strength: Int) {
// See if this controls the hot bar element
@@ -2424,8 +2457,12 @@ open class PokemonEntity(
fun rideFovMult(): Float {
val driver = this.controllingPassenger as? Player ?: return 1.0f
val fovEffectScale = Minecraft.getInstance().options.fovEffectScale().get().toFloat()
return ifRidingAvailableSupply(fallback = 1.0f) { behaviour, settings, state ->
behaviour.rideFovMultiplier(settings, state, this, driver)
val rideFov = behaviour.rideFovMultiplier(settings, state, this, driver)
1f + (rideFov - 1f) * fovEffectScale
}
}
@@ -45,10 +45,12 @@ class DrowsySensor : Sensor<PokemonEntity>(20) {
entity.brain.eraseMemory(CobblemonMemories.POKEMON_DROWSY)
}
} else {
if (!isDrowsy && shouldBeDrowsy) {
if (!isDrowsy && shouldBeDrowsy && entity.random.nextFloat() < rest.drowsyChance) {
entity.brain.setMemory(CobblemonMemories.POKEMON_DROWSY, true)
} else if (isDrowsy && !shouldBeDrowsy) {
entity.brain.eraseMemory(CobblemonMemories.POKEMON_DROWSY)
} else if (isDrowsy && entity.random.nextFloat() < rest.rouseChance) {
entity.brain.eraseMemory(CobblemonMemories.POKEMON_DROWSY)
}
}
}
Loaded 100 of 463 files, more files were not shown because too many files have changed in this diff. Show more