Compare commits

...
220 Commits
Author SHA1 Message Date
terrainwax 767fc05387 feat: add test on weather and ensure weather is good 2026-02-25 22:26:43 +01:00
terrainwax 1941636113 feat: add a CompetitiveBattleAI WIP need more testing on 2v2 battle 2026-01-01 18:24:09 +01:00
Hiroku 1a3b655d3f Merge branch 'the-tiniest-of-tiny-changes' into 'main'
Edit pokemon_model.json

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

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

See merge request cable-mc/cobblemon!2090
2025-12-20 01:59:22 +00:00
Spicy 645cf61911 Fixed the run_script Molang function to not fail if the environment's context is null. 2025-12-19 19:48:27 -06:00
Hiroku 87d5ede3dd Merge branch 'RestoreCombeeLeavesPathing' into 'main'
RestoreCombeeLeavesPathing

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

See merge request cable-mc/cobblemon!1985
2025-12-19 07:36:21 +00:00
JazzMcNade f6ad7ff865 Fix changelog 2025-12-17 21:08:46 -06:00
JazzMcNade 54e8763470 Fixed a bug in which Combees could become stuck inside of leaves 2025-12-13 21:17:59 -06:00
JazzMcNade db34687df0 Merge branch 'main' into RestoreCombeeLeavesPathing 2025-12-13 21:11:57 -06:00
Polymeta 06ebf51078 Merge branch 'Wii-Break-It-Wii-Fix-It' into 'main'
Boat Tags and Mint Leaves

Closes #1855

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

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

See merge request cable-mc/cobblemon!2081
2025-12-12 06:17:17 +00:00
Micky 08ea21fff4 Fix Soothe Bell not tagged as cobblemon:held/is_held_item 2025-12-10 13:27:23 +00:00
JazzMcNade 9dac604a93 Merge branch 'main' into RestoreCombeeLeavesPathing 2025-12-10 00:20:49 -06:00
Polymeta 28c0f8ab7d Merge branch 'item-interactions-sync' into 'main'
Item interactions sync

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

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

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

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

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

Closes #1833

See merge request cable-mc/cobblemon!2078
2025-12-08 11:36:41 +00:00
Jacob Hartmann 17f3edf2be Changelog 2025-12-07 20:35:11 -07:00
Jacob Hartmann 9c5bfda20f Updating texture paths 2025-12-07 20:34:09 -07:00
Gertimoshka 6d545fc1ca Fixes a crash caused by malformed riding prespectives json file when trying to ride a pokemon 2025-12-07 11:47:04 +02:00
Polymeta 9a8ad27122 Merge branch 'fix-spectator-crash' into 'main'
fix spectator crash

Closes #1847

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

See merge request cable-mc/cobblemon!2074
2025-12-07 00:49:48 +00:00
jasoncramer 64c34230bf chore: add to changelog 2025-12-06 19:28:41 -05:00
500-internal-server-error 36881b70be fix spectator crash 2025-12-06 20:51:48 +07:00
jasoncramer 9b052478f6 fix: use regional form data for spawn bait influence 2025-12-05 14:23:05 -05:00
Hiroku 339f8339f0 Merge branch 'RestorePokemonItemPickup' into 'main'
Restore Pokemon item pickup

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

See merge request cable-mc/cobblemon!2072
2025-12-05 07:45:29 +00:00
JazzMcNade e5666e7a82 Restore Pokemon item pickup. Remove unnecessary check in shouldBeSaved() 2025-12-05 01:30:20 -06:00
Neovitalism a588213323 Update CHANGELOG.md 2025-12-04 03:05:03 -06:00
Neovitalism 56b2ba08ad Prevents outside influences from crashing the logical server.
Before when it was completed exceptionally w/ the pokesnack spawner, it would crash the server.
2025-12-04 03:03:57 -06:00
Khvrram 7e6ca7be5e Merge branch cobblemon:main into main 2025-12-04 01:59:17 +00:00
Neovitalism 25ce784a60 Hopefully fixes changelog
Idk why gitlab didn't detect that when merging main
2025-12-03 10:17:48 -06:00
Neovitalism 5358cccb62 Merge branch 'main' into label-consistency 2025-12-03 10:09:41 -06:00
Polymeta c43b3bbb4d Merge branch 'fix-can-drop-held-item' into 'main'
Fix can drop held item function

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

Closes #1832

See merge request cable-mc/cobblemon!2067
2025-12-03 08:15:26 +00:00
JazzMcNade b9cf18adbd Changelog 2025-12-02 20:16:56 -06:00
JazzMcNade 30650e1f9f Code simplication 2025-12-02 20:12:37 -06:00
JazzMcNade eaa558a7c8 Check for creative on using empty bottle 2025-12-02 00:36:53 -06:00
JazzMcNade 0dc725e513 Can only receive honey at max age. Adds result to hand if possible. Checks for creative and server/client 2025-12-02 00:28:23 -06:00
Sterrezo a575c6bc3d Update CHANGELOG.md 2025-12-01 15:47:34 +01:00
Sterrezo 9c102a7850 Merge branch 'main' into sterr/ride-sound-changes 2025-12-01 15:45:40 +01:00
Sterrezo 22a1eaab19 Volume balancing 2025-12-01 15:35:22 +01:00
Sterrezo 0118ace5fc Implemented stereo flight loops 2025-12-01 15:09:37 +01:00
Sterrezo d4a93abbf7 Added support for submerged ride sounds 2025-12-01 14:27:55 +01:00
Sterrezo d0ee3f3e9d Added an option for ride sounds to not play for the passengers 2025-12-01 13:56:56 +01:00
JazzMcNade 698fa9dff9 Merge branch 'main' into RestoreCombeeLeavesPathing 2025-12-01 00:46:50 -06:00
Khvrram 46ae53f565 Merge branch cobblemon:main into main 2025-12-01 04:25:52 +00:00
Hiroku 7d4e5892ba Merge branch 'main' into 'main'
Orientation after crash fix

See merge request cable-mc/cobblemon!2061
2025-12-01 00:23:40 +00:00
KhvrramandHiroku f9de415020 Orientation after crash fix 2025-12-01 00:23:39 +00:00
Khurram Valiyev 4d123b6e4b Fixed shiny Pokémon not respecting the silent flag on spawn. 2025-11-29 22:50:52 -05:00
Polymeta 54dc2e7f6f tidy up changelog [skip-ci] 2025-11-29 12:57:24 +00:00
Polymeta 1d60ba093f Merge branch 'convention-tags-not-being-consistent-grrrrrrrr' into 'main'
Why

See merge request cable-mc/cobblemon!2060
2025-11-29 11:53:50 +00:00
Hiroku 0fef74dce7 Edit OneToOneFileStoreAdapter.kt 2025-11-29 09:46:27 +00:00
Hiroku b08647bb32 Revert "Merge branch 'main' into 'main'"
This reverts merge request !2062
2025-11-29 04:26:57 +00:00
Hiroku de9e89f040 Merge branch 'main' into 'main'
Editar en_us.json

See merge request cable-mc/cobblemon!2062
2025-11-29 04:25:10 +00:00
Ezequiel Neri Ferreira 84a29ebc2a Editar en_us.json 2025-11-29 04:22:49 +00:00
Paul 37b13ae959 Unsnaps your shot 2025-11-29 12:44:40 +10:00
dhi_holo 1cacbe8306 Why
It's called Shears, that's the item name, why does NeoForge use a Shear tag instead
Atleast Fabric has that tag as well so it's not a hard fix
2025-11-29 12:30:45 +10:00
Paul 12d224d15a Reinforce switchForm for crashes 2025-11-29 11:25:58 +10:00
Paul a57eee3afc Changelog updates [skip-ci] 2025-11-29 10:46:19 +10:00
Hiroku 57595c999c Merge branch 'remove-snapshot' into 'main'
Remove snapshot for release

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

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

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

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

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

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

See merge request cable-mc/cobblemon!2050
2025-11-28 15:38:40 +00:00
Guilherme Pereira c155b5575d change free look binding default to left alt 2025-11-28 09:35:13 -03:00
Jerzykv 9c6b8b231a oops 2 2025-11-28 13:30:16 +01:00
Jerzykv 92b404dac4 oops 2025-11-28 13:27:19 +01:00
Jerzykv 8a1c536d68 Fixed Furfrou's pink and magenta trims being swapped 2025-11-28 12:56:07 +01:00
Jerzykv 04719d22a5 Fixed a tiny issue with Ceruledge's poser 2025-11-28 12:55:54 +01:00
Jerzykv dacc312756 Implemented Tinkatink line remodels 2025-11-28 12:55:26 +01:00
Polymeta ee4a3d46ba Merge branch '1.7.1-spawns' into 'main'
Updated some spawns.

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

See merge request cable-mc/cobblemon!2039
2025-11-27 19:31:24 +00:00
Guilherme Pereira c4d8eae47e add defend owner task once again 2025-11-27 12:16:54 -03:00
Polymeta 2a5a292386 fix saccharine hanging signs sometimes dropping oak wood signs
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-11-27 11:27:58 +00:00
Polymeta e8a09f5c48 Merge branch 'json-fixing' into 'main'
JSON Fixes

Closes #1819

See merge request cable-mc/cobblemon!2048
2025-11-27 10:42:07 +00:00
dhi_holo e82193cdfd Update interact_with_pokemon.json
Removed accidental flag
2025-11-27 19:56:33 +10:00
dhi_holo 87e7cdebf6 Fabric Tags that aren't true c tags are the bane of my existence
Fixed the concrete tags to behave properly, changelog
2025-11-27 15:25:53 +10:00
dhi_holo 9ca84d852f Update interact_with_pokemon.json
Should close #1819
2025-11-27 14:22:06 +10:00
Carmendar 8194f93d13 Merge branch 'main' into implement_pokemon 2025-11-26 20:54:41 -06:00
Carmendar c158ec33fa Braviary sleep cry fix 2025-11-26 20:53:46 -06:00
Carmendar 1e27c7494e Merge remote-tracking branch 'origin/implement_pokemon' into implement_pokemon 2025-11-26 20:52:27 -06:00
Carmendar 54a1f8d932 Braviary battle pose fix 2025-11-26 20:51:45 -06:00
Polymeta 635f29e1ac fix glass bottle not being returned when using honey on saccharine leaves
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-11-26 22:08:12 +00:00
Polymeta db2001b60a Merge branch 'attempted-jvm-fix' into 'main'
Fixed JVM crash from campfire pot

Closes #1795

See merge request cable-mc/cobblemon!2040
2025-11-26 21:40:30 +00:00
HirokuandPolymeta b5098a2ad0 Fixed JVM crash from campfire pot 2025-11-26 21:40:29 +00:00
Kenji_64 d2af0c9332 Fixed Swampert line's missing recoils crashing the game 2025-11-26 15:40:40 +01:00
Polymeta 8c939ee276 Merge branch 'lets-wind-down' into 'main'
Fixes wind charges recalling pastured pokémon

Closes #1805

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

Closes #1789 and #1787

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

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

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

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

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

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

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

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

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

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

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

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

Closes #1793

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Translation: Cobblemon/Mod
Translate-URL: http://lang.cobblemon.com/projects/cobblemon/mod/ja/
2025-11-22 01:45:14 +00:00
Nick 3a2ff9de75 1.7.1 SNAPSHOT in prep for next development cycle 2025-11-21 17:06:08 -08:00
Neovitalism 46d278ca55 key \:) 2025-11-17 03:18:02 -06:00
Neovitalism 339038956d Support for deserialization from any key
...I think.
2025-11-17 03:09:21 -06:00
Neovitalism 116a93d8cc Update CHANGELOG.md 2025-11-16 04:19:32 -06:00
Neovitalism acebc35d3f Who are we if not consistent?
Before, this property was called `PokemonFlagProperty`, had the key `tag`, and checked pokémon's labels. It was also in a package by itself.

Now, it's moved down into the properties package, renamed to LabelProperty, and the key "label" was added, keeping "tag" as a secondary for those that are using it currently.
2025-11-16 04:10:45 -06:00
318 changed files with 25512 additions and 7013 deletions
+39 -996
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
# Changelog
## [1.7.1 (November 29th, 2025)](#1-7-1)
### Additions
- Added trades for Saccharine Saplings, Hearty Grains, Chipped Pot, and Masterpiece Teacup to the Wandering Trader.
- Added brewing recipe for Throat Spray.
- Added vanilla sprinting's field of view change to Pokémon land mounts.
- Added a customizable keybind for the riding freelook button.
- Added double tap to sprint on land mounts.
### Changes
- Made Jet pitch turning 1.5x faster across all Pokémon.
- Party Pokémon now attack only mobs that are attacking their owner, instead of randomly targeting unrelated mobs and getting themselves hurt.
- Reduced the cost of the Vivichoke Seed trade with the Wandering Trader.
### Fixes
- Fixed incorrect camera pivot on Bird, Jet, and Dolphin mounts, leading to some disorienting riding.
- Fixed players suffocating while on vanilla mounts.
- Fixed a graphics crash that could occur on some machines when campfire pots were nearby.
- Fixed ride controls overlay being displayed to passengers. The passengers probably don't need to know about those.
- Fixed mount jumping so that it doesn't switch back to ground animations.
- Fixed ridden Pokémon land collision and some issues around stepping up blocks. Land mounts are smoother overall now.
- Fixed Furfrou's pink and magenta trims being swapped. Only a man could make that mistake.
- Fixed friendship being reset to the default value when evolving a Pokémon.
- Fixed Combees not depositing honey upon leaving a hive if they entered it with nectar.
- Fixed pastured Combees not moving during the night or in rain.
- Fixed a case in which a Combee might try to path to a flower that no longer exists.
- Fixed Combees attempting to enter a full hive.
- Fixed a crash involving Combees and hives. 500 bee bugs. Bugged bees? We can be bees. Mark, this is good news.
- Fixed a crash related to NPC navigation.
- Fixed a crash when opening a PC box with certain wallpapers while using *VulkanMod*.
- Fixed a crash when you place a block in the way of hearty grains 2nd block.
- Fixed passengers hearing the shiny noise of a ridden shiny Pokémon.
- Ponigiri can no longer be eaten at full hunger.
- Fixed Poké Snack spawning sounds not coming from the block.
- Medicinal Brew's Campfire Pot recipe now correctly displays that it can be made using an empty glass bottle or a filled bottle.
- Fixed glass bottle not being returned when slathering honey on a saccharine leaf block.
- Fixed saccharine hanging signs sometimes dropping their oak wood counterpart when being broken.
- Fixed a bunch of incompatibilities with NeoForge mods.
- Fixed a bug where wind charges hitting pasture blocks would recall all Pokémon in it.
- Fixed the Home on the Range advancement triggering when opening the Pokémon interaction wheel.
- Fixed recipes that use concrete not working on NeoForge.
- Fixed Boltund's model.
- Fixed Cyclizar's textures.
### Developer
### Molang & Datapacks
- Removed automatic generation of PC wallpaper screen glow if no asset is provided; it is now optional.
- This is because there's no practical way to do this without blowing up things like *VulkanMod*.
### Particles
### Localization
- Updated translations for:
- French
- Japanese
- Korean
- Brazilian Portuguese
- Simplified Chinese
- Traditional Chinese
@@ -17,6 +17,7 @@ import net.minecraft.nbt.CompoundTag;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.ai.memory.MemoryStatus;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.BeehiveBlock;
@@ -52,7 +53,9 @@ public abstract class BeeOccupantMixin {
var facing = state.getValue(HorizontalDirectionalBlock.FACING);
newPos = pos.relative(facing).getCenter();
newYaw = facing.toYRot();
brain.setMemory(CobblemonMemories.HIVE_LOCATION, pos); // This needs to be set in the case that the hive was picked up and moved.
if (brain.checkMemory(CobblemonMemories.HIVE_LOCATION, MemoryStatus.REGISTERED)) {
brain.setMemory(CobblemonMemories.HIVE_LOCATION, pos); // This needs to be set in the case that the hive was picked up and moved.
}
} else {
// Block has likely been destroyed
var center = pos.getCenter();
@@ -62,25 +65,31 @@ public abstract class BeeOccupantMixin {
center.z + level.random.nextFloat() * 0.6 - 0.3
);
newYaw = level.random.nextFloat() * 360F;
brain.eraseMemory(CobblemonMemories.HIVE_LOCATION);
if (brain.checkMemory(CobblemonMemories.HIVE_LOCATION, MemoryStatus.REGISTERED)) {
brain.eraseMemory(CobblemonMemories.HIVE_LOCATION);
}
}
entity.yRotO = newYaw;
entity.setPos(newPos);
// Do honey logic
var hasNectar = brain.getMemory(CobblemonMemories.HAS_NECTAR).orElse(false);
if (hasNectar) {
// Remove nectar and reset got to hive cooldown
brain.eraseMemory(CobblemonMemories.HAS_NECTAR);
brain.setMemoryWithExpiry(CobblemonMemories.HIVE_COOLDOWN, true, PathToBeeHiveTaskConfig.STAY_OUT_OF_HIVE_COOLDOWN);
// Increment honey level of the hive
if (state.is(BlockTags.BEEHIVES, (blockStateBase) -> blockStateBase.hasProperty(BeehiveBlock.HONEY_LEVEL))) {
int i = state.getValue(BeehiveBlock.HONEY_LEVEL);
if (i < 5) {
int j = level.random.nextInt(100) == 0 ? 2 : 1;
if (i + j > 5) {
--j;
if (brain.checkMemory(CobblemonMemories.HAS_NECTAR, MemoryStatus.REGISTERED)) {
var hasNectar = brain.getMemory(CobblemonMemories.HAS_NECTAR).orElse(false);
if (hasNectar) {
// Remove nectar and reset got to hive cooldown
brain.eraseMemory(CobblemonMemories.HAS_NECTAR);
if (brain.checkMemory(CobblemonMemories.HIVE_LOCATION, MemoryStatus.REGISTERED)) {
brain.setMemoryWithExpiry(CobblemonMemories.HIVE_COOLDOWN, true, PathToBeeHiveTaskConfig.STAY_OUT_OF_HIVE_COOLDOWN);
}
// Increment honey level of the hive
if (state.is(BlockTags.BEEHIVES, (blockStateBase) -> blockStateBase.hasProperty(BeehiveBlock.HONEY_LEVEL))) {
int i = state.getValue(BeehiveBlock.HONEY_LEVEL);
if (i < 5) {
int j = level.random.nextInt(100) == 0 ? 2 : 1;
if (i + j > 5) {
--j;
}
level.setBlockAndUpdate(pos, state.setValue(BeehiveBlock.HONEY_LEVEL, i + j));
}
level.setBlockAndUpdate(pos, state.setValue(BeehiveBlock.HONEY_LEVEL, i + j));
}
}
}
@@ -92,12 +101,13 @@ public abstract class BeeOccupantMixin {
@Inject(method = "of", at = @At("HEAD"), cancellable = true)
private static void cobblemon$of(Entity entity, CallbackInfoReturnable<BeehiveBlockEntity.Occupant> cir) {
if (entity instanceof PokemonEntity pokemonEntity) {
List<String> IGNORED_BEE_TAGS = Arrays.asList("Air", "ArmorDropChances", "ArmorItems", "Brain", "CanPickUpLoot", "DeathTime", "FallDistance", "FallFlying", "Fire", "HandDropChances", "HandItems", "HurtByTimestamp", "HurtTime", "LeftHanded", "Motion", "NoGravity", "OnGround", "PortalCooldown", "Pos", "Rotation", "Passengers", "leash", "UUID");
List<String> IGNORED_BEE_TAGS = Arrays.asList("Air", "ArmorDropChances", "ArmorItems", "CanPickUpLoot", "DeathTime", "FallDistance", "FallFlying", "Fire", "HandDropChances", "HandItems", "HurtByTimestamp", "HurtTime", "LeftHanded", "Motion", "NoGravity", "OnGround", "PortalCooldown", "Pos", "Rotation", "Passengers", "leash", "UUID");
CompoundTag compoundTag = new CompoundTag();
entity.save(compoundTag);
Objects.requireNonNull(compoundTag);
IGNORED_BEE_TAGS.forEach(compoundTag::remove);
Boolean hasNectar = pokemonEntity.getBrain().getMemory(CobblemonMemories.HAS_NECTAR).orElse(false);
boolean hasNectar = pokemonEntity.getBrain().checkMemory(CobblemonMemories.HAS_NECTAR, MemoryStatus.REGISTERED)
&& pokemonEntity.getBrain().getMemory(CobblemonMemories.HAS_NECTAR).orElse(false);
cir.setReturnValue(new BeehiveBlockEntity.Occupant(CustomData.of(compoundTag), 0, hasNectar ? 2400 : 600));
cir.cancel();
}
@@ -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.mixin;
import com.cobblemon.mod.common.block.PreEmptsExplosion;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Explosion;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(value = Explosion.class)
public class ExplosionMixin {
@Redirect(
method = "finalizeExplosion",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/level/Level;getBlockState(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;",
ordinal = 0
)
)
public BlockState cobblemon$whenExploded(Level world, BlockPos pos) {
BlockState blockState = world.getBlockState(pos);
if(blockState.getBlock() instanceof PreEmptsExplosion preExplosionBlock) {
preExplosionBlock.whenExploded(world, blockState, pos);
}
return blockState;
}
}
@@ -1,32 +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.mixin;
import net.minecraft.stats.RecipeBookSettings;
import net.minecraft.world.inventory.RecipeBookType;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Map;
@Mixin(RecipeBookSettings.class)
public class RecipeBookSettingsMixin {
@Shadow @Final public Map<RecipeBookType, RecipeBookSettings.TypeSettings> states;
@Inject(method = "<init>", at = @At("TAIL"))
private void cobblemon$init(CallbackInfo ci) {
states.put(RecipeBookType.valueOf("COOKING_POT"), new RecipeBookSettings.TypeSettings(true, true));
}
}
@@ -11,11 +11,15 @@ 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.riding.RidingStyle;
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner;
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory;
import com.cobblemon.mod.common.api.stats.CobblemonStats;
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.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
@@ -89,4 +93,32 @@ public abstract class ServerPlayerMixin implements PlayerSpawnerAccessor {
private boolean cobblemon$cancelSavingPokemonMounts(boolean original, @Local(ordinal=0) Entity entity) {
return original && !(entity instanceof PokemonEntity);
}
@WrapOperation(method = "checkRidingStatistics", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;getVehicle()Lnet/minecraft/world/entity/Entity;"))
public Entity cobblemon$checkRidingStatistics(ServerPlayer player, Operation<Entity> original, @Local int distance) {
var entity = original.call(player);
if (!(entity instanceof PokemonEntity pokemonEntity)) {
return entity;
}
var ridingController = pokemonEntity.getRidingController();
if (ridingController == null) return entity;
var activeContext = ridingController.getContext();
if (activeContext == null) return entity;
var ridingStyle = activeContext.getStyle();
CobblemonStats.CobblemonStat stat = switch (ridingStyle) {
case RidingStyle.LAND -> CobblemonStats.RIDING_LAND;
case RidingStyle.AIR -> CobblemonStats.RIDING_AIR;
case RidingStyle.LIQUID -> CobblemonStats.RIDING_LIQUID;
default -> null;
};
if (stat != null) {
player.awardStat(CobblemonStats.getStat(stat), distance);
}
return entity;
}
}
@@ -33,7 +33,7 @@ public class AbstractClientPlayerMixin {
cir.setReturnValue(usageContext.getFovMultiplier());
} else {
// Only modify fov through riding if the pokedex is not open
LocalPlayer player = (LocalPlayer) (Object) this;
AbstractClientPlayer player = (AbstractClientPlayer) (Object) this;
// Only modify fov for the driver and not the passengers
if (player != null && player.isPassenger() && player.getVehicle() instanceof PokemonEntity && player.getVehicle().getControllingPassenger() == player) {
PokemonEntity ride = (PokemonEntity) player.getVehicle();
@@ -13,6 +13,7 @@ import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.api.orientation.OrientationController;
import com.cobblemon.mod.common.client.CobblemonClient;
import com.cobblemon.mod.common.client.keybind.keybinds.PartySendBinding;
import com.cobblemon.mod.common.client.keybind.keybinds.RidingFreelookBinding;
import com.cobblemon.mod.common.duck.RidePassenger;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.pokedex.scanner.PokedexUsageContext;
@@ -156,7 +157,7 @@ public abstract class MouseHandlerMixin {
}
var vehicleController = controllableVehicle.getOrientationController();
var lookaroundButtonPressed = Minecraft.getInstance().mouseHandler.isMiddlePressed();
var freeLooking = RidingFreelookBinding.INSTANCE.isDown();
Pair<Boolean, Boolean> mouseShouldModifyDriver = ((PokemonEntity) vehicle).ifRidingAvailableSupply(new Pair(false, false), (behaviour, settings, state) -> {
return behaviour.mouseModifiesDriverRotation(
settings,
@@ -170,7 +171,7 @@ public abstract class MouseHandlerMixin {
// affect the vehicle orientation when looking around.
// If the client is a passenger it will modify these rotations and return
//-------------------------------------------------------------------------
if (lookaroundButtonPressed || player.getControlledVehicle() == null) {
if (freeLooking || player.getControlledVehicle() == null) {
// Modify driver rotations
cobblemon$setDriverRotations(player, cursorDeltaX, cursorDeltaY);
@@ -192,10 +193,10 @@ public abstract class MouseHandlerMixin {
// Smoothly return driver rotations to zero when needed
//-------------------------------------------------------------------------
var playerRotater = (RidePassenger)player;
if( !lookaroundButtonPressed && !mouseShouldModifyDriver.getFirst()){
if( !freeLooking && !mouseShouldModifyDriver.getFirst()){
playerRotater.cobblemon$setRideYRot(lerp(min(5.0f * (float)movementTime, 1.0f), Mth.wrapDegrees(playerRotater.cobblemon$getRideYRot()), 0.0f));
}
if( !lookaroundButtonPressed && !mouseShouldModifyDriver.getSecond()) {
if( !freeLooking && !mouseShouldModifyDriver.getSecond()) {
playerRotater.cobblemon$setRideXRot(lerp(min(5.0f * (float)movementTime, 1.0f), Mth.wrapDegrees(playerRotater.cobblemon$getRideXRot()), 0.0f));
}
@@ -11,6 +11,7 @@ package com.cobblemon.mod.common.mixin.entity;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.duck.RidePassenger;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.pokemon.Pokemon;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.player.LocalPlayer;
@@ -23,6 +24,7 @@ import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.Nullable;
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;
@@ -48,16 +50,8 @@ public abstract class EntityMixin {
)
)
public boolean cobblemon$forceOnGroundForStepUp(Entity entity, Operation<Boolean> original) {
if (entity instanceof PokemonEntity && entity.hasControllingPassenger()) {
BlockPos below = entity.blockPosition().below();
Level level = entity.level();
var blockStateBelow = level.getBlockState(below);
boolean isAirOrLiquid = blockStateBelow.isAir() || !blockStateBelow.getFluidState().isEmpty();
boolean canSupportEntity = blockStateBelow.isFaceSturdy(level, below, Direction.UP);
boolean standingOnSolid = canSupportEntity && !isAirOrLiquid;
if (standingOnSolid) {
return true;
}
if (entity instanceof PokemonEntity vehicle && entity.hasControllingPassenger()) {
return true;
}
return original.call(entity);
}
@@ -68,18 +62,7 @@ public abstract class EntityMixin {
cancellable = true
)
private void cobblemon$modifyEyePosition_partial(float partialTicks, CallbackInfoReturnable<Vec3> cir) {
var entity = (Entity)(Object)this;
if (entity.level().isClientSide) return;
if (!(entity instanceof Player player)) return;
if (!(player instanceof RidePassenger ridePassenger)) return;
if (!(player.getVehicle() instanceof OrientationControllable vehicle)) return;
var vehicleController = vehicle.getOrientationController();
if (vehicleController == null) return;
// TODO: Determine if it needs partialtick involved.
Vec3 customEyePos = ridePassenger.cobblemon$getRideEyePos();
cir.setReturnValue(customEyePos);
cobblemon$getCustomEyePos(cir);
}
@Inject(
@@ -88,15 +71,20 @@ public abstract class EntityMixin {
cancellable = true
)
private void cobblemon$modifyEyePosition_noPartial(CallbackInfoReturnable<Vec3> cir) {
cobblemon$getCustomEyePos(cir);
}
@Unique
private void cobblemon$getCustomEyePos(CallbackInfoReturnable<Vec3> cir) {
var entity = (Entity)(Object)this;
if (entity.level().isClientSide) return;
if (!(entity instanceof Player player)) return;
if (!(player instanceof RidePassenger ridePassenger)) return;
if (!(player.getVehicle() instanceof OrientationControllable vehicle)) return;
if (!(vehicle instanceof PokemonEntity)) return;
var vehicleController = vehicle.getOrientationController();
if (vehicleController == null) return;
// TODO: Determine if it needs partialtick involved.
Vec3 customEyePos = ridePassenger.cobblemon$getRideEyePos();
cir.setReturnValue(customEyePos);
}
@@ -42,8 +42,8 @@ import com.cobblemon.mod.common.api.scheduling.ServerRealTimeTaskTracker
import com.cobblemon.mod.common.api.scheduling.ServerTaskTracker
import com.cobblemon.mod.common.api.spawning.BestSpawner
import com.cobblemon.mod.common.api.spawning.CobblemonSpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
import com.cobblemon.mod.common.api.spawning.SpawningZoneGenerator
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePositionResolver
import com.cobblemon.mod.common.api.starter.StarterHandler
import com.cobblemon.mod.common.api.stats.CobblemonStats
import com.cobblemon.mod.common.api.storage.PokemonStoreManager
@@ -76,12 +76,7 @@ import com.cobblemon.mod.common.config.LastChangedVersion
import com.cobblemon.mod.common.config.constraint.IntConstraint
import com.cobblemon.mod.common.config.starter.StarterConfig
import com.cobblemon.mod.common.data.CobblemonDataProvider
import com.cobblemon.mod.common.events.AdvancementHandler
import com.cobblemon.mod.common.events.EntityCallbackHandler
import com.cobblemon.mod.common.events.CallbackHandler
import com.cobblemon.mod.common.events.PokedexHandler
import com.cobblemon.mod.common.events.ServerTickHandler
import com.cobblemon.mod.common.events.StatHandler
import com.cobblemon.mod.common.events.*
import com.cobblemon.mod.common.net.messages.client.settings.ServerSettingsPacket
import com.cobblemon.mod.common.permission.LaxPermissionValidator
import com.cobblemon.mod.common.platform.events.PlatformEvents
@@ -96,7 +91,7 @@ import com.cobblemon.mod.common.pokemon.feature.SlowpokeTailRegrowthSpeciesFeatu
import com.cobblemon.mod.common.pokemon.feature.TagSeasonResolver
import com.cobblemon.mod.common.pokemon.helditem.CobblemonHeldItemManager
import com.cobblemon.mod.common.pokemon.properties.*
import com.cobblemon.mod.common.pokemon.properties.tags.PokemonFlagProperty
import com.cobblemon.mod.common.pokemon.properties.LabelProperty
import com.cobblemon.mod.common.pokemon.stat.CobblemonStatProvider
import com.cobblemon.mod.common.starter.CobblemonStarterHandler
import com.cobblemon.mod.common.util.*
@@ -206,6 +201,7 @@ object Cobblemon {
implementation.registerEntityDataSerializers()
implementation.registerCriteria()
implementation.registerEntitySubPredicates()
implementation.registerMobEffects()
DispenserBehaviorRegistry.registerDispenserBehaviors()
DropEntry.register("command", CommandDropEntry::class.java)
@@ -319,7 +315,7 @@ object Cobblemon {
CustomPokemonProperty.register(UncatchableProperty)
CustomPokemonProperty.register(BattleCloneProperty)
CustomPokemonProperty.register(PokemonFlagProperty)
CustomPokemonProperty.register(LabelProperty)
CustomPokemonProperty.register(HiddenAbilityPropertyType)
CustomPokemonProperty.register(AspectPropertyType)
CustomPokemonProperty.register(UnaspectPropertyType)
@@ -215,7 +215,7 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val SACCHARINE_WALL_SIGN = this.create("saccharine_wall_sign", CobblemonWallSignBlock(BlockBehaviour.Properties.ofFullCopy(Blocks.OAK_SIGN), SACCHARINE_WOOD_TYPE))
@JvmField
val SACCHARINE_HANGING_SIGN = this.create("saccharine_hanging_sign", CobblemonHangingSignBlock(BlockBehaviour.Properties.ofFullCopy(Blocks.OAK_WALL_HANGING_SIGN), SACCHARINE_WOOD_TYPE))
val SACCHARINE_HANGING_SIGN = this.create("saccharine_hanging_sign", CobblemonHangingSignBlock(BlockBehaviour.Properties.ofFullCopy(Blocks.OAK_HANGING_SIGN), SACCHARINE_WOOD_TYPE))
@JvmField
val SACCHARINE_WALL_HANGING_SIGN = this.create("saccharine_wall_hanging_sign", CobblemonWallHangingSignBlock(BlockBehaviour.Properties.ofFullCopy(Blocks.OAK_HANGING_SIGN), SACCHARINE_WOOD_TYPE))
@JvmField
@@ -117,6 +117,11 @@ interface CobblemonImplementation {
fun registerEntitySubPredicates()
/**
* TODO
*/
fun registerMobEffects()
/**
* Add a feature to the current platform implementation.
@@ -0,0 +1,33 @@
/*
* 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.cooking.effects.CleanseAllEffect
import com.cobblemon.mod.common.api.cooking.effects.CleanseNegativeEffect
import com.cobblemon.mod.common.api.cooking.effects.MentalRestorationEffect
import com.cobblemon.mod.common.platform.PlatformRegistry
import net.minecraft.core.Registry
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.core.registries.Registries
import net.minecraft.resources.ResourceKey
import net.minecraft.world.effect.MobEffect
object CobblemonMobEffects : PlatformRegistry<Registry<MobEffect>, ResourceKey<Registry<MobEffect>>, MobEffect>() {
override val registry: Registry<MobEffect> = BuiltInRegistries.MOB_EFFECT
override val resourceKey: ResourceKey<Registry<MobEffect>> = Registries.MOB_EFFECT
@JvmField
val CLEANSE_NEGATIVE = create("cleanse_negative", CleanseNegativeEffect())
@JvmField
val MENTAL_RESTORATION = create("mental_restoration", MentalRestorationEffect())
@JvmField
val CLEANSE_ALL = create("cleanse_all", CleanseAllEffect())
}
@@ -21,15 +21,12 @@ import com.cobblemon.mod.common.client.net.callback.partymove.OpenPartyMoveCallb
import com.cobblemon.mod.common.client.net.cooking.ToggleCookingPotLidHandler
import com.cobblemon.mod.common.client.net.data.CobblemonMechanicsSyncHandler
import com.cobblemon.mod.common.client.net.data.DataRegistrySyncPacketHandler
import com.cobblemon.mod.common.client.net.data.PokemonInteractionsSyncHandler
import com.cobblemon.mod.common.client.net.data.RideSettingsSyncHandler
import com.cobblemon.mod.common.client.net.debug.OpenRidingStatsDebugGUIHandler
import com.cobblemon.mod.common.client.net.dialogue.DialogueClosedHandler
import com.cobblemon.mod.common.client.net.dialogue.DialogueOpenedHandler
import com.cobblemon.mod.common.client.net.effect.PokeSnackBlockParticlesHandler
import com.cobblemon.mod.common.client.net.effect.RunPosableMoLangHandler
import com.cobblemon.mod.common.client.net.effect.SaccharineLogBlockParticlesHandler
import com.cobblemon.mod.common.client.net.effect.SpawnSnowstormEntityParticleHandler
import com.cobblemon.mod.common.client.net.effect.SpawnSnowstormParticleHandler
import com.cobblemon.mod.common.client.net.effect.*
import com.cobblemon.mod.common.client.net.gui.ExpGainedDataPacketHandler
import com.cobblemon.mod.common.client.net.gui.InteractPokemonUIPacketHandler
import com.cobblemon.mod.common.client.net.gui.PokedexUIPacketHandler
@@ -37,11 +34,7 @@ import com.cobblemon.mod.common.client.net.gui.SummaryUIPacketHandler
import com.cobblemon.mod.common.client.net.npc.CloseNPCEditorHandler
import com.cobblemon.mod.common.client.net.npc.OpenNPCEditorHandler
import com.cobblemon.mod.common.client.net.orientation.ClientboundUpdateOrientationHandler
import com.cobblemon.mod.common.client.net.pasture.ClosePastureHandler
import com.cobblemon.mod.common.client.net.pasture.OpenPastureHandler
import com.cobblemon.mod.common.client.net.pasture.PokemonPasturedHandler
import com.cobblemon.mod.common.client.net.pasture.PokemonUnpasturedHandler
import com.cobblemon.mod.common.client.net.pasture.UpdatePastureConflictFlagHandler
import com.cobblemon.mod.common.client.net.pasture.*
import com.cobblemon.mod.common.client.net.pokedex.ServerConfirmedRegisterHandler
import com.cobblemon.mod.common.client.net.pokemon.update.ClientboundUpdateRidingStateHandler
import com.cobblemon.mod.common.client.net.pokemon.update.PokemonUpdatePacketHandler
@@ -59,14 +52,7 @@ import com.cobblemon.mod.common.client.net.storage.party.SetPartyPokemonHandler
import com.cobblemon.mod.common.client.net.storage.party.SetPartyReferenceHandler
import com.cobblemon.mod.common.client.net.storage.pc.*
import com.cobblemon.mod.common.client.net.toast.ToastPacketHandler
import com.cobblemon.mod.common.client.net.trade.TradeAcceptanceChangedHandler
import com.cobblemon.mod.common.client.net.trade.TradeCancelledHandler
import com.cobblemon.mod.common.client.net.trade.TradeCompletedHandler
import com.cobblemon.mod.common.client.net.trade.TradeOfferExpiredHandler
import com.cobblemon.mod.common.client.net.trade.TradeOfferNotificationHandler
import com.cobblemon.mod.common.client.net.trade.TradeProcessStartedHandler
import com.cobblemon.mod.common.client.net.trade.TradeStartedHandler
import com.cobblemon.mod.common.client.net.trade.TradeUpdatedHandler
import com.cobblemon.mod.common.client.net.trade.*
import com.cobblemon.mod.common.net.PacketRegisterInfo
import com.cobblemon.mod.common.net.messages.client.CalculateSeatPositionsPacket
import com.cobblemon.mod.common.net.messages.client.OpenBehaviourEditorPacket
@@ -83,20 +69,12 @@ import com.cobblemon.mod.common.net.messages.client.data.*
import com.cobblemon.mod.common.net.messages.client.debug.RequestOpenRidingStatsDebugGUIPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueClosedPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueOpenedPacket
import com.cobblemon.mod.common.net.messages.client.effect.PokeSnackBlockParticlesPacket
import com.cobblemon.mod.common.net.messages.client.effect.RunPosableMoLangPacket
import com.cobblemon.mod.common.net.messages.client.effect.SaccharineLogBlockParticlesPacket
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormEntityParticlePacket
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormParticlePacket
import com.cobblemon.mod.common.net.messages.client.effect.*
import com.cobblemon.mod.common.net.messages.client.npc.CloseNPCEditorPacket
import com.cobblemon.mod.common.net.messages.client.npc.OpenNPCEditorPacket
import com.cobblemon.mod.common.net.messages.client.orientation.ClientboundUpdateDriverInputPacket
import com.cobblemon.mod.common.net.messages.client.orientation.ClientboundUpdateOrientationPacket
import com.cobblemon.mod.common.net.messages.client.pasture.ClosePasturePacket
import com.cobblemon.mod.common.net.messages.client.pasture.OpenPasturePacket
import com.cobblemon.mod.common.net.messages.client.pasture.PokemonPasturedPacket
import com.cobblemon.mod.common.net.messages.client.pasture.PokemonUnpasturedPacket
import com.cobblemon.mod.common.net.messages.client.pasture.UpdatePastureConflictFlagPacket
import com.cobblemon.mod.common.net.messages.client.pasture.*
import com.cobblemon.mod.common.net.messages.client.pokedex.ServerConfirmedRegisterPacket
import com.cobblemon.mod.common.net.messages.client.pokemon.update.*
import com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution.AddEvolutionPacket
@@ -116,43 +94,19 @@ import com.cobblemon.mod.common.net.messages.client.storage.party.InitializePart
import com.cobblemon.mod.common.net.messages.client.storage.party.MoveClientPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.party.SetPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.party.SetPartyReferencePacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.ClosePCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.InitializePCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.MoveClientPCPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.OpenPCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.RenamePCBoxPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.SetPCBoxPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.SetPCPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.*
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.ChangePCBoxWallpaperPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.RequestPCBoxWallpapersPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.SetPCBoxWallpapersPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.UnlockPCBoxWallpaperPacket
import com.cobblemon.mod.common.net.messages.client.toast.ToastPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeAcceptanceChangedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeCancelledPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeCompletedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeOfferExpiredPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeOfferNotificationPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeProcessStartedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeStartedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeUpdatedPacket
import com.cobblemon.mod.common.net.messages.client.trade.*
import com.cobblemon.mod.common.net.messages.client.ui.ExpGainedDataPacket
import com.cobblemon.mod.common.net.messages.client.ui.InteractPokemonUIPacket
import com.cobblemon.mod.common.net.messages.client.ui.PokedexUIPacket
import com.cobblemon.mod.common.net.messages.client.ui.SummaryUIPacket
import com.cobblemon.mod.common.net.messages.server.BattleChallengePacket
import com.cobblemon.mod.common.net.messages.server.BattleChallengeResponsePacket
import com.cobblemon.mod.common.net.messages.server.BenchMovePacket
import com.cobblemon.mod.common.net.messages.server.RequestMoveSwapPacket
import com.cobblemon.mod.common.net.messages.server.RequestPlayerInteractionsPacket
import com.cobblemon.mod.common.net.messages.server.SelectStarterPacket
import com.cobblemon.mod.common.net.messages.server.SendOutPokemonPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleSelectActionsPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamLeavePacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamRequestPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamResponsePacket
import com.cobblemon.mod.common.net.messages.server.battle.RemoveSpectatorPacket
import com.cobblemon.mod.common.net.messages.server.battle.SpectateBattlePacket
import com.cobblemon.mod.common.net.messages.server.*
import com.cobblemon.mod.common.net.messages.server.battle.*
import com.cobblemon.mod.common.net.messages.server.behaviour.DamageOnCollisionPacket
import com.cobblemon.mod.common.net.messages.server.behaviour.SetEntityBehaviourPacket
import com.cobblemon.mod.common.net.messages.server.block.AdjustBlockEntityViewerCountPacket
@@ -177,11 +131,7 @@ import com.cobblemon.mod.common.net.messages.server.pasture.UnpasturePokemonPack
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.FinishScanningPacket
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.StartScanningPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.interact.InteractPokemonPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.ServerboundUpdateRidingStatePacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetActiveMarkPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetItemHiddenPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetMarkingsPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetNicknamePacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.*
import com.cobblemon.mod.common.net.messages.server.pokemon.update.evolution.AcceptEvolutionPacket
import com.cobblemon.mod.common.net.messages.server.riding.DismountPokemonPacket
import com.cobblemon.mod.common.net.messages.server.riding.ServerboundUpdateDriverInputPacket
@@ -192,21 +142,11 @@ import com.cobblemon.mod.common.net.messages.server.storage.party.MovePartyPokem
import com.cobblemon.mod.common.net.messages.server.storage.party.ReleasePartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.party.SwapPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.pc.*
import com.cobblemon.mod.common.net.messages.server.trade.AcceptTradeRequestPacket
import com.cobblemon.mod.common.net.messages.server.trade.CancelTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.ChangeTradeAcceptancePacket
import com.cobblemon.mod.common.net.messages.server.trade.OfferTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.PerformTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.UpdateTradeOfferPacket
import com.cobblemon.mod.common.net.messages.server.trade.*
import com.cobblemon.mod.common.net.serverhandling.ChallengeHandler
import com.cobblemon.mod.common.net.serverhandling.ChallengeResponseHandler
import com.cobblemon.mod.common.net.serverhandling.RequestInteractionsHandler
import com.cobblemon.mod.common.net.serverhandling.battle.BattleSelectActionsHandler
import com.cobblemon.mod.common.net.serverhandling.battle.RemoveSpectatorHandler
import com.cobblemon.mod.common.net.serverhandling.battle.SpectateBattleHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamLeaveHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamRequestHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamRequestResponseHandler
import com.cobblemon.mod.common.net.serverhandling.battle.*
import com.cobblemon.mod.common.net.serverhandling.behaviour.DamageOnCollisionPacketHandler
import com.cobblemon.mod.common.net.serverhandling.behaviour.SetEntityBehaviourHandler
import com.cobblemon.mod.common.net.serverhandling.block.AdjustBlockEntityViewerCountHandler
@@ -232,11 +172,7 @@ import com.cobblemon.mod.common.net.serverhandling.pasture.UnpasturePokemonHandl
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.FinishScanningHandler
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.StartScanningHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.interact.InteractPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.ServerboundUpdateRidingStateHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetActiveMarkHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetItemHiddenHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetMarkingsHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetNicknameHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.*
import com.cobblemon.mod.common.net.serverhandling.riding.DismountPokemonPacketHandler
import com.cobblemon.mod.common.net.serverhandling.riding.DriverInputPacketHandler
import com.cobblemon.mod.common.net.serverhandling.riding.ServerboundUpdateRiderRotationHandler
@@ -250,12 +186,7 @@ import com.cobblemon.mod.common.net.serverhandling.storage.party.MovePartyPokemo
import com.cobblemon.mod.common.net.serverhandling.storage.party.ReleasePCPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.storage.party.SwapPartyPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.storage.pc.*
import com.cobblemon.mod.common.net.serverhandling.trade.AcceptTradeRequestHandler
import com.cobblemon.mod.common.net.serverhandling.trade.CancelTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.ChangeTradeAcceptanceHandler
import com.cobblemon.mod.common.net.serverhandling.trade.OfferTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.PerformTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.UpdateTradeOfferHandler
import com.cobblemon.mod.common.net.serverhandling.trade.*
import com.cobblemon.mod.common.util.server
import net.minecraft.server.level.ServerPlayer
@@ -401,6 +332,7 @@ object CobblemonNetwork {
// Data registries
list.add(PacketRegisterInfo(AbilityRegistrySyncPacket.ID, AbilityRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(CobblemonMechanicsSyncPacket.ID, CobblemonMechanicsSyncPacket::decode, CobblemonMechanicsSyncHandler))
list.add(PacketRegisterInfo(PokemonInteractionsSyncPacket.ID, PokemonInteractionsSyncPacket::decode, PokemonInteractionsSyncHandler))
list.add(PacketRegisterInfo(MovesRegistrySyncPacket.ID, MovesRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(BerryRegistrySyncPacket.ID, BerryRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(SpeciesRegistrySyncPacket.ID, SpeciesRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
@@ -9,14 +9,15 @@
package com.cobblemon.mod.common
import net.minecraft.client.RecipeBookCategories
import net.minecraft.world.inventory.RecipeBookType
enum class CobblemonRecipeCategories {
COOKING_POT_SEARCH("COOKING_POT_SEARCH"),
COOKING_POT_FOODS("COOKING_POT_FOODS"),
COOKING_POT_MEDICINES("COOKING_POT_MEDICINES"),
COOKING_POT_COMPLEX_DISHES("COOKING_POT_COMPLEX_DISHES"),
COOKING_POT_MISC("COOKING_POT_MISC");
COOKING_POT_SEARCH("COBBLEMON_COOKING_POT_SEARCH"),
COOKING_POT_FOODS("COBBLEMON_COOKING_POT_FOODS"),
COOKING_POT_MEDICINES("COBBLEMON_COOKING_POT_MEDICINES"),
COOKING_POT_COMPLEX_DISHES("COBBLEMON_COOKING_POT_COMPLEX_DISHES"),
COOKING_POT_MISC("COBBLEMON_COOKING_POT_MISC");
companion object {
val customAggregateCategories: Map<RecipeBookCategories, List<RecipeBookCategories>> = mapOf(
@@ -35,6 +36,9 @@ enum class CobblemonRecipeCategories {
fun toVanillaCategory(): RecipeBookCategories {
return RecipeBookCategories.valueOf(this.id)
}
}
object CobblemonRecipeBookTypes {
const val COOKING_POT_NAME = "COBBLEMON_COOKING_POT"
val COOKING_POT: RecipeBookType = RecipeBookType.valueOf(COOKING_POT_NAME)
}
@@ -89,7 +89,14 @@ object CobblemonTradeOffers {
* @return The resulting list.
*/
fun resolveWanderingTradeOffers(): List<WandererTradeOffer> = listOf(
WandererTradeOffer(false, listOf(VillagerTrades.ItemsForEmeralds(CobblemonItems.VIVICHOKE_SEEDS, 24, 1, 1, 6)))
WandererTradeOffer(false, listOf(
VillagerTrades.ItemsForEmeralds(CobblemonItems.VIVICHOKE_SEEDS, 6, 1, 1, 6),
VillagerTrades.ItemsForEmeralds(CobblemonItems.SACCHARINE_SAPLING, 5, 1, 4, 1),
VillagerTrades.ItemsForEmeralds(CobblemonItems.HEARTY_GRAINS, 1, 1, 12, 1),
VillagerTrades.ItemsForEmeralds(CobblemonItems.CHIPPED_POT, 5, 1, 1, 2),
VillagerTrades.ItemsForEmeralds(CobblemonItems.MASTERPIECE_TEACUP, 5, 1, 1, 2)
))
)
/**
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai.config.task
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
import com.cobblemon.mod.common.api.ai.WrapperLivingEntityTask
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.tasks.DefendOwnerTask
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.BehaviorControl
import net.minecraft.world.entity.ai.memory.MemoryModuleType
class DefendOwnerTaskConfig : SingleTaskConfig {
override fun getVariables(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): List<MoLangConfigVariable> {
return emptyList()
}
override fun createTask(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): BehaviorControl<in LivingEntity>? {
behaviourConfigurationContext.addMemories(
MemoryModuleType.ATTACK_TARGET,
CobblemonMemories.NEAREST_VISIBLE_ATTACKER,
)
return WrapperLivingEntityTask(
DefendOwnerTask.create(),
PokemonEntity::class.java
)
}
}
@@ -109,7 +109,7 @@ class PollinateFlowerTaskConfig : SingleTaskConfig {
}
override fun stop(level: ServerLevel, entity: LivingEntity, gameTime: Long) {
hoverPos = null
if (successfulPollinationTicks > REQUIRED_SUCCESSFUL_POLLINATION_TICKS) {
entity.brain.setMemory(CobblemonMemories.HAS_NECTAR, true)
}
@@ -105,6 +105,7 @@ interface TaskConfig {
cobblemonResource("go_to_land") to GoToLandTaskConfig::class.java,
cobblemonResource("manage_flight_in_battle") to ManageFlightInBattleTaskConfig::class.java,
cobblemonResource("attack_hostile_mobs") to AttackHostileMobsTaskConfig::class.java,
cobblemonResource("defend_owner") to DefendOwnerTaskConfig::class.java,
cobblemonResource("move_to_sweet_berry_bush") to MoveToSweetBerryBushTaskConfig::class.java,
cobblemonResource("stop_moving_to_sweet_berry_bush") to StopTryingToReachSweetBerryBushTaskConfig::class.java,
cobblemonResource("harvest_sweet_berry_bush") to HarvestSweetBerryBushTaskConfig::class.java,
@@ -9,13 +9,8 @@
package com.cobblemon.mod.common.api.cooking
import com.cobblemon.mod.common.api.riding.stats.RidingStat
import com.cobblemon.mod.common.util.math.geometry.toDegrees
import com.cobblemon.mod.common.util.math.geometry.toRadians
import net.minecraft.util.FastColor
import net.minecraft.world.item.ItemStack
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
private val colourMap = mapOf(
Flavour.SPICY to 0xFEB37D,
@@ -34,27 +29,30 @@ private val bubbleColourMap = mapOf(
)
fun getColourMixFromSeasonings(seasonings: List<ItemStack>, forBubbles: Boolean = false): Int? {
val flavors = seasonings
.mapNotNull { Seasonings.getFlavoursFromItemStack(it) }
.flatMap { it.entries }
.groupingBy { it.key }
.fold(0) { acc, entry -> acc + entry.value }
val flavourValues = mutableMapOf<Flavour, Int>()
for (seasoning in seasonings) {
val flavours = Seasonings.getFlavoursFromItemStack(seasoning) ?: continue
for ((flavour, value) in flavours.entries) {
flavourValues[flavour] = (flavourValues[flavour] ?: 0) + value
}
}
if (flavors.isEmpty()) return null
if (flavourValues.isEmpty()) {
return null
}
val maxFlavorValue = flavors.values.maxOrNull()
val dominantFlavors = flavors.filter { it.value == maxFlavorValue }.map { it.key }
val maxFlavorValue = flavourValues.values.maxOrNull()
val dominantFlavours = flavourValues.filter { it.value == maxFlavorValue }.map { it.key }
return getColourMixFromFlavours(dominantFlavors, forBubbles)
return getColourMixFromFlavours(dominantFlavours, forBubbles)
}
fun getColourMixFromFlavours(dominantFlavours: List<Flavour>, forBubbles: Boolean = false): Int? {
val colors =
dominantFlavours.mapNotNull { if (forBubbles) bubbleColourMap[it] else colourMap[it] }
dominantFlavours
.mapNotNull { if (forBubbles) bubbleColourMap[it] else colourMap[it] }
.map { FastColor.ARGB32.opaque(it) }
if (colors.isEmpty()) return null
return getColourMixFromColors(colors)
}
@@ -62,104 +60,33 @@ fun getColourMixFromRideStatBoosts(dominantBoosts: Iterable<RidingStat>, forBubb
return getColourMixFromFlavours(dominantBoosts.map { it.flavour }, forBubbles)
}
// Mixing RGB colors directly often gives poor results
// Converting them to HSL and averaging hues circularly produces more natural blends
fun getColourMixFromColors(colors: List<Int>): Int? {
if (colors.isEmpty()) return null
if (colors.size == 1) return colors[0]
val hslColors = colors.map {
val red = FastColor.ARGB32.red(it)
val green = FastColor.ARGB32.green(it)
val blue = FastColor.ARGB32.blue(it)
val firstWeight = 0.7f
val otherWeightTotal = 1f - firstWeight
val otherWeight = otherWeightTotal / (colors.size - 1)
rgbToHsl(red, green, blue)
var alphaSum = 0f
var redSum = 0f
var greenSum = 0f
var blueSum = 0f
for (i in colors.indices) {
val weight = if (i == 0) firstWeight else otherWeight
val color = colors[i]
alphaSum += FastColor.ARGB32.alpha(color) * weight
redSum += FastColor.ARGB32.red(color) * weight
greenSum += FastColor.ARGB32.green(color) * weight
blueSum += FastColor.ARGB32.blue(color) * weight
}
val hues = hslColors.map { it.h.toDouble() }
val huesAverage = hueAverage(hues).toFloat()
val saturations = hslColors.map { it.s }
val saturationsAverage = average(saturations)
val lightness = hslColors.map { it.l }
val lightnessAverage = average(lightness)
val rgbColor = hslToRgb(huesAverage, saturationsAverage, lightnessAverage)
return FastColor.ARGB32.color(rgbColor.r, rgbColor.g, rgbColor.b)
}
// Based on https://iter.ca/post/hue-avg
fun hueAverage(hues: List<Double>): Float {
val points = hues.map { hue ->
val rad = hue.toRadians()
Pair(cos(rad), sin(rad))
}
val avgX = points.sumOf { it.first.toDouble() } / points.size
val avgY = points.sumOf { it.second.toDouble() } / points.size
var degrees = atan2(avgY, avgX).toDegrees()
if (degrees < 0) {
degrees += 360f
} else if (degrees > 360) {
degrees -= 360f
}
return degrees
}
fun average(values: List<Float>): Float {
if (values.isEmpty()) return 0f
return values.sum() / values.size
}
data class HSL(val h: Float, val s: Float, val l: Float)
fun rgbToHsl(r: Int, g: Int, b: Int): HSL {
val rf = r / 255f
val gf = g / 255f
val bf = b / 255f
val max = maxOf(rf, gf, bf)
val min = minOf(rf, gf, bf)
val delta = max - min
var h = 0f
val l = (max + min) / 2f
val s = if (delta == 0f) 0f else delta / (1f - kotlin.math.abs(2f * l - 1f))
if (delta != 0f) {
h = when (max) {
rf -> ((gf - bf) / delta) % 6f
gf -> ((bf - rf) / delta) + 2f
else -> ((rf - gf) / delta) + 4f
}
h *= 60f
if (h < 0) h += 360f
}
return HSL(h, s, l)
}
data class RGB(val r: Int, val g: Int, val b: Int)
fun hslToRgb(h: Float, s: Float, l: Float): RGB {
val c = (1f - kotlin.math.abs(2f * l - 1f)) * s
val x = c * (1f - kotlin.math.abs((h / 60f) % 2f - 1f))
val m = l - c / 2f
val (rf, gf, bf) = when {
h < 60f -> Triple(c, x, 0f)
h < 120f -> Triple(x, c, 0f)
h < 180f -> Triple(0f, c, x)
h < 240f -> Triple(0f, x, c)
h < 300f -> Triple(x, 0f, c)
else -> Triple(c, 0f, x)
}
return RGB(
((rf + m) * 255).toInt().coerceIn(0, 255),
((gf + m) * 255).toInt().coerceIn(0, 255),
((bf + m) * 255).toInt().coerceIn(0, 255)
return FastColor.ARGB32.color(
alphaSum.toInt(),
redSum.toInt(),
greenSum.toInt(),
blueSum.toInt()
)
}
@@ -0,0 +1,22 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cooking.effects
import net.minecraft.world.effect.MobEffect
import net.minecraft.world.effect.MobEffectCategory
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.LivingEntity
class CleanseAllEffect : MobEffect(MobEffectCategory.BENEFICIAL, 0xFFFFFF) {
override fun isInstantenous() = true
override fun applyInstantenousEffect(source: Entity?, indirect: Entity?, target: LivingEntity, amplifier: Int, proximity: Double) {
target.removeAllEffects()
}
override fun shouldApplyEffectTickThisTick(duration: Int, amplifier: Int) = false
}
@@ -0,0 +1,27 @@
/*
* 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.cooking.effects
import net.minecraft.world.effect.MobEffect
import net.minecraft.world.effect.MobEffectCategory
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.LivingEntity
class CleanseNegativeEffect : MobEffect(MobEffectCategory.BENEFICIAL, 0xFFFFFF) {
override fun isInstantenous() = true
override fun applyInstantenousEffect(source: Entity?, indirect: Entity?, target: LivingEntity, amplifier: Int, proximity: Double) {
val snapshot = target.activeEffects.toList()
for (ei in snapshot) {
if (ei.effect.value().category == MobEffectCategory.HARMFUL) {
target.removeEffect(ei.effect)
}
}
}
override fun shouldApplyEffectTickThisTick(duration: Int, amplifier: Int) = false
}
@@ -0,0 +1,33 @@
/*
* 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.cooking.effects
import net.minecraft.server.level.ServerPlayer
import net.minecraft.stats.Stats
import net.minecraft.world.effect.MobEffect
import net.minecraft.world.effect.MobEffectCategory
import net.minecraft.world.entity.LivingEntity
class MentalRestorationEffect : MobEffect(MobEffectCategory.BENEFICIAL, 0xAA66FF) {
override fun applyEffectTick(entity: LivingEntity, amplifier: Int): Boolean {
if (entity is ServerPlayer) {
val stats = entity.stats
val statType = Stats.CUSTOM.get(Stats.TIME_SINCE_REST)
val current = stats.getValue(statType)
val reduction = 31 * (amplifier + 1)// reduce by half a minute every second per level (30 ticks removed per tick + 1 to account for the tick passing)
val newValue = Math.max(current - reduction, 0)
stats.setValue(entity, statType, newValue)
}
return true
}
override fun shouldApplyEffectTickThisTick(duration: Int, amplifier: Int): Boolean {
return true
}
}
@@ -37,8 +37,6 @@ import net.minecraft.resources.ResourceLocation
import net.minecraft.util.FormattedCharSequence
import org.joml.Matrix4f
import org.joml.Vector3f
import org.lwjgl.opengl.GL30
import java.nio.ByteBuffer
@JvmOverloads
fun blitk(
@@ -357,10 +355,10 @@ fun renderSprite(matrixStack: PoseStack, sprite: ResourceLocation) {
val matrix: PoseStack.Pose = matrixStack.last()
matrix.pose().translate(-1f, 0f, 0f)
RenderSystem.setShaderTexture(0, sprite);
RenderSystem.setShaderTexture(0, sprite)
RenderSystem.setShader(GameRenderer::getPositionTexShader)
var buffer = Tesselator.getInstance().begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX)
val buffer = Tesselator.getInstance().begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX)
buffer.addVertex(matrix, 2f, 0f, 0.0f).setUv(1f, 0f)
buffer.addVertex(matrix, 0f, 0f, 0.0f).setUv(0f, 0f)
@@ -369,25 +367,3 @@ fun renderSprite(matrixStack: PoseStack, sprite: ResourceLocation) {
BufferUploader.drawWithShader(buffer.buildOrThrow())
}
fun getPixelRGB(x: Int, y: Int): Triple<Int, Int, Int> {
val window = Minecraft.getInstance().window
val scale = window.guiScale
val buffer = ByteBuffer.allocateDirect(4)
RenderSystem.readPixels(
(x * scale).toInt(),
(window.height - y * scale - scale).toInt(),
1,
1,
GL30.GL_RGBA,
GL30.GL_UNSIGNED_BYTE,
buffer
)
return Triple(
buffer[0].toInt() and 0xFF,
buffer[1].toInt() and 0xFF,
buffer[2].toInt() and 0xFF
)
}
@@ -12,10 +12,10 @@ import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.requirement.OwnerQueryRequirement
import com.cobblemon.mod.common.api.pokemon.requirement.Requirement
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.data.PokemonInteractionsSyncPacket
import com.cobblemon.mod.common.pokemon.adapters.CobblemonRequirementAdapter
import com.cobblemon.mod.common.pokemon.evolution.adapters.LegacyItemConditionWrapperAdapter
import com.cobblemon.mod.common.pokemon.requirements.PokemonPropertiesRequirement
@@ -51,7 +51,12 @@ object PokemonInteractions : JsonDataRegistry<PokemonInteractionSet> {
val speciesInteractions = mutableListOf<PokemonInteractionSet>()
val generalInteractions = mutableListOf<PokemonInteractionSet>()
override fun sync(player: ServerPlayer) {}
override fun sync(player: ServerPlayer) {
PokemonInteractionsSyncPacket(
speciesInteractions,
generalInteractions,
).sendToPlayer(player)
}
override fun reload(data: Map<ResourceLocation, PokemonInteractionSet>) {
speciesInteractions.clear()
@@ -15,11 +15,10 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.pokemon.requirement.Requirement
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.sound.UnvalidatedPlaySoundS2CPacket
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.giveOrDropItemStack
import com.cobblemon.mod.common.util.withPlayerValue
import com.cobblemon.mod.common.util.withQueryValue
import com.cobblemon.mod.common.pokemon.adapters.CobblemonRequirementAdapter
import com.cobblemon.mod.common.util.*
import net.minecraft.core.registries.Registries
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundSource
@@ -28,30 +27,83 @@ import net.minecraft.world.entity.EquipmentSlot
import net.minecraft.world.entity.item.ItemEntity
import net.minecraft.world.item.ItemStack
/**
* A
*/
data class PokemonInteractionSet(
val requirements: List<Requirement> = listOf(),
val interactions: List<PokemonInteraction> = listOf()
)
val interactions: List<PokemonInteraction> = listOf(),
) {
fun encode(buffer: FriendlyByteBuf) {
buffer.writeCollection(requirements) { it, requirement ->
CobblemonRequirementAdapter.encode(requirement, it)
}
buffer.writeCollection(interactions) { it, interaction ->
interaction.encode(it)
}
}
companion object {
fun decode(buffer: FriendlyByteBuf): PokemonInteractionSet {
val requirements = buffer.readList { it ->
CobblemonRequirementAdapter.decode(it)
}
val interactions = buffer.readList { it ->
PokemonInteraction.decode(it)
}
return PokemonInteractionSet(
requirements,
interactions,
)
}
}
}
data class PokemonInteraction(
val grouping: ResourceLocation,
val requirements: List<Requirement> = listOf(),
val effects: List<InteractionEffect> = listOf(),
val cooldown: ExpressionLike = "0".asExpressionLike()
)
) {
fun encode(buffer: FriendlyByteBuf) {
buffer.writeResourceLocation(grouping)
buffer.writeCollection(requirements) { it, requirement ->
CobblemonRequirementAdapter.encode(requirement, it)
}
buffer.writeExpressionLike(cooldown)
}
companion object {
fun decode(buffer: FriendlyByteBuf): PokemonInteraction {
val grouping = buffer.readResourceLocation()
val requirements = buffer.readList { it ->
CobblemonRequirementAdapter.decode(it)
}
// No need to sync these for now
val effects = emptyList<InteractionEffect>()
val cooldown = buffer.readExpressionLike()
return PokemonInteraction(
grouping,
requirements,
effects,
cooldown,
)
}
}
}
interface InteractionEffect {
fun applyEffect(pokemon: PokemonEntity, player: ServerPlayer)
}
class DropItemEffect(val item: ResourceLocation, val amount: IntRange?): InteractionEffect {
class DropItemEffect(val item: ResourceLocation, val amount: IntRange?) : InteractionEffect {
override fun applyEffect(
pokemon: PokemonEntity,
player: ServerPlayer
) {
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(item) ?: throw IllegalArgumentException("Cannot load item with id: $item")
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(item) ?: throw IllegalArgumentException(
"Cannot load item with id: $item"
)
val stack = ItemStack(item)
stack.count = amount?.randomOrNull() ?: 1
if (stack.isEmpty)
@@ -68,12 +120,14 @@ class DropItemEffect(val item: ResourceLocation, val amount: IntRange?): Interac
}
}
class GiveItemEffect(val item: ResourceLocation, val amount: IntRange?): InteractionEffect {
class GiveItemEffect(val item: ResourceLocation, val amount: IntRange?) : InteractionEffect {
override fun applyEffect(
pokemon: PokemonEntity,
player: ServerPlayer
) {
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(item) ?: throw IllegalArgumentException("Cannot load item with id: $item")
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(item) ?: throw IllegalArgumentException(
"Cannot load item with id: $item"
)
val stack = ItemStack(item)
stack.count = amount?.randomOrNull() ?: 1
if (stack.isEmpty)
@@ -86,14 +140,29 @@ class GiveItemEffect(val item: ResourceLocation, val amount: IntRange?): Interac
}
}
class PlaySoundEffect(val sound: ResourceLocation, val soundSource: SoundSource?, val playAround: Boolean = true, val distance: Double = 64.0, val volume: Float = 1.0F, val pitch: Float = 1.0F): InteractionEffect {
class PlaySoundEffect(
val sound: ResourceLocation,
val soundSource: SoundSource?,
val playAround: Boolean = true,
val distance: Double = 64.0,
val volume: Float = 1.0F,
val pitch: Float = 1.0F
) : InteractionEffect {
override fun applyEffect(
pokemon: PokemonEntity,
player: ServerPlayer
) {
val packet = UnvalidatedPlaySoundS2CPacket(sound, soundSource ?: SoundSource.NEUTRAL, pokemon.x, pokemon.y, pokemon.z, volume, pitch)
val packet = UnvalidatedPlaySoundS2CPacket(
sound,
soundSource ?: SoundSource.NEUTRAL,
pokemon.x,
pokemon.y,
pokemon.z,
volume,
pitch
)
if (playAround) {
packet.sendToPlayersAround( pokemon.x, pokemon.y, pokemon.z, distance, pokemon.level().dimension())
packet.sendToPlayersAround(pokemon.x, pokemon.y, pokemon.z, distance, pokemon.level().dimension())
} else {
packet.sendToPlayer(player)
}
@@ -104,7 +173,7 @@ class PlaySoundEffect(val sound: ResourceLocation, val soundSource: SoundSource?
}
}
class ShrinkItemEffect(val amount: Int = 1): InteractionEffect {
class ShrinkItemEffect(val amount: Int = 1) : InteractionEffect {
override fun applyEffect(
pokemon: PokemonEntity,
player: ServerPlayer
@@ -120,7 +189,7 @@ class ShrinkItemEffect(val amount: Int = 1): InteractionEffect {
}
}
class ScriptEffect(val script: ExpressionLike): InteractionEffect {
class ScriptEffect(val script: ExpressionLike) : InteractionEffect {
override fun applyEffect(
pokemon: PokemonEntity,
player: ServerPlayer
@@ -311,7 +311,7 @@ object MoLangFunctions {
runtime.environment.variable = params.environment.variable
val args = params.params.subList(1, params.params.size)
runtime.environment.context = ContextStruct(
params.environment.context.map + args.mapIndexed { index, value -> "arg_${index + 1}" to value }.toMap()
(params.environment.context?.map ?: emptyMap()) + args.mapIndexed { index, value -> "arg_${index + 1}" to value }.toMap()
)
val script = params.getString(0).asIdentifierDefaultingNamespace()
// store the args in the
@@ -779,6 +779,20 @@ object MoLangFunctions {
battleStartResult.ifSuccessful { returnValue = it.struct }
return@put returnValue
}
map.put("get_custom_stat") { params ->
val statName = params.getString(0)
val resourceLocation = ResourceLocation.tryParse(statName) ?: return@put DoubleValue.ZERO
val statResourceLocation = BuiltInRegistries.CUSTOM_STAT.get(resourceLocation) ?: return@put DoubleValue.ZERO
val exists = net.minecraft.stats.Stats.CUSTOM.contains(statResourceLocation)
if (!exists) return@put DoubleValue.ZERO
val stat = net.minecraft.stats.Stats.CUSTOM.get(statResourceLocation)
val value = player.stats.getValue(stat)
DoubleValue(value)
}
}
map
}
@@ -1739,22 +1753,35 @@ object MoLangFunctions {
pokemon.removeCosmeticItem()
}
map.put("add_marks") { params ->
var appliedMark = false
for (param in params.params) {
val identifier = param.asString().asIdentifierDefaultingNamespace()
val mark = Marks.getByIdentifier(identifier)
if (mark != null) pokemon.exchangeMark(mark, true)
if (mark != null) {
pokemon.exchangeMark(mark, true)
appliedMark = true
}
}
DoubleValue(appliedMark)
}
map.put("add_marks_with_chance") { params ->
var appliedMark = false
for (param in params.params) {
val identifier = param.asString().asIdentifierDefaultingNamespace()
val mark = Marks.getByIdentifier(identifier)
mark?.let {
val probability = it.chance.coerceIn(0F, 1F) * 100
val randomValue = Random.nextDouble(0.0, 100.0)
if (randomValue < probability) pokemon.exchangeMark(it, true)
if (randomValue < probability) {
pokemon.exchangeMark(it, true)
appliedMark = true
}
}
}
DoubleValue(appliedMark)
}
map.put("add_potential_marks") { params ->
for (param in params.params) {
@@ -409,13 +409,7 @@ open class PokemonProperties {
shiny?.let { pokemon.shiny = it }
gender?.let { pokemon.gender = it }
level?.let { pokemon.level = it }
friendship.also {
if (it != null) {
pokemon.setFriendship(it)
} else {
pokemon.setFriendship(pokemon.form.baseFriendship)
}
}
friendship?.also { pokemon.setFriendship(it) }
fullness?.let { pokemon.currentFullness = it }
pokeball?.let { PokeBalls.getPokeBall(it.asIdentifierDefaultingNamespace())?.let { pokeball -> pokemon.caughtBall = pokeball } }
nature?.let { Natures.getNature(it.asIdentifierDefaultingNamespace())?.let { nature -> pokemon.nature = nature } }
@@ -80,7 +80,9 @@ open class ChoiceSpeciesFeatureProvider(
internal constructor(): this(emptyList())
override fun get(pokemon: Pokemon) = pokemon.getFeature<StringSpeciesFeature>(keys.first())
override fun get(pokemon: Pokemon): StringSpeciesFeature? {
return pokemon.features.filterIsInstance<StringSpeciesFeature>().find { it.name in keys }
}
override fun invoke(pokemon: Pokemon): StringSpeciesFeature? {
val existing = get(pokemon)
@@ -101,15 +103,15 @@ open class ChoiceSpeciesFeatureProvider(
}
override fun invoke(nbt: CompoundTag): StringSpeciesFeature? {
return if (nbt.contains(keys.first())) {
StringSpeciesFeature(keys.first(), "").also { it.loadFromNBT(nbt) }
} else null
val key = keys.find { nbt.contains(it) }
if (key == null) return null
return StringSpeciesFeature(key, "").also { it.loadFromNBT(nbt) }
}
override fun invoke(json: JsonObject): StringSpeciesFeature? {
return if (json.has(keys.first())) {
StringSpeciesFeature(keys.first(), "").also { it.loadFromJSON(json) }
} else null
val key = keys.find { json.has(it) }
if (key == null) return null
return StringSpeciesFeature(key, "").also { it.loadFromJSON(json) }
}
override fun fromString(value: String?): StringSpeciesFeature? {
@@ -131,7 +133,7 @@ open class ChoiceSpeciesFeatureProvider(
override fun provide(properties: PokemonProperties): Set<String> {
return if (isAspect) {
val feature = properties.customProperties.filterIsInstance<StringSpeciesFeature>().find { it.name == keys.first() }
val feature = properties.customProperties.filterIsInstance<StringSpeciesFeature>().find { it.name in keys }
if (feature != null) {
setOf(getAspect(feature))
} else {
@@ -132,7 +132,9 @@ class FlagSpeciesFeatureProvider : SynchronizedSpeciesFeatureProvider<FlagSpecie
constructor(vararg keys: String) : this(keys.toList())
override fun get(pokemon: Pokemon) = pokemon.getFeature<FlagSpeciesFeature>(keys.first())
override fun get(pokemon: Pokemon): FlagSpeciesFeature? {
return pokemon.features.filterIsInstance<FlagSpeciesFeature>().find { it.name in keys }
}
override fun invoke(pokemon: Pokemon): FlagSpeciesFeature? {
return get(pokemon)
@@ -144,15 +146,15 @@ class FlagSpeciesFeatureProvider : SynchronizedSpeciesFeatureProvider<FlagSpecie
}
override fun invoke(nbt: CompoundTag): FlagSpeciesFeature? {
return if (nbt.contains(keys.first())) {
FlagSpeciesFeature(keys.first(), false).also { it.loadFromNBT(nbt) }
} else null
val key = keys.find { nbt.contains(it) }
if (key == null) return null
return FlagSpeciesFeature(key, false).also { it.loadFromNBT(nbt) }
}
override fun invoke(json: JsonObject): FlagSpeciesFeature? {
return if (json.has(keys.first())) {
FlagSpeciesFeature(keys.first(), false).also { it.loadFromJSON(json) }
} else null
val key = keys.find { json.has(it) }
if (key == null) return null
return FlagSpeciesFeature(key, false).also { it.loadFromJSON(json) }
}
override fun fromString(value: String?): FlagSpeciesFeature? {
@@ -170,16 +172,26 @@ class FlagSpeciesFeatureProvider : SynchronizedSpeciesFeatureProvider<FlagSpecie
}
override fun provide(pokemon: Pokemon): Set<String> {
return if (isAspect && pokemon.getFeature<FlagSpeciesFeature>(keys.first())?.enabled == true) {
setOf(keys.first())
return if (isAspect) {
val feature = pokemon.features.filterIsInstance<FlagSpeciesFeature>().find { it.name in keys }
if (feature != null && feature.enabled) {
setOf(keys.first())
} else {
emptySet()
}
} else {
emptySet()
}
}
override fun provide(properties: PokemonProperties): Set<String> {
return if (isAspect && properties.customProperties.filterIsInstance<FlagSpeciesFeature>().find { it.name == keys.first() }?.enabled == true) {
setOf(keys.first())
return if (isAspect) {
val feature = properties.customProperties.filterIsInstance<FlagSpeciesFeature>().find { it.name in keys }
if (feature != null && feature.enabled) {
setOf(keys.first())
} else {
emptySet()
}
} else {
emptySet()
}
@@ -142,19 +142,15 @@ class IntSpeciesFeatureProvider : SynchronizedSpeciesFeatureProvider<IntSpeciesF
}
override fun invoke(nbt: CompoundTag): IntSpeciesFeature? {
return if (nbt.contains(keys.first())) {
IntSpeciesFeature(keys.first(), nbt.getInt(keys.first()))
} else {
null
}
val key = keys.find { nbt.contains(it) }
if (key == null) return null
return IntSpeciesFeature(keys.first(), nbt.getInt(key));
}
override fun invoke(json: JsonObject): IntSpeciesFeature? {
return if (json.has(keys.first())) {
IntSpeciesFeature(keys.first(), json.get(keys.first()).asInt)
} else {
null
}
val key = keys.find { json.has(it) }
if (key == null) return null
return IntSpeciesFeature(keys.first(), json.get(key).asInt)
}
override fun get(pokemon: Pokemon) = pokemon.features.filterIsInstance<IntSpeciesFeature>().find { it.name in keys }
@@ -12,6 +12,7 @@ import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.pokemon.Pokemon
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.player.Player
/**
* A [Requirement] that expects a [LivingEntity] to be attached to the [Pokemon].
@@ -26,7 +27,7 @@ interface OwnerQueryRequirement : Requirement {
override fun check(pokemon: Pokemon): Boolean {
val owner = pokemon.getOwnerEntity() ?: return false
return when (owner) {
is ServerPlayer -> {
is Player -> {
this.checkPlayer(pokemon, owner)
}
is NPCEntity -> {
@@ -37,13 +38,13 @@ interface OwnerQueryRequirement : Requirement {
}
/**
* Checks if the given [Pokemon] & [ServerPlayer] satisfies the requirement.
* Checks if the given [Pokemon] & [Player] satisfies the requirement.
*
* @param pokemon The [Pokemon] being queried.
* @param owner The [ServerPlayer] that owns this [Pokemon].
* @param owner The [Player] that owns this [Pokemon].
* @return If the requirement was satisfied.
*/
fun checkPlayer(pokemon: Pokemon, owner: ServerPlayer): Boolean
fun checkPlayer(pokemon: Pokemon, owner: Player): Boolean
/**
* Checks if the given [Pokemon] & [NPCEntity] satisfies the requirement.
@@ -322,7 +322,7 @@ class JetBehaviour : RidingBehaviour<JetSettings, JetState> {
val pitchRot = handling * state.currMouseYForce.get()
// Roll
val rollRot = handling * 1.5 * state.currMouseXForce.get()
val rollRot = handling * state.currMouseXForce.get()
val mouseRotation = Vec3(0.0, pitchRot, rollRot)
@@ -114,10 +114,10 @@ class HorseBehaviour : RidingBehaviour<HorseSettings, HorseState> {
input: Vec3
) {
if (vehicle.level().isClientSide) {
handleSprinting(state)
handleSprinting(state, driver)
inAirCheck(state, vehicle)
tickStamina(settings, state, vehicle)
state.walking.set(state.rideVelocity.get().horizontalDistance() > 0.01)
state.walking.set(state.rideVelocity.get().horizontalDistance() > 0.01 || driver.zza != 0.0f)
}
}
@@ -139,9 +139,24 @@ class HorseBehaviour : RidingBehaviour<HorseSettings, HorseState> {
}
fun handleSprinting(
state: HorseState
state: HorseState,
driver: Player
) {
val tryingToSprint = Minecraft.getInstance().options.keySprint.isDown() && Minecraft.getInstance().options.keyUp.isDown()
val sprintTimerTickLength = 7 // Emulate vanillas tick timer length
var hasDoubleTapped = false
// If you are not sprinting and you just pressed down and theres no
if (!state.forwardHeldLastTick.get() && Minecraft.getInstance().options.keyUp.isDown() && !state.sprinting.get() && state.sprintTickTimer.get() == 0) {
state.sprintTickTimer.set(sprintTimerTickLength)
} else if (!state.forwardHeldLastTick.get() && Minecraft.getInstance().options.keyUp.isDown() && state.sprintTickTimer.get() != 0) { // If has double tapped
hasDoubleTapped = true
}else if(!state.sprinting.get() && state.sprintTickTimer.get() > 0) {
state.sprintTickTimer.set(state.sprintTickTimer.get() - 1)
} else {
state.sprintTickTimer.set(0)
}
val tryingToSprint = (Minecraft.getInstance().options.keySprint.isDown() && Minecraft.getInstance().options.keyUp.isDown()) || hasDoubleTapped
if (state.stamina.get() <= 0.0f || !Minecraft.getInstance().options.keyUp.isDown()) {
// If stamina runs out or the player is not holding forward then stop sprinting
@@ -156,6 +171,9 @@ class HorseBehaviour : RidingBehaviour<HorseSettings, HorseState> {
// If you are trying to sprint and the toggle allows it then start sprinting and disable toggle
state.sprinting.set(true)
}
// Take not of whether forward was held last tick
state.forwardHeldLastTick.set(Minecraft.getInstance().options.keyUp.isDown())
}
fun tickStamina(
@@ -227,7 +245,7 @@ class HorseBehaviour : RidingBehaviour<HorseSettings, HorseState> {
// Normalize the current rotation diff
val rotDiff = Mth.wrapDegrees(driver.yRot - vehicle.yRot).coerceIn(-maxYawDiff,maxYawDiff)
val rotDiffNorm = rotDiff / maxYawDiff
val rotDiffNorm = rotDiff / maxYawDiff//(rotDiff / maxYawDiff).coerceIn(-0.2f, 0.2f)
// Take the square root so that the ride levels out quicker when at lower differences between entity
// y and driver y
@@ -240,7 +258,7 @@ class HorseBehaviour : RidingBehaviour<HorseSettings, HorseState> {
val walkSpeed = getWalkSpeed(vehicle)
val w = max(walkSpeed, vehicle.deltaMovement.horizontalDistance())
val invRelSpeed = (RidingBehaviour.scaleToRange(w, walkSpeed, topSpeed ) - 1.0f) * -1.0f
val turnRate = ((handling.toFloat() / 20.0f) * max(walkHandlingBoost * invRelSpeed, 1.0)).toFloat()
val turnRate = ((handling.toFloat() / 20.0f) * max(walkHandlingBoost * sqrt(abs(invRelSpeed)), 1.0)).toFloat()
// Ensure you only ever rotate as much difference as there is between the angles.
val turnSpeed = turnRate * rotDiffMod
@@ -273,30 +291,24 @@ class HorseBehaviour : RidingBehaviour<HorseSettings, HorseState> {
val rideTopSpeed = vehicle.runtime.resolveDouble(settings.speedExpr ?: globalHorse.speedExpr!!)
val walkSpeed = getWalkSpeed(vehicle)
val topSpeed = if(canSprint && state.sprinting.get()) rideTopSpeed else walkSpeed
val accel = topSpeed / (vehicle.runtime.resolveDouble(settings.accelerationExpr ?: globalHorse.accelerationExpr!!) * 20.0)
val accelTime = if(state.sprinting.get()) (vehicle.runtime.resolveDouble(settings.accelerationExpr ?: globalHorse.accelerationExpr!!) * 20.0) else 0.5 * 20.0
val accel = topSpeed / accelTime
var activeInput = false
/******************************************************
* Gather the previous velocity and check for horizontal
* collisions
*****************************************************/
val dmSpeed = vehicle.deltaMovement.length()
val rvSpeed = state.rideVelocity.get().length()
var newVelocity = state.rideVelocity.get() //.normalize().scale(vehicle.deltaMovement.horizontalDistance())
/******************************************************
* Gather the previous velocity and check for horizontal
* collisions
*****************************************************/
// if (dmSpeed > rvSpeed) {
// // align the velocity vector to be in local vehicle space
// newVelocity = vehicle.deltaMovement
// val yawAligned = Matrix3f().rotateY(-vehicle.yRot.toRadians())
// newVelocity = (newVelocity.toVector3f().mul(yawAligned)).toVec3d()
// }
if (vehicle.horizontalCollision) {
newVelocity = newVelocity.normalize().scale(vehicle.deltaMovement.length())
var horizontalVec = Vec3(newVelocity.x, 0.0, newVelocity.z)
horizontalVec = horizontalVec.normalize().scale(min(horizontalVec.length(), 0.3))
newVelocity = Vec3(horizontalVec.x, newVelocity.y, horizontalVec.z)
}
/******************************************************
@@ -372,7 +384,7 @@ class HorseBehaviour : RidingBehaviour<HorseSettings, HorseState> {
state.jumpTicks.set(state.jumpTicks.get() + 1)
}
//Zero out lateral velocity possibly picked up from a controller transition
// Zero out lateral velocity possibly picked up from a controller transition
newVelocity = Vec3(0.0, newVelocity.y, newVelocity.z)
@@ -461,7 +473,7 @@ class HorseBehaviour : RidingBehaviour<HorseSettings, HorseState> {
vehicle: PokemonEntity,
driver: Player
): Float {
return 1.0f
return if (state.sprinting.get()) 1.15f else 1.0f
}
override fun useAngVelSmoothing(
@@ -520,6 +532,14 @@ class HorseBehaviour : RidingBehaviour<HorseSettings, HorseState> {
return true
}
override fun maxUpStep(
settings: HorseSettings,
state: HorseState,
vehicle: PokemonEntity
): Float? {
return if (state.inAir.get()) 0.0f else 1.2f
}
override fun getRideSounds(
settings: HorseSettings,
state: HorseState,
@@ -618,6 +638,8 @@ class HorseState : RidingBehaviourState() {
var sprintToggleable = ridingState(false, Side.CLIENT)
var inAir = ridingState(false, Side.CLIENT)
var jumpTicks = ridingState(0, Side.CLIENT)
var sprintTickTimer = ridingState(0, Side.CLIENT)
var forwardHeldLastTick = ridingState(false, Side.CLIENT)
override fun encode(buffer: FriendlyByteBuf) {
super.encode(buffer)
@@ -626,6 +648,8 @@ class HorseState : RidingBehaviourState() {
buffer.writeBoolean(sprintToggleable.get())
buffer.writeBoolean(inAir.get())
buffer.writeInt(jumpTicks.get())
buffer.writeInt(sprintTickTimer.get())
buffer.writeBoolean(forwardHeldLastTick.get())
}
override fun decode(buffer: FriendlyByteBuf) {
@@ -635,6 +659,8 @@ class HorseState : RidingBehaviourState() {
sprintToggleable.set(buffer.readBoolean(), forced = true)
inAir.set(buffer.readBoolean(), forced = true)
jumpTicks.set(buffer.readInt(), forced = true)
sprintTickTimer.set(buffer.readInt(), forced = true)
forwardHeldLastTick.set(buffer.readBoolean(), forced = true)
}
override fun reset() {
@@ -644,6 +670,8 @@ class HorseState : RidingBehaviourState() {
sprintToggleable.set(false, forced = true)
inAir.set(false, forced = true)
jumpTicks.set(0, forced = true)
sprintTickTimer.set(0, forced = true)
forwardHeldLastTick.set(false, forced = true)
}
override fun copy() = HorseState().also {
@@ -654,6 +682,8 @@ class HorseState : RidingBehaviourState() {
it.sprintToggleable.set(this.sprintToggleable.get(), forced = true)
it.inAir.set(this.inAir.get(), forced = true)
it.jumpTicks.set(this.jumpTicks.get(), forced = true)
it.sprintTickTimer.set(this.sprintTickTimer.get(), forced = true)
it.forwardHeldLastTick.set(this.forwardHeldLastTick.get(), forced = true)
}
override fun shouldSync(previous: RidingBehaviourState): Boolean {
@@ -12,6 +12,7 @@ package com.cobblemon.mod.common.api.riding.sound
import com.cobblemon.mod.common.api.riding.behaviour.types.composite.CompositeBehaviour
import com.cobblemon.mod.common.api.riding.behaviour.types.composite.CompositeState
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.getIsSubmerged
import com.cobblemon.mod.common.util.resolveDouble
import net.minecraft.client.Minecraft
import net.minecraft.client.resources.sounds.AbstractTickableSoundInstance
@@ -65,6 +66,21 @@ class RideLoopSound(val ride: PokemonEntity, val soundSettings: RideSoundSetting
return
}
// Mute the sound if it's not meant to play for the passenger
if (this.isPassenger && !this.soundSettings.playForPassengers) {
this.volume = 0.0f
return
}
// Mute sounds when submerged unless they support underwater sounds, underwater sounds don't play above water
if (this.ride.getIsSubmerged() && !this.soundSettings.submerged) {
this.volume = 0.0f
return
} else if (!this.ride.getIsSubmerged() && this.soundSettings.submerged) {
this.volume = 0.0f
return
}
// Perform data driven pitch and volume calculation
this.volume = ((soundSettings.volumeExpr.let { ride.runtime.resolveDouble(it) })).toFloat()
this.volume *= this.fade
@@ -27,7 +27,9 @@ data class RideSoundSettings(
val soundLocation: ResourceLocation,
@SerializedName("volumeExpr") private val _volumeExpr: Expression? = "1.0".asExpression(),
@SerializedName("pitchExpr") private val _pitchExpr: Expression? = "1.0".asExpression(),
val playForPassengers: Boolean = true,
val playForNonPassengers: Boolean = false,
val submerged: Boolean = false,
val muffleEnabled: Boolean = false,
@SerializedName("attenuationModel") private val _attenuationModel: RideAttenuationModel? = RideAttenuationModel.NONE
): Encodable {
@@ -46,7 +48,9 @@ data class RideSoundSettings(
buffer.writeResourceLocation(soundLocation)
buffer.writeExpression(volumeExpr)
buffer.writeExpression(pitchExpr)
buffer.writeBoolean(playForPassengers)
buffer.writeBoolean(playForNonPassengers)
buffer.writeBoolean(submerged)
buffer.writeBoolean(muffleEnabled)
buffer.writeEnum(attenuationModel)
}
@@ -57,7 +61,9 @@ data class RideSoundSettings(
val soundLocation = buffer.readResourceLocation()
val volumeExpr = buffer.readExpression()
val pitchExpr = buffer.readExpression()
val playForPassengers = buffer.readBoolean()
val playForNonPassengers = buffer.readBoolean()
val submerged = buffer.readBoolean()
val muffleEnabled = buffer.readBoolean()
val attenuationModel = buffer.readEnum(RideAttenuationModel::class.java)
@@ -65,7 +71,9 @@ data class RideSoundSettings(
soundLocation,
volumeExpr,
pitchExpr,
playForPassengers,
playForNonPassengers,
submerged,
muffleEnabled,
attenuationModel
)
@@ -40,9 +40,12 @@ class PokemonSpawnAction(
override fun createEntity(): PokemonEntity {
val props = props.copy()
props.level = levelRange.random()
val sourcePlayer = spawnablePosition.cause.entity as? ServerPlayer
val entity = props.createEntity(spawnablePosition.world, sourcePlayer)
entity.spawnCause = spawnablePosition.cause
entity.pokemon.setFriendship(entity.pokemon.form.baseFriendship)
SeasonFeatureHandler.updateSeason(entity.pokemon, Cobblemon.seasonResolver(spawnablePosition.world, spawnablePosition.position))
val heldItem = heldItem
if (heldItem != ItemStack.EMPTY && heldItem != null) {
@@ -44,11 +44,7 @@ abstract class SpawnAction<R>(
spawnablePosition.applyInfluences { it.affectAction(this) }
val result = run()
if (result != null) {
future.complete(result)
} else {
future.completeExceptionally(Exception("Nothing was spawned."))
}
future.complete(result)
return result
}
@@ -61,7 +61,9 @@ open class SpawnBaitInfluence(val effects: List<SpawnBait.Effect>, val onUsed: (
val baitEVStat = effects.firstOrNull { it.type == Effects.EV }?.subcategory?.path?.let { Stats.getStat(it) }
if (detailSpecies != null && baitEVStat != null) {
val evYieldValue = detailSpecies.evYield[baitEVStat]?.toFloat() ?: 0f
val detailAspects = detail.pokemon.aspects
val formData = detailSpecies.getForm(detailAspects)
val evYieldValue = formData.evYield[baitEVStat]?.toFloat() ?: 0f
return when {
evYieldValue > 0 -> {
markUsed()
@@ -83,7 +85,9 @@ open class SpawnBaitInfluence(val effects: List<SpawnBait.Effect>, val onUsed: (
val baitTypingEffect = baitEffect?.subcategory?.path?.let { ElementalTypes.get(it) }
if (detailSpecies != null && baitTypingEffect != null) {
val isMatchingType = detailSpecies.types.contains(baitTypingEffect)
val detailAspects = detail.pokemon.aspects
val formData = detailSpecies.getForm(detailAspects)
val isMatchingType = formData.types.contains(baitTypingEffect)
return when {
isMatchingType -> {
markUsed()
@@ -100,6 +104,9 @@ open class SpawnBaitInfluence(val effects: List<SpawnBait.Effect>, val onUsed: (
val detailSpecies = detail.pokemon.species?.let { PokemonSpecies.getByName(it) }
if (detailSpecies != null) {
val detailAspects = detail.pokemon.aspects
val formData = detailSpecies.getForm(detailAspects)
// Collect all the egg group effects
val eggGroupEffects = effects.filter { it.type == Effects.EGG_GROUP }
@@ -111,7 +118,7 @@ open class SpawnBaitInfluence(val effects: List<SpawnBait.Effect>, val onUsed: (
LOGGER.warn("Unknown egg group identifier: $effectEggGroupKey")
return@firstOrNull false
}
detailSpecies.eggGroups.contains(eggGroup)
formData.eggGroups.contains(eggGroup)
}
if (matchingEffect != null) {
@@ -8,30 +8,58 @@
package com.cobblemon.mod.common.api.stats
import com.cobblemon.mod.common.Cobblemon
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.resources.ResourceLocation
import net.minecraft.stats.StatFormatter
object CobblemonStats {
val stats = mutableMapOf<String, ResourceLocation>()
val stats = mutableMapOf<String, CobblemonStat>()
var CAPTURED: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "captured") //makeCustomStat("captured", StatFormatter.DEFAULT)
val SHINIES_CAPTURED: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "shinies_captured") //makeCustomStat("shinies_captured", StatFormatter.DEFAULT)
val RELEASED: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "released") //makeCustomStat("released", StatFormatter.DEFAULT)
val EVOLVED: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "evolved") //makeCustomStat("evolved", StatFormatter.DEFAULT)
val LEVEL_UP: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "level_up") //makeCustomStat("level_up", StatFormatter.DEFAULT)
val BATTLES_WON: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "battles_won") //makeCustomStat("battles_won", StatFormatter.DEFAULT)
val BATTLES_LOST: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "battles_lost") //makeCustomStat("battles_lost", StatFormatter.DEFAULT)
val BATTLES_FLED: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "battles_fled") //makeCustomStat("battles_fled", StatFormatter.DEFAULT)
val BATTLES_TOTAL: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "battles_total") //makeCustomStat("battles_total", StatFormatter.DEFAULT)
val DEX_ENTRIES: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "dex_entries") //makeCustomStat("dex_entries", StatFormatter.DEFAULT)
val EGGS_COLLECTED: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "eggs_collected") //makeCustomStat("eggs_collected", StatFormatter.DEFAULT)
val EGGS_HATCHED: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "eggs_hatched") //makeCustomStat("eggs_hatched", StatFormatter.DEFAULT)
val TRADED: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "traded") //makeCustomStat("traded", StatFormatter.DEFAULT)
val FOSSILS_REVIVED: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "fossils_revived") //makeCustomStat("fossils_revived", StatFormatter.DEFAULT)
val TIMES_RIDDEN: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "times_ridden")
val ROD_CASTS: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "rod_casts")
val REEL_INS: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "reel_ins")
val POKEMON_INTERACTED_WITH: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", "pokemon_interacted_with") //makeCustomStat("pokemon_interacted_with", StatFormatter.DEFAULT)
//TODO block, stats (interact, gimmi), riding (styles?)
@JvmField
var CAPTURED: CobblemonStat = CobblemonStat("captured")
@JvmField
val SHINIES_CAPTURED: CobblemonStat = CobblemonStat("shinies_captured")
@JvmField
val RELEASED: CobblemonStat = CobblemonStat("released")
@JvmField
val EVOLVED: CobblemonStat = CobblemonStat("evolved")
@JvmField
val LEVEL_UP: CobblemonStat = CobblemonStat("level_up")
@JvmField
val BATTLES_WON: CobblemonStat = CobblemonStat("battles_won")
@JvmField
val BATTLES_LOST: CobblemonStat = CobblemonStat("battles_lost")
@JvmField
val BATTLES_FLED: CobblemonStat = CobblemonStat("battles_fled")
@JvmField
val BATTLES_TOTAL: CobblemonStat = CobblemonStat("battles_total")
@JvmField
val DEX_ENTRIES: CobblemonStat = CobblemonStat("dex_entries")
@JvmField
val EGGS_COLLECTED: CobblemonStat = CobblemonStat("eggs_collected")
@JvmField
val EGGS_HATCHED: CobblemonStat = CobblemonStat("eggs_hatched")
@JvmField
val TRADED: CobblemonStat = CobblemonStat("traded")
@JvmField
val FOSSILS_REVIVED: CobblemonStat = CobblemonStat("fossils_revived")
@JvmField
val TIMES_RIDDEN: CobblemonStat = CobblemonStat("times_ridden")
@JvmField
val ROD_CASTS: CobblemonStat = CobblemonStat("rod_casts")
@JvmField
val REEL_INS: CobblemonStat = CobblemonStat("reel_ins")
@JvmField
val POKEMON_INTERACTED_WITH: CobblemonStat = CobblemonStat("pokemon_interacted_with")
@JvmField
val RIDING_LAND: CobblemonStat = CobblemonStat("riding_land", StatFormatter.DISTANCE)
@JvmField
val RIDING_AIR: CobblemonStat = CobblemonStat("riding_air", StatFormatter.DISTANCE)
@JvmField
val RIDING_LIQUID: CobblemonStat = CobblemonStat("riding_liquid", StatFormatter.DISTANCE)
//TODO block, stats (interact, gimmi)
fun registerStats() {
stats["captured"] = CAPTURED
@@ -52,5 +80,22 @@ object CobblemonStats {
stats["rod_casts"] = ROD_CASTS
stats["reel_ins"] = REEL_INS
stats["pokemon_interacted_with"] = POKEMON_INTERACTED_WITH
stats["riding_land"] = RIDING_LAND
stats["riding_air"] = RIDING_AIR
stats["riding_liquid"] = RIDING_LIQUID
}
@JvmStatic
fun getStat(cobblemonStat: CobblemonStat) : ResourceLocation {
val resourceLocation = cobblemonStat.resourceLocation
val stat = BuiltInRegistries.CUSTOM_STAT.get(resourceLocation)
if (stat == null) {
Cobblemon.LOGGER.error("Could not find stat with id {}", resourceLocation)
}
return stat ?: throw NullPointerException("Could not find stat with id $resourceLocation")
}
data class CobblemonStat(val path: String, val formatter: StatFormatter = StatFormatter.DEFAULT) {
val resourceLocation: ResourceLocation = ResourceLocation.fromNamespaceAndPath("cobblemon", path);
}
}
@@ -88,7 +88,6 @@ abstract class OneToOneFileStoreAdapter<S>(
result
}
else {
storeClass.getConstructor(UUID::class.java).newInstance(uuid)
var result = storeClass.getConstructor(UUID::class.java).newInstance(uuid)
save(storeClass, uuid, serialize(result, registryAccess))
oldFile.delete()
@@ -0,0 +1,671 @@
package com.cobblemon.mod.common.battles.ai
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.ai.BattleAI
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.battles.*
import com.cobblemon.mod.common.battles.ai.competitive.*
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.net.messages.client.battle.BattleHealthChangePacket
import kotlin.random.Random
class CompetitiveBattleAI(private val skill: Int) : BattleAI {
companion object {
private const val DEBUG_TOP_MOVES = 4
}
private val skillLevel = skill.coerceIn(0, 5)
private val aiDebugEnabled = isAiDebugEnabled()
private val battleTracker = EnhancedActiveTracker()
private val opponentPredictor = OpponentPredictor()
private val strategyAnalyzer = StrategyAnalyzer()
private lateinit var damageCalculator: DamageCalculator
private lateinit var moveEvaluator: MoveEvaluator
private lateinit var switchDecisionMaker: SwitchDecisionMaker
private lateinit var doublesDecisionMaker: DoublesDecisionMaker
private var currentBattle: PokemonBattle? = null
private var currentGamePlan: GamePlan? = null
private var battleState: BattleState? = null
private var lastMoveEvaluation: MoveEvaluator.MoveEvaluation? = null
private var isDoublesMode: Boolean = false
private var currentSlot: Int = 0
private var currentTurn: Int = -1
private val turnDecisions: MutableMap<Int, DoublesDecisionMaker.MoveTargetPair> = mutableMapOf()
private var firstSlotDecided: Boolean = false
override fun choose(
activeBattlePokemon: ActiveBattlePokemon,
battle: PokemonBattle,
aiSide: BattleSide,
moveset: ShowdownMoveset?,
forceSwitch: Boolean
): ShowdownActionResponse {
try {
initializeComponents(battle)
updateBattleState(activeBattlePokemon, battle)
if (currentGamePlan == null) {
val npcTeam = battle.side2.actors.first().pokemonList.map { it.effectedPokemon }
currentGamePlan = strategyAnalyzer.analyzeTeamComposition(npcTeam)
}
if (battle.turn != currentTurn) {
currentTurn = battle.turn
turnDecisions.clear()
firstSlotDecided = false
}
if (forceSwitch || activeBattlePokemon.isGone()) {
return handleForcedSwitch(activeBattlePokemon, battle)
}
if (!passesSkillCheck()) {
debugLog("Skill check failed; selecting random move.")
return makeRandomMove(moveset, activeBattlePokemon)
}
updateOpponentModel(battle)
if (isDoublesMode) {
return chooseForDoubles(activeBattlePokemon, battle, moveset)
}
if (shouldAttemptSwitch() && shouldSwitch(activeBattlePokemon, battle)) {
val switchTarget = findBestSwitch(activeBattlePokemon, battle)
if (switchTarget != null) {
switchTarget.willBeSwitchedIn = true
logSwitchDecision("voluntary_switch", battle, listOf(switchTarget), switchTarget)
return SwitchActionResponse(switchTarget.uuid)
}
}
val bestMove = chooseBestMove(moveset, activeBattlePokemon, battle)
logMoveDecision("move_select", bestMove, activeBattlePokemon)
return executeMove(bestMove, activeBattlePokemon)
} catch (e: Exception) {
e.printStackTrace()
return PassActionResponse
}
}
private fun chooseForDoubles(
activeBattlePokemon: ActiveBattlePokemon,
battle: PokemonBattle,
moveset: ShowdownMoveset?
): ShowdownActionResponse {
val state = battleState ?: return PassActionResponse
val plan = currentGamePlan ?: return PassActionResponse
val myPokemon = battleTracker.p2Active.getActiveAt(currentSlot) ?: return PassActionResponse
val partnerSlot = 1 - currentSlot
val partner = battleTracker.p2Active.getActiveAt(partnerSlot)
val opponents = battleTracker.p1Active.getAllActive()
val decision = doublesDecisionMaker.findBestMoveAndTarget(
moveset,
activeBattlePokemon,
myPokemon,
opponents,
partner,
state,
plan
)
if (decision == null) {
debugLog("Doubles: no evaluated option; selecting random move.")
return makeRandomMove(moveset, activeBattlePokemon)
}
turnDecisions[currentSlot] = decision
firstSlotDecided = true
debugLog(
"Doubles: chose move=${decision.move.id} target=${decision.targetPNX ?: "auto"} " +
"score=${"%.2f".format(decision.evaluation.totalScore)} " +
"expectedDamage=${"%.1f".format(decision.evaluation.expectedDamage)} " +
"strategic=${"%.1f".format(decision.evaluation.strategicValue)} " +
"risk=${"%.2f".format(decision.evaluation.riskFactor)}"
)
return executeDoublesMove(decision, activeBattlePokemon)
}
private fun executeDoublesMove(
decision: DoublesDecisionMaker.MoveTargetPair,
activeBattlePokemon: ActiveBattlePokemon
): ShowdownActionResponse {
return if (decision.targetPNX != null) {
MoveActionResponse(decision.move.id, decision.targetPNX)
} else {
MoveActionResponse(decision.move.id)
}
}
private fun initializeComponents(battle: PokemonBattle) {
if (currentBattle != battle) {
currentBattle = battle
isDoublesMode = battle.format.battleType.pokemonPerSide > 1
damageCalculator = DamageCalculator(battle)
moveEvaluator = MoveEvaluator(damageCalculator, opponentPredictor, isDoublesMode)
switchDecisionMaker = SwitchDecisionMaker(damageCalculator, opponentPredictor, strategyAnalyzer)
doublesDecisionMaker = DoublesDecisionMaker(moveEvaluator, damageCalculator)
}
}
private fun updateBattleState(activeBattlePokemon: ActiveBattlePokemon, battle: PokemonBattle) {
val request = activeBattlePokemon.actor.request ?: return
// Determine current slot from PNX (e.g., "p2a" = slot 0, "p2b" = slot 1)
val pnx = activeBattlePokemon.getPNX()
currentSlot = if (pnx.length >= 3) pnx[2] - 'a' else 0
if (isDoublesMode) {
battle.side1.activePokemon.forEachIndexed { index, active ->
active.battlePokemon?.let { pokemon ->
battleTracker.updatePokemonSlot(battleTracker.p1Active, index, pokemon)
}
}
battle.side2.activePokemon.forEachIndexed { index, active ->
active.battlePokemon?.let { pokemon ->
battleTracker.updatePokemonSlot(battleTracker.p2Active, index, pokemon)
}
}
battleTracker.p1Active.activePokemon =
battleTracker.p1Active.getActiveAt(currentSlot) ?: battleTracker.p1Active.activePokemon
battleTracker.p2Active.activePokemon =
battleTracker.p2Active.getActiveAt(currentSlot) ?: battleTracker.p2Active.activePokemon
} else {
battle.side1.activePokemon.firstOrNull()?.battlePokemon?.let { playerPokemon ->
battleTracker.updatePokemon(battleTracker.p1Active.activePokemon, playerPokemon)
battleTracker.updateParty(battleTracker.p1Active, battleTracker.p1Active.activePokemon)
}
activeBattlePokemon.battlePokemon?.let { aiPokemon ->
battleTracker.updatePokemon(battleTracker.p2Active.activePokemon, aiPokemon)
battleTracker.updateParty(battleTracker.p2Active, battleTracker.p2Active.activePokemon)
}
}
updateRevealedMoves(battle)
battleState = createBattleState(battle, activeBattlePokemon)
}
private fun createBattleState(battle: PokemonBattle, activeBattlePokemon: ActiveBattlePokemon): BattleState? {
val p1Actor = battle.side1.actors.firstOrNull() ?: return null
val p2Actor = battle.side2.actors.firstOrNull() ?: return null
val playerActiveTracker = if (isDoublesMode) {
battleTracker.p1Active.getActiveAt(currentSlot) ?: battleTracker.p1Active.activePokemon
} else {
battleTracker.p1Active.activePokemon
}
val npcActiveTracker = if (isDoublesMode) {
battleTracker.p2Active.getActiveAt(currentSlot) ?: battleTracker.p2Active.activePokemon
} else {
battleTracker.p2Active.activePokemon
}
val playerActive = playerActiveTracker.pokemon ?: return null
val npcActive = npcActiveTracker.pokemon ?: return null
val playerActiveSlots = battleTracker.p1Active.getAllActive()
.mapNotNull { it.pokemon }
.ifEmpty { listOf(playerActive) }
val npcActiveSlots = battleTracker.p2Active.getAllActive()
.mapNotNull { it.pokemon }
.ifEmpty { listOf(npcActive) }
val partnerSlot = if (isDoublesMode) {
val mySlot = currentSlot
1 - mySlot
} else {
null
}
return BattleState(
turn = battle.turn,
playerTeam = p1Actor.pokemonList.map { it.effectedPokemon },
npcTeam = p2Actor.pokemonList.map { it.effectedPokemon },
playerActive = playerActive,
npcActive = npcActive,
weather = getWeather(battle),
terrain = getTerrain(battle),
playerHazards = getHazards(battle.side1),
npcHazards = getHazards(battle.side2),
playerBoosts = battleTracker.p1Active.activePokemon.boosts,
npcBoosts = battleTracker.p2Active.activePokemon.boosts,
battleContext = battle.contextManager,
isDoubles = isDoublesMode,
playerActiveSlots = playerActiveSlots,
npcActiveSlots = npcActiveSlots,
mySlotIndex = currentSlot,
partnerSlotIndex = partnerSlot
)
}
private fun handleForcedSwitch(activeBattlePokemon: ActiveBattlePokemon, battle: PokemonBattle): ShowdownActionResponse {
val availableSwitches = getAvailableSwitches(activeBattlePokemon)
if (availableSwitches.isEmpty()) {
return ForfeitActionResponse()
}
val state = battleState
val plan = currentGamePlan
val bestSwitch = if (state != null && plan != null) {
switchDecisionMaker.findBestSwitch(
availableSwitches,
battleTracker.p1Active.activePokemon,
state,
plan
)
} else {
null
}
val switchTarget = bestSwitch ?: availableSwitches.first()
switchTarget.willBeSwitchedIn = true
logSwitchDecision("forced_switch", battle, availableSwitches, switchTarget)
return SwitchActionResponse(switchTarget.uuid)
}
private fun shouldSwitch(activeBattlePokemon: ActiveBattlePokemon, battle: PokemonBattle): Boolean {
val availableSwitches = getAvailableSwitches(activeBattlePokemon)
if (availableSwitches.isEmpty()) return false
val state = battleState ?: return false
val plan = currentGamePlan ?: return false
return switchDecisionMaker.shouldVoluntarilySwitch(
battleTracker.p2Active.activePokemon,
battleTracker.p1Active.activePokemon,
state,
plan,
availableSwitches
)
}
private fun findBestSwitch(activeBattlePokemon: ActiveBattlePokemon, battle: PokemonBattle): BattlePokemon? {
val availableSwitches = getAvailableSwitches(activeBattlePokemon)
val state = battleState ?: return null
val plan = currentGamePlan ?: return null
return switchDecisionMaker.findBestSwitch(
availableSwitches,
battleTracker.p1Active.activePokemon,
state,
plan
)
}
private fun chooseBestMove(
moveset: ShowdownMoveset?,
activeBattlePokemon: ActiveBattlePokemon,
battle: PokemonBattle
): InBattleMove? {
if (moveset == null) return null
val usableMoves = moveset.moves.filter { it.canBeUsed() }
if (usableMoves.isEmpty()) return null
val state = battleState ?: return null
val plan = currentGamePlan ?: return null
val evaluations = usableMoves.map { move ->
moveEvaluator.evaluateMove(
move,
battleTracker.p2Active.activePokemon,
battleTracker.p1Active.activePokemon,
state,
plan
)
}
val bestEvaluation = evaluations.maxByOrNull { it.totalScore }
logMoveRanking(evaluations)
lastMoveEvaluation = bestEvaluation
return bestEvaluation?.move
}
private fun executeMove(move: InBattleMove?, activeBattlePokemon: ActiveBattlePokemon): ShowdownActionResponse {
if (move == null) {
return makeRandomMove(null, activeBattlePokemon)
}
val target = if (move.mustBeUsed()) {
null
} else {
move.target.targetList(activeBattlePokemon)
}
return if (target == null) {
MoveActionResponse(move.id)
} else {
val chosenTarget = target.filter { !it.isAllied(activeBattlePokemon) }.randomOrNull()
?: target.random()
MoveActionResponse(move.id, (chosenTarget as ActiveBattlePokemon).getPNX())
}
}
private fun makeRandomMove(moveset: ShowdownMoveset?, activeBattlePokemon: ActiveBattlePokemon): ShowdownActionResponse {
if (moveset == null) return PassActionResponse
val usableMoves = moveset.moves.filter { it.canBeUsed() }
val move = usableMoves.randomOrNull() ?: return PassActionResponse
val target = if (move.mustBeUsed()) null else move.target.targetList(activeBattlePokemon)
return if (target == null) {
MoveActionResponse(move.id)
} else {
val chosenTarget = target.filter { !it.isAllied(activeBattlePokemon) }.randomOrNull()
?: target.random()
MoveActionResponse(move.id, (chosenTarget as ActiveBattlePokemon).getPNX())
}
}
private fun updateOpponentModel(battle: PokemonBattle) {
val situation = categorizeSituation(battleState ?: return)
val opponentActives = if (isDoublesMode) {
battleTracker.p1Active.getAllActive()
} else {
listOf(battleTracker.p1Active.activePokemon)
}
for (opponentActive in opponentActives) {
for (move in opponentActive.revealedMoves) {
opponentPredictor.updateMoveHistory(
opponentActive.species ?: "unknown",
move,
situation.name.lowercase()
)
}
}
}
override fun onHealthChange(packet: BattleHealthChangePacket) {
try {
val battle = currentBattle ?: return
val pnx = packet.pnx
val activePokemon = battle.activePokemon.find { it.getPNX() == pnx } ?: return
val newHealth = packet.newHealth.toInt()
val isPlayerPokemon = activePokemon.actor == battle.side1.actors.firstOrNull()
val teamTracker = if (isPlayerPokemon) {
battleTracker.p1Active
} else {
battleTracker.p2Active
}
val tracker = battleTracker.getTrackedPokemonByPNX(pnx, teamTracker) ?: teamTracker.activePokemon
val oldHealth = tracker.currentHp
val hpChange = newHealth - oldHealth
tracker.previousHp = oldHealth
tracker.currentHp = newHealth
tracker.lastHpChange = hpChange
tracker.currentHpPercent = if (tracker.maxHp > 0) newHealth.toDouble() / tracker.maxHp else 0.0
updateMomentum(hpChange, isPlayerPokemon)
detectCriticalThresholds(tracker, oldHealth, newHealth, isPlayerPokemon)
} catch (e: Exception) {
}
}
private fun updateMomentum(hpChange: Int, isPlayerPokemon: Boolean) {
val momentumChange = when {
hpChange > 0 -> -5.0 // Opponent healing shifts momentum toward them
hpChange < 0 -> kotlin.math.abs(hpChange) / 10.0 // Damage shifts momentum
else -> 0.0
}
if (isPlayerPokemon) {
// Player took damage = AI momentum increases
battleTracker.p2Active.momentum += momentumChange
battleTracker.p1Active.momentum -= momentumChange
} else {
// AI took damage = Player momentum increases
battleTracker.p2Active.momentum -= momentumChange
battleTracker.p1Active.momentum += momentumChange
}
// Clamp momentum to -100 to 100
battleTracker.p1Active.momentum = battleTracker.p1Active.momentum.coerceIn(-100.0, 100.0)
battleTracker.p2Active.momentum = battleTracker.p2Active.momentum.coerceIn(-100.0, 100.0)
}
private fun detectCriticalThresholds(
tracker: TrackedPokemon,
oldHealth: Int,
newHealth: Int,
isPlayerPokemon: Boolean
) {
if (newHealth <= 0 && oldHealth > 0) {
if (isPlayerPokemon) {
battleTracker.p2Active.momentum += 20.0
} else {
battleTracker.p1Active.momentum += 20.0
}
}
}
private fun categorizeSituation(battleState: BattleState): BattleSituation {
val myHP = battleState.npcActive.currentHealth.toDouble() / battleState.npcActive.maxHealth
val oppHP = battleState.playerActive.currentHealth.toDouble() / battleState.playerActive.maxHealth
return when {
myHP < 0.25 -> BattleSituation.DESPERATE
oppHP < 0.25 -> BattleSituation.WINNING
myHP > 0.8 && oppHP > 0.8 -> BattleSituation.EARLY_GAME
battleState.npcTeam.count { it.currentHealth > 0 } <= 2 -> BattleSituation.ENDGAME
battleState.playerBoosts.values.any { it >= 2 } -> BattleSituation.OPPONENT_SETUP
battleState.npcBoosts.values.any { it >= 2 } -> BattleSituation.MY_SETUP
battleState.weather != null -> BattleSituation.WEATHER_CONTROL
else -> BattleSituation.NEUTRAL
}
}
private fun getAvailableSwitches(activeBattlePokemon: ActiveBattlePokemon): List<BattlePokemon> {
val currentUuid = activeBattlePokemon.battlePokemon?.uuid
return activeBattlePokemon.actor.pokemonList.filter {
it.health > 0 &&
it.uuid != currentUuid &&
try { it.canBeSentOut() } catch (e: Exception) { true }
}
}
private fun passesSkillCheck(): Boolean {
if (skillLevel == 5) return true
if (skillLevel == 0) return false
val baseChance = skillLevel * 20
val turnBonus = if ((battleState?.turn ?: 0) > 10) 10 else 0
return Random.nextInt(100) < (baseChance + turnBonus)
}
private fun shouldAttemptSwitch(): Boolean {
return when (skillLevel) {
in 0..2 -> false
3 -> Random.nextDouble() < 0.25
4 -> Random.nextDouble() < 0.70
5 -> true
else -> false
}
}
private fun getWeather(battle: PokemonBattle): String? {
return try {
battle.contextManager.get(BattleContext.Type.WEATHER)
?.firstOrNull()?.id
} catch (e: Exception) {
null
}
}
private fun getTerrain(battle: PokemonBattle): String? {
return try {
battle.contextManager.get(BattleContext.Type.TERRAIN)
?.firstOrNull()?.id
} catch (e: Exception) {
null
}
}
private fun getHazards(side: BattleSide): List<String> {
return try {
side.contextManager.get(BattleContext.Type.HAZARD)
?.map { it.id } ?: emptyList()
} catch (e: Exception) {
emptyList()
}
}
private fun updateRevealedMoves(battle: PokemonBattle) {
updateTeamRevealedMoves(battle, battleTracker.p1Active)
updateTeamRevealedMoves(battle, battleTracker.p2Active)
}
private fun updateTeamRevealedMoves(battle: PokemonBattle, team: TeamTracker) {
val active = team.getAllActive().ifEmpty { listOf(team.activePokemon) }
for (tracked in active) {
val pokemon = tracked.pokemon ?: continue
val majorMessage = battle.majorBattleActions[pokemon.uuid]
val minorMessage = battle.minorBattleActions[pokemon.uuid]
val candidates = listOfNotNull(majorMessage, minorMessage)
for (message in candidates) {
val id = message.id.lowercase()
if (id != "move" && id != "-singlemove" && id != "singlemove") continue
val moveId = message.effectAt(1)?.id?.lowercase() ?: continue
tracked.revealedMoves.add(moveId)
}
}
}
private fun logMoveDecision(
reason: String,
move: InBattleMove?,
activeBattlePokemon: ActiveBattlePokemon
) {
if (!aiDebugEnabled || move == null) return
val eval = lastMoveEvaluation
val self = activeBattlePokemon.battlePokemon?.effectedPokemon?.species?.name ?: "unknown"
val opp = battleTracker.p1Active.activePokemon.species ?: "unknown"
val plan = currentGamePlan?.winCondition?.name ?: "unknown"
if (eval == null) {
debugLog("Decision=$reason self=$self opp=$opp move=${move.id} plan=$plan")
return
}
debugLog(
"Decision=$reason self=$self opp=$opp move=${move.id} plan=$plan " +
"score=${"%.2f".format(eval.totalScore)} " +
"expectedDamage=${"%.1f".format(eval.expectedDamage)} " +
"strategic=${"%.1f".format(eval.strategicValue)} " +
"risk=${"%.2f".format(eval.riskFactor)}"
)
}
private fun logMoveRanking(evaluations: List<MoveEvaluator.MoveEvaluation>) {
if (!aiDebugEnabled || evaluations.isEmpty()) return
val plan = currentGamePlan?.winCondition?.name ?: "unknown"
val situation = battleState?.let { categorizeSituation(it).name } ?: "unknown"
val top = evaluations.sortedByDescending { it.totalScore }.take(DEBUG_TOP_MOVES)
val state = battleState
val planData = currentGamePlan
val attacker = battleTracker.p2Active.activePokemon
val defender = battleTracker.p1Active.activePokemon
val details = top.joinToString(", ") { eval ->
val debug = if (state != null && planData != null) {
moveEvaluator.buildDebugDetails(eval.move, attacker, defender, state, planData)
} else {
null
}
val flags = if (debug != null) {
listOf(
"stab=${debug.hasStab}",
"eff=${"%.2f".format(debug.effectiveness)}",
"acc=${"%.2f".format(debug.accuracy)}",
"prio=${debug.priority}",
"status=${debug.isStatusMove}",
"setup=${debug.isSetup}",
"hazardSet=${debug.setsHazards}",
"hazardClear=${debug.removesHazards}",
"weather=${debug.setsWeather}",
"recovery=${debug.isRecovery}",
"pivot=${debug.isPivot}",
"risky=${debug.isRisky}"
).joinToString(" ")
} else {
"n/a"
}
"${eval.move.id}:" +
"score=${"%.2f".format(eval.totalScore)} " +
"raw=${"%.1f".format(eval.rawDamage)} " +
"dmg=${"%.1f".format(eval.expectedDamage)} " +
"strat=${"%.1f".format(eval.strategicValue)} " +
"align=${"%.1f".format(eval.gamePlanAlignment)} " +
"turn=${"%.1f".format(eval.turnAdvantage)} " +
"risk=${"%.2f".format(eval.riskFactor)} " +
flags
}
debugLog("MoveRank plan=$plan situation=$situation top=[$details]")
}
private fun logSwitchDecision(
reason: String,
battle: PokemonBattle,
availableSwitches: List<BattlePokemon>,
chosen: BattlePokemon?
) {
if (!aiDebugEnabled) return
val state = battleState
val plan = currentGamePlan
val opponent = battleTracker.p1Active.activePokemon
val ranked = if (state != null && plan != null) {
availableSwitches.map { candidate ->
val score = switchDecisionMaker.evaluateSwitchOption(
candidate.effectedPokemon,
opponent,
state,
plan
)
candidate to score
}.sortedByDescending { it.second }
.take(3)
.joinToString(", ") { (candidate, score) ->
"${candidate.effectedPokemon.species.name}:${"%.1f".format(score)}"
}
} else {
"n/a"
}
val chosenName = chosen?.effectedPokemon?.species?.name ?: "none"
debugLog("Decision=$reason chosen=$chosenName options=[$ranked]")
}
private fun debugLog(message: String) {
if (aiDebugEnabled) {
Cobblemon.LOGGER.info("[CompetitiveBattleAI] $message")
}
}
private fun isAiDebugEnabled(): Boolean {
return true
}
}
@@ -0,0 +1,20 @@
package com.cobblemon.mod.common.battles.ai.competitive
object AiBattleUtils {
fun normalizeWeatherId(weather: String?): String? {
val id = weather?.lowercase() ?: return null
return when (id) {
"sun", "sunny", "sunnyday", "harshsunlight" -> "sunnyday"
"rain", "raindance", "heavyrain" -> "raindance"
"sand", "sandstorm" -> "sandstorm"
"hail" -> "hail"
"snow", "snowscape" -> "snow"
else -> id
}
}
fun hasStab(moveTypeName: String, pokemonTypes: List<String>): Boolean {
val moveType = moveTypeName.lowercase()
return pokemonTypes.any { it.lowercase() == moveType }
}
}
@@ -0,0 +1,236 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.moves.Move
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.battles.interpreter.ContextManager
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.pokemon.Pokemon
data class BattleState(
val turn: Int,
val playerTeam: List<Pokemon>,
val npcTeam: List<Pokemon>,
val playerActive: Pokemon,
val npcActive: Pokemon,
val weather: String?,
val terrain: String?,
val playerHazards: List<String>,
val npcHazards: List<String>,
val playerBoosts: Map<Stat, Int>,
val npcBoosts: Map<Stat, Int>,
val battleContext: ContextManager,
val gamePhase: GamePhase = determineGamePhase(turn, npcTeam, playerTeam),
val isDoubles: Boolean = false,
val playerActiveSlots: List<Pokemon> = listOf(playerActive),
val npcActiveSlots: List<Pokemon> = listOf(npcActive),
val mySlotIndex: Int = 0,
val partnerSlotIndex: Int? = null
) {
companion object {
private fun determineGamePhase(turn: Int, npcTeam: List<Pokemon>, playerTeam: List<Pokemon>): GamePhase {
val npcAlive = npcTeam.count { it.currentHealth > 0 }
val playerAlive = playerTeam.count { it.currentHealth > 0 }
return when {
turn <= 3 -> GamePhase.EARLY_GAME
npcAlive <= 2 || playerAlive <= 2 -> GamePhase.ENDGAME
turn >= 15 -> GamePhase.LATE_GAME
else -> GamePhase.MID_GAME
}
}
}
}
enum class GamePhase {
EARLY_GAME,
MID_GAME,
LATE_GAME,
ENDGAME
}
data class TrackedPokemon(
var pokemon: Pokemon? = null,
var originalPokemon: Pokemon? = null,
var species: String? = null,
var currentHp: Int = 0,
var currentHpPercent: Double = 0.0,
var maxHp: Int = 0,
var previousHp: Int = 0,
var lastHpChange: Int = 0,
var boosts: Map<Stat, Int> = mapOf(),
var atkBoost: Int = 0,
var defBoost: Int = 0,
var spaBoost: Int = 0,
var spdBoost: Int = 0,
var speBoost: Int = 0,
var currentAbility: String? = null,
var currentTypes: MutableList<String>? = null,
var stats: Map<Stat, Int> = mapOf(),
var moves: List<Move> = listOf(),
var revealedMoves: MutableSet<String> = mutableSetOf(),
var cachedAttack: Double = 0.0,
var cachedDefense: Double = 0.0,
var cachedSpAttack: Double = 0.0,
var cachedSpDefense: Double = 0.0,
var cachedSpeed: Double = 0.0,
var sideConditions: Map<String, Any> = mapOf(),
var firstTurn: Int = 0,
var protectCount: Int = 0,
var lastMoveDamage: Int = 0,
var statusTurns: Int = 0,
var switchInTurn: Int = 0,
var predictedRole: PokemonRole = PokemonRole.UNKNOWN,
var threatLevel: Int = 0
)
enum class PokemonRole {
UNKNOWN,
SWEEPER,
SETUP_SWEEPER,
WALLBREAKER,
REVENGE_KILLER,
TANK,
SUPPORT,
PIVOT,
BALANCED
}
data class TeamTracker(
var party: MutableList<TrackedPokemon> = mutableListOf(),
var uuid: String? = null,
var activePokemon: TrackedPokemon = TrackedPokemon(),
var activeSlots: MutableList<TrackedPokemon> = mutableListOf(),
var nRemainingMons: Int = 0,
var hazardDamage: Int = 0,
var teamSynergy: Double = 0.0,
var momentum: Double = 0.0
) {
fun getActiveAt(slotIndex: Int): TrackedPokemon? = activeSlots.getOrNull(slotIndex)
fun getAllActive(): List<TrackedPokemon> = activeSlots.filter { it.currentHp > 0 }
fun syncLegacyActivePokemon() {
if (activeSlots.isNotEmpty()) {
activePokemon = activeSlots[0]
}
}
}
class EnhancedActiveTracker {
var p1Active: TeamTracker = TeamTracker()
var p2Active: TeamTracker = TeamTracker()
private var lastStateUpdateTurn: Int = -1
fun updatePokemonSlot(team: TeamTracker, slotIndex: Int, battlePokemon: BattlePokemon) {
while (team.activeSlots.size <= slotIndex) {
team.activeSlots.add(TrackedPokemon())
}
updatePokemon(team.activeSlots[slotIndex], battlePokemon)
team.syncLegacyActivePokemon()
}
fun getTrackedPokemonByPNX(pnx: String, team: TeamTracker): TrackedPokemon? {
if (pnx.length < 3) return null
val slotIndex = pnx[2] - 'a'
return team.getActiveAt(slotIndex)
}
fun updatePokemon(tracker: TrackedPokemon, battlePokemon: BattlePokemon) {
val pokemon = battlePokemon.effectedPokemon
tracker.pokemon = pokemon
tracker.originalPokemon = battlePokemon.originalPokemon
tracker.species = pokemon.species.name
tracker.currentHp = pokemon.currentHealth
tracker.maxHp = pokemon.maxHealth
tracker.currentHpPercent = pokemon.currentHealth.toDouble() / pokemon.maxHealth
tracker.currentTypes = pokemon.types.map { it.name.lowercase() }.toMutableList()
tracker.currentAbility = pokemon.ability.name
tracker.moves = pokemon.moveSet.getMoves()
tracker.boosts = battlePokemon.statChanges
tracker.atkBoost = battlePokemon.statChanges[Stats.ATTACK] ?: 0
tracker.defBoost = battlePokemon.statChanges[Stats.DEFENCE] ?: 0
tracker.spaBoost = battlePokemon.statChanges[Stats.SPECIAL_ATTACK] ?: 0
tracker.spdBoost = battlePokemon.statChanges[Stats.SPECIAL_DEFENCE] ?: 0
tracker.speBoost = battlePokemon.statChanges[Stats.SPEED] ?: 0
cacheStats(tracker)
}
private fun cacheStats(tracker: TrackedPokemon) {
val pokemon = tracker.pokemon ?: return
tracker.cachedAttack = calculateStat(pokemon, Stats.ATTACK)
tracker.cachedDefense = calculateStat(pokemon, Stats.DEFENCE)
tracker.cachedSpAttack = calculateStat(pokemon, Stats.SPECIAL_ATTACK)
tracker.cachedSpDefense = calculateStat(pokemon, Stats.SPECIAL_DEFENCE)
tracker.cachedSpeed = calculateStat(pokemon, Stats.SPEED)
}
private fun calculateStat(pokemon: Pokemon, stat: Stat): Double {
val base = pokemon.species.baseStats[stat] ?: 0
val iv = pokemon.ivs[stat] ?: 31
val ev = pokemon.evs[stat] ?: 0
val level = pokemon.level
return ((2.0 * base + iv + ev / 4.0) * level / 100.0 + 5.0)
}
fun updateParty(team: TeamTracker, pokemon: TrackedPokemon) {
val existingIndex = team.party.indexOfFirst { it.pokemon?.uuid == pokemon.pokemon?.uuid }
if (existingIndex == -1) {
team.party.add(pokemon)
} else {
team.party[existingIndex] = pokemon
}
}
}
data class FearThreat(
val pokemon: Pokemon,
val hasEndeavor: Boolean,
val hasPriorityMove: Boolean,
val hasFocusItem: Boolean,
val threatLevel: Int
)
enum class BattleSituation {
DESPERATE,
WINNING,
EARLY_GAME,
ENDGAME,
OPPONENT_SETUP,
MY_SETUP,
NEUTRAL,
HAZARD_ADVANTAGE,
WEATHER_CONTROL
}
data class MatchupEvaluation(
val offensiveAdvantage: Double,
val defensiveAdvantage: Double,
val speedAdvantage: Double,
val overallScore: Double,
val recommendation: MatchupRecommendation
)
enum class MatchupRecommendation {
STAY_IN_AND_ATTACK,
STAY_IN_AND_SETUP,
STAY_IN_AND_STATUS,
CONSIDER_SWITCHING,
SWITCH_OUT,
PIVOT_OUT
}
@@ -0,0 +1,483 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.types.ElementalType
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.battles.InBattleMove
import com.cobblemon.mod.common.pokemon.Pokemon
import net.minecraft.core.registries.BuiltInRegistries
import java.util.*
import kotlin.math.max
class DamageCalculator(
private val battle: PokemonBattle?,
private val consumedItems: MutableMap<UUID, MutableList<String>> = mutableMapOf()
) {
fun calculateDamage(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
weather: String? = null,
criticalHit: Boolean = false,
isSpreadMove: Boolean = false,
targetCount: Int = 1
): Double {
val moveData = Moves.getByName(move.id) ?: return 0.0
var power = moveData.power.toDouble()
if (power <= 0) return 0.0
if (isSpreadMove && targetCount > 1) {
power *= 0.75
}
val immunityMultiplier = checkAllImmunities(moveData, attacker, defender)
if (immunityMultiplier == 0.0) return 0.0
val level = attacker.pokemon?.level ?: 50
val attackStat = getAttackStat(moveData, attacker)
val defenseStat = getDefenseStat(moveData, defender)
var damage = ((2.0 * level / 5.0 + 2.0) * power * (attackStat / defenseStat)) / 50.0 + 2.0
damage *= calculateCriticalMultiplier(criticalHit)
damage *= calculateSTABMultiplier(attacker, moveData)
damage *= TypeEffectiveness.calculateTypeEffectiveness(moveData.elementalType, defender.currentTypes ?: listOf())
damage *= calculateWeatherMultiplier(moveData, AiBattleUtils.normalizeWeatherId(weather))
damage *= calculateAbilityMultiplier(attacker, defender, moveData)
damage *= calculateItemMultiplier(attacker, defender, moveData)
damage *= calculateBurnMultiplier(attacker, moveData)
damage *= calculateScreenMultiplier(attacker, defender, moveData)
damage *= calculateRandomFactor()
damage = calculateMultiHitDamage(moveData, damage)
val finalDamage = max(1.0, damage)
if (finalDamage > 0) {
trackConsumedItems(defender, moveData)
}
return finalDamage
}
private fun getAttackStat(moveData: MoveTemplate, attacker: TrackedPokemon): Double {
val pokemon = attacker.pokemon ?: return 100.0
val baseStat = if (moveData.damageCategory == DamageCategories.PHYSICAL) {
getEffectiveStat(pokemon, Stats.ATTACK)
} else {
getEffectiveStat(pokemon, Stats.SPECIAL_ATTACK)
}
val boost = if (moveData.damageCategory == DamageCategories.PHYSICAL) {
attacker.atkBoost
} else {
attacker.spaBoost
}
return applyStatBoosts(baseStat, boost)
}
private fun getDefenseStat(moveData: MoveTemplate, defender: TrackedPokemon): Double {
val pokemon = defender.pokemon ?: return 100.0
val baseStat = if (moveData.damageCategory == DamageCategories.PHYSICAL) {
getEffectiveStat(pokemon, Stats.DEFENCE)
} else {
getEffectiveStat(pokemon, Stats.SPECIAL_DEFENCE)
}
val boost = if (moveData.damageCategory == DamageCategories.PHYSICAL) {
defender.defBoost
} else {
defender.spdBoost
}
return applyStatBoosts(baseStat, boost)
}
private fun applyStatBoosts(baseStat: Double, boost: Int): Double {
val clampedBoost = boost.coerceIn(-6, 6)
return if (clampedBoost >= 0) {
baseStat * (2.0 + clampedBoost) / 2.0
} else {
baseStat * 2.0 / (2.0 - clampedBoost)
}
}
private fun getEffectiveStat(pokemon: Pokemon, stat: Stat): Double {
val base = pokemon.species.baseStats[stat] ?: 0
val iv = pokemon.ivs[stat] ?: 31
val ev = pokemon.evs[stat] ?: 0
val level = pokemon.level
return if (stat == Stats.HP) {
((2.0 * base + iv + ev / 4.0) * level / 100.0 + level + 10.0)
} else {
((2.0 * base + iv + ev / 4.0) * level / 100.0 + 5.0)
}
}
private fun checkAllImmunities(
moveData: MoveTemplate,
attacker: TrackedPokemon,
defender: TrackedPokemon
): Double {
if (checkTypeImmunity(moveData, defender)) return 0.0
if (checkAbilityImmunity(moveData, attacker, defender)) return 0.0
if (checkItemImmunity(moveData, defender)) return 0.0
if (checkMoveSpecificImmunity(moveData, defender)) return 0.0
return 1.0
}
private fun checkTypeImmunity(moveData: MoveTemplate, defender: TrackedPokemon): Boolean {
val defenderTypes = (defender.currentTypes ?: defender.pokemon?.types?.map { it.name } ?: listOf())
.map { it.lowercase() }
return when (moveData.elementalType) {
ElementalTypes.GROUND -> "flying" in defenderTypes
ElementalTypes.ELECTRIC -> "ground" in defenderTypes
ElementalTypes.NORMAL, ElementalTypes.FIGHTING -> "ghost" in defenderTypes
ElementalTypes.PSYCHIC -> "dark" in defenderTypes
ElementalTypes.POISON -> "steel" in defenderTypes
ElementalTypes.DRAGON -> "fairy" in defenderTypes
ElementalTypes.GHOST -> "normal" in defenderTypes
else -> false
}
}
private fun checkAbilityImmunity(
moveData: MoveTemplate,
attacker: TrackedPokemon,
defender: TrackedPokemon
): Boolean {
val defenderAbility = defender.currentAbility?.lowercase()
?: defender.pokemon?.ability?.name?.lowercase()
?: return false
val attackerAbility = attacker.currentAbility?.lowercase()
?: attacker.pokemon?.ability?.name?.lowercase()
if (attackerAbility in listOf("moldbreaker", "turboblaze", "teravolt")) {
if (defenderAbility != "wonderguard") return false
}
return when (defenderAbility) {
"levitate" -> moveData.elementalType == ElementalTypes.GROUND
"voltabsorb", "lightningrod", "motordrive" -> moveData.elementalType == ElementalTypes.ELECTRIC
"flashfire", "wellbakedbody" -> moveData.elementalType == ElementalTypes.FIRE
"waterabsorb", "stormdrain", "dryskin" -> moveData.elementalType == ElementalTypes.WATER
"sapsipper" -> moveData.elementalType == ElementalTypes.GRASS
"eartheater" -> moveData.elementalType == ElementalTypes.GROUND
"soundproof" -> isSoundMove(moveData)
"bulletproof" -> isBallOrBombMove(moveData)
"queenlymajesty", "dazzling" -> moveData.priority > 0
"wonderguard" -> {
val effectiveness = TypeEffectiveness.calculateTypeEffectiveness(
moveData.elementalType,
defender.currentTypes ?: listOf()
)
effectiveness <= 1.0
}
"goodasgold" -> moveData.power.toInt() == 0
else -> false
}
}
private fun checkItemImmunity(moveData: MoveTemplate, defender: TrackedPokemon): Boolean {
val defenderItem = defender.pokemon?.heldItem()?.item?.let {
BuiltInRegistries.ITEM.getKey(it).toString()
} ?: return false
return when (defenderItem) {
"cobblemon:air_balloon" -> {
val notConsumed = consumedItems[defender.pokemon?.uuid]?.contains("airballoon") != true
notConsumed && moveData.elementalType == ElementalTypes.GROUND
}
"cobblemon:safety_goggles" -> isPowderMove(moveData) || isWeatherDamageMove(moveData)
else -> false
}
}
private fun checkMoveSpecificImmunity(moveData: MoveTemplate, defender: TrackedPokemon): Boolean {
val defenderTypes = (defender.currentTypes ?: defender.pokemon?.types?.map { it.name } ?: listOf())
.map { it.lowercase() }
val defenderAbility = defender.currentAbility?.lowercase()
?: defender.pokemon?.ability?.name?.lowercase()
return when (moveData.name.lowercase()) {
"thunderwave" -> {
"electric" in defenderTypes || "ground" in defenderTypes ||
defenderAbility in listOf("limber", "volt absorb", "lightningrod", "motordrive")
}
"willowisp" -> {
"fire" in defenderTypes ||
defenderAbility in listOf("waterveil", "waterbubble", "flashfire")
}
"toxic", "poisonpowder", "poisongas" -> {
"poison" in defenderTypes || "steel" in defenderTypes ||
defenderAbility in listOf("immunity", "poisonheal", "pastelveil")
}
"sleeppowder", "spore", "hypnosis", "sing" -> {
defenderAbility in listOf("insomnia", "vitalspirit", "comatose", "sweetveil")
}
else -> false
}
}
private fun calculateCriticalMultiplier(criticalHit: Boolean): Double {
return if (criticalHit) 1.5 else 1.0
}
private fun calculateSTABMultiplier(attacker: TrackedPokemon, moveData: MoveTemplate): Double {
val pokemonTypes = attacker.currentTypes ?: attacker.pokemon?.types?.map { it.name } ?: return 1.0
return if (AiBattleUtils.hasStab(moveData.elementalType.name, pokemonTypes)) {
if (attacker.currentAbility?.lowercase() == "adaptability") 2.0 else 1.5
} else 1.0
}
private fun calculateWeatherMultiplier(moveData: MoveTemplate, weather: String?): Double {
return when (weather) {
"sunnyday" -> when {
moveData.elementalType == ElementalTypes.FIRE -> 1.5
moveData.elementalType == ElementalTypes.WATER && moveData.name != "hydrosteam" -> 0.5
moveData.name == "hydrosteam" -> 1.5
else -> 1.0
}
"raindance" -> when {
moveData.elementalType == ElementalTypes.WATER -> 1.5
moveData.elementalType == ElementalTypes.FIRE -> 0.5
else -> 1.0
}
"sandstorm", "hail", "snow" -> if (moveData.name == "weatherball") 2.0 else 1.0
else -> 1.0
}
}
private fun calculateAbilityMultiplier(
attacker: TrackedPokemon,
defender: TrackedPokemon,
moveData: MoveTemplate
): Double {
var multiplier = 1.0
when (attacker.currentAbility?.lowercase()) {
"hugepower", "purepower" -> {
if (moveData.damageCategory == DamageCategories.PHYSICAL) multiplier *= 2.0
}
"guts" -> {
if (attacker.pokemon?.status != null && moveData.damageCategory == DamageCategories.PHYSICAL) {
multiplier *= 1.5
}
}
"overgrow" -> {
if (moveData.elementalType == ElementalTypes.GRASS && attacker.currentHpPercent <= 0.33) {
multiplier *= 1.5
}
}
"blaze" -> {
if (moveData.elementalType == ElementalTypes.FIRE && attacker.currentHpPercent <= 0.33) {
multiplier *= 1.5
}
}
"torrent" -> {
if (moveData.elementalType == ElementalTypes.WATER && attacker.currentHpPercent <= 0.33) {
multiplier *= 1.5
}
}
"swarm" -> {
if (moveData.elementalType == ElementalTypes.BUG && attacker.currentHpPercent <= 0.33) {
multiplier *= 1.5
}
}
}
when (defender.currentAbility?.lowercase()) {
"thickfat" -> {
if (moveData.elementalType == ElementalTypes.FIRE || moveData.elementalType == ElementalTypes.ICE) {
multiplier *= 0.5
}
}
"filter", "solidrock" -> {
val effectiveness = TypeEffectiveness.calculateTypeEffectiveness(
moveData.elementalType,
defender.currentTypes ?: listOf()
)
if (effectiveness > 1.0) multiplier *= 0.75
}
}
return multiplier
}
private fun calculateItemMultiplier(
attacker: TrackedPokemon,
defender: TrackedPokemon,
moveData: MoveTemplate
): Double {
var multiplier = 1.0
val attackerItem = attacker.pokemon?.heldItem()?.item?.let {
BuiltInRegistries.ITEM.getKey(it).toString()
}
when (attackerItem) {
"cobblemon:choice_band" -> {
if (moveData.damageCategory == DamageCategories.PHYSICAL) multiplier *= 1.5
}
"cobblemon:choice_specs" -> {
if (moveData.damageCategory == DamageCategories.SPECIAL) multiplier *= 1.5
}
"cobblemon:life_orb" -> multiplier *= 1.3
}
return multiplier
}
private fun calculateBurnMultiplier(attacker: TrackedPokemon, moveData: MoveTemplate): Double {
return if (attacker.pokemon?.status?.status?.showdownName == "burn" &&
moveData.damageCategory == DamageCategories.PHYSICAL &&
attacker.currentAbility != "guts") {
0.5
} else {
1.0
}
}
private fun calculateScreenMultiplier(
attacker: TrackedPokemon,
defender: TrackedPokemon,
moveData: MoveTemplate
): Double {
val attackerAbility = attacker.currentAbility?.lowercase() ?: attacker.pokemon?.ability?.name?.lowercase()
if (attackerAbility == "infiltrator") {
return 1.0
}
val sideConditions = defender.sideConditions.keys.map { it.lowercase() }
val hasReflect = "reflect" in sideConditions
val hasLightScreen = "lightscreen" in sideConditions || "light screen" in sideConditions
val hasAuroraVeil = "auroraveil" in sideConditions || "aurora veil" in sideConditions
val multiplier = when (moveData.damageCategory) {
DamageCategories.PHYSICAL -> {
when {
hasAuroraVeil -> 0.5
hasReflect -> 0.5
else -> 1.0
}
}
DamageCategories.SPECIAL -> {
when {
hasAuroraVeil -> 0.5
hasLightScreen -> 0.5
else -> 1.0
}
}
else -> 1.0
}
return multiplier
}
private fun calculateRandomFactor(): Double = 0.925
private fun calculateMultiHitDamage(moveData: MoveTemplate, baseDamage: Double): Double {
val moveName = moveData.name.lowercase()
return when {
moveName == "populationbomb" -> {
val accuracy = moveData.accuracy / 100.0
var expectedDamage = 0.0
var probability = 1.0
for (hit in 1..10) {
expectedDamage += baseDamage * probability
probability *= accuracy
}
expectedDamage
}
moveName in listOf("triplekick", "tripleaxel") -> {
val accuracy = moveData.accuracy / 100.0
baseDamage * 1.0 +
baseDamage * 2.0 * accuracy +
baseDamage * 3.0 * accuracy * accuracy
}
moveName in MoveData.multiHitMoves.keys -> {
val hitRange = MoveData.multiHitMoves[moveName]!!
val minHits = hitRange.keys.minOrNull() ?: 1
val maxHits = hitRange.values.maxOrNull() ?: 1
when {
minHits == 2 && maxHits == 5 -> baseDamage * 3.0
minHits == 2 && maxHits == 2 -> baseDamage * 2.0
minHits == 3 && maxHits == 3 -> baseDamage * 3.0
else -> baseDamage * ((minHits + maxHits) / 2.0)
}
}
else -> baseDamage
}
}
private fun isSoundMove(moveData: MoveTemplate): Boolean {
return moveData.name.lowercase() in listOf(
"boomburst", "bugbuzz", "chatter", "clangoroussoul", "disarmingvoice",
"echoedvoice", "grasswhistle", "growl", "hypervoice", "metalsound",
"nobleroar", "overdrive", "perishsong", "relicsong", "roar", "round",
"screech", "sing", "snore", "sparklingaria", "supersonic", "uproar"
)
}
private fun isBallOrBombMove(moveData: MoveTemplate): Boolean {
return moveData.name.lowercase() in listOf(
"acidspray", "aurasphere", "barrage", "bulletseed", "eggbomb",
"electroball", "energyball", "focusblast", "gyroball", "iceball",
"magnetbomb", "mistball", "mudbomb", "octazooka", "pollenpuff",
"rockblast", "searingshot", "seedbomb", "shadowball", "sludgebomb",
"weatherball", "zapcannon"
)
}
private fun isPowderMove(moveData: MoveTemplate): Boolean {
return moveData.name.lowercase() in listOf(
"sleeppowder", "stunspore", "poisonpowder", "cottonspore",
"ragepowder", "spore"
)
}
private fun isWeatherDamageMove(moveData: MoveTemplate): Boolean {
return moveData.name.lowercase() in listOf("sandstorm", "hail")
}
private fun trackConsumedItems(defender: TrackedPokemon, moveData: MoveTemplate) {
val pokemon = defender.pokemon ?: return
val defenderItem = pokemon.heldItem()?.item?.let {
BuiltInRegistries.ITEM.getKey(it).toString()
} ?: return
if (defenderItem == "cobblemon:air_balloon") {
val alreadyConsumed = consumedItems[pokemon.uuid]?.contains("airballoon") == true
if (!alreadyConsumed) {
addConsumedItem(pokemon.uuid, "airballoon")
}
}
}
private fun addConsumedItem(pokemonUuid: UUID, item: String) {
val items = consumedItems.getOrPut(pokemonUuid) { mutableListOf() }
items.add(item)
}
}
@@ -0,0 +1,135 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.battles.ActiveBattlePokemon
import com.cobblemon.mod.common.battles.InBattleMove
import com.cobblemon.mod.common.battles.ShowdownMoveset
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
class DoublesDecisionMaker(
private val moveEvaluator: MoveEvaluator,
private val damageCalculator: DamageCalculator
) {
data class MoveTargetPair(
val move: InBattleMove,
val targetPNX: String?,
val evaluation: MoveEvaluator.MoveEvaluation
)
fun findBestMoveAndTarget(
moveset: ShowdownMoveset?,
activeBattlePokemon: ActiveBattlePokemon,
myPokemon: TrackedPokemon,
opponents: List<TrackedPokemon>,
partner: TrackedPokemon?,
battleState: BattleState,
gamePlan: GamePlan
): MoveTargetPair? {
if (moveset == null) return null
val usableMoves = moveset.moves.filter { it.canBeUsed() }
if (usableMoves.isEmpty()) return null
val allOptions = mutableListOf<MoveTargetPair>()
for (move in usableMoves) {
val moveData = Moves.getByName(move.id) ?: continue
val targetList = move.target.targetList(activeBattlePokemon)
if (targetList == null) {
val evaluation = moveEvaluator.evaluateMoveForDoubles(
move, myPokemon, opponents.firstOrNull() ?: TrackedPokemon(),
opponents, partner, battleState, gamePlan
)
allOptions.add(MoveTargetPair(move, null, evaluation))
} else {
val opponentTargets = targetList.filterIsInstance<ActiveBattlePokemon>()
.filter { !it.isAllied(activeBattlePokemon) }
for (target in opponentTargets) {
val targetPokemon = opponents.find {
it.pokemon?.uuid == target.battlePokemon?.uuid
} ?: continue
val evaluation = moveEvaluator.evaluateMoveForDoubles(
move, myPokemon, targetPokemon,
opponents, partner, battleState, gamePlan
)
allOptions.add(MoveTargetPair(move, target.getPNX(), evaluation))
}
}
}
return allOptions.maxByOrNull { it.evaluation.totalScore }
}
fun scoreTarget(
target: TrackedPokemon,
myPokemon: TrackedPokemon,
allOpponents: List<TrackedPokemon>,
battleState: BattleState
): Double {
var score = 0.0
score += target.threatLevel * 20.0
val mySpeed = myPokemon.cachedSpeed
val targetSpeed = target.cachedSpeed
if (targetSpeed > mySpeed && target.currentHpPercent < 0.5) {
score += 100.0
}
if (target.boosts.values.any { it >= 2 }) {
score += 120.0
}
if (target.cachedAttack > 100 || target.cachedSpAttack > 100) {
score += 40.0
}
if (target.currentHpPercent < 0.3) {
score += 80.0
}
return score
}
fun selectBestTarget(
move: InBattleMove,
activeBattlePokemon: ActiveBattlePokemon,
myPokemon: TrackedPokemon,
opponents: List<TrackedPokemon>,
battleState: BattleState
): String? {
val targetList = move.target.targetList(activeBattlePokemon) ?: return null
val opponentTargets = targetList.filterIsInstance<ActiveBattlePokemon>()
.filter { !it.isAllied(activeBattlePokemon) }
if (opponentTargets.isEmpty()) return null
val targetScores = opponentTargets.mapNotNull { target ->
val targetPokemon = opponents.find {
it.pokemon?.uuid == target.battlePokemon?.uuid
}
if (targetPokemon != null) {
val damage = damageCalculator.calculateDamage(
move, myPokemon, targetPokemon, battleState.weather
)
val targetScore = scoreTarget(targetPokemon, myPokemon, opponents, battleState)
val totalScore = damage + targetScore
target to totalScore
} else {
null
}
}
return targetScores.maxByOrNull { it.second }?.first?.getPNX()
}
}
@@ -0,0 +1,178 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.pokemon.status.Statuses
object MoveData {
val multiHitMoves: Map<String, Map<Int, Int>> = mapOf(
"armthrust" to mapOf(2 to 5),
"barrage" to mapOf(2 to 5),
"bonerush" to mapOf(2 to 5),
"bulletseed" to mapOf(2 to 5),
"cometpunch" to mapOf(2 to 5),
"doubleslap" to mapOf(2 to 5),
"furyattack" to mapOf(2 to 5),
"furyswipes" to mapOf(2 to 5),
"iciclespear" to mapOf(2 to 5),
"pinmissile" to mapOf(2 to 5),
"rockblast" to mapOf(2 to 5),
"scaleshot" to mapOf(2 to 5),
"spikecannon" to mapOf(2 to 5),
"tailslap" to mapOf(2 to 5),
"watershuriken" to mapOf(2 to 5),
"bonemerang" to mapOf(2 to 2),
"doublehit" to mapOf(2 to 2),
"doubleironbash" to mapOf(2 to 2),
"doublekick" to mapOf(2 to 2),
"dragondarts" to mapOf(2 to 2),
"dualchop" to mapOf(2 to 2),
"dualwingbeat" to mapOf(2 to 2),
"geargrind" to mapOf(2 to 2),
"twinbeam" to mapOf(2 to 2),
"twineedle" to mapOf(2 to 2),
"surgingstrikes" to mapOf(3 to 3),
"tripledive" to mapOf(3 to 3),
"tripleaxel" to mapOf(1 to 3),
"triplekick" to mapOf(1 to 3),
"populationbomb" to mapOf(1 to 10)
)
val statusMoves: Map<MoveTemplate?, String> = mapOf(
Moves.getByName("willowisp") to Statuses.BURN.showdownName,
Moves.getByName("scald") to Statuses.BURN.showdownName,
Moves.getByName("scorchingsands") to Statuses.BURN.showdownName,
Moves.getByName("flamethrower") to Statuses.BURN.showdownName,
Moves.getByName("sacredfire") to Statuses.BURN.showdownName,
Moves.getByName("glare") to Statuses.PARALYSIS.showdownName,
Moves.getByName("nuzzle") to Statuses.PARALYSIS.showdownName,
Moves.getByName("stunspore") to Statuses.PARALYSIS.showdownName,
Moves.getByName("thunderwave") to Statuses.PARALYSIS.showdownName,
Moves.getByName("bodyslam") to Statuses.PARALYSIS.showdownName,
Moves.getByName("discharge") to Statuses.PARALYSIS.showdownName,
Moves.getByName("spark") to Statuses.PARALYSIS.showdownName,
Moves.getByName("darkvoid") to Statuses.SLEEP.showdownName,
Moves.getByName("hypnosis") to Statuses.SLEEP.showdownName,
Moves.getByName("lovelykiss") to Statuses.SLEEP.showdownName,
Moves.getByName("relicsong") to Statuses.SLEEP.showdownName,
Moves.getByName("sing") to Statuses.SLEEP.showdownName,
Moves.getByName("sleeppowder") to Statuses.SLEEP.showdownName,
Moves.getByName("spore") to Statuses.SLEEP.showdownName,
Moves.getByName("yawn") to Statuses.SLEEP.showdownName,
Moves.getByName("chatter") to "confusion",
Moves.getByName("confuseray") to "confusion",
Moves.getByName("dynamicpunch") to "confusion",
Moves.getByName("flatter") to "confusion",
Moves.getByName("supersonic") to "confusion",
Moves.getByName("swagger") to "confusion",
Moves.getByName("sweetkiss") to "confusion",
Moves.getByName("teeterdance") to "confusion",
Moves.getByName("poisongas") to Statuses.POISON.showdownName,
Moves.getByName("poisonpowder") to Statuses.POISON.showdownName,
Moves.getByName("poisonjab") to Statuses.POISON.showdownName,
Moves.getByName("sludgebomb") to Statuses.POISON.showdownName,
Moves.getByName("crosspoison") to Statuses.POISON.showdownName,
Moves.getByName("toxic") to Statuses.POISON_BADLY.showdownName,
Moves.getByName("toxicthread") to Statuses.POISON.showdownName,
Moves.getByName("curse") to "cursed",
Moves.getByName("leechseed") to "leech"
)
val boostFromMoves: Map<String, Map<Stat, Int>> = mapOf(
"bellydrum" to mapOf(Stats.ATTACK to 6),
"swordsdance" to mapOf(Stats.ATTACK to 2),
"honeclaws" to mapOf(Stats.ATTACK to 1, Stats.ACCURACY to 1),
"bulkup" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1),
"curse" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPEED to -1),
"howl" to mapOf(Stats.ATTACK to 1),
"meditate" to mapOf(Stats.ATTACK to 1),
"sharpen" to mapOf(Stats.ATTACK to 1),
"workup" to mapOf(Stats.ATTACK to 1, Stats.SPECIAL_ATTACK to 1),
"acidarmor" to mapOf(Stats.DEFENCE to 2),
"barrier" to mapOf(Stats.DEFENCE to 2),
"cottonguard" to mapOf(Stats.DEFENCE to 3),
"defensecurl" to mapOf(Stats.DEFENCE to 1),
"irondefense" to mapOf(Stats.DEFENCE to 2),
"stockpile" to mapOf(Stats.DEFENCE to 1, Stats.SPECIAL_DEFENCE to 1),
"stuffcheeks" to mapOf(Stats.DEFENCE to 2),
"shelter" to mapOf(Stats.DEFENCE to 2, Stats.EVASION to 1),
"withdraw" to mapOf(Stats.DEFENCE to 1),
"harden" to mapOf(Stats.DEFENCE to 1),
"nastyplot" to mapOf(Stats.SPECIAL_ATTACK to 2),
"calmmind" to mapOf(Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1),
"quiverdance" to mapOf(Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1, Stats.SPEED to 1),
"tailglow" to mapOf(Stats.SPECIAL_ATTACK to 3),
"takeheart" to mapOf(Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1),
"geomancy" to mapOf(Stats.SPECIAL_ATTACK to 2, Stats.SPECIAL_DEFENCE to 2, Stats.SPEED to 2),
"growth" to mapOf(Stats.SPECIAL_ATTACK to 1),
"amnesia" to mapOf(Stats.SPECIAL_DEFENCE to 2),
"agility" to mapOf(Stats.SPEED to 2),
"autotomize" to mapOf(Stats.SPEED to 2),
"rockpolish" to mapOf(Stats.SPEED to 2),
"dragondance" to mapOf(Stats.ATTACK to 1, Stats.SPEED to 1),
"shiftgear" to mapOf(Stats.ATTACK to 1, Stats.SPEED to 2),
"clangoroussoul" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1, Stats.SPEED to 1),
"extremeevoboost" to mapOf(Stats.ATTACK to 2, Stats.DEFENCE to 2, Stats.SPECIAL_ATTACK to 2, Stats.SPECIAL_DEFENCE to 2, Stats.SPEED to 2),
"clangoroussoulblaze" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1, Stats.SPEED to 1),
"filletaway" to mapOf(Stats.ATTACK to 2, Stats.SPECIAL_ATTACK to 2, Stats.SPEED to 2),
"noretreat" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPECIAL_ATTACK to 1, Stats.SPECIAL_DEFENCE to 1, Stats.SPEED to 1),
"shellsmash" to mapOf(Stats.ATTACK to 2, Stats.DEFENCE to -1, Stats.SPECIAL_ATTACK to 2, Stats.SPECIAL_DEFENCE to -1, Stats.SPEED to 2),
"tidyup" to mapOf(Stats.ATTACK to 1, Stats.SPEED to 1),
"victorydance" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.SPEED to 1),
"coil" to mapOf(Stats.ATTACK to 1, Stats.DEFENCE to 1, Stats.ACCURACY to 1),
"minimize" to mapOf(Stats.EVASION to 2),
"doubleteam" to mapOf(Stats.EVASION to 1)
)
val weatherMoves: Map<String, String> = mapOf(
"raindance" to "rain",
"sunnyday" to "sun",
"sandstorm" to "sand",
"hail" to "hail",
"snowscape" to "snow"
)
val weatherDependentMoves: Map<String, String> = mapOf(
"thunder" to "rain",
"hurricane" to "rain",
"blizzard" to "hail",
"solarbeam" to "sun",
"hydrosteam" to "sun",
"weatherball" to "any"
)
val weatherAbilities: Map<String, String> = mapOf(
"swiftswim" to "rain",
"raindish" to "rain",
"dryskin" to "rain",
"chlorophyll" to "sun",
"leafguard" to "sun",
"flowergift" to "sun",
"solarpower" to "sun",
"sandrush" to "sand",
"sandveil" to "sand",
"sandforce" to "sand",
"icebody" to "hail",
"snowcloak" to "hail"
)
val entryHazards = listOf("spikes", "stealthrock", "stickyweb", "toxicspikes")
val antiHazardsMoves = listOf("rapidspin", "defog", "tidyup")
val antiBoostMoves = listOf("clearsmog", "haze", "roar", "whirlwind")
val pivotMoves = listOf("uturn", "flipturn", "partingshot", "batonpass", "voltswitch", "teleport")
val setupMoves = setOf("tailwind", "trickroom", "auroraveil", "lightscreen", "reflect")
val selfRecoveryMoves = listOf("healorder", "milkdrink", "recover", "rest", "roost", "slackoff", "softboiled")
val priorityMoves = listOf(
"quickattack", "extremespeed", "bulletpunch", "machpunch",
"aquajet", "shadowsneak", "accelerock", "iceshard",
"suckerpunch", "firstimpression", "fakeout"
)
val recoilMoves = listOf(
"doubleedge", "bravebird", "closecombat", "superpower",
"overheat", "dracometeor", "leafstorm", "flareblitz"
)
}
@@ -0,0 +1,737 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.battles.InBattleMove
import com.cobblemon.mod.common.battles.MoveTarget
import com.cobblemon.mod.common.pokemon.Pokemon
import kotlin.math.abs
import kotlin.math.max
class MoveEvaluator(
private val damageCalculator: DamageCalculator,
private val opponentPredictor: OpponentPredictor,
private val isDoublesMode: Boolean = false
) {
data class MoveEvaluation(
val move: InBattleMove,
val rawDamage: Double,
val expectedDamage: Double,
val strategicValue: Double,
val riskFactor: Double,
val priority: Int,
val turnAdvantage: Double,
val gamePlanAlignment: Double
) {
val totalScore: Double = (expectedDamage + strategicValue + gamePlanAlignment + turnAdvantage) / riskFactor
}
data class MoveDebugDetails(
val moveId: String,
val moveType: String,
val accuracy: Double,
val rawDamage: Double,
val expectedDamage: Double,
val effectiveness: Double,
val hasStab: Boolean,
val priority: Int,
val isStatusMove: Boolean,
val setsWeather: Boolean,
val setsHazards: Boolean,
val removesHazards: Boolean,
val isRecovery: Boolean,
val isPivot: Boolean,
val isSetup: Boolean,
val isRisky: Boolean
)
fun buildDebugDetails(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): MoveDebugDetails? {
val moveData = Moves.getByName(move.id) ?: return null
val rawDamage = damageCalculator.calculateDamage(move, attacker, defender, battleState.weather)
val accuracy = getEffectiveAccuracy(move, attacker, defender, battleState.weather)
val expectedDamage = rawDamage * accuracy
val effectiveness = TypeEffectiveness.calculateTypeEffectiveness(
moveData.elementalType,
defender.currentTypes ?: listOf()
)
val attackerTypes = attacker.currentTypes ?: attacker.pokemon?.types?.map { it.name } ?: listOf()
val hasStab = AiBattleUtils.hasStab(moveData.elementalType.name, attackerTypes)
val moveId = move.id.lowercase()
val isStatusMove = MoveData.statusMoves.keys.any { it?.name?.lowercase() == moveId }
val setsWeather = moveId in MoveData.weatherMoves.keys
val setsHazards = moveId in MoveData.entryHazards
val removesHazards = moveId in MoveData.antiHazardsMoves
val isRecovery = moveId in MoveData.selfRecoveryMoves
val isPivot = moveId in MoveData.pivotMoves
val isSetup = moveId in MoveData.boostFromMoves.keys
val isRisky = moveId in gamePlan.riskyMoves
return MoveDebugDetails(
moveId = move.id,
moveType = moveData.elementalType.name,
accuracy = accuracy,
rawDamage = rawDamage,
expectedDamage = expectedDamage,
effectiveness = effectiveness,
hasStab = hasStab,
priority = moveData.priority,
isStatusMove = isStatusMove,
setsWeather = setsWeather,
setsHazards = setsHazards,
removesHazards = removesHazards,
isRecovery = isRecovery,
isPivot = isPivot,
isSetup = isSetup,
isRisky = isRisky
)
}
fun evaluateMove(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): MoveEvaluation {
val moveData = Moves.getByName(move.id)
?: return MoveEvaluation(move, 0.0, 0.0, 0.0, 1.0, 0, 0.0, 0.0)
val rawDamage = damageCalculator.calculateDamage(move, attacker, defender, battleState.weather)
val accuracy = getEffectiveAccuracy(move, attacker, defender, battleState.weather)
val expectedDamage = rawDamage * accuracy
val strategicValue = calculateStrategicValue(move, attacker, defender, battleState, gamePlan)
val riskFactor = calculateRiskFactor(move, attacker, defender, battleState, gamePlan)
val turnAdvantage = calculateTurnAdvantage(move, attacker, defender, battleState)
val gamePlanAlignment = calculateGamePlanAlignment(move, attacker, defender, battleState, gamePlan)
return MoveEvaluation(
move = move,
rawDamage = rawDamage,
expectedDamage = expectedDamage,
strategicValue = strategicValue,
riskFactor = riskFactor,
priority = moveData.priority,
turnAdvantage = turnAdvantage,
gamePlanAlignment = gamePlanAlignment
)
}
private fun getEffectiveAccuracy(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
weather: String?
): Double {
val moveData = Moves.getByName(move.id) ?: return 0.0
var accuracy = if (moveData.accuracy <= 0) 1.0 else moveData.accuracy / 100.0
val normalizedWeather = AiBattleUtils.normalizeWeatherId(weather)
when (normalizedWeather) {
"raindance" -> {
if (move.id.lowercase() in listOf("thunder", "hurricane")) accuracy = 1.0
}
"hail", "snow" -> {
if (move.id.lowercase() == "blizzard") accuracy = 1.0
}
"sunnyday" -> {
if (move.id.lowercase() in listOf("thunder", "hurricane")) accuracy *= 0.5
}
}
when (attacker.currentAbility?.lowercase()) {
"compoundeyes" -> accuracy *= 1.3
"noguard" -> accuracy = 1.0
}
when (defender.currentAbility?.lowercase()) {
"noguard" -> accuracy = 1.0
"sandveil" -> if (normalizedWeather == "sandstorm") accuracy *= 0.8
"snowcloak" -> if (normalizedWeather in listOf("hail", "snow")) accuracy *= 0.8
}
val accuracyBoost = attacker.boosts.getOrDefault(Stats.ACCURACY, 0)
val evasionBoost = defender.boosts.getOrDefault(Stats.EVASION, 0)
val netBoost = accuracyBoost - evasionBoost
if (netBoost != 0) {
accuracy *= if (netBoost > 0) {
(3.0 + netBoost) / 3.0
} else {
3.0 / (3.0 - netBoost)
}
}
return accuracy.coerceIn(0.0, 1.0)
}
private fun calculateStrategicValue(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
var value = 0.0
if (move.id.lowercase() in MoveData.weatherMoves.keys) {
value += evaluateWeatherSetting(move, gamePlan, battleState)
}
if (move.id.lowercase() in MoveData.boostFromMoves.keys) {
value += evaluateSetupMove(move, attacker, defender, battleState, gamePlan)
}
val moveId = move.id.lowercase()
val isStatusMove = MoveData.statusMoves.keys.any { it?.name?.lowercase() == moveId }
if (isStatusMove) {
value += evaluateStatusMove(move, attacker, defender, battleState, gamePlan)
}
if (move.id.lowercase() in MoveData.entryHazards) {
value += evaluateHazardSetting(battleState, gamePlan)
}
if (move.id.lowercase() in MoveData.antiHazardsMoves) {
value += evaluateHazardRemoval(battleState, gamePlan)
}
val moveData = Moves.getByName(move.id)
if (moveData != null && moveData.priority > 0) {
value += evaluatePriorityMove(move, attacker, defender, battleState)
}
if (move.id.lowercase() in MoveData.selfRecoveryMoves) {
value += evaluateRecoveryMove(attacker, battleState, gamePlan)
}
if (move.id.lowercase() in MoveData.pivotMoves) {
value += evaluatePivotMove(move, attacker, defender, battleState)
}
if (battleState.gamePhase == GamePhase.ENDGAME) {
value += evaluateEndgameMove(move, battleState)
}
return value
}
private fun calculateGamePlanAlignment(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
val moveData = Moves.getByName(move.id) ?: return 0.0
var alignment = 0.0
val normalizedWeather = AiBattleUtils.normalizeWeatherId(battleState.weather)
if (move.id.lowercase() in gamePlan.riskyMoves) {
alignment -= 100.0
}
alignment += when (gamePlan.winCondition) {
WinCondition.RAIN_OFFENSE -> {
var score = 0.0
if (move.id.lowercase() == "raindance") score += 200.0
if (normalizedWeather == "raindance") {
if (moveData.elementalType.name.lowercase() == "water") score += 50.0
if (move.id.lowercase() in listOf("thunder", "hurricane")) score += 100.0
}
score
}
WinCondition.SUN_OFFENSE -> {
var score = 0.0
if (move.id.lowercase() == "sunnyday") score += 200.0
if (normalizedWeather == "sunnyday") {
if (moveData.elementalType.name.lowercase() == "fire") score += 50.0
if (move.id.lowercase() == "solarbeam") score += 100.0
}
score
}
WinCondition.SETUP_SWEEP -> {
var score = 0.0
if (move.id.lowercase() in MoveData.boostFromMoves.keys &&
attacker.species in gamePlan.keyPokemon
) {
score += 150.0
}
if (attacker.boosts.values.any { it >= 2 } && moveData.power > 80) {
score += 100.0
}
score
}
WinCondition.AGGRESSIVE_SWEEP -> {
var score = 0.0
if (moveData.power >= 90) score += 60.0
if (moveData.priority > 0 && defender.currentHpPercent < 0.5) score += 80.0
score
}
WinCondition.DEFENSIVE_STALL -> {
var score = 0.0
if (MoveData.statusMoves.keys.any { it?.name?.lowercase() == move.id.lowercase() }) score += 70.0
if (move.id.lowercase() in MoveData.selfRecoveryMoves) score += 60.0
if (moveData.power > 80) score -= 30.0
score
}
WinCondition.HAZARD_CONTROL -> {
var score = 0.0
if (move.id.lowercase() == "stealthrock" && "stealthrock" !in battleState.playerHazards) {
score += 250.0
}
if (move.id.lowercase() in MoveData.entryHazards) score += 100.0
score
}
WinCondition.REVENGE_KILLING -> {
var score = 0.0
if (moveData.priority > 0) score += 120.0
if (defender.currentHp > 50 && moveData.power >= 40 && moveData.power <= 80) score += 40.0
score
}
else -> 0.0
}
if (moveData.power > 0) {
alignment += moveData.power * gamePlan.aggressionLevel * 0.3
} else {
alignment += (1.0 - gamePlan.aggressionLevel) * 30.0
}
return alignment
}
private fun calculateRiskFactor(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
val moveData = Moves.getByName(move.id) ?: return 1.0
var risk = 1.0
val accuracy = getEffectiveAccuracy(move, attacker, defender, battleState.weather)
risk += (1.0 - accuracy) * 0.5
if (move.id.lowercase() in MoveData.recoilMoves) {
risk += if (attacker.currentHpPercent < 0.5) 0.5 else 0.2
}
if (move.id.lowercase() in listOf("explosion", "selfdestruct", "memento", "healingwish")) {
if (attacker.species in gamePlan.keyPokemon) {
risk += 2.0
} else if (attacker.currentHpPercent < 0.3) {
risk += 0.2
} else {
risk += 0.8
}
}
if (move.pp <= 1) {
risk += 0.3
}
val mySpeed = attacker.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
val oppSpeed = defender.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
if (abs(mySpeed - oppSpeed) < 5) {
risk += 0.1
}
val teamAdvantage = calculateTeamAdvantage(battleState)
if (teamAdvantage > 0) {
risk *= 0.8
} else if (teamAdvantage < -1) {
risk *= 1.2
}
return max(0.1, risk)
}
private fun calculateTurnAdvantage(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState
): Double {
val moveData = Moves.getByName(move.id) ?: return 0.0
var advantage = 0.0
val mySpeed = attacker.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
val oppSpeed = defender.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
if (mySpeed > oppSpeed) advantage += 15.0
if (moveData.priority > 0) advantage += moveData.priority * 25.0
if (battleState.gamePhase == GamePhase.ENDGAME && moveData.power > 90) {
advantage += 20.0
}
return advantage
}
private fun evaluateWeatherSetting(move: InBattleMove, gamePlan: GamePlan, battleState: BattleState): Double {
val moveName = move.id.lowercase()
val weatherType = MoveData.weatherMoves[moveName] ?: return 0.0
return when (gamePlan.winCondition) {
WinCondition.RAIN_OFFENSE -> if (weatherType == "rain") 300.0 else 0.0
WinCondition.SUN_OFFENSE -> if (weatherType == "sun") 300.0 else 0.0
WinCondition.SAND_OFFENSE -> if (weatherType == "sand") 300.0 else 0.0
WinCondition.WEATHER_CONTROL -> 150.0
else -> 30.0
}
}
private fun evaluateSetupMove(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
val boosts = MoveData.boostFromMoves[move.id.lowercase()] ?: return 0.0
var value = 0.0
val totalBoosts = boosts.values.sum()
value += totalBoosts * 40.0 * gamePlan.setupPriority
val relevantBoosts = boosts.keys.mapNotNull { stat -> attacker.boosts[stat] }
if (relevantBoosts.any { it >= 3 }) value *= 0.2
val predictedMoves = opponentPredictor.predictMoveHeuristically(defender, attacker)
val dangerLevel = predictedMoves.count { (moveName, _) ->
(Moves.getByName(moveName)?.power ?: 0.0) > 80
}
if (dangerLevel > 0 && attacker.currentHpPercent < 0.7) {
value *= 0.4
}
return value
}
private fun evaluateStatusMove(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
if (defender.pokemon?.status != null) return 0.0
return when (move.id.lowercase()) {
"thunderwave", "stunspore", "glare" -> {
if (defender.predictedRole == PokemonRole.SWEEPER) 90.0 else 50.0
}
"willowisp" -> {
if (defender.predictedRole in listOf(PokemonRole.WALLBREAKER, PokemonRole.SWEEPER)) {
80.0
} else 40.0
}
"toxic" -> {
if (gamePlan.winCondition == WinCondition.DEFENSIVE_STALL) 100.0 else 60.0
}
"spore", "sleeppowder" -> 120.0
else -> 30.0
}
}
private fun evaluateHazardSetting(battleState: BattleState, gamePlan: GamePlan): Double {
val remainingOpponents = battleState.playerTeam.count { it.currentHealth > 0 }
if (remainingOpponents < 2) return 15.0
var value = remainingOpponents * 20.0
if (gamePlan.winCondition == WinCondition.HAZARD_CONTROL) value *= 1.8
return value
}
private fun evaluateHazardRemoval(battleState: BattleState, gamePlan: GamePlan): Double {
val myHazards = battleState.npcHazards.size
if (myHazards == 0) return 5.0
var value = myHazards * 40.0
val remainingTeam = battleState.npcTeam.count { it.currentHealth > 0 }
if (remainingTeam >= 3) value *= 1.3
return value
}
private fun evaluatePriorityMove(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState
): Double {
val moveData = Moves.getByName(move.id) ?: return 0.0
var value = moveData.priority * 20.0
if (defender.currentHpPercent < 0.5) {
val damage = damageCalculator.calculateDamage(move, attacker, defender, battleState.weather)
if (damage >= defender.currentHp) value += 150.0
}
val mySpeed = attacker.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
val oppSpeed = defender.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
if (abs(mySpeed - oppSpeed) < 10) value += 50.0
return value
}
private fun evaluateRecoveryMove(
attacker: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
if (attacker.currentHpPercent > 0.8) return 5.0
var value = (1.0 - attacker.currentHpPercent) * 140.0
if (gamePlan.winCondition == WinCondition.DEFENSIVE_STALL) value *= 1.5
return value
}
private fun evaluatePivotMove(
move: InBattleMove,
attacker: TrackedPokemon,
defender: TrackedPokemon,
battleState: BattleState
): Double {
var value = 40.0
val damage = damageCalculator.calculateDamage(move, attacker, defender, battleState.weather)
value += damage * 0.3
return value
}
private fun evaluateEndgameMove(move: InBattleMove, battleState: BattleState): Double {
val moveData = Moves.getByName(move.id) ?: return 0.0
var bonus = 0.0
if (moveData.power > 0 && moveData.accuracy >= 90) {
bonus += 35.0
}
if (moveData.accuracy < 80) {
bonus -= 30.0
}
if (moveData.priority > 0) {
bonus += 50.0
}
return bonus
}
private fun calculateTeamAdvantage(battleState: BattleState): Int {
val npcAlive = battleState.npcTeam.count { it.currentHealth > 0 }
val playerAlive = battleState.playerTeam.count { it.currentHealth > 0 }
return npcAlive - playerAlive
}
private fun getEffectiveStat(pokemon: Pokemon, stat: Stats): Double {
val base = pokemon.species.baseStats[stat] ?: 0
val iv = pokemon.ivs[stat] ?: 31
val ev = pokemon.evs[stat] ?: 0
val level = pokemon.level
return if (stat == Stats.HP) {
((2.0 * base + iv + ev / 4.0) * level / 100.0 + level + 10.0)
} else {
((2.0 * base + iv + ev / 4.0) * level / 100.0 + 5.0)
}
}
fun evaluateMoveForDoubles(
move: InBattleMove,
attacker: TrackedPokemon,
primaryTarget: TrackedPokemon,
allOpponents: List<TrackedPokemon>,
partner: TrackedPokemon?,
battleState: BattleState,
gamePlan: GamePlan
): MoveEvaluation {
val moveData = Moves.getByName(move.id)
?: return MoveEvaluation(move, 0.0, 0.0, 0.0, 1.0, 0, 0.0, 0.0)
if (isSpreadMove(moveData.target)) {
return evaluateSpreadMove(move, attacker, allOpponents, partner, battleState, gamePlan)
}
val baseEvaluation = evaluateMove(move, attacker, primaryTarget, battleState, gamePlan)
var targetingBonus = 0.0
if (primaryTarget.threatLevel > 5) {
targetingBonus += 20.0
}
return MoveEvaluation(
move = baseEvaluation.move,
rawDamage = baseEvaluation.rawDamage,
expectedDamage = baseEvaluation.expectedDamage,
strategicValue = baseEvaluation.strategicValue + targetingBonus,
riskFactor = baseEvaluation.riskFactor,
priority = baseEvaluation.priority,
turnAdvantage = baseEvaluation.turnAdvantage,
gamePlanAlignment = baseEvaluation.gamePlanAlignment
)
}
private fun evaluateSpreadMove(
move: InBattleMove,
attacker: TrackedPokemon,
allOpponents: List<TrackedPokemon>,
partner: TrackedPokemon?,
battleState: BattleState,
gamePlan: GamePlan
): MoveEvaluation {
val moveData = Moves.getByName(move.id)
?: return MoveEvaluation(move, 0.0, 0.0, 0.0, 1.0, 0, 0.0, 0.0)
var totalDamage = 0.0
var totalStrategicValue = 0.0
val activeOpponents = allOpponents.filter { it.currentHp > 0 }
val targetCount = activeOpponents.size + (if (hitsPartner(moveData.target) && partner != null) 1 else 0)
for (opponent in activeOpponents) {
val damage = damageCalculator.calculateDamage(
move, attacker, opponent, battleState.weather,
isSpreadMove = true,
targetCount = targetCount
)
totalDamage += damage
if (damage >= opponent.currentHp) {
totalStrategicValue += 150.0
}
}
val multiTargetBonus = activeOpponents.size * 40.0
totalStrategicValue += multiTargetBonus
var partnerPenalty = 0.0
if (hitsPartner(moveData.target) && partner != null && partner.currentHp > 0) {
val partnerDamage = damageCalculator.calculateDamage(
move, attacker, partner, battleState.weather,
isSpreadMove = true,
targetCount = targetCount
)
partnerPenalty = when {
partnerDamage >= partner.currentHp -> -999999.0
partner.species in gamePlan.keyPokemon -> -partnerDamage * 5.0
partner.currentHpPercent < 0.3 -> -partnerDamage * 4.0
else -> -partnerDamage * 2.0
}
totalStrategicValue += partnerPenalty
}
val accuracy =
getEffectiveAccuracy(move, attacker, activeOpponents.firstOrNull() ?: TrackedPokemon(), battleState.weather)
val expectedDamage = totalDamage * accuracy
val riskFactor = if (partnerPenalty < -1000) {
999.0
} else if (partnerPenalty < 0) {
max(1.5, 1.0 + abs(partnerPenalty) / 100.0)
} else {
1.0
}
val gamePlanAlignment = if (activeOpponents.isNotEmpty()) {
calculateGamePlanAlignment(move, attacker, activeOpponents.first(), battleState, gamePlan)
} else {
0.0
}
return MoveEvaluation(
move = move,
rawDamage = totalDamage,
expectedDamage = expectedDamage,
strategicValue = totalStrategicValue,
riskFactor = riskFactor,
priority = moveData.priority,
turnAdvantage = 0.0,
gamePlanAlignment = gamePlanAlignment
)
}
private fun isSpreadMove(target: MoveTarget): Boolean {
return target in listOf(
MoveTarget.allAdjacent,
MoveTarget.allAdjacentFoes,
MoveTarget.all
)
}
private fun hitsPartner(target: MoveTarget): Boolean {
return target == MoveTarget.allAdjacent
}
}
@@ -0,0 +1,157 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.pokemon.Pokemon
import kotlin.math.min
class OpponentPredictor {
private val moveHistory = mutableMapOf<String, MutableList<String>>()
private val switchHistory = mutableMapOf<String, MutableList<String>>()
private val situationResponses = mutableMapOf<String, MutableMap<String, Int>>()
private val aggressionLevel = mutableMapOf<String, Double>()
private val setupTendency = mutableMapOf<String, Double>()
private val switchFrequency = mutableMapOf<String, Double>()
fun updateMoveHistory(pokemon: String, move: String, situation: String) {
moveHistory.getOrPut(pokemon) { mutableListOf() }.add(move)
situationResponses.getOrPut(situation) { mutableMapOf() }.merge(move, 1, Int::plus)
val moveData = Moves.getByName(move)
if (moveData != null) {
val currentAggression = aggressionLevel[pokemon] ?: 0.5
val newAggression = if (moveData.power > 0) {
currentAggression * 0.9 + 0.8 * 0.1
} else {
currentAggression * 0.9 + 0.2 * 0.1
}
aggressionLevel[pokemon] = newAggression
if (move in MoveData.boostFromMoves.keys) {
val currentSetup = setupTendency[pokemon] ?: 0.3
setupTendency[pokemon] = min(1.0, currentSetup + 0.15)
}
}
}
fun updateSwitchPattern(fromPokemon: String, toPokemon: String, reason: String) {
switchHistory.getOrPut(fromPokemon) { mutableListOf() }.add(toPokemon)
situationResponses.getOrPut("switch_$reason") { mutableMapOf() }.merge(toPokemon, 1, Int::plus)
val totalActions = (moveHistory[fromPokemon]?.size ?: 0) + (switchHistory[fromPokemon]?.size ?: 0)
if (totalActions > 0) {
switchFrequency[fromPokemon] = (switchHistory[fromPokemon]?.size ?: 0).toDouble() / totalActions
}
}
fun predictNextMove(pokemon: String, situation: String): Map<String, Double> {
val responses = situationResponses[situation] ?: return emptyMap()
val total = responses.values.sum().toDouble()
return if (total > 0) {
responses.mapValues { it.value / total }
} else {
emptyMap()
}
}
fun getSwitchProbability(pokemon: String, situation: String): Double {
val switches = switchHistory[pokemon]?.size ?: 0
val totalActions = (moveHistory[pokemon]?.size ?: 0) + switches
if (totalActions < 5) return 0.2
return min(0.8, switches.toDouble() / totalActions + 0.1)
}
private fun getAggressionLevel(pokemon: String): Double = aggressionLevel[pokemon] ?: 0.5
private fun getSetupTendency(pokemon: String): Double = setupTendency[pokemon] ?: 0.3
private fun getSwitchFrequency(pokemon: String): Double = switchFrequency[pokemon] ?: 0.2
fun predictMoveHeuristically(
opponent: TrackedPokemon,
myPokemon: TrackedPokemon
): Map<String, Double> {
val predictions = mutableMapOf<String, Double>()
val aggression = getAggressionLevel(opponent.species ?: "unknown")
for (moveName in opponent.revealedMoves) {
val moveData = Moves.getByName(moveName) ?: continue
var probability = 0.25
probability *= if (moveData.power > 0) {
aggression + 0.3
} else {
(1.0 - aggression) + 0.3
}
val myTypes = myPokemon.currentTypes ?: listOf()
val effectiveness = TypeEffectiveness.calculateTypeEffectiveness(moveData.elementalType, myTypes)
probability *= (0.5 + effectiveness)
predictions[moveName] = probability
}
val total = predictions.values.sum()
return if (total > 0) {
predictions.mapValues { it.value / total }
} else {
emptyMap()
}
}
fun predictSwitchTarget(
fromPokemon: String,
myActivePokemon: TrackedPokemon
): Map<String, Double> {
val pastSwitches = switchHistory[fromPokemon] ?: return emptyMap()
val switchCounts = mutableMapOf<String, Int>()
for (target in pastSwitches) {
switchCounts.merge(target, 1, Int::plus)
}
val total = switchCounts.values.sum().toDouble()
return if (total > 0) {
switchCounts.mapValues { it.value / total }
} else {
emptyMap()
}
}
fun categorizePlaystyle(pokemon: String): OpponentPlaystyle {
val aggression = getAggressionLevel(pokemon)
val setupTend = getSetupTendency(pokemon)
val switchFreq = getSwitchFrequency(pokemon)
return when {
setupTend > 0.5 -> OpponentPlaystyle.SETUP_ORIENTED
aggression > 0.7 && switchFreq < 0.3 -> OpponentPlaystyle.HYPER_OFFENSIVE
switchFreq > 0.5 -> OpponentPlaystyle.MOMENTUM_BASED
aggression < 0.4 -> OpponentPlaystyle.DEFENSIVE_STALL
else -> OpponentPlaystyle.BALANCED
}
}
fun reset() {
moveHistory.clear()
switchHistory.clear()
situationResponses.clear()
aggressionLevel.clear()
setupTendency.clear()
switchFrequency.clear()
}
}
enum class OpponentPlaystyle {
HYPER_OFFENSIVE,
SETUP_ORIENTED,
MOMENTUM_BASED,
DEFENSIVE_STALL,
BALANCED
}
@@ -0,0 +1,340 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.pokemon.Pokemon
import kotlin.math.max
class StrategyAnalyzer {
fun analyzeTeamComposition(team: List<Pokemon>): GamePlan {
val roles = team.map { identifyPokemonRole(it) }
val sweepers = roles.count { it == PokemonRole.SWEEPER }
val setupSweepers = roles.count { it == PokemonRole.SETUP_SWEEPER }
val wallbreakers = roles.count { it == PokemonRole.WALLBREAKER }
val supports = roles.count { it in listOf(PokemonRole.SUPPORT, PokemonRole.TANK) }
val revengeKillers = roles.count { it == PokemonRole.REVENGE_KILLER }
val weatherStrategy = identifyWeatherStrategy(team)
if (weatherStrategy != null) {
return createWeatherGamePlan(weatherStrategy, team)
}
val hazardStrategy = identifyHazardStrategy(team)
if (hazardStrategy != null) {
return createHazardGamePlan(team)
}
val winCondition = when {
setupSweepers >= 2 && supports >= 2 -> WinCondition.SETUP_SWEEP
sweepers + wallbreakers >= 4 -> WinCondition.AGGRESSIVE_SWEEP
supports >= 3 -> WinCondition.DEFENSIVE_STALL
revengeKillers >= 2 -> WinCondition.REVENGE_KILLING
else -> WinCondition.BALANCED
}
return createGamePlan(winCondition, team, roles)
}
private fun identifyWeatherStrategy(team: List<Pokemon>): WinCondition? {
val rainSetters = team.count { hasWeatherSettingMove(it, "rain") || hasWeatherSettingAbility(it, "rain") }
val rainAbusers = team.count { hasWeatherAbility(it, "rain") || hasWeatherMoves(it, "rain") }
val sunSetters = team.count { hasWeatherSettingMove(it, "sun") || hasWeatherSettingAbility(it, "sun") }
val sunAbusers = team.count { hasWeatherAbility(it, "sun") || hasWeatherMoves(it, "sun") }
val sandSetters = team.count { hasWeatherSettingMove(it, "sand") || hasWeatherSettingAbility(it, "sand") }
val sandAbusers = team.count { hasWeatherAbility(it, "sand") }
return when {
rainSetters >= 1 && rainAbusers >= 2 -> WinCondition.RAIN_OFFENSE
sunSetters >= 1 && sunAbusers >= 2 -> WinCondition.SUN_OFFENSE
sandSetters >= 1 && sandAbusers >= 2 -> WinCondition.SAND_OFFENSE
rainSetters >= 1 || sunSetters >= 1 || sandSetters >= 1 -> WinCondition.WEATHER_CONTROL
else -> null
}
}
private fun identifyHazardStrategy(team: List<Pokemon>): Boolean {
val hazardSetters = team.count { pokemon ->
pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.entryHazards }
}
val hazardRemovers = team.count { pokemon ->
pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.antiHazardsMoves }
}
return hazardSetters >= 2 && hazardRemovers >= 1
}
private fun createWeatherGamePlan(winCondition: WinCondition, team: List<Pokemon>): GamePlan {
val weatherType = when (winCondition) {
WinCondition.RAIN_OFFENSE -> "rain"
WinCondition.SUN_OFFENSE -> "sun"
WinCondition.SAND_OFFENSE -> "sand"
else -> "any"
}
val setters =
team.filter { hasWeatherSettingMove(it, weatherType) || hasWeatherSettingAbility(it, weatherType) }
val abusers = team.filter { hasWeatherAbility(it, weatherType) || hasWeatherMoves(it, weatherType) }
val keyPokemon = (setters + abusers).distinct().map { it.species.name }
val priorities = when (winCondition) {
WinCondition.RAIN_OFFENSE -> listOf(
"set_rain_immediately",
"preserve_rain_setter",
"sweep_with_rain_abusers",
"use_perfect_accuracy_moves",
"prevent_weather_changes"
)
WinCondition.SUN_OFFENSE -> listOf(
"set_sun_immediately",
"preserve_sun_setter",
"sweep_with_chlorophyll",
"use_solar_beam",
"prevent_weather_changes"
)
WinCondition.SAND_OFFENSE -> listOf(
"set_sandstorm",
"abuse_sand_rush",
"chip_damage_from_sand",
"prevent_weather_changes"
)
else -> listOf("maintain_weather", "use_weather_abusers")
}
val riskyMoves = listOf(
"explosion",
"selfdestruct",
"memento"
)
return GamePlan(
winCondition = winCondition,
keyPokemon = keyPokemon,
priorities = priorities,
riskyMoves = riskyMoves,
aggressionLevel = 0.8,
setupPriority = 0.9
)
}
private fun createHazardGamePlan(team: List<Pokemon>): GamePlan {
val hazardSetters = team.filter { pokemon ->
pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.entryHazards }
}.map { it.species.name }
return GamePlan(
winCondition = WinCondition.HAZARD_CONTROL,
keyPokemon = hazardSetters,
priorities = listOf(
"set_stealth_rock_early",
"stack_hazards_if_safe",
"remove_opponent_hazards",
"force_switches",
"preserve_hazard_setters"
),
riskyMoves = listOf("explosion"),
aggressionLevel = 0.6,
setupPriority = 0.7
)
}
private fun createGamePlan(winCondition: WinCondition, team: List<Pokemon>, roles: List<PokemonRole>): GamePlan {
val keyPokemon = identifyKeyPokemon(winCondition, team, roles)
val priorities = when (winCondition) {
WinCondition.SETUP_SWEEP -> listOf(
"create_setup_opportunity",
"execute_setup_moves",
"sweep_after_setup",
"preserve_setup_sweeper",
"remove_priority_threats"
)
WinCondition.AGGRESSIVE_SWEEP -> listOf(
"maintain_offensive_pressure",
"use_coverage_moves",
"preserve_sweepers",
"use_priority_to_finish",
"momentum_over_setup"
)
WinCondition.DEFENSIVE_STALL -> listOf(
"spread_status_conditions",
"use_recovery_moves",
"set_up_hazards",
"stall_out_threats",
"remove_opponent_hazards"
)
WinCondition.REVENGE_KILLING -> listOf(
"preserve_revenge_killer",
"use_priority_strategically",
"chip_into_priority_range",
"scout_with_protect",
"maintain_momentum"
)
WinCondition.HAZARD_CONTROL -> listOf(
"set_stealth_rock_early",
"stack_hazards_safely",
"remove_opponent_hazards",
"force_switches"
)
else -> listOf(
"maintain_balance",
"adapt_to_opponent",
"resource_management",
"take_calculated_risks"
)
}
val riskyMoves = when (winCondition) {
WinCondition.SETUP_SWEEP -> listOf("explosion", "memento", "healingwish", "lunardance")
WinCondition.REVENGE_KILLING -> listOf("doubleedge", "bravebird", "flareblitz")
WinCondition.DEFENSIVE_STALL -> MoveData.recoilMoves
else -> listOf("explosion", "memento")
}
val aggressionLevel = when (winCondition) {
WinCondition.AGGRESSIVE_SWEEP -> 0.9
WinCondition.SETUP_SWEEP -> 0.7
WinCondition.REVENGE_KILLING -> 0.75
WinCondition.DEFENSIVE_STALL -> 0.3
else -> 0.6
}
val setupPriority = when (winCondition) {
WinCondition.SETUP_SWEEP -> 0.95
WinCondition.DEFENSIVE_STALL -> 0.6
else -> 0.5
}
return GamePlan(
winCondition = winCondition,
keyPokemon = keyPokemon,
priorities = priorities,
riskyMoves = riskyMoves,
aggressionLevel = aggressionLevel,
setupPriority = setupPriority
)
}
private fun identifyKeyPokemon(
winCondition: WinCondition,
team: List<Pokemon>,
roles: List<PokemonRole>
): List<String> {
return when (winCondition) {
WinCondition.SETUP_SWEEP -> {
team.filterIndexed { index, _ -> roles[index] == PokemonRole.SETUP_SWEEPER }
.map { it.species.name }
}
WinCondition.AGGRESSIVE_SWEEP -> {
team.filterIndexed { index, _ -> roles[index] in listOf(PokemonRole.SWEEPER, PokemonRole.WALLBREAKER) }
.map { it.species.name }
}
WinCondition.REVENGE_KILLING -> {
team.filterIndexed { index, _ -> roles[index] == PokemonRole.REVENGE_KILLER }
.map { it.species.name }
}
WinCondition.DEFENSIVE_STALL -> {
team.filterIndexed { index, _ -> roles[index] in listOf(PokemonRole.TANK, PokemonRole.SUPPORT) }
.map { it.species.name }
}
else -> team.take(2).map { it.species.name }
}
}
fun identifyPokemonRole(pokemon: Pokemon): PokemonRole {
val attack = pokemon.species.baseStats[Stats.ATTACK] ?: 0
val spAttack = pokemon.species.baseStats[Stats.SPECIAL_ATTACK] ?: 0
val defense = pokemon.species.baseStats[Stats.DEFENCE] ?: 0
val spDefense = pokemon.species.baseStats[Stats.SPECIAL_DEFENCE] ?: 0
val speed = pokemon.species.baseStats[Stats.SPEED] ?: 0
val hp = pokemon.species.baseStats[Stats.HP] ?: 0
val maxAttack = max(attack, spAttack)
val avgDefense = (defense + spDefense) / 2.0
val bulk = hp + avgDefense
val hasSetupMoves = pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.boostFromMoves.keys }
val hasPriorityMoves = pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.priorityMoves }
val hasPivotMoves = pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.pivotMoves }
val hasRecoveryMoves = pokemon.moveSet.getMoves().any { it.name.lowercase() in MoveData.selfRecoveryMoves }
return when {
hasPivotMoves -> PokemonRole.PIVOT
hasSetupMoves && speed >= 90 && maxAttack >= 90 -> PokemonRole.SETUP_SWEEPER
speed >= 110 && hasPriorityMoves -> PokemonRole.REVENGE_KILLER
speed >= 100 && maxAttack >= 100 -> PokemonRole.SWEEPER
maxAttack >= 120 -> PokemonRole.WALLBREAKER
bulk >= 180 && maxAttack < 80 -> PokemonRole.TANK
hasRecoveryMoves || hasSetupMoves -> PokemonRole.SUPPORT
else -> PokemonRole.BALANCED
}
}
private fun hasWeatherSettingMove(pokemon: Pokemon, weather: String): Boolean {
val moves = pokemon.moveSet.getMoves().map { it.name.lowercase() }
return when (weather) {
"rain" -> "raindance" in moves
"sun" -> "sunnyday" in moves
"sand" -> "sandstorm" in moves
else -> moves.any { it in MoveData.weatherMoves.keys }
}
}
private fun hasWeatherSettingAbility(pokemon: Pokemon, weather: String): Boolean {
val ability = pokemon.ability.name.lowercase()
return when (weather) {
"rain" -> ability == "drizzle"
"sun" -> ability == "drought"
"sand" -> ability == "sandstream"
else -> false
}
}
private fun hasWeatherAbility(pokemon: Pokemon, weather: String): Boolean {
val ability = pokemon.ability.name.lowercase()
return when (weather) {
"rain" -> ability in listOf("swiftswim", "raindish", "dryskin")
"sun" -> ability in listOf("chlorophyll", "leafguard", "flowergift", "solarpower")
"sand" -> ability in listOf("sandrush", "sandveil", "sandforce")
else -> MoveData.weatherAbilities[ability] == weather
}
}
private fun hasWeatherMoves(pokemon: Pokemon, weather: String): Boolean {
val moves = pokemon.moveSet.getMoves().map { it.name.lowercase() }
return when (weather) {
"rain" -> moves.any { it in listOf("thunder", "hurricane") }
"sun" -> moves.any { it in listOf("solarbeam", "hydrosteam") }
else -> false
}
}
}
enum class WinCondition {
AGGRESSIVE_SWEEP,
SETUP_SWEEP,
DEFENSIVE_STALL,
HAZARD_CONTROL,
REVENGE_KILLING,
BALANCED,
RAIN_OFFENSE,
SUN_OFFENSE,
SAND_OFFENSE,
WEATHER_CONTROL
}
data class GamePlan(
val winCondition: WinCondition,
val keyPokemon: List<String>,
val priorities: List<String>,
val riskyMoves: List<String>,
val aggressionLevel: Double = 0.6,
val setupPriority: Double = 0.5
)
@@ -0,0 +1,324 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.pokemon.Pokemon
import kotlin.math.max
class SwitchDecisionMaker(
private val damageCalculator: DamageCalculator,
private val opponentPredictor: OpponentPredictor,
private val strategyAnalyzer: StrategyAnalyzer
) {
fun shouldVoluntarilySwitch(
myPokemon: TrackedPokemon,
opponentPokemon: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan,
availableSwitches: List<BattlePokemon>
): Boolean {
if (availableSwitches.isEmpty()) return false
val currentMatchup = evaluateMatchup(myPokemon, opponentPokemon, battleState)
val bestSwitchScore = availableSwitches.maxOfOrNull { switchOption ->
evaluateSwitchOption(
switchOption.effectedPokemon,
opponentPokemon,
battleState,
gamePlan
)
} ?: 0.0
val switchThreshold = 40.0
if (bestSwitchScore > currentMatchup.overallScore + switchThreshold) {
return true
}
val shouldSwitch = checkSwitchConditions(
myPokemon,
opponentPokemon,
battleState,
gamePlan,
currentMatchup
)
return shouldSwitch
}
fun findBestSwitch(
availableSwitches: List<BattlePokemon>,
opponentPokemon: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): BattlePokemon? {
if (availableSwitches.isEmpty()) return null
return availableSwitches.maxByOrNull { switchOption ->
evaluateSwitchOption(
switchOption.effectedPokemon,
opponentPokemon,
battleState,
gamePlan
)
}
}
fun evaluateSwitchOption(
candidate: Pokemon,
opponent: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan
): Double {
var score = 0.0
val candidateTracker = createTrackerPokemon(candidate)
val matchup = evaluateMatchup(candidateTracker, opponent, battleState)
score += matchup.overallScore
val hpFactor = candidateTracker.currentHpPercent
if (hpFactor < 0.5) {
score -= (1.0 - hpFactor) * 40.0
}
if (gamePlan.keyPokemon.contains(candidate.species.name)) {
score += 20.0
}
val mySpeed = getEffectiveStat(candidate, Stats.SPEED)
val oppSpeed = opponent.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
if (mySpeed > oppSpeed) score += 25.0
val hazardDamage = calculateEntryHazardDamage(candidate, battleState.npcHazards)
score -= hazardDamage * 0.5
val role = strategyAnalyzer.identifyPokemonRole(candidate)
score += evaluateRoleSynergy(role, gamePlan, battleState)
return score
}
private fun evaluateMatchup(
myPokemon: TrackedPokemon,
opponent: TrackedPokemon,
battleState: BattleState
): MatchupEvaluation {
val ourBestEffectiveness = calculateBestMoveEffectiveness(myPokemon, opponent)
val offensiveAdvantage = (ourBestEffectiveness - 1.0) * 40.0
val theirBestEffectiveness = calculateBestMoveEffectiveness(opponent, myPokemon)
val defensiveAdvantage = -(theirBestEffectiveness - 1.0) * 35.0
val mySpeed = myPokemon.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
val oppSpeed = opponent.pokemon?.let { getEffectiveStat(it, Stats.SPEED) } ?: 0.0
val speedAdvantage = if (mySpeed > oppSpeed) 20.0 else -15.0
val overallScore = offensiveAdvantage + defensiveAdvantage + speedAdvantage
val recommendation = when {
overallScore > 60 && myPokemon.currentHpPercent > 0.7 -> MatchupRecommendation.STAY_IN_AND_SETUP
overallScore > 30 -> MatchupRecommendation.STAY_IN_AND_ATTACK
overallScore > 0 -> MatchupRecommendation.CONSIDER_SWITCHING
overallScore > -30 -> MatchupRecommendation.PIVOT_OUT
else -> MatchupRecommendation.SWITCH_OUT
}
return MatchupEvaluation(
offensiveAdvantage = offensiveAdvantage,
defensiveAdvantage = defensiveAdvantage,
speedAdvantage = speedAdvantage,
overallScore = overallScore,
recommendation = recommendation
)
}
private fun checkSwitchConditions(
myPokemon: TrackedPokemon,
opponent: TrackedPokemon,
battleState: BattleState,
gamePlan: GamePlan,
matchup: MatchupEvaluation
): Boolean {
val theirBestEffectiveness = calculateBestMoveEffectiveness(opponent, myPokemon)
if (theirBestEffectiveness >= 2.0 && myPokemon.currentHpPercent > 0.3) {
return true
}
if (myPokemon.currentHpPercent < 0.3 && matchup.overallScore < 30) {
return true
}
if (opponent.boosts.values.any { it >= 2 } && matchup.overallScore < 20) {
return true
}
if (myPokemon.species in gamePlan.keyPokemon &&
myPokemon.currentHpPercent < 0.5 &&
theirBestEffectiveness > 1.0
) {
return true
}
return false
}
private fun calculateBestMoveEffectiveness(
attacker: TrackedPokemon,
defender: TrackedPokemon
): Double {
val moves = attacker.pokemon?.moveSet?.getMoves() ?: return 1.0
return moves.maxOfOrNull { move ->
TypeEffectiveness.calculateTypeEffectiveness(
move.type,
defender.currentTypes ?: listOf()
)
} ?: 1.0
}
private fun calculateEntryHazardDamage(pokemon: Pokemon, hazards: List<String>): Double {
var damage = 0.0
val maxHP = getEffectiveStat(pokemon, Stats.HP)
for (hazard in hazards) {
when (hazard.lowercase()) {
"stealthrock" -> {
val rockEffectiveness = pokemon.types.fold(1.0) { acc, type ->
acc * TypeEffectiveness.getDamageMultiplier(
com.cobblemon.mod.common.api.types.ElementalTypes.ROCK,
type
)
}
damage += maxHP * rockEffectiveness * 0.125
}
"spikes" -> damage += maxHP * 0.125
"toxicspikes" -> {
if (!pokemon.types.any { it.name.lowercase() in listOf("poison", "steel") }) {
damage += maxHP * 0.15
}
}
"stickyweb" -> damage += 10.0
}
}
return damage
}
private fun evaluateRoleSynergy(
role: PokemonRole,
gamePlan: GamePlan,
battleState: BattleState
): Double {
return when (gamePlan.winCondition) {
WinCondition.SETUP_SWEEP -> {
when (role) {
PokemonRole.SETUP_SWEEPER -> 50.0
PokemonRole.SUPPORT -> if (battleState.gamePhase == GamePhase.EARLY_GAME) 30.0 else 10.0
else -> 0.0
}
}
WinCondition.AGGRESSIVE_SWEEP -> {
when (role) {
PokemonRole.SWEEPER, PokemonRole.WALLBREAKER -> 40.0
PokemonRole.REVENGE_KILLER -> 30.0
else -> 0.0
}
}
WinCondition.DEFENSIVE_STALL -> {
when (role) {
PokemonRole.TANK, PokemonRole.SUPPORT -> 45.0
else -> -10.0
}
}
WinCondition.REVENGE_KILLING -> {
when (role) {
PokemonRole.REVENGE_KILLER -> 60.0
PokemonRole.PIVOT -> 25.0
else -> 0.0
}
}
WinCondition.RAIN_OFFENSE, WinCondition.SUN_OFFENSE, WinCondition.SAND_OFFENSE -> {
when (role) {
PokemonRole.SWEEPER -> 35.0
else -> 0.0
}
}
else -> 0.0
}
}
private fun createTrackerPokemon(pokemon: Pokemon): TrackedPokemon {
val tracker = TrackedPokemon(
pokemon = pokemon,
originalPokemon = pokemon,
species = pokemon.species.name,
currentHp = pokemon.currentHealth,
maxHp = pokemon.maxHealth,
currentHpPercent = pokemon.currentHealth.toDouble() / pokemon.maxHealth,
currentTypes = pokemon.types.map { it.name.lowercase() }.toMutableList(),
currentAbility = pokemon.ability.name,
moves = pokemon.moveSet.getMoves(),
predictedRole = strategyAnalyzer.identifyPokemonRole(pokemon),
cachedAttack = getEffectiveStat(pokemon, Stats.ATTACK),
cachedDefense = getEffectiveStat(pokemon, Stats.DEFENCE),
cachedSpAttack = getEffectiveStat(pokemon, Stats.SPECIAL_ATTACK),
cachedSpDefense = getEffectiveStat(pokemon, Stats.SPECIAL_DEFENCE),
cachedSpeed = getEffectiveStat(pokemon, Stats.SPEED)
)
return tracker
}
private fun getEffectiveStat(pokemon: Pokemon, stat: Stats): Double {
val base = pokemon.species.baseStats[stat] ?: 0
val iv = pokemon.ivs[stat] ?: 31
val ev = pokemon.evs[stat] ?: 0
val level = pokemon.level
return if (stat == Stats.HP) {
((2.0 * base + iv + ev / 4.0) * level / 100.0 + level + 10.0)
} else {
((2.0 * base + iv + ev / 4.0) * level / 100.0 + 5.0)
}
}
}
@@ -0,0 +1,403 @@
package com.cobblemon.mod.common.battles.ai.competitive
import com.cobblemon.mod.common.api.types.ElementalType
import com.cobblemon.mod.common.api.types.ElementalTypes
object TypeEffectiveness {
val typeEffectivenessMap: Map<ElementalType, Map<ElementalType, Double>> = mapOf(
ElementalTypes.NORMAL to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 0.5,
ElementalTypes.GHOST to 0.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.FIRE to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 0.5,
ElementalTypes.WATER to 0.5,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 2.0,
ElementalTypes.ICE to 2.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 2.0,
ElementalTypes.ROCK to 0.5,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 0.5,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 2.0,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.WATER to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 2.0,
ElementalTypes.WATER to 0.5,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 0.5,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 2.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 2.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 0.5,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 1.0,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.ELECTRIC to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 2.0,
ElementalTypes.ELECTRIC to 0.5,
ElementalTypes.GRASS to 0.5,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 0.0,
ElementalTypes.FLYING to 2.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 0.5,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 1.0,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.GRASS to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 0.5,
ElementalTypes.WATER to 2.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 0.5,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 0.5,
ElementalTypes.GROUND to 2.0,
ElementalTypes.FLYING to 0.5,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 0.5,
ElementalTypes.ROCK to 2.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 0.5,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.ICE to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 0.5,
ElementalTypes.WATER to 0.5,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 2.0,
ElementalTypes.ICE to 0.5,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 2.0,
ElementalTypes.FLYING to 2.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 2.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.FIGHTING to mapOf(
ElementalTypes.NORMAL to 2.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 2.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 0.5,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 0.5,
ElementalTypes.PSYCHIC to 0.5,
ElementalTypes.BUG to 0.5,
ElementalTypes.ROCK to 2.0,
ElementalTypes.GHOST to 0.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 2.0,
ElementalTypes.STEEL to 2.0,
ElementalTypes.FAIRY to 0.5
),
ElementalTypes.POISON to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 2.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 0.5,
ElementalTypes.GROUND to 0.5,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 0.5,
ElementalTypes.GHOST to 0.5,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.0,
ElementalTypes.FAIRY to 2.0
),
ElementalTypes.GROUND to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 2.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 2.0,
ElementalTypes.GRASS to 0.5,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 2.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 0.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 0.5,
ElementalTypes.ROCK to 2.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 2.0,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.FLYING to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 0.5,
ElementalTypes.GRASS to 2.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 2.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 2.0,
ElementalTypes.ROCK to 0.5,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.PSYCHIC to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 2.0,
ElementalTypes.POISON to 2.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 0.5,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 0.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.BUG to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 0.5,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 2.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 0.5,
ElementalTypes.POISON to 0.5,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 0.5,
ElementalTypes.PSYCHIC to 2.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 0.5,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 2.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 0.5
),
ElementalTypes.ROCK to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 2.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 2.0,
ElementalTypes.FIGHTING to 0.5,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 0.5,
ElementalTypes.FLYING to 2.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 2.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.GHOST to mapOf(
ElementalTypes.NORMAL to 0.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 2.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 2.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 0.5,
ElementalTypes.STEEL to 1.0,
ElementalTypes.FAIRY to 1.0
),
ElementalTypes.DRAGON to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 2.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 0.0
),
ElementalTypes.DARK to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 1.0,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 0.5,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 2.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 2.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 0.5,
ElementalTypes.STEEL to 1.0,
ElementalTypes.FAIRY to 0.5
),
ElementalTypes.STEEL to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 0.5,
ElementalTypes.WATER to 0.5,
ElementalTypes.ELECTRIC to 0.5,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 2.0,
ElementalTypes.FIGHTING to 1.0,
ElementalTypes.POISON to 1.0,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 2.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 1.0,
ElementalTypes.DARK to 1.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 2.0
),
ElementalTypes.FAIRY to mapOf(
ElementalTypes.NORMAL to 1.0,
ElementalTypes.FIRE to 0.5,
ElementalTypes.WATER to 1.0,
ElementalTypes.ELECTRIC to 1.0,
ElementalTypes.GRASS to 1.0,
ElementalTypes.ICE to 1.0,
ElementalTypes.FIGHTING to 2.0,
ElementalTypes.POISON to 0.5,
ElementalTypes.GROUND to 1.0,
ElementalTypes.FLYING to 1.0,
ElementalTypes.PSYCHIC to 1.0,
ElementalTypes.BUG to 1.0,
ElementalTypes.ROCK to 1.0,
ElementalTypes.GHOST to 1.0,
ElementalTypes.DRAGON to 2.0,
ElementalTypes.DARK to 2.0,
ElementalTypes.STEEL to 0.5,
ElementalTypes.FAIRY to 1.0
)
)
fun getDamageMultiplier(attackerType: ElementalType, defenderType: ElementalType): Double {
return typeEffectivenessMap[attackerType]?.get(defenderType) ?: 1.0
}
fun calculateTypeEffectiveness(moveType: ElementalType, defenderTypes: List<String>): Double {
var multiplier = 1.0
for (typeString in defenderTypes) {
val defenderType = ElementalTypes.get(typeString.lowercase())
if (defenderType != null) {
multiplier *= getDamageMultiplier(moveType, defenderType)
}
}
return multiplier
}
fun getEffectivenessCategory(effectiveness: Double): EffectivenessCategory {
return when {
effectiveness == 0.0 -> EffectivenessCategory.IMMUNE
effectiveness < 1.0 -> EffectivenessCategory.NOT_VERY_EFFECTIVE
effectiveness == 1.0 -> EffectivenessCategory.NEUTRAL
else -> EffectivenessCategory.SUPER_EFFECTIVE
}
}
enum class EffectivenessCategory {
IMMUNE, NOT_VERY_EFFECTIVE, NEUTRAL, SUPER_EFFECTIVE
}
}
@@ -31,7 +31,6 @@ class WeatherInstruction(val message: BattleMessage): InterpreterInstruction {
val weather = message.effectAt(0)?.id ?: return
val source = message.battlePokemonFromOptional(battle)
source?.let { ShowdownInterpreter.broadcastOptionalAbility(battle, message.effect(), source) }
battle.dispatchWaiting(1.5F) {
val lang = when {
message.hasOptionalArgument("upkeep") -> battleLang("weather.$weather.upkeep")
@@ -19,7 +19,6 @@ import net.minecraft.server.level.ServerLevel
import net.minecraft.tags.FluidTags
import net.minecraft.util.Mth
import net.minecraft.util.RandomSource
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.*
import net.minecraft.world.level.block.Block
@@ -175,7 +174,7 @@ class HeartyGrainsBlock(settings: Properties) : CropBlock(settings), SimpleWater
UPDATE_CLIENTS
)
}
else if (stateAbove.getValue(HALF) == DoubleBlockHalf.UPPER) {
else if (stateAbove.hasProperty(HALF) && stateAbove.getValue(HALF) == DoubleBlockHalf.UPPER) {
world.setBlock(
abovePos,
stateAbove.setValue(AGE, newAge),
@@ -34,6 +34,7 @@ import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.Explosion
import net.minecraft.world.level.Level
import net.minecraft.world.level.LevelAccessor
import net.minecraft.world.level.LevelReader
@@ -55,9 +56,10 @@ import net.minecraft.world.phys.shapes.VoxelShape
import java.util.*
import net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED
import net.minecraft.world.level.gameevent.GameEvent
import java.util.function.BiConsumer
@Suppress("OVERRIDE_DEPRECATION", "DEPRECATION")
class PastureBlock(settings: Properties): BaseEntityBlock(settings), SimpleWaterloggedBlock, PreEmptsExplosion {
class PastureBlock(settings: Properties): BaseEntityBlock(settings), SimpleWaterloggedBlock {
companion object {
val CODEC = simpleCodec(::PastureBlock)
@@ -213,9 +215,19 @@ class PastureBlock(settings: Properties): BaseEntityBlock(settings), SimpleWater
return super.playerWillDestroy(world, pos, state, player)
}
override fun whenExploded(world: Level, state: BlockState, pos: BlockPos) {
val blockEntity = world.getBlockEntity(pos) as? PokemonPastureBlockEntity ?: return
blockEntity.onBroken()
override fun onExplosionHit(
state: BlockState,
level: Level,
pos: BlockPos,
explosion: Explosion,
dropConsumer: BiConsumer<ItemStack?, BlockPos?>
) {
val interaction = explosion.blockInteraction
if (interaction == Explosion.BlockInteraction.DESTROY || interaction == Explosion.BlockInteraction.DESTROY_WITH_DECAY) {
val blockEntity = level.getBlockEntity(pos) as? PokemonPastureBlockEntity ?: return
blockEntity.onBroken()
}
super.onExplosionHit(state, level, pos, explosion, dropConsumer)
}
override fun <T : BlockEntity?> getTicker(world: Level, state: BlockState, type: BlockEntityType<T>): BlockEntityTicker<T>? {
@@ -1,17 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.block
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.core.BlockPos
import net.minecraft.world.level.Level
interface PreEmptsExplosion {
fun whenExploded(world: Level, state: BlockState, pos: BlockPos)
}
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.block
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.entity.MoLangScriptingEntity
import com.cobblemon.mod.common.util.isServerSide
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.core.dispenser.DispenseItemBehavior
@@ -263,23 +264,39 @@ class SaccharineLeafBlock(settings: Properties) : LeavesBlock(settings) {
val isGlassBottle = itemStack.`is`(Items.GLASS_BOTTLE)
val isHoneyBottle = itemStack.`is`(Items.HONEY_BOTTLE)
if (isGlassBottle && !isAtMinAge(state)) {
if (isGlassBottle && isAtMaxAge(state)) {
if (level.isClientSide) return ItemInteractionResult.SUCCESS
// Decrement stack if not in creative mode
itemStack.consume(1, player)
// Give player honey bottle for now
player.addItem(Items.HONEY_BOTTLE.defaultInstance)
// Give player a honey bottle
if (stack.isEmpty) {
// Replace the consumed empty bottle with a honey bottle
player.setItemInHand(hand, ItemStack(Items.HONEY_BOTTLE));
} else {
player.addItem(Items.HONEY_BOTTLE.defaultInstance)
}
level.playSound(null, pos, SoundEvents.BOTTLE_FILL, SoundSource.BLOCKS)
level.setBlock(pos, state.setValue(AGE, 0), 2)
level.setBlock(pos, state.setValue(AGE, 0), UPDATE_CLIENTS)
level.gameEvent(null, GameEvent.BLOCK_CHANGE, pos)
return ItemInteractionResult.SUCCESS
} else if (isHoneyBottle && !isAtMaxAge(state)) {
if (level.isClientSide) return ItemInteractionResult.SUCCESS
// Decrement stack if not in creative mode
itemStack.consume(1, player)
if (!player.isCreative) {
itemStack.consume(1, player)
if (stack.isEmpty) {
// Replace the consumed honey bottle with an empty bottle
player.setItemInHand(hand, ItemStack(Items.GLASS_BOTTLE));
} else {
player.addItem(Items.GLASS_BOTTLE.defaultInstance)
}
}
level.playSound(null, pos, SoundEvents.HONEY_BLOCK_PLACE, SoundSource.BLOCKS)
level.setBlock(pos, state.setValue(AGE, 2), 2)
level.setBlock(pos, state.setValue(AGE, 2), UPDATE_CLIENTS)
level.gameEvent(null, GameEvent.BLOCK_CHANGE, pos)
return ItemInteractionResult.SUCCESS
}
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.block.campfirepot
import com.cobblemon.mod.common.CobblemonMenuType
import com.cobblemon.mod.common.CobblemonRecipeBookTypes.COOKING_POT
import com.cobblemon.mod.common.CobblemonRecipeTypes
import com.cobblemon.mod.common.api.cooking.Seasonings
import com.cobblemon.mod.common.block.entity.CampfireBlockEntity
@@ -211,7 +212,7 @@ class CookingPotMenu : RecipeBookMenu<CraftingInput, CookingPotRecipeBase>, Cont
}
override fun getRecipeBookType(): RecipeBookType {
return RecipeBookType.valueOf("COOKING_POT")
return COOKING_POT
}
override fun shouldMoveToInventory(slotIndex: Int): Boolean {
@@ -111,13 +111,18 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
BlockEntitySoundTracker.stop(pos, campfireBlockEntity.ambientSound.location)
}
campfireBlockEntity.brothColor =
getColourMixFromSeasonings(campfireBlockEntity.getSeasonings())
?: BASE_BROTH_COLOR
val seasonings = campfireBlockEntity.getSeasonings()
if (seasonings.size != campfireBlockEntity.lastSeasoningStacks.size || seasonings.any { it !in campfireBlockEntity.lastSeasoningStacks }) {
campfireBlockEntity.lastSeasoningStacks = seasonings
campfireBlockEntity.bubbleColor =
getColourMixFromSeasonings(campfireBlockEntity.getSeasonings(), true)
?: BASE_BROTH_BUBBLE_COLOR
campfireBlockEntity.brothColor =
getColourMixFromSeasonings(seasonings)
?: BASE_BROTH_COLOR
campfireBlockEntity.bubbleColor =
getColourMixFromSeasonings(seasonings, true)
?: BASE_BROTH_BUBBLE_COLOR
}
if (campfireBlockEntity.particleCooldown > 0) {
campfireBlockEntity.particleCooldown--
@@ -241,6 +246,9 @@ class CampfireBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlock
var bubbleColor: Int = BASE_BROTH_BUBBLE_COLOR
var time: Int = 0
/** Used to know if we should be changing the colour of the broth or if we can go with the previous colour. */
var lastSeasoningStacks = listOf<ItemStack>()
var dataAccess: ContainerData = object : ContainerData {
override fun get(index: Int): Int {
return when (index) {
@@ -49,7 +49,6 @@ 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 java.util.*
open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
TintBlockEntity(CobblemonBlockEntities.POKE_SNACK, pos, state),
@@ -120,7 +119,7 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
level.dimension(),
)
block.eat(level, blockPos, blockState, action.spawnablePosition.cause.entity as? Player)
block.eat(level, blockPos, blockState, null)
}
}
@@ -186,7 +186,7 @@ object CobblemonClient {
) { it is PokemonEntity }
nearbyPokemon?.forEach { entity ->
if (entity is PokemonEntity && !entity.isSilent) {
if (entity is PokemonEntity && !entity.isSilent && !entity.passengers.contains(player)) {
if (player.isLookingAt(entity) && !player.isSpectator && entity.pokemon.shiny) entity.delegate.spawnShinyParticle(player)
entity.delegate.spawnAspectParticle()
}
@@ -38,7 +38,7 @@ import kotlin.math.max
class RideControlsOverlay : Gui(Minecraft.getInstance()) {
companion object {
private const val FADE_FRAMES = 20F
private const val CONTROLS_FADE_FRAMES = 20F
private const val BASELINE_FPS = 60F
private const val HALF_SCALE = 0.5F
@@ -63,9 +63,9 @@ class RideControlsOverlay : Gui(Minecraft.getInstance()) {
private val screenExemptions: List<Class<out Screen>> = listOf(ChatScreen::class.java)
private var maxDurationFrames = Cobblemon.config.displayControlSeconds * BASELINE_FPS
private var durationFrames = maxDurationFrames
private var fadeFrames = FADE_FRAMES
private var controlsMaxDurationFrames = Cobblemon.config.displayControlSeconds * BASELINE_FPS
private var controlsDurationFrames = controlsMaxDurationFrames
private var controlsFadeFrames = CONTROLS_FADE_FRAMES
private var lastMouseX = 0
private var lastMouseY = 0
@@ -88,123 +88,132 @@ class RideControlsOverlay : Gui(Minecraft.getInstance()) {
if (minecraft.options.hideGui || minecraft.debugOverlay.showDebugScreen()) return
val riddenEntity = minecraft.player?.vehicle
if (riddenEntity != null && riddenEntity is PokemonEntity) {
// Update max duration if user has changed config
val configDuration = Cobblemon.config.displayControlSeconds * BASELINE_FPS
if (maxDurationFrames != configDuration) {
maxDurationFrames = configDuration
resetOverlayState()
}
// Don't render if duration config set to 0 or less
if (maxDurationFrames <= 0F) return
val rideBehaviourSettings = riddenEntity.ridingController?.context?.settings ?: return
val ridingBehaviourState = riddenEntity.ridingController?.context?.state ?: return
val rideBehaviourKey = rideBehaviourSettings.key
// Update current ride behaviour if changed
// If composite, get active behaviour instead
if (rideBehaviourKey == CompositeBehaviour.KEY) {
val activeBehaviour = (ridingBehaviourState as CompositeState).activeBehaviour.get()
if (currentBehaviourKey != activeBehaviour) {
currentBehaviourKey = activeBehaviour
resetOverlayState()
minecraft.player?.let { player ->
val riddenEntity = player.vehicle
if (riddenEntity != null && riddenEntity is PokemonEntity) {
// Only show controls to driver
if (riddenEntity === player.controlledVehicle) {
renderControls(minecraft, context, tickCounter, riddenEntity)
}
} else if (currentBehaviourKey != rideBehaviourKey) {
currentBehaviourKey = rideBehaviourKey
resetOverlayState()
} else {
resetControlsOverlayState()
}
// Render elements if entity has ride behaviour and if display duration has not ended yet
if (rideBehaviourSettings != null && fadeFrames > 0) {
val tickDelta = tickCounter.realtimeDeltaTicks.takeIf { !minecraft.isPaused } ?: 0F
val updateInterval = (tickDelta / 20F) * BASELINE_FPS
durationFrames = max(durationFrames - updateInterval, 0F)
if (durationFrames == 0F && fadeFrames > 0) fadeFrames = max(fadeFrames - updateInterval, 0F)
var showVerticalMouse = true // Show moving mouse up and down with arrows
var showHorizontalMouse = true // Show moving mouse left and right with arrows
var showSneakKey = true
var showJumpKey = true
var showMovementKeys = true
var disableHorizontalMovementKeys = false // Disable left and right movement keys
// Configure what controls to show for each behaviour
when (currentBehaviourKey) {
BirdBehaviour.KEY -> {}
BoatBehaviour.KEY -> {
showVerticalMouse = false
showHorizontalMouse = false
showSneakKey = false
}
BurstBehaviour.KEY -> {}
DolphinBehaviour.KEY -> {
disableHorizontalMovementKeys = true
showSneakKey = false
showJumpKey = false
}
GliderBehaviour.KEY -> {}
HelicopterBehaviour.KEY -> {}
HorseBehaviour.KEY -> {
showVerticalMouse = false
disableHorizontalMovementKeys = true
showSneakKey = false
}
HoverBehaviour.KEY -> {
showVerticalMouse = false
}
JetBehaviour.KEY -> {
showSneakKey = false
showJumpKey = false
}
MinekartBehaviour.KEY -> {
showVerticalMouse = false
showHorizontalMouse = false
showSneakKey = false
}
RocketBehaviour.KEY -> {
showVerticalMouse = false
showHorizontalMouse = false
}
VehicleBehaviour.KEY -> {}
}
val centerX = minecraft.window.guiScaledWidth / 2
val centerY = minecraft.window.guiScaledHeight / 2
renderMouseControls(
context,
!showMovementKeys && !showJumpKey && !showSneakKey,
showHorizontalMouse,
showVerticalMouse,
centerX,
centerY / 2,
fadeFrames / FADE_FRAMES
)
renderKeyControls(
context,
!showHorizontalMouse && !showVerticalMouse,
showMovementKeys,
disableHorizontalMovementKeys,
showJumpKey,
showSneakKey,
minecraft.player?.input,
centerX,
centerY / 2,
fadeFrames / FADE_FRAMES
)
}
} else {
resetOverlayState()
}
}
private fun resetOverlayState() {
if (durationFrames != maxDurationFrames) durationFrames = maxDurationFrames
if (fadeFrames != FADE_FRAMES) fadeFrames = FADE_FRAMES
private fun renderControls(minecraft: Minecraft, context: GuiGraphics, tickCounter: DeltaTracker, riddenEntity: PokemonEntity) {
// Update max duration if user has changed config
val configDuration = Cobblemon.config.displayControlSeconds * BASELINE_FPS
if (controlsMaxDurationFrames != configDuration) {
controlsMaxDurationFrames = configDuration
resetControlsOverlayState()
}
// Don't render if duration config set to 0 or less
if (controlsMaxDurationFrames <= 0F) return
val rideBehaviourSettings = riddenEntity.ridingController?.context?.settings ?: return
val ridingBehaviourState = riddenEntity.ridingController?.context?.state ?: return
val rideBehaviourKey = rideBehaviourSettings.key
// Update current ride behaviour if changed
// If composite, get active behaviour instead
if (rideBehaviourKey == CompositeBehaviour.KEY) {
val activeBehaviour = (ridingBehaviourState as CompositeState).activeBehaviour.get()
if (currentBehaviourKey != activeBehaviour) {
currentBehaviourKey = activeBehaviour
resetControlsOverlayState()
}
} else if (currentBehaviourKey != rideBehaviourKey) {
currentBehaviourKey = rideBehaviourKey
resetControlsOverlayState()
}
// Render elements if entity has ride behaviour and if display duration has not ended yet
if (rideBehaviourSettings != null && controlsFadeFrames > 0) {
val tickDelta = tickCounter.realtimeDeltaTicks.takeIf { !minecraft.isPaused } ?: 0F
val updateInterval = (tickDelta / 20F) * BASELINE_FPS
controlsDurationFrames = max(controlsDurationFrames - updateInterval, 0F)
if (controlsDurationFrames == 0F && controlsFadeFrames > 0) controlsFadeFrames = max(controlsFadeFrames - updateInterval, 0F)
var showVerticalMouse = true // Show moving mouse up and down with arrows
var showHorizontalMouse = true // Show moving mouse left and right with arrows
var showSneakKey = true
var showJumpKey = true
var showMovementKeys = true
var disableHorizontalMovementKeys = false // Disable left and right movement keys
// Configure what controls to show for each behaviour
when (currentBehaviourKey) {
BirdBehaviour.KEY -> {}
BoatBehaviour.KEY -> {
showVerticalMouse = false
showHorizontalMouse = false
showSneakKey = false
}
BurstBehaviour.KEY -> {}
DolphinBehaviour.KEY -> {
disableHorizontalMovementKeys = true
showSneakKey = false
showJumpKey = false
}
GliderBehaviour.KEY -> {}
HelicopterBehaviour.KEY -> {}
HorseBehaviour.KEY -> {
showVerticalMouse = false
disableHorizontalMovementKeys = true
showSneakKey = false
}
HoverBehaviour.KEY -> {
showVerticalMouse = false
}
JetBehaviour.KEY -> {
showSneakKey = false
showJumpKey = false
}
MinekartBehaviour.KEY -> {
showVerticalMouse = false
showHorizontalMouse = false
showSneakKey = false
}
RocketBehaviour.KEY -> {
showVerticalMouse = false
showHorizontalMouse = false
}
VehicleBehaviour.KEY -> {}
}
val centerX = minecraft.window.guiScaledWidth / 2
val centerY = minecraft.window.guiScaledHeight / 2
renderMouseControls(
context,
!showMovementKeys && !showJumpKey && !showSneakKey,
showHorizontalMouse,
showVerticalMouse,
centerX,
centerY / 2,
controlsFadeFrames / CONTROLS_FADE_FRAMES
)
renderKeyControls(
context,
!showHorizontalMouse && !showVerticalMouse,
showMovementKeys,
disableHorizontalMovementKeys,
showJumpKey,
showSneakKey,
minecraft.player?.input,
centerX,
centerY / 2,
controlsFadeFrames / CONTROLS_FADE_FRAMES
)
}
}
private fun resetControlsOverlayState() {
if (controlsDurationFrames != controlsMaxDurationFrames) controlsDurationFrames = controlsMaxDurationFrames
if (controlsFadeFrames != CONTROLS_FADE_FRAMES) controlsFadeFrames = CONTROLS_FADE_FRAMES
if (mouseMovedUp != null) {
mouseMovedUp = null
lastMouseY = 0
@@ -413,7 +422,7 @@ class RideControlsOverlay : Gui(Minecraft.getInstance()) {
if (showJump || showSneak) {
val offsetY = if (showMovement) 0 else -14
var label: MutableComponent? = null
var label: MutableComponent?
if (showJump && showSneak) {
label = "key.sneak".asTranslated().append("/").append("key.jump".asTranslated())
@@ -11,7 +11,6 @@ package com.cobblemon.mod.common.client.gui.pc
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.gui.getPixelRGB
import com.cobblemon.mod.common.api.storage.StorePosition
import com.cobblemon.mod.common.api.storage.party.PartyPosition
import com.cobblemon.mod.common.api.storage.pc.PCPosition
@@ -284,20 +283,6 @@ class StorageWidget(
height = 189,
alpha = if (screenLoaded) 1F else ((pcGui.ticksElapsed).toFloat() / 10F).coerceIn(0F, 1F)
)
} else {
val rgb = getPixelRGB(x + (SCREEN_WIDTH / 2), y + (SCREEN_HEIGHT / 2))
blitk(
matrixStack = matrices,
texture = screenGlowResource,
x = x - 17,
y = y - 17,
width = 208,
height = 189,
red = rgb.first / 255F,
green = rgb.second / 255F,
blue = rgb.third / 255F,
alpha = if (screenLoaded) 1F else ((pcGui.ticksElapsed).toFloat() / 10F).coerceIn(0F, 1F)
)
}
blitk(
@@ -592,6 +592,10 @@ class PokemonInfoWidget(val pX: Int, val pY: Int, val updateForm: (PokedexForm)
}
private fun switchForm(nextIndex: Boolean) {
if (visibleForms.isEmpty() || currentEntry == null) {
return
}
selectedPoseIndex = 0
if (nextIndex) {
@@ -602,6 +606,8 @@ class PokemonInfoWidget(val pX: Int, val pY: Int, val updateForm: (PokedexForm)
else selectedFormIndex = visibleForms.lastIndex
}
selectedFormIndex = selectedFormIndex.coerceIn(0, visibleForms.lastIndex)
setupButtons(currentEntry!!, visibleForms[selectedFormIndex])
updateAspects()
}
@@ -38,6 +38,7 @@ object CobblemonKeyBinds {
val PARTY_OVERLAY_DOWN = this.queue(DownShiftPartyBinding)
val PARTY_OVERLAY_UP = this.queue(UpShiftPartyBinding)
val SEND_OUT_POKEMON = this.queue(PartySendBinding)
val RIDING_FREELOOK = this.queue(RidingFreelookBinding)
fun register(registrar: (KeyMapping) -> Unit) {
this.keyBinds.forEach(registrar::invoke)
@@ -143,18 +143,18 @@ object PartySendBinding : CobblemonBlockingKeyBinding(
}
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) {
return true
}
val vehicle = player.vehicle as? PokemonEntity ?: return false
if (player != vehicle.controllingPassenger) return true
val isAirRide = vehicle.ridingController?.context?.style == RidingStyle.AIR
val hasLandRide = vehicle.rideProp.behaviours?.get(RidingStyle.LAND) != null
return if (isAirRide && hasLandRide) {
false
} else {
vehicle.pokemon.uuid == selectedPartyPokemon?.uuid
if (!Cobblemon.config.enableInFlightDismounting && isAirRide && hasLandRide) {
return false
}
return vehicle.pokemon.uuid == selectedPartyPokemon?.uuid
}
private fun isRidingPokemon(player: LocalPlayer, ignoreControlling: Boolean = false): Boolean {
@@ -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.client.keybind.keybinds
import com.cobblemon.mod.common.client.keybind.CobblemonBlockingKeyBinding
import com.cobblemon.mod.common.client.keybind.KeybindCategories
import com.mojang.blaze3d.platform.InputConstants
object RidingFreelookBinding : CobblemonBlockingKeyBinding(
"key.cobblemon.ridingfreelook",
InputConstants.Type.KEYSYM,
InputConstants.KEY_LALT,
KeybindCategories.COBBLEMON_CATEGORY
) {
override fun onTick() {
}
override fun onRelease() {
}
override fun onPress() {
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.data
import com.cobblemon.mod.common.api.interaction.PokemonInteractions
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.net.messages.client.data.PokemonInteractionsSyncPacket
import net.minecraft.client.Minecraft
object PokemonInteractionsSyncHandler : ClientNetworkPacketHandler<PokemonInteractionsSyncPacket> {
override fun handle(
packet: PokemonInteractionsSyncPacket,
client: Minecraft,
) {
PokemonInteractions.speciesInteractions.clear()
PokemonInteractions.speciesInteractions.addAll(packet.speciesInteractions)
PokemonInteractions.generalInteractions.clear()
PokemonInteractions.generalInteractions.addAll(packet.generalInteractions)
}
}
@@ -8,6 +8,8 @@
package com.cobblemon.mod.common.client.persisted
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.config.CobblemonConfig
import java.io.BufferedReader
import java.io.BufferedWriter
@@ -31,7 +33,14 @@ object ClientPersistedData {
fun readSnapshotsData() : SnapshotAcknowledgementData? {
if (snapshotsPath.exists()) {
BufferedReader(InputStreamReader(FileInputStream(snapshotsPath.toFile()))).use { reader ->
return gson.fromJson(reader, SnapshotAcknowledgementData::class.java)
try {
return gson.fromJson(reader, SnapshotAcknowledgementData::class.java)
} catch (exception: Exception) {
LOGGER.error("Failed to load snapshots.json... Resetting File")
val data = SnapshotAcknowledgementData(Cobblemon.VERSION, false)
writeSnapshotsData(data)
return data
}
}
}
@@ -46,16 +55,23 @@ object ClientPersistedData {
fun readRidingPerspectivesData() : RidingPerspectiveData? {
if (ridingPerspectivesPath.exists()) {
BufferedReader(InputStreamReader(FileInputStream(ridingPerspectivesPath.toFile()))).use { reader ->
return gson.fromJson(reader, RidingPerspectiveData::class.java)
try {
return gson.fromJson(reader, RidingPerspectiveData::class.java)
} catch (exception: Exception) {
LOGGER.error("Failed to load riding_perspectives.json... Resetting File")
val data = RidingPerspectiveData()
saveRidingPerspectivesData(data)
return data
}
}
}
return null
}
fun saveRidingPerspectivesData() {
fun saveRidingPerspectivesData(data: RidingPerspectiveData) {
ridingPerspectivesPath.toFile().parentFile.mkdirs()
BufferedWriter(FileWriter(ridingPerspectivesPath.toFile())).use { writer -> gson.toJson(ridingPerspectives, writer) }
BufferedWriter(FileWriter(ridingPerspectivesPath.toFile())).use { writer -> gson.toJson(data, writer) }
}
data class SnapshotAcknowledgementData(val version: String, val dontShowAgain: Boolean)
@@ -64,7 +80,7 @@ object ClientPersistedData {
) {
fun updatePerspective(key: ResourceLocation, cameraType: CameraType) {
perspectives[key] = cameraType
saveRidingPerspectivesData()
saveRidingPerspectivesData(this)
}
}
}
@@ -34,6 +34,7 @@ import net.minecraft.world.phys.HitResult
import net.minecraft.world.phys.Vec3
import org.joml.Matrix3f
import org.joml.Quaternionf
import org.joml.Vector3d
import org.joml.Vector3f
import kotlin.math.min
import kotlin.math.sign
@@ -78,15 +79,15 @@ object MountedCameraRenderer {
// Get additional offset from poser and add to the eyeHeight offset
val currEyeHeight: Double = Mth.lerp(instance.partialTickTime.toDouble(), eyeHeight, eyeHeightOld)
val offset = Vec3(0.0, currEyeHeight - (driver.bbHeight / 2), 0.0)
val eyeOffset = Vec3(0.0, currEyeHeight - (driver.bbHeight / 2), 0.0)
var offset = Vec3(0.0, currEyeHeight - (driver.bbHeight / 2), 0.0)
var eyeOffset = Vec3(0.0, currEyeHeight - (driver.bbHeight / 2), 0.0)
val shouldFlip = !(vehicleController.active && vehicleController.orientation != null) // Do not flip the offset for 3rd person reverse unless we are using normal mc camera rotation.
val isFirstPerson = Minecraft.getInstance().options.cameraType == CameraType.FIRST_PERSON
eyeOffset.add(getFirstPersonOffset(model, locatorName))
eyeOffset = eyeOffset.add(getFirstPersonOffset(model, locatorName))
// Get the offset based on first person, third person, or third person with view bobbing enabled from the posers.
offset.add(
offset = offset.add(
if (isFirstPerson) {
getFirstPersonOffset(model, locatorName)
} else /* if (Cobblemon.config.thirdPersonViewBobbing) */ {
@@ -101,8 +102,8 @@ object MountedCameraRenderer {
else Quaternionf()
.rotateY((Math.PI.toFloat() - Mth.lerp(instance.partialTickTime, vehicle.yRotO, vehicle.yRot).toRadians()))
rotation.transform(offset.toVector3f())
rotation.transform(eyeOffset.toVector3f())
offset = rotation.transform(Vector3d(offset.x, offset.y, offset.z)).let { Vec3(it.x, it.y, it.z) }
eyeOffset = rotation.transform(Vector3d(eyeOffset.x, eyeOffset.y, eyeOffset.z)).let { Vec3(it.x, it.y, it.z) }
val eyeLocatorOffset = Vec3(locator.matrix.getTranslation(Vector3f()))
val eyePos = eyeLocatorOffset.add(entityPos)
@@ -1,120 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen9
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
import com.cobblemon.mod.common.client.render.models.blockbench.createTransformation
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BimanualFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BipedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
import com.cobblemon.mod.common.client.render.models.blockbench.pose.ModelPartTransformation
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.util.isBattling
import net.minecraft.client.model.geom.ModelPart
import net.minecraft.world.phys.Vec3
class TinkatinkModel (root: ModelPart) : PokemonPosableModel(root), HeadedFrame, BipedFrame, BimanualFrame {
override val rootPart = root.registerChildWithAllChildren("tinkatink")
override val head = getPart("head")
override val rightArm = getPart("arm_right")
override val leftArm = getPart("arm_left")
override val rightLeg = getPart("leg_right")
override val leftLeg = getPart("leg_left")
override var portraitScale = 2.0F
override var portraitTranslation = Vec3(0.0, -1.01, 0.0)
override var profileScale = 1.0F
override var profileTranslation = Vec3(0.0, 0.25, 0.0)
lateinit var standing: Pose
lateinit var walking: Pose
lateinit var sleep: Pose
lateinit var battleidle: Pose
lateinit var shoulderLeft: Pose
lateinit var shoulderRight: Pose
val shoulderOffset = 1
override val cryAnimation = CryProvider { bedrockStateful("tinkatink", "cry") }
override fun registerPoses() {
val blink = quirk { bedrockStateful("tinkatink", "blink") }
sleep = registerPose(
poseType = PoseType.SLEEP,
animations = arrayOf(bedrock("tinkatink", "sleep"))
)
standing = registerPose(
poseName = "standing",
poseTypes = PoseType.STATIONARY_POSES + PoseType.UI_POSES,
transformTicks = 10,
condition = { !it.isBattling },
quirks = arrayOf(blink),
animations = arrayOf(
singleBoneLook(),
bedrock("tinkatink", "ground_idle")
)
)
walking = registerPose(
poseName = "walking",
poseTypes = PoseType.MOVING_POSES,
transformTicks = 10,
quirks = arrayOf(blink),
animations = arrayOf(
singleBoneLook(),
bedrock("tinkatink", "ground_walk")
)
)
battleidle = registerPose(
poseName = "battle_idle",
poseTypes = PoseType.STATIONARY_POSES,
transformTicks = 10,
quirks = arrayOf(blink),
condition = { it.isBattling },
animations = arrayOf(
singleBoneLook(),
bedrock("tinkatink", "battle_idle")
)
)
shoulderLeft = registerPose(
poseType = PoseType.SHOULDER_LEFT,
quirks = arrayOf(blink),
condition = { !it.isBattling },
animations = arrayOf(
singleBoneLook(),
bedrock("tinkatink", "shoulder_left")
),
transformedParts = arrayOf(
rootPart.createTransformation().addPosition(ModelPartTransformation.X_AXIS, shoulderOffset)
)
)
shoulderRight = registerPose(
poseType = PoseType.SHOULDER_RIGHT,
quirks = arrayOf(blink),
condition = { !it.isBattling },
animations = arrayOf(
singleBoneLook(),
bedrock("tinkatink", "shoulder_right")
),
transformedParts = arrayOf(
rootPart.createTransformation().addPosition(ModelPartTransformation.X_AXIS, -shoulderOffset)
)
)
}
override fun getFaintAnimation(state: PosableState) = if (state.isPosedIn(standing, walking, battleidle, sleep)) bedrockStateful("tinkatink", "faint") else null
}
@@ -1,87 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen9
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BimanualFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BipedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.util.isBattling
import net.minecraft.client.model.geom.ModelPart
import net.minecraft.world.phys.Vec3
class TinkatuffModel (root: ModelPart) : PokemonPosableModel(root), HeadedFrame, BipedFrame, BimanualFrame {
override val rootPart = root.registerChildWithAllChildren("tinkatuff")
override val head = getPart("head")
override val rightArm = getPart("arm_right")
override val leftArm = getPart("arm_left")
override val rightLeg = getPart("leg_right")
override val leftLeg = getPart("leg_left")
override var portraitScale = 1.9F
override var portraitTranslation = Vec3(0.0, -0.41, 0.0)
override var profileScale = 0.9F
override var profileTranslation = Vec3(0.0, 0.4, 0.0)
lateinit var standing: Pose
lateinit var walking: Pose
lateinit var sleep: Pose
lateinit var battleidle: Pose
override val cryAnimation = CryProvider { bedrockStateful("tinkatuff", "cry") }
override fun registerPoses() {
val blink = quirk { bedrockStateful("tinkatuff", "blink") }
sleep = registerPose(
poseType = PoseType.SLEEP,
animations = arrayOf(bedrock("tinkatuff", "sleep"))
)
standing = registerPose(
poseName = "standing",
poseTypes = PoseType.STATIONARY_POSES + PoseType.UI_POSES,
transformTicks = 10,
condition = { !it.isBattling },
quirks = arrayOf(blink),
animations = arrayOf(
singleBoneLook(),
bedrock("tinkatuff", "ground_idle")
)
)
walking = registerPose(
poseName = "walking",
poseTypes = PoseType.MOVING_POSES,
transformTicks = 10,
quirks = arrayOf(blink),
animations = arrayOf(
singleBoneLook(),
bedrock("tinkatuff", "ground_walk")
)
)
battleidle = registerPose(
poseName = "battle_idle",
poseTypes = PoseType.STATIONARY_POSES,
transformTicks = 10,
quirks = arrayOf(blink),
condition = { it.isBattling },
animations = arrayOf(
singleBoneLook(),
bedrock("tinkatuff", "battle_idle")
)
)
}
override fun getFaintAnimation(state: PosableState) = if (state.isPosedIn(standing, walking, battleidle, sleep)) bedrockStateful("tinkatuff", "faint") else null
}
@@ -477,8 +477,6 @@ object VaryingModelRepository {
inbuilt("illumise", ::IllumiseModel)
inbuilt("yamper", ::YamperModel)
inbuilt("boltund", ::BoltundModel)
inbuilt("tinkatink", ::TinkatinkModel)
inbuilt("tinkatuff", ::TinkatuffModel)
inbuilt("fuecoco", :: FuecocoModel)
inbuilt("crocalor", :: CrocalorModel)
inbuilt("skeledirge", :: SkeledirgeModel)
@@ -329,6 +329,9 @@ class CobblemonConfig {
@CobblemonConfigField(Category.Riding, lang = "remember_riding_camera", CLIENT)
var rememberRidingCamera = false
@CobblemonConfigField(Category.Riding, lang = "enable_in_flight_dismounting", CLIENT)
var enableInFlightDismounting = false
@CobblemonConfigField(Category.Debug, lang = "enable_debug_keys", CLIENT)
var enableDebugKeys = false
@@ -39,15 +39,16 @@ class FollowWalkTargetTask(
private var lookTargetPos: BlockPos? = null
private var speed = 0f
override fun checkExtraStartConditions(arg: ServerLevel, arg2: PathfinderMob): Boolean {
override fun checkExtraStartConditions(level: ServerLevel, entity: PathfinderMob): Boolean {
if (this.pathUpdateCountdownTicks > 0) {
--this.pathUpdateCountdownTicks
return false
} else {
val brain = arg2.brain
val brain = entity.brain
val walkTarget = brain.getMemory(MemoryModuleType.WALK_TARGET).get()
val hasReached = this.hasReached(arg2, walkTarget)
if (!hasReached && this.hasFinishedPath(arg2, walkTarget, arg.gameTime)) {
val hasReached = this.hasReached(entity, walkTarget)
resetPathUpdateCountdownTicks(entity)
if (!hasReached && this.hasFinishedPath(entity, walkTarget, level.gameTime)) {
this.lookTargetPos = walkTarget.target.currentBlockPosition()
return true
} else {
@@ -61,6 +62,10 @@ class FollowWalkTargetTask(
}
}
fun resetPathUpdateCountdownTicks(entity: PathfinderMob) {
pathUpdateCountdownTicks = entity.random.nextInt(20) + 20
}
override fun canStillUse(arg: ServerLevel, arg2: PathfinderMob, l: Long): Boolean {
if (this.path != null && this.lookTargetPos != null) {
val optional = arg2.brain.getMemory(MemoryModuleType.WALK_TARGET)
@@ -75,7 +80,7 @@ class FollowWalkTargetTask(
override fun stop(world: ServerLevel, entity: PathfinderMob, l: Long) {
val walkTarget = entity.brain.getMemory(MemoryModuleType.WALK_TARGET).orElse(null)
if (walkTarget != null && !this.hasReached(entity, walkTarget) && entity.navigation.isStuck) {
this.pathUpdateCountdownTicks = world.getRandom().nextInt(40)
resetPathUpdateCountdownTicks(entity)
}
entity.navigation.stop()
@@ -186,7 +186,7 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
val blockState = this.world.getBlockState(target)
val path = if (!blockState.isSolid
|| (blockState.`is`(BlockTags.LEAVES) && blockState.block == CobblemonBlocks.SACCHARINE_LEAVES && (this.entity as PokemonEntity).canPathThroughSaccLeaves())) {
|| (blockState.`is`(BlockTags.LEAVES) && blockState.block == CobblemonBlocks.SACCHARINE_LEAVES && pather.canPathThroughSaccLeaves())) {
findPath(target, distance)
} else {
blockPos = target.above()
@@ -269,9 +269,6 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
// If we can fly and we're airborne, return the current Y position
return vec.y
}
if (world.getBlockState(blockPos).block == CobblemonBlocks.SACCHARINE_LEAVES && (this.entity as PokemonEntity).canPathThroughSaccLeaves()) {
return vec.y + 0.5
}
return if ((canFloat()) && blockGetter.getFluidState(blockPos).`is`(FluidTags.WATER)) vec.y + 0.5 else super.getGroundY(vec)
}
@@ -802,13 +802,14 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
spawnAction.complete()
val result = spawnAction.future
if (result.isCompletedExceptionally) {
val resultingSpawn = result.get()
if (resultingSpawn == null) {
noBite(player)
return false
}
var spawnedPokemon: PokemonEntity? = null
val resultingSpawn = result.get()
if (resultingSpawn is EntitySpawnResult) {
for (entity in resultingSpawn.entities) {
@@ -14,6 +14,7 @@ import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.api.battles.model.actor.EntityBackedBattleActor
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.storage.party.PartyStore
import com.cobblemon.mod.common.battles.ai.CompetitiveBattleAI
import com.cobblemon.mod.common.battles.ai.StrongBattleAI
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.net.messages.client.battle.BattleEndPacket
@@ -30,7 +31,7 @@ class NPCBattleActor(
) : AIBattleActor(
gameId = npc.uuid,
pokemonList = pokemonList.let { if (npc.npc.randomizePartyOrder) it.shuffled() else it },
battleAI = StrongBattleAI(skill)
battleAI = CompetitiveBattleAI(skill)
), EntityBackedBattleActor<NPCEntity> {
override val entity = npc
override val type = ActorType.NPC
@@ -547,6 +547,11 @@ open class PokemonEntity(
ifRidingAvailable { _, _, state -> pokemon.rideStamina = state.stamina.get() }
ridingController?.context?.state?.reset()
ridingAnimationData.clear()
// reset riding orientation
if (this is OrientationControllable) {
this.orientationController.reset()
}
}
}
@@ -1203,14 +1208,13 @@ open class PokemonEntity(
}
}
if (hand == InteractionHand.MAIN_HAND && player is ServerPlayer) {
if (player.isShiftKeyDown) {
if (hand == InteractionHand.MAIN_HAND) {
if (player.isShiftKeyDown && player is ServerPlayer) {
showInteractionWheel(player, itemStack)
return InteractionResult.sidedSuccess(level().isClientSide)
}
else if (pokemon.getOwnerPlayer() == player) {
// TODO #105
if (this.attemptItemInteraction(player, player.getItemInHand(hand))) return InteractionResult.SUCCESS
else if (this.ownerUUID == player.uuid && this.attemptItemInteraction(player, player.getItemInHand(hand))) {
return InteractionResult.sidedSuccess(level().isClientSide)
}
}
@@ -1277,7 +1281,7 @@ open class PokemonEntity(
}
override fun shouldBeSaved(): Boolean {
if (ownerUUID == null && !pokemon.isNPCOwned() && (Cobblemon.config.savePokemonToWorld || isPersistenceRequired || this.pokemon.canDropHeldItem)) {
if (ownerUUID == null && !pokemon.isNPCOwned() && (Cobblemon.config.savePokemonToWorld || isPersistenceRequired)) {
CobblemonEvents.POKEMON_ENTITY_SAVE_TO_WORLD.postThen(PokemonEntitySaveToWorldEvent(this)) {
return true
}
@@ -1386,18 +1390,24 @@ open class PokemonEntity(
return bagItemLike.handleInteraction(player, battlePokemon, stack)
}
if (player !is ServerPlayer || this.isBusy) {
if (this.isBusy) {
return false
}
val interaction = PokemonInteractions.findInteraction(this)
if (interaction != null && !pokemon.isOnInteractionCooldown(interaction.grouping)) {
interaction.effects.forEach { it.applyEffect(this, player) }
pokemon.interactionCooldowns.put(interaction.grouping, runtime.resolveInt(interaction.cooldown))
if (player is ServerPlayer) {
interaction.effects.forEach { it.applyEffect(this, player) }
pokemon.interactionCooldowns.put(interaction.grouping, runtime.resolveInt(interaction.cooldown))
}
return true
}
if (player !is ServerPlayer) {
return false
}
// Evolution item logic
if (pokemon.getOwnerPlayer() == player) {
val context = ItemInteractionEvolution.ItemInteractionContext(stack, player.level())
@@ -10,8 +10,11 @@ package com.cobblemon.mod.common.entity.pokemon.ai.sensors
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.getMemorySafely
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.Mob
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.NearestVisibleLivingEntities
import net.minecraft.world.entity.ai.sensing.Sensor
@@ -28,8 +31,17 @@ class DefendOwnerSensor : Sensor<PokemonEntity>(10) {
}
private fun setNearestAttacker(entity: PokemonEntity, visibleMobs: NearestVisibleLivingEntities, owner: LivingEntity) {
val nearestAttacker = visibleMobs.findClosest { mob ->
mob.lastHurtMob == owner
val nearestAttacker = visibleMobs.findClosest { livingEntity ->
// Prevents Pokémons from attacking players as owned Pokémons are currently invulnerable
if (livingEntity is ServerPlayer) return@findClosest false
if (livingEntity.lastHurtMob == owner) return@findClosest true
if (livingEntity is Mob && livingEntity.target == owner) return@findClosest true
val mobAttackTarget = livingEntity.brain.getMemorySafely(MemoryModuleType.ATTACK_TARGET)
if (mobAttackTarget.isPresent && mobAttackTarget.get() == owner) return@findClosest true
return@findClosest false
}.map { mob -> mob as LivingEntity }
entity.brain.setMemory(CobblemonMemories.NEAREST_VISIBLE_ATTACKER, nearestAttacker)
@@ -10,18 +10,10 @@ package com.cobblemon.mod.common.entity.pokemon.ai.tasks
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.google.common.collect.ImmutableMap
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.attributes.Attributes
import net.minecraft.world.entity.ai.behavior.Behavior
import net.minecraft.world.entity.ai.behavior.OneShot
import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder
import net.minecraft.world.entity.ai.behavior.declarative.Trigger
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.MemoryStatus
import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.entity.ai.targeting.TargetingConditions
object DefendOwnerTask {
fun create(): OneShot<PokemonEntity> {
@@ -52,8 +52,9 @@ object PlaceHoneyInHiveTask {
if (blockEntity.isFull) {
// Erase Hive memory and give the bee a moment to locate a new hive
entity.brain.eraseMemory(CobblemonMemories.HIVE_LOCATION)
} else {
blockEntity.addOccupant(entity)
}
blockEntity.addOccupant(entity)
}
return@Trigger true
@@ -30,21 +30,13 @@ import com.cobblemon.mod.common.api.events.battles.BattleStartedEvent
import com.cobblemon.mod.common.api.events.battles.BattleVictoryEvent
import com.cobblemon.mod.common.api.events.fishing.PokerodCastEvent
import com.cobblemon.mod.common.api.events.fishing.PokerodReelEvent
import com.cobblemon.mod.common.api.events.pokemon.CollectEggEvent
import com.cobblemon.mod.common.api.events.pokemon.FossilRevivedEvent
import com.cobblemon.mod.common.api.events.pokemon.HatchEggEvent
import com.cobblemon.mod.common.api.events.pokemon.LevelUpEvent
import com.cobblemon.mod.common.api.events.pokemon.PokemonCapturedEvent
import com.cobblemon.mod.common.api.events.pokemon.PokemonGainedEvent
import com.cobblemon.mod.common.api.events.pokemon.RidePokemonEvent
import com.cobblemon.mod.common.api.events.pokemon.TradeEvent
import com.cobblemon.mod.common.api.events.pokemon.*
import com.cobblemon.mod.common.api.events.pokemon.evolution.EvolutionCompleteEvent
import com.cobblemon.mod.common.api.events.storage.ReleasePokemonEvent
import com.cobblemon.mod.common.api.stats.CobblemonStats.getStat
import com.cobblemon.mod.common.util.getPlayer
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import java.util.UUID
import java.util.*
object StatHandler : EventHandler {
override fun registerListeners() {
@@ -100,13 +92,13 @@ object StatHandler : EventHandler {
fun onBattleStart(event : BattleStartedEvent) {
event.battle.players.forEach { player ->
player.awardStat(this.getStat(getStat(Cobblemon.statistics.BATTLES_TOTAL)))
player.awardStat(getStat(Cobblemon.statistics.BATTLES_TOTAL))
}
}
fun onDexEntryGain(event : PokemonGainedEvent) {
val player = event.pokemon.getOwnerPlayer()
player?.awardStat(getStat(getStat(Cobblemon.statistics.DEX_ENTRIES)))
player?.awardStat(getStat(Cobblemon.statistics.DEX_ENTRIES))
}
fun onCollectEgg(event : CollectEggEvent) {
@@ -139,12 +131,4 @@ object StatHandler : EventHandler {
fun onReelIn(event : PokerodReelEvent) {
event.player.awardStat(getStat(Cobblemon.statistics.REEL_INS))
}
fun getStat(resourceLocation: ResourceLocation) : ResourceLocation {
val stat = BuiltInRegistries.CUSTOM_STAT.get(resourceLocation)
if (stat == null) {
Cobblemon.LOGGER.error("Could not find stat with id {}", resourceLocation)
}
return stat ?: throw NullPointerException("Could not find stat with id $resourceLocation")
}
}
@@ -25,9 +25,7 @@ class MochiItem(stat: Stats): EVIncreaseItem(stat, 4) {
}
pokemon.feedPokemon(1)
stack.consume(1, player)
return super.applyToPokemon(player, stack, pokemon)
}
}
@@ -23,7 +23,6 @@ import net.minecraft.world.level.Level
class PonigiriItem : Item(
Properties().stacksTo(64)
.food(FoodProperties.Builder()
.alwaysEdible()
.nutrition(2)
.saturationModifier(0.55f)
.build())
@@ -35,9 +35,16 @@ class SinisterTeaItem : Item(
val effectsComponent = stack.get(CobblemonItemComponents.MOB_EFFECTS)
if (effectsComponent != null) {
for (effect in effectsComponent.mobEffects) {
val effectCopy = MobEffectInstance(effect)
user.addEffect(effectCopy)
for (raw in effectsComponent.mobEffects) {
val inst = MobEffectInstance(raw)
val holder = inst.effect
val effect = holder.value()
if (effect.isInstantenous) {
effect.applyInstantenousEffect(user as? Player, null, user, inst.amplifier, 1.0)
} else {
user.addEffect(inst)
}
}
}
}
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.data
import com.cobblemon.mod.common.api.interaction.PokemonInteractionSet
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
class PokemonInteractionsSyncPacket(
val speciesInteractions: List<PokemonInteractionSet>,
val generalInteractions: List<PokemonInteractionSet>,
) : NetworkPacket<PokemonInteractionsSyncPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeCollection(speciesInteractions) { it, speciesInteraction ->
speciesInteraction.encode(it)
}
buffer.writeCollection(generalInteractions) { it, generalInteraction ->
generalInteraction.encode(it)
}
}
companion object {
val ID = cobblemonResource("pokemon_interactions_sync")
fun decode(buffer: RegistryFriendlyByteBuf): PokemonInteractionsSyncPacket {
val speciesInteractions = buffer.readList { it ->
PokemonInteractionSet.decode(it)
}
val generalInteractions = buffer.readList { it ->
PokemonInteractionSet.decode(it)
}
return PokemonInteractionsSyncPacket(
speciesInteractions,
generalInteractions,
)
}
}
}
@@ -21,7 +21,9 @@ class PokemonStateUpdatePacket(pokemon: () -> Pokemon?, value: PokemonState): Si
value.writeToBuffer(buffer)
}
override fun set(pokemon: Pokemon, value: PokemonState) { pokemon.state = value }
override fun set(pokemon: Pokemon, value: PokemonState) {
pokemon.state = value
}
companion object {
val ID = cobblemonResource("state_update")
@@ -13,30 +13,24 @@ import com.cobblemon.mod.common.api.mark.Marks
import com.cobblemon.mod.common.api.pokeball.PokeBalls
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.riding.stats.RidingStat
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.entity.PlatformType
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readEnumConstant
import com.cobblemon.mod.common.util.readIdentifier
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.readText
import com.cobblemon.mod.common.util.writeEnumConstant
import com.cobblemon.mod.common.util.writeIdentifier
import com.cobblemon.mod.common.util.writeString
import com.cobblemon.mod.common.util.writeText
import java.util.UUID
import com.cobblemon.mod.common.util.*
import net.minecraft.client.multiplayer.ClientLevel
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.network.chat.MutableComponent
import net.minecraft.network.protocol.game.ClientboundAddEntityPacket
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.entity.Entity
import java.util.*
class SpawnPokemonPacket(
var ownerId: UUID?,
var pokemonUUID: UUID,
var storageUUID: UUID?,
var scaleModifier: Float,
var speciesId: ResourceLocation,
var gender: Gender,
@@ -61,6 +55,7 @@ class SpawnPokemonPacket(
var tickSpawned: Int,
var rideBoosts: Map<RidingStat, Float>,
var rideStamina: Float,
var silent: Boolean,
vanillaSpawnPacket: ClientboundAddEntityPacket,
) : SpawnExtraDataEntityPacket<SpawnPokemonPacket, PokemonEntity>(vanillaSpawnPacket) {
@@ -69,6 +64,7 @@ class SpawnPokemonPacket(
constructor(entity: PokemonEntity, vanillaSpawnPacket: ClientboundAddEntityPacket) : this(
entity.ownerUUID,
entity.pokemon.uuid,
entity.pokemon.storeCoordinates.get()?.store?.uuid,
entity.pokemon.scaleModifier,
entity.exposedSpecies.resourceIdentifier,
entity.pokemon.gender,
@@ -93,12 +89,14 @@ class SpawnPokemonPacket(
entity.tickCount,
entity.entityData.get(PokemonEntity.RIDE_BOOSTS),
entity.entityData.get(PokemonEntity.RIDE_STAMINA),
entity.isSilent,
vanillaSpawnPacket
)
override fun encodeEntityData(buffer: RegistryFriendlyByteBuf) {
buffer.writeNullable(ownerId) { _, v -> buffer.writeUUID(v) }
buffer.writeUUID(this.pokemonUUID)
buffer.writeNullable(this.storageUUID) { pb, value -> pb.writeUUID(value) }
buffer.writeFloat(this.scaleModifier)
buffer.writeIdentifier(this.speciesId)
buffer.writeEnumConstant(this.gender)
@@ -127,10 +125,21 @@ class SpawnPokemonPacket(
{ _, value -> buffer.writeFloat(value) }
)
buffer.writeFloat(rideStamina)
buffer.writeBoolean(silent)
}
override fun applyData(entity: PokemonEntity, level: ClientLevel) {
entity.ownerUUID = ownerId
// Reuse the stored Pokémon instance to avoid creating a duplicate,
// preventing client/entity desync issues.
this@SpawnPokemonPacket.storageUUID?.let {
val storedPokemon = CobblemonClient.storage.locatePokemon(it, this@SpawnPokemonPacket.pokemonUUID)
if (storedPokemon != null) {
entity.pokemon = storedPokemon
}
}
entity.pokemon.apply {
uuid = this@SpawnPokemonPacket.pokemonUUID
scaleModifier = this@SpawnPokemonPacket.scaleModifier
@@ -162,6 +171,7 @@ class SpawnPokemonPacket(
entity.entityData.set(PokemonEntity.RIDE_BOOSTS, rideBoosts)
entity.entityData.set(PokemonEntity.RIDE_STAMINA, rideStamina)
entity.entityData.set(PokemonEntity.SCALE_MODIFIER, scaleModifier)
entity.isSilent = silent
entity.ejectPassengers()
passengers.forEach {
@@ -179,6 +189,7 @@ class SpawnPokemonPacket(
fun decode(buffer: RegistryFriendlyByteBuf): SpawnPokemonPacket {
val ownerId = buffer.readNullable { buffer.readUUID() }
val pokemonUUID = buffer.readUUID()
val storageUUID = buffer.readNullable { buffer.readUUID() }
val scaleModifier = buffer.readFloat()
val speciesId = buffer.readIdentifier()
val gender = buffer.readEnumConstant(Gender::class.java)
@@ -206,11 +217,13 @@ class SpawnPokemonPacket(
{ buffer.readFloat() }
)
val rideStamina = buffer.readFloat()
val silent = buffer.readBoolean()
val vanillaPacket = decodeVanillaPacket(buffer)
return SpawnPokemonPacket(
ownerId,
pokemonUUID,
storageUUID,
scaleModifier,
speciesId,
gender,
@@ -235,6 +248,7 @@ class SpawnPokemonPacket(
tickSpawned,
rideBoosts,
rideStamina,
silent,
vanillaPacket
)
}
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.net.serverhandling.riding
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.api.riding.RidingStyle
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.server.riding.DismountPokemonPacket
import net.minecraft.server.MinecraftServer
@@ -25,12 +26,16 @@ object DismountPokemonPacketHandler : ServerNetworkPacketHandler<DismountPokemon
val pokemon = player.vehicle as PokemonEntity
if (!canPlayerStopRidingPokemon(pokemon, player)) return
val isRidingInAir = pokemon.ridingController?.context?.style == RidingStyle.AIR
if (pokemon.controllingPassenger == player) {
pokemon.passengers.forEach { it.stopRiding() }
}
else {
} else {
player.stopRiding()
}
if (isRidingInAir) pokemon.setFlying(true)
}
private fun canPlayerStopRidingPokemon(pokemon: PokemonEntity, player: ServerPlayer): Boolean {
@@ -1437,6 +1437,11 @@ open class Pokemon : ShowdownIdentifiable {
}
fun getOwnerEntity(): LivingEntity? {
if (isClient) {
val ownerUUID = entity?.ownerUUID ?: return null
return entity?.level()?.getPlayerByUUID(ownerUUID)
}
return storeCoordinates.get()?.let {
if (isPlayerOwned()) {
server()?.playerList?.getPlayer(it.store.uuid)
@@ -8,13 +8,22 @@
package com.cobblemon.mod.common.pokemon.adapters
import com.cobblemon.mod.common.api.interaction.InteractionEffect
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.adapters.RequirementAdapter
import com.cobblemon.mod.common.api.pokemon.requirement.Requirement
import com.cobblemon.mod.common.pokemon.evolution.adapters.LegacyItemConditionWrapperAdapter
import com.cobblemon.mod.common.pokemon.requirements.*
import com.cobblemon.mod.common.util.adapters.*
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeString
import com.google.common.collect.HashBiMap
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonElement
import com.google.gson.JsonSerializationContext
import com.google.gson.*
import net.minecraft.advancements.critereon.ItemPredicate
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.LowerCaseEnumTypeAdapterFactory
import java.lang.reflect.Type
import kotlin.reflect.KClass
@@ -60,21 +69,60 @@ object CobblemonRequirementAdapter : RequirementAdapter {
this.registerType(OwnerHoldsItemRequirement.ADAPTER_VARIANT, OwnerHoldsItemRequirement::class)
}
val gson: Gson by lazy {
GsonBuilder()
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(PokemonProperties::class.java, pokemonPropertiesShortAdapter)
.registerTypeAdapter(ItemPredicate::class.java, LegacyItemConditionWrapperAdapter)
.registerTypeAdapter(Requirement::class.java, CobblemonRequirementAdapter)
.registerTypeAdapter(InteractionEffect::class.java, InteractionEffectAdapter)
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
.registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory())
.setPrettyPrinting()
.create()
}
override fun <T : Requirement> registerType(id: String, type: KClass<T>) {
this.types[id.lowercase()] = type
}
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Requirement {
val variant = json.asJsonObject.get(VARIANT).asString.lowercase()
val type = this.types[variant] ?: throw IllegalArgumentException("Cannot resolve evolution requirement type for variant $variant")
val type = getRequirementType(variant)
return context.deserialize(json, type.java)
}
override fun serialize(src: Requirement, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
val json = context.serialize(src, src::class.java).asJsonObject
val variant = this.types.inverse()[src::class] ?: throw IllegalArgumentException("Cannot resolve evolution requirement for type ${src::class.qualifiedName}")
override fun serialize(requirement: Requirement, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
val json = context.serialize(requirement, requirement::class.java).asJsonObject
val variant = getVariant(requirement)
json.addProperty(VARIANT, variant)
return json
}
fun encode(requirement: Requirement, buffer: FriendlyByteBuf) {
buffer.writeString(gson.toJson(requirement, Requirement::class.java))
}
fun decode(buffer: FriendlyByteBuf): Requirement {
return gson.fromJson(buffer.readString(), Requirement::class.java)
}
private fun getVariant(requirement: Requirement): String {
val variant = this.types.inverse()[requirement::class]
if (variant == null) {
throw IllegalArgumentException("Cannot resolve evolution requirement for type ${requirement::class.qualifiedName}")
}
return variant
}
private fun getRequirementType(variant: String): KClass<out Requirement> {
val requirementType = this.types[variant]
if (requirementType == null) {
throw IllegalArgumentException("Cannot resolve evolution requirement type for variant $variant")
}
return requirementType
}
}
@@ -562,8 +562,8 @@ class OmniPathNodeMaker : NodeEvaluator() {
//// PathType.WALKABLE
//// } else if (blockState.canPathfindThrough(world, pos, NavigationType.AIR) && !blockStateBelow.isSideSolid(world, below, Direction.UP, SideShapeType.FULL)) {
//// PathType.OPEN
// } else if (blockState.`is`(BlockTags.LEAVES) && blockState.block == CobblemonBlocks.SACCHARINE_LEAVES && canPathThroughLeaves()) {
// return PathType.OPEN
if (canPathThroughLeaves() && blockState.block == CobblemonBlocks.SACCHARINE_LEAVES)
return PathType.OPEN
// } else PathType.BLOCKED
// This is the 1.6.1 logic but with some optimizations
@@ -633,9 +633,6 @@ class OmniPathNodeMaker : NodeEvaluator() {
pfContext, x, y, z, sizeX, sizeY, sizeZ, canOpenDoors, canPassDoors, set, PathType.BLOCKED, BlockPos(x, y, z), returnedEarlyPathType
)
if (PathType.LEAVES in set) {
return PathType.OPEN
}
if (PathType.FENCE in set) {
return PathType.FENCE
@@ -755,7 +752,7 @@ class OmniPathNodeMaker : NodeEvaluator() {
} else if (type == PathType.RAIL && block !is BaseRailBlock && pfContext.getCachedBlockState(pos.below()).block !is BaseRailBlock) {
PathType.UNPASSABLE_RAIL
} else if (type == PathType.LEAVES) {
if (blockState.block === CobblemonBlocks.SACCHARINE_LEAVES && canPathThroughLeaves()) {
if (canPathThroughLeaves() && blockState.block === CobblemonBlocks.SACCHARINE_LEAVES) {
PathType.OPEN
} else {
PathType.BLOCKED
@@ -6,19 +6,15 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.pokemon.properties.tags
package com.cobblemon.mod.common.pokemon.properties
import com.cobblemon.mod.common.api.properties.CustomPokemonPropertyType
import com.cobblemon.mod.common.pokemon.properties.StringProperty
object PokemonFlagProperty : CustomPokemonPropertyType<StringProperty> {
private const val KEY = "tag"
override val keys = setOf(KEY)
object LabelProperty : CustomPokemonPropertyType<StringProperty> {
override val keys = setOf("label", "tag")
override val needsKey = true
override fun fromString(value: String?) = if (value == null) null else StringProperty(KEY, value, { _, _ -> }, { pokemon, underlyingValue -> pokemon.hasLabels(underlyingValue) })
override fun fromString(value: String?) = if (value == null) null else StringProperty(keys.first(), value, { _, _ -> }, { pokemon, underlyingValue -> pokemon.hasLabels(underlyingValue) })
override fun examples() = emptySet<String>()
}
@@ -11,9 +11,9 @@ package com.cobblemon.mod.common.pokemon.requirements
import com.cobblemon.mod.common.api.pokemon.requirement.OwnerQueryRequirement
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.pokemon.Pokemon
import net.minecraft.server.level.ServerPlayer
import net.minecraft.resources.ResourceLocation
import kotlin.collections.iterator
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.player.Player
/**
* An [com.cobblemon.mod.common.api.pokemon.requirement.Requirement] that checks if the player has a certain [net.minecraft.advancements.Advancement]
@@ -26,13 +26,16 @@ class AdvancementRequirement(val requiredAdvancement: ResourceLocation) : OwnerQ
override fun checkPlayer(
pokemon: Pokemon,
owner: ServerPlayer
owner: Player
): Boolean {
for (entry in owner.advancements.progress) {
val serverPlayer = owner as? ServerPlayer ?: return false
for (entry in serverPlayer.advancements.progress) {
if (entry.key.id == requiredAdvancement && entry.value.isDone) {
return true
}
}
return false
}
@@ -12,11 +12,9 @@ import com.cobblemon.mod.common.api.pokemon.requirement.OwnerQueryRequirement
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.pokemon.Pokemon
import net.minecraft.advancements.critereon.ItemPredicate
import net.minecraft.server.level.ServerPlayer
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.InteractionHand
import net.minecraft.world.entity.LivingEntity
import kotlin.collections.iterator
import net.minecraft.world.entity.player.Player
/**
* An [OwnerQueryRequirement] that checks if the player/npc is holding a certain [ItemPredicate]
@@ -30,7 +28,7 @@ class OwnerHoldsItemRequirement(val itemCondition: ItemPredicate)
override fun checkPlayer(
pokemon: Pokemon,
owner: ServerPlayer
owner: Player
): Boolean {
return checkItem(owner)
}
@@ -8,22 +8,22 @@
package com.cobblemon.mod.common.util.adapters
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import com.google.gson.*
import com.mojang.serialization.Codec
import com.mojang.serialization.JsonOps
import java.lang.reflect.Type
class CodecBackedAdapter<T>(val codec: Codec<T>) : JsonDeserializer<T>, JsonSerializer<T> {
override fun deserialize(
jElement: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): T {
return JsonOps.INSTANCE.withDecoder(codec).apply(jElement).result().get().first as T
val ops = RegistryOpsProvider.getOpsWithDefaultFallback()
val result = ops.withDecoder(codec).apply(jElement)
return result.result().orElseThrow {
IllegalStateException("Failed to deserialize $jElement: ${result.error().orElse(null)}")
}.first
}
override fun serialize(
@@ -31,6 +31,10 @@ class CodecBackedAdapter<T>(val codec: Codec<T>) : JsonDeserializer<T>, JsonSeri
typeOfSrc: Type?,
context: JsonSerializationContext?
): JsonElement {
return JsonOps.INSTANCE.withEncoder(codec).apply(src).result().get()
val ops = RegistryOpsProvider.getOpsWithDefaultFallback()
val result = ops.withEncoder(codec).apply(src)
return result.result().orElseThrow {
IllegalStateException("Failed to serialize $src: ${result.error().orElse(null)}")
}
}
}
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.util.adapters
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Environment
import com.cobblemon.mod.common.util.server
import com.google.gson.JsonElement
import com.mojang.serialization.DynamicOps
import com.mojang.serialization.JsonOps
import net.minecraft.client.Minecraft
import net.minecraft.resources.RegistryOps
/**
* Utility for obtaining RegistryOps with the correct client/server registry access.
* Used when manually encoding/decoding registry-backed values (e.g., items, predicates).
*/
object RegistryOpsProvider {
fun getOpsWithDefaultFallback(): DynamicOps<JsonElement> {
if (Cobblemon.implementation.environment() == Environment.CLIENT) {
val minecraft = Minecraft.getInstance()
if (minecraft != null && minecraft.level != null)
return RegistryOps.create(JsonOps.INSTANCE, minecraft.level!!.registryAccess())
}
if (Cobblemon.implementation.environment() == Environment.SERVER) {
val server = server()
if (server != null)
return RegistryOps.create(JsonOps.INSTANCE, server.registryAccess())
}
return JsonOps.INSTANCE
}
}
@@ -297,6 +297,11 @@
"1.0833": [0, 0, 0]
}
}
},
"sound_effects": {
"0.25": {
"effect": "pokemon.ditto.cry"
}
}
},
"animation.ditto.blink": {

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