Compare commits

...
Author SHA1 Message Date
jackowes 3d54e51fb9 properly switch pose to walking when walk speed is super slow. 2026-10-05 15:38:25 -05:00
jackowes e3c1fff02a Update speedtuner with the ability to slow animations, +- buttons on the slider. and a working pause. 2026-10-05 15:19:48 -05:00
jackowes c672a4d690 Merge remote-tracking branch 'origin/main' into mon-smoothening 2026-10-04 18:44:12 -05:00
jackowes e64df48441 allow for the speed tuner to set walk speeds during the session 2026-10-04 18:34:03 -05:00
Polymeta 7511facebc Merge branch 'decorative-items-for-all' into 'main'
More Decorative Items

See merge request cable-mc/cobblemon!2430
2026-10-04 06:58:14 +00:00
dhi_holo 1486f5cfc2 Merge branch 'main' into decorative-items-for-all 2026-10-04 12:43:57 +10:00
dhi_holo 558e2d89ff Mipmap Fix
shhhh, I did it the cheap and nasty way rather than tweaking the models, it's for the greater good though
2026-10-04 12:01:29 +10:00
dhi_holo ea7b688655 Asset Updates 2026-10-04 11:35:22 +10:00
Polymeta fdafa3435c Merge branch 'flora-heals-an-aching-soul-' into 'main'
Flora Update

See merge request cable-mc/cobblemon!2412
2026-10-04 00:02:50 +00:00
dhi_holo f17c90f6ea Changelog
(also fixed some issues from the merge conflicts I missed)
2026-10-04 09:14:54 +10:00
dhi_holo 1dc45669bb Recipe Updates the Third 2026-10-04 09:14:12 +10:00
dhi_holo f3658614a4 Merge branch 'main' into decorative-items-for-all 2026-10-04 08:35:05 +10:00
Polymeta 422f60f587 Merge branch 'feat/tm-creative-tab' into 'main'
Add Technical Machines creative tab

See merge request cable-mc/cobblemon!2443
2026-10-03 22:31:41 +00:00
dhi_holo 8190ddf0e9 Merge branch 'main' into flora-heals-an-aching-soul- 2026-10-04 08:31:26 +10:00
jackowes acd375f533 - fix jitter in dynamic animations.
- Lookahead scales with turn distance
- Walk instead of sprint through narrow path sections
- Eased head look
- No looking around while travelling
- Pitch tilt applies to the whole model
- Slower floatier turns in water and air
- Large Pokémon slide onto close targets instead of spinning
2026-10-03 17:24:59 -05:00
dhi_holo af281d0bde Pa'uvillea 2026-10-04 07:57:39 +10:00
Gitoido 754e9bc158 chore: changelog skip_ci 2026-10-03 20:48:36 +03:00
Gitoido 2df8504139 feat: lore correct sorting 2026-10-03 20:35:44 +03:00
Gitoido 2838f97c56 feat: add Technical Machines dynamic creative tab 2026-10-03 19:55:32 +03:00
Gitoido 68c75deeeb feat: Technical Machines creative tab i18n 2026-10-03 19:55:04 +03:00
jackowes 9076c659de run script to ungate many riding overlay animations 2026-10-02 20:19:16 -05:00
jackowes 9292504f8c Move away from non minecrafty speed holding and redo how rotations are handled. move away from reynolds model. 2026-10-02 20:00:56 -05:00
dhi_holo e9bffd3ad7 Minty Fresh
Updated Mint Leaf Litter to use a texture closer to Vanilla to make the tiling nicer
2026-10-02 17:18:54 +10:00
dhi_holo 460c472c55 Asset Updates
Approved assets for all Terrain Plants, Mint Leaf Litter, and three of the nectar flowers (at my own fault). Potted plants already match approval
2026-10-02 16:09:11 +10:00
dhi_holo 69c3b6142d Merge branch 'main' into flora-heals-an-aching-soul- 2026-10-02 14:56:06 +10:00
jackowes ad484abf56 Merge remote-tracking branch 'origin/main' into mon-smoothening 2026-10-01 23:14:46 -05:00
jackowes fc1c7cf6ca allow for dynamic animations outside of rides. 2026-10-01 23:13:01 -05:00
Sterrezo 2aa4ab4b52 Added sounds to bright powder illuminating signs 2026-10-01 17:40:13 +02:00
Gitoido 113977990e feat: helper functions to register plants and pots 2026-10-01 17:22:09 +03:00
jackowes 33abca9b43 First draft of sprint and walk speed tuning tool 2026-09-29 21:40:04 -05:00
jackowes ea3c051772 Sprint now in blocks per second and default sprint speed is player sprint speed. 2026-09-29 20:44:00 -05:00
dhi_holo 3bd3ce5688 Delete wisteria_root_side.json 2026-09-30 10:58:27 +10:00
dhi_holo 881a87216b Merge branch 'main' into flora-heals-an-aching-soul- 2026-09-30 10:11:19 +10:00
dhi_holo 1d473b82af Refactor
Renamed Voltconia to Voltaconia, and also fixed the models being in a temp folder for easier file editing because that's not needed anymore
2026-09-30 10:10:55 +10:00
jackowes 4cde730259 these are not the correct file 2026-09-27 21:53:02 -05:00
jackowes 2bcce06318 - fix sliding after being hit while walking on a path.
- make it so running starts when they mon is farther
- smaller mons get closer before stopping when traveling to the owner.
- make it so mons immediately sprint when the player is sprinting.
2026-09-27 21:45:35 -05:00
jackowes 930c3568bb add temp sprint speeds to all sprinters. add sprint to attacktargettask as well 2026-09-27 21:03:42 -05:00
jackowes ffe480b12f Smoother path following, steering move control, body rotation and AI sprinting 2026-09-27 20:20:54 -05:00
dhi_holo e0d0c38232 Merge branch 'main' into decorative-items-for-all 2026-09-25 15:02:09 +10:00
dhi_holo 217d4cda42 Update CHANGELOG.md 2026-09-22 10:44:12 +10:00
dhi_holo 08daa43aae Update blank_tm.json 2026-09-22 10:41:01 +10:00
dhi_holo 184d889831 TM Boost 2026-09-22 10:36:59 +10:00
dhi_holo c6e0f431ae Update never_melt_ice.json 2026-09-22 10:31:59 +10:00
dhi_holo 93daeab840 Merge branch 'main' into decorative-items-for-all 2026-09-22 10:16:29 +10:00
dhi_holo f132b297c6 Never-Melted
Recipe Update
2026-09-22 10:16:17 +10:00
dhi_holo c0a524a718 Jars and Generators and a forgotten Pouch
6 new items ready to decorate your worlds
2026-09-21 11:58:11 +10:00
dhi_holo 92b0ef84e9 Yeah, I've Got Time
This'll hopefully be ready in time for 1.8.2 so I'm making the decision to treat it as if it is now, so it almost certainly will be
2026-09-14 16:37:09 +10:00
dhi_holo dc8d18384c Merge branch 'main' into flora-heals-an-aching-soul- 2026-09-14 16:33:26 +10:00
dhi_holo 44c056ecd5 Update CHANGELOG-DecorativeWv2.md
Fixed that up for cleanup reasons
2026-09-14 11:34:16 +10:00
dhi_holo dcc707c20d Model Fix
Improved the visuals and log spam of the Wistensu blocks
2026-09-14 10:53:12 +10:00
Gertimoshka 07b1b22c2c Wistensu Flower Logic Implementation 2026-09-13 15:08:44 +02:00
dhi_holo 23078eccba Wistensu Root Models
Fixed a tag and added the models for the Wistensu Roots
2026-09-13 21:11:11 +10:00
dhi_holo 8a82f35edf Seasonings 2026-09-13 19:59:19 +10:00
dhi_holo bec664c073 Merge Conflict Fixes 2026-09-13 13:01:20 +10:00
dhi_holo 3e61424d8f Merge branch 'main' into flora-heals-an-aching-soul- 2026-09-13 12:28:17 +10:00
dhi_holo c1606c6fc9 Textures!
Terrain Plants now have proper textures, finishing them off beyond some data file work waiting on some conversations
2026-09-13 11:42:42 +10:00
dhi_holo e0108cc99f Merge branch 'main' into flora-heals-an-aching-soul- 2026-09-13 10:47:37 +10:00
dhi_holo c0de8a2591 Update TerrainSeedBlock.kt
Actually fixed it this time
2026-09-12 15:19:09 +10:00
dhi_holo b8733eed52 Revert "Update TerrainSeedBlock.kt"
This reverts commit bd0511a9fb.
2026-09-12 14:31:19 +10:00
dhi_holo 4aa30efec7 Merge branch 'main' into flora-heals-an-aching-soul- 2026-09-12 13:04:47 +10:00
dhi_holo 7930ec52fa Merge branch 'main' into flora-heals-an-aching-soul- 2026-09-07 15:20:47 +10:00
dhi_holo 57eb306e4f Update TODO List - Flora Heals An Aching Soul.md 2026-09-06 21:25:18 +10:00
dhi_holo bd0511a9fb Update TerrainSeedBlock.kt
Terrain crops now break properly after growing to be 2 blocks tall
2026-09-06 21:24:28 +10:00
dhi_holo c14f3292aa Merge branch 'main' into flora-heals-an-aching-soul- 2026-09-06 19:57:15 +10:00
dhi_holo 8fb2b1cf3d Merge branch 'main' into flora-heals-an-aching-soul- 2026-07-12 08:20:14 +10:00
dhi_holo 5c7d1cbe10 Fixed Case of the Missing Loot Table 2026-06-04 15:47:55 +10:00
dhi_holo 3348bfc362 Merge branch 'main' into flora-heals-an-aching-soul- 2026-06-04 14:35:35 +10:00
dhi_holo 8db9252434 Potted Berries for All 2026-05-25 18:09:49 +10:00
dhi_holo d9d4fe15cc Update TODO List - Flora Heals An Aching Soul.md 2026-05-25 08:40:11 +10:00
dhi_holo 8453e2d514 Milling Integration
with Create
2026-05-25 08:15:47 +10:00
dhi_holo 4d3070c0c2 Compost Levels 2026-05-25 08:03:54 +10:00
dhi_holo 15f273a9ef Bright Powder on Signs
It's so wonderful
2026-05-25 07:16:36 +10:00
dhi_holo 007c079cd6 WIP 2026-05-25 05:17:30 +10:00
dhi_holo 9851b1e8ad Merge branch 'main' into flora-heals-an-aching-soul- 2026-05-25 04:22:38 +10:00
dhi_holo c65c2610cd Merge branch 'main' into flora-heals-an-aching-soul- 2026-05-16 21:29:13 +10:00
dhi_holo 2be08a3fcc terminology corrections
also tag additions
2026-05-16 21:26:05 +10:00
dhi_holo 4cdf440eee More Tagging Work 2026-05-13 09:48:52 +10:00
dhi_holo 1545ee8fc2 Merge branch 'main' into flora-heals-an-aching-soul- 2026-05-13 09:12:37 +10:00
dhi_holo a009fabfe4 Create TODO List - Flora Heals An Aching Soul.md 2026-05-12 22:51:33 +10:00
dhi_holo 46e4bfdd13 CONSUME 2026-05-12 22:27:07 +10:00
dhi_holo e24c82860d No Bone 2026-05-12 22:11:53 +10:00
dhi_holo 1291dee1e1 Data Files 2026-05-12 21:39:03 +10:00
dhi_holo 69732be09b Mistleaf's Fae Magics 2026-05-12 18:24:23 +10:00
dhi_holo 8ce4ed7dee Update TerrainFlowerBlock.kt
Removed unused imports
2026-05-12 14:59:52 +10:00
dhi_holo 33d3cc0104 Merge branch 'flora-heals-an-aching-soul-' of https://gitlab.com/cable-mc/cobblemon into flora-heals-an-aching-soul- 2026-05-12 14:52:36 +10:00
dhi_holo 33b6b519ed Merge branch 'flora-heals-an-aching-soul-' of https://gitlab.com/cable-mc/cobblemon into flora-heals-an-aching-soul- 2026-05-12 12:26:39 +10:00
dhi_holo 5ec849f4e3 Merge branch 'main' into flora-heals-an-aching-soul- 2026-05-12 12:26:09 +10:00
Gertimoshka 6f4a2d2cd5 Made the flowers block entities so they can tick and adjust to weather changes 2026-05-11 21:35:48 +03:00
Gertimoshka 0bca89e77d Lucibell will now properly light up if any part of the block has an entity in it, by designating lower part as the 'driver' who controls the states 2026-05-11 17:21:22 +03:00
dhi_holo 8f9f162011 Merge branch 'main' into flora-heals-an-aching-soul- 2026-05-11 09:56:11 +10:00
dhi_holo ac3bea3d5e Update TerrainFlowerBlock.kt
Compacted but it STILL DOESN'T WORK
2026-05-10 21:23:17 +10:00
dhi_holo 178d4f199b Update TerrainFlowerBlock.kt
I feel so close, but the weather checks don't work
2026-05-09 23:04:07 +10:00
dhi_holo 933819380d Merge branch 'main' into flora-heals-an-aching-soul- 2026-05-09 21:15:27 +10:00
dhi_holo d68131e1b9 Update TerrainFlowerBlock.kt
It's starting to get there, probably. Not fully functional yet
2026-05-09 21:15:04 +10:00
dhi_holo ffbc379c0b Some More Work 2026-05-08 14:01:31 +10:00
dhi_holo 7053d12508 Update TerrainSeedBlock.kt
In theory this makes it grow slower on dry dirt, which would be pretty cool
2026-05-07 20:38:47 +10:00
dhi_holo d41442200f Models and Temp Textures and start of some code for glowing 2026-05-07 20:08:15 +10:00
dhi_holo d2564c343e Terrain Seed Beginnings 2026-05-04 22:43:54 +10:00
dhi_holo d1c19e8837 Visual FIxes 2026-05-04 14:30:31 +10:00
dhi_holo 0ec3bfabad Merge branch 'main' into flora-heals-an-aching-soul- 2026-05-04 11:37:59 +10:00
dhi_holo 43ffb04621 FINALLY FUCKING FIXED IT 2026-05-04 11:34:04 +10:00
dhi_holo eeaedf76a9 progress 2026-05-04 09:43:59 +10:00
dhi_holo 120fb4829a Pa'uvillea complete
Maash's tweaks to the wall version, plus placement logic working as desired (although may need further tweaks depending on feedback)
2026-05-03 11:10:00 +10:00
dhi_holo 12b93e53a7 Revenge of the Pa'uvillea 2026-05-02 22:43:34 +10:00
dhi_holo 00b17b9764 Update CobblemonBlocks.kt 2026-05-02 18:55:21 +10:00
dhi_holo 06192f809d Merge branch 'main' into flora-heals-an-aching-soul- 2026-05-02 18:54:04 +10:00
dhi_holo d09eb0d3ff Update LobailaFlowerBlock.kt 2026-04-23 08:05:13 +10:00
dhi_holo 416321679c Updated Textures 2026-04-22 20:29:02 +10:00
dhi_holo 72c4e6e54f Merge branch 'main' into flora-heals-an-aching-soul- 2026-04-19 16:45:10 +10:00
dhi_holo d3738e3424 Merge branch 'main' into flora-heals-an-aching-soul- 2025-12-13 18:45:37 +10:00
dhi_holo 2e183db903 Re-rename
Lobaila and Pa'uvillea changes
2025-12-02 12:33:55 +10:00
dhi_holo fa174b88dd OVERSIGHTS (may need to copy to another branch) 2025-12-01 17:09:00 +10:00
dhi_holo 0b4abe6d16 Texture Updates 2025-12-01 17:08:42 +10:00
dhi_holo 759fb0ecd8 Rename!
Lobalia, Pa'uvilla, and Pomohana are here!
2025-12-01 16:37:53 +10:00
dhi_holo 429e927c3f Potted Berries Part 1
All the data/asset stuff needed for Potted Berries.
Need to handle the codeside now, which will be trickier.
2025-11-30 22:27:29 +10:00
dhi_holo 643aed9133 Update HydrangeaFlowerBlock.kt
Comment
2025-11-30 12:02:26 +10:00
dhi_holo e701642de2 Hydrangea loot and tag starts 2025-11-30 12:00:14 +10:00
dhi_holo a3a03e3da6 Lobelia Polish and Hydrangea
Added start of Hydrangea flowers
2025-11-29 20:51:25 +10:00
dhi_holo 5dd3d5b552 Mint Rework
Totally reworked the Mint Leaf Litter blocks to not rely on Pink Petals due to the differences in behaviour, finally added Craspedia loot table, updated Craspedia texture, fixed Lobelia being shaded incorrectly
2025-11-29 16:20:45 +10:00
dhi_holo 1bae246e6c Gestdate
Ges updated textures
2025-11-29 14:39:30 +10:00
dhi_holo c94ef10189 Merge branch 'main' into flora-heals-an-aching-soul- 2025-11-29 14:24:30 +10:00
dhi_holo dfacf1f9df Merge branch 'main' into flora-heals-an-aching-soul- 2025-11-27 18:33:35 +10:00
dhi_holo 06c5e34f48 Merge branch 'main' into flora-heals-an-aching-soul- 2025-11-23 20:33:34 +10:00
dhi_holo 1ab8b35e98 Merge branch 'main' into flora-heals-an-aching-soul- 2025-11-23 12:50:52 +10:00
dhi_holo 261b61ee6c Updated Assets and removed TODO 2025-11-22 15:31:12 +10:00
dhi_holo 5abf8e1bab Merge branch 'main' into flora-heals-an-aching-soul- 2025-11-22 14:16:33 +10:00
dhi_holo 7c0b463637 Merge branch 'main' into flora-heals-an-aching-soul- 2025-11-21 18:14:42 +10:00
dhi_holo e156ab2432 Craspedia assets and Lobelia waterlogging
Still needs some more work done on both, but it's real nice now
2025-11-21 18:14:29 +10:00
dhi_holo 0b46a7163b Merge branch 'main' into flora-heals-an-aching-soul- 2025-11-21 13:35:16 +10:00
dhi_holo 21b76c008a Craspedia start
Needs work, but it's fully functional
2025-11-21 13:30:19 +10:00
dhi_holo b7e9f8248d Oricorio Start
Lobelia (Red) flower partially set up.
Wanna look into letting it be waterlogged, and no dye crafting recipe is set up yet
2025-11-20 19:34:10 +10:00
dhi_holo 8b21e76a36 Merge branch 'main' into flora-heals-an-aching-soul- 2025-11-20 17:06:31 +10:00
dhi_holo 515471185f Create CHANGELOG-DecorativeWv2.md 2025-11-19 19:54:49 +10:00
dhi_holo 628e8f4e5e Mint Leaf Litter and Potted Mint
"It's not fucking weed you piece of shit stoner" -Kermit the Frog

Needs some cleanup when it comes to how I've stolen the Pink Petals (due to Leaf Litter being a future block), but what we got rn is a good starting point
2025-11-19 19:42:38 +10:00
788 changed files with 23416 additions and 1144 deletions
+14 -3
View File
@@ -3,8 +3,8 @@
### Additions
- The following items can now be placed on the ground as blocks:
- King's Rock, Galarica Cuff, Unremarkable Teacup, Masterpiece Teacup, Chipped Pot, Cracked Pot, Sachet, Metal Powder, Quick Powder, Bright Powder, Icy Rock, Heat Rock, Smooth Rock, Damp Rock, Destiny Knot, Luminous Moss, Never-Melt Ice, Wide Lens, Scope Lens, and Zoom Lens.
- Iron Ball, Metal Coat, Throat Spray, HP Up, Protein, Iron, Carbos, Calcium, Zinc, PP Up, PP Max, Upgrade, Dubious Disc, and Loaded Dice.
- King's Rock, Galarica Cuff, Unremarkable Teacup, Masterpiece Teacup, Chipped Pot, Cracked Pot, Sachet, Metal Powder, Quick Powder, Bright Powder, Soft Sand, Electirizer, Magmarizer, Icy Rock, Heat Rock, Smooth Rock, Damp Rock, Destiny Knot, Luminous Moss, Never-Melt Ice, Wide Lens, Scope Lens, and Zoom Lens.
- Iron Ball, Metal Coat, Throat Spray, Terrain Extender, Medicinal Brew, Moomoo Milk, HP Up, Protein, Iron, Carbos, Calcium, Zinc, PP Up, PP Max, Upgrade, Dubious Disc, and Loaded Dice.
- The following items can now be placed on various surfaces:
- Galarica Wreath, Magnet, Flame Orb, Life Orb, Toxic Orb, Light Ball, and Smoke Ball.
- The following items can now be placed as blocks and have additional functionality:
@@ -13,6 +13,10 @@
- Galarica Wreath, Cleanse Tag, Spell Tag, Sharp Beak, Covert Cloak, Shell Helmet, and Never-Melt Ice.
- Wearable items can now be equipped by a player by holding it and right-clicking (or rebound use/interact)
- Wearable items that can be placed down are exempt from this behaviour.
- Mint Leaves can now be placed on the ground as Leaf Litter, or in Flower Pots
- Added Oricorio Flowers
- Added Terrain Plants
- Berries can now be placed in Flower Pots
- The evolution tree can now be viewed on the Pokédex, showing a Pokémon's full family line and what's needed to evolve into each stage.
- Added custom sounds to a bunch of items that can be placed as blocks: liquid bottles, pill bottles, powders and dice.
@@ -81,11 +85,18 @@
- Bellossom
### Changes
- Terrain Seeds can now be planted
- Terrain Seeds are now consumed during PvP and NPC battles
- Glow Item Frames can be crafted using Bright Powder as an alternative to Glow Ink Sacs
- Bright Powder can be used on Signs and Hanging Signs to make text glow, as an alternative to Glow Ink Sacs
- Updated various recipes to display in Recipe Groups within the Recipe Book
- Updated most recipes to display more logically/consistently within the Recipe Book's categories
- Updated the following recipes
- Never-Melt Ice, Focus Sash, Blunder Policy, Weakness Policy, Blank TM
- Make ride rotations less jittery in multiplayer.
- Changes tm shelf discs to use textures instead of tinting and overlays to line up with item display
- Changes Disc Shelf TMs to use textures instead of tinting and overlays to line up with item display
- Blank TM can now be inserted into the Disc Shelf
- Added TM creative tab
- Recall animation now renders ball in hand with the player arm raised.
- Made regional and Vivillon split-evo locations now mutually exclusive
- Item evolutions now unlock **all** locked evolutions whose requirements are met instead of stopping after the first.
@@ -109,4 +109,21 @@ object CobblemonBlockEntities : PlatformRegistry<Registry<BlockEntityType<*>>, R
"poke_snack",
BlockEntityType.Builder.of(::PokeSnackBlockEntity, CobblemonBlocks.POKE_SNACK, CobblemonBlocks.POKE_CAKE).build(null)
)
@JvmField
val TERRAIN_PLANT: BlockEntityType<TerrainPlantBlockEntity> = this.create(
"terrain_plant",
BlockEntityType.Builder.of(::TerrainPlantBlockEntity,
CobblemonBlocks.VOLTACONIA,
CobblemonBlocks.LUCIBELL,
CobblemonBlocks.VERDROOT
).build(null)
)
@JvmField
val MISTLEAF_FLOWER: BlockEntityType<MistleafFlowerBlockEntity> = this.create(
"mistleaf",
BlockEntityType.Builder.of(::MistleafFlowerBlockEntity,
CobblemonBlocks.MISTLEAF
).build(null)
)
}
@@ -16,8 +16,7 @@ import com.cobblemon.mod.common.block.MintBlock.MintType
import com.cobblemon.mod.common.block.campfirepot.CampfireBlock
import com.cobblemon.mod.common.block.campfirepot.CampfirePotBlock
import com.cobblemon.mod.common.block.chest.GildedChestBlock
import com.cobblemon.mod.common.block.decor.ActivatableDecorationBlock
import com.cobblemon.mod.common.block.decor.RingTargetBlock
import com.cobblemon.mod.common.block.decor.*
import com.cobblemon.mod.common.block.general.BaleBlock
import com.cobblemon.mod.common.block.general.HorizontalRotationCarpetBlock
import com.cobblemon.mod.common.block.general.HorizontalRotationalBlock
@@ -28,6 +27,7 @@ import com.cobblemon.mod.common.block.sign.CobblemonSignBlock
import com.cobblemon.mod.common.block.sign.CobblemonWallHangingSignBlock
import com.cobblemon.mod.common.block.sign.CobblemonWallSignBlock
import com.cobblemon.mod.common.block.tmmachine.TMMachineBlock
import com.cobblemon.mod.common.block.general.MintLitterBlock
import com.cobblemon.mod.common.mixin.invoker.BlocksInvoker
import com.cobblemon.mod.common.mixin.invoker.DoorBlockInvoker
import com.cobblemon.mod.common.mixin.invoker.FireBlockInvoker
@@ -653,6 +653,10 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
val CHIPPED_POT = create("chipped_pot", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DECORATED_POT).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.TEAPOT))
@JvmField
val CRACKED_POT = create("cracked_pot", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DECORATED_POT).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.TEAPOT))
@JvmField
val ELECTIRIZER = create("electirizer", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.IZER))
@JvmField
val MAGMARIZER = create("magmarizer", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.IZER))
@JvmField
val SACHET = create("sachet", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
@@ -662,6 +666,8 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
val QUICK_POWDER = create("quick_powder", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_POWDER_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
@JvmField
val BRIGHT_POWDER = create("bright_powder", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_POWDER_SOUNDS).mapColor(MapColor.NONE).noOcclusion().lightLevel{3}, DecorativeItemBlock.DecorativeItemBlockType.POUCH))
@JvmField
val SOFT_SAND = create("soft_sand", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_POWDER_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
@JvmField
val NEVER_MELT_ICE = create("never_melt_ice", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion().friction(0.989F), DecorativeItemBlock.DecorativeItemBlockType.NEVERMELT))
@@ -703,6 +709,12 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val THROAT_SPRAY = create("throat_spray", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.THROAT_SPRAY))
@JvmField
val TERRAIN_EXTENDER = create("terrain_extender", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.EXTENDER))
@JvmField
val MOOMOO_MILK = create("moomoo_milk", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.GLASS_BOTTLE))
@JvmField
val MEDICINAL_BREW = create("medicinal_brew", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.GLASS_BOTTLE))
@JvmField
val FULL_HEAL = create("full_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@@ -857,6 +869,64 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val POTTED_SACCHARINE_SAPLING = this.create("potted_saccharine_sapling", BlocksInvoker.createFlowerPotBlock(SACCHARINE_SAPLING))
@JvmField
val RED_MINT_LEAF_LITTER = this.mintLitter("red_mint_leaf_litter", MapColor.COLOR_RED)
@JvmField
val POTTED_RED_MINT = this.pottedPlant("potted_red_mint", RED_MINT_LEAF_LITTER)
@JvmField
val BLUE_MINT_LEAF_LITTER = this.mintLitter("blue_mint_leaf_litter", MapColor.COLOR_BLUE)
@JvmField
val POTTED_BLUE_MINT = this.pottedPlant("potted_blue_mint", BLUE_MINT_LEAF_LITTER)
@JvmField
val CYAN_MINT_LEAF_LITTER = this.mintLitter("cyan_mint_leaf_litter", MapColor.COLOR_CYAN)
@JvmField
val POTTED_CYAN_MINT = this.pottedPlant("potted_cyan_mint", CYAN_MINT_LEAF_LITTER)
@JvmField
val PINK_MINT_LEAF_LITTER = this.mintLitter("pink_mint_leaf_litter", MapColor.COLOR_PINK)
@JvmField
val POTTED_PINK_MINT = this.pottedPlant("potted_pink_mint", PINK_MINT_LEAF_LITTER)
@JvmField
val GREEN_MINT_LEAF_LITTER = this.mintLitter("green_mint_leaf_litter", MapColor.COLOR_GREEN)
@JvmField
val POTTED_GREEN_MINT = this.pottedPlant("potted_green_mint", GREEN_MINT_LEAF_LITTER)
@JvmField
val WHITE_MINT_LEAF_LITTER = this.mintLitter("white_mint_leaf_litter", MapColor.SNOW)
@JvmField
val POTTED_WHITE_MINT = this.pottedPlant("potted_white_mint", WHITE_MINT_LEAF_LITTER)
@JvmField
val LOBAILA = this.create("lobaila", LobailaFlowerBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).noCollission().instabreak().sound(SoundType.GRASS).offsetType(BlockBehaviour.OffsetType.XZ).ignitedByLava().pushReaction(PushReaction.DESTROY)))
@JvmField
val POMOHANA = this.create("pomohana", PomohanaFlowerBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).noCollission().instabreak().sound(SoundType.GRASS).ignitedByLava().pushReaction(PushReaction.DESTROY)))
@JvmField
val PAUVILLEA = this.create("pauvillea",PauvilleaFlowerBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).noCollission().sound(SoundType.GRASS).instabreak().ignitedByLava().pushReaction(PushReaction.DESTROY)))
@JvmField
val WISTENSU = this.create("wistensu", WistensuFlowerHeadBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).noCollission().sound(SoundType.GRASS).instabreak().ignitedByLava().pushReaction(PushReaction.DESTROY).lightLevel{11}))
@JvmField
val WISTENSU_VINE = this.create("wistensu_vine", WistensuFlowerVineBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).noCollission().sound(SoundType.GRASS).instabreak().ignitedByLava().pushReaction(PushReaction.DESTROY)))
@JvmField
val ELECTRIC_SEED = this.terrainSeed("electric_seed", TerrainSeedBlock.TerrainSeedType.ELECTRIC)
@JvmField
val PSYCHIC_SEED = this.terrainSeed("psychic_seed", TerrainSeedBlock.TerrainSeedType.PSYCHIC)
@JvmField
val MISTY_SEED = this.terrainSeed("misty_seed", TerrainSeedBlock.TerrainSeedType.MISTY)
@JvmField
val GRASSY_SEED = this.terrainSeed("grassy_seed", TerrainSeedBlock.TerrainSeedType.GRASSY)
@JvmField
val VOLTACONIA = this.terrainPlant("voltaconia") { state -> if (state.getValue(TerrainPlantBlock.GLOWING)) 8 else 0 }
@JvmField
val VERDROOT = this.terrainPlant("verdroot") { state -> if (state.getValue(TerrainPlantBlock.GLOWING)) 5 else 0 }
@JvmField
val LUCIBELL = this.terrainPlant("lucibell") { state -> if (state.getValue(TerrainPlantBlock.GLOWING)) 8 else 0 }
@JvmField
val MISTLEAF = this.create(
"mistleaf",
MistleafFlowerBlock {
if ((it.getValue(MistleafFlowerBlock.GLOWING) >= 1)) it.getValue(MistleafFlowerBlock.GLOWING) + 2 else 0
}
)
@JvmField
val HEARTY_GRAIN_BALE = this.create("hearty_grain_bale", BaleBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).instrument(NoteBlockInstrument.BANJO).strength(0.5F).sound(CobblemonSounds.HEARTY_GRAIN_BALE_SOUNDS)))
@JvmField
@@ -1033,6 +1103,147 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
val YACHE_BERRY = this.berryBlock("yache")
//val BINDING_SOIL = this.create("binding_soil", BindingSoilBlock(BlockBehaviour.Properties.of()))
@JvmField
val POTTED_AGUAV_BERRY = this.create("potted_aguav_berry", BlocksInvoker.createFlowerPotBlock(AGUAV_BERRY))
@JvmField
val POTTED_APICOT_BERRY = this.create("potted_apicot_berry", BlocksInvoker.createFlowerPotBlock(APICOT_BERRY))
@JvmField
val POTTED_ASPEAR_BERRY = this.create("potted_aspear_berry", BlocksInvoker.createFlowerPotBlock(ASPEAR_BERRY))
@JvmField
val POTTED_BABIRI_BERRY = this.create("potted_babiri_berry", BlocksInvoker.createFlowerPotBlock(BABIRI_BERRY))
@JvmField
val POTTED_BELUE_BERRY = this.create("potted_belue_berry", BlocksInvoker.createFlowerPotBlock(BELUE_BERRY))
@JvmField
val POTTED_BLUK_BERRY = this.create("potted_bluk_berry", BlocksInvoker.createFlowerPotBlock(BLUK_BERRY))
@JvmField
val POTTED_CHARTI_BERRY = this.create("potted_charti_berry", BlocksInvoker.createFlowerPotBlock(CHARTI_BERRY))
@JvmField
val POTTED_CHERI_BERRY = this.create("potted_cheri_berry", BlocksInvoker.createFlowerPotBlock(CHERI_BERRY))
@JvmField
val POTTED_CHESTO_BERRY = this.create("potted_chesto_berry", BlocksInvoker.createFlowerPotBlock(CHESTO_BERRY))
@JvmField
val POTTED_CHILAN_BERRY = this.create("potted_chilan_berry", BlocksInvoker.createFlowerPotBlock(CHILAN_BERRY))
@JvmField
val POTTED_CHOPLE_BERRY = this.create("potted_chople_berry", BlocksInvoker.createFlowerPotBlock(CHOPLE_BERRY))
@JvmField
val POTTED_COBA_BERRY = this.create("potted_coba_berry", BlocksInvoker.createFlowerPotBlock(COBA_BERRY))
@JvmField
val POTTED_COLBUR_BERRY = this.create("potted_colbur_berry", BlocksInvoker.createFlowerPotBlock(COLBUR_BERRY))
@JvmField
val POTTED_CORNN_BERRY = this.create("potted_cornn_berry", BlocksInvoker.createFlowerPotBlock(CORNN_BERRY))
@JvmField
val POTTED_CUSTAP_BERRY = this.create("potted_custap_berry", BlocksInvoker.createFlowerPotBlock(CUSTAP_BERRY))
@JvmField
val POTTED_DURIN_BERRY = this.create("potted_durin_berry", BlocksInvoker.createFlowerPotBlock(DURIN_BERRY))
@JvmField
val POTTED_EGGANT_BERRY = this.create("potted_eggant_berry", BlocksInvoker.createFlowerPotBlock(EGGANT_BERRY))
@JvmField
val POTTED_ENIGMA_BERRY = this.create("potted_enigma_berry", BlocksInvoker.createFlowerPotBlock(ENIGMA_BERRY))
@JvmField
val POTTED_FIGY_BERRY = this.create("potted_figy_berry", BlocksInvoker.createFlowerPotBlock(FIGY_BERRY))
@JvmField
val POTTED_GANLON_BERRY = this.create("potted_ganlon_berry", BlocksInvoker.createFlowerPotBlock(GANLON_BERRY))
@JvmField
val POTTED_GREPA_BERRY = this.create("potted_grepa_berry", BlocksInvoker.createFlowerPotBlock(GREPA_BERRY))
@JvmField
val POTTED_HABAN_BERRY = this.create("potted_haban_berry", BlocksInvoker.createFlowerPotBlock(HABAN_BERRY))
@JvmField
val POTTED_HONDEW_BERRY = this.create("potted_hondew_berry", BlocksInvoker.createFlowerPotBlock(HONDEW_BERRY))
@JvmField
val POTTED_HOPO_BERRY = this.create("potted_hopo_berry", BlocksInvoker.createFlowerPotBlock(HOPO_BERRY))
@JvmField
val POTTED_IAPAPA_BERRY = this.create("potted_iapapa_berry", BlocksInvoker.createFlowerPotBlock(IAPAPA_BERRY))
@JvmField
val POTTED_JABOCA_BERRY = this.create("potted_jaboca_berry", BlocksInvoker.createFlowerPotBlock(JABOCA_BERRY))
@JvmField
val POTTED_KASIB_BERRY = this.create("potted_kasib_berry", BlocksInvoker.createFlowerPotBlock(KASIB_BERRY))
@JvmField
val POTTED_KEBIA_BERRY = this.create("potted_kebia_berry", BlocksInvoker.createFlowerPotBlock(KEBIA_BERRY))
@JvmField
val POTTED_KEE_BERRY = this.create("potted_kee_berry", BlocksInvoker.createFlowerPotBlock(KEE_BERRY))
@JvmField
val POTTED_KELPSY_BERRY = this.create("potted_kelpsy_berry", BlocksInvoker.createFlowerPotBlock(KELPSY_BERRY))
@JvmField
val POTTED_LANSAT_BERRY = this.create("potted_lansat_berry", BlocksInvoker.createFlowerPotBlock(LANSAT_BERRY))
@JvmField
val POTTED_LEPPA_BERRY = this.create("potted_leppa_berry", BlocksInvoker.createFlowerPotBlock(LEPPA_BERRY))
@JvmField
val POTTED_LIECHI_BERRY = this.create("potted_liechi_berry", BlocksInvoker.createFlowerPotBlock(LIECHI_BERRY))
@JvmField
val POTTED_LUM_BERRY = this.create("potted_lum_berry", BlocksInvoker.createFlowerPotBlock(LUM_BERRY))
@JvmField
val POTTED_MAGO_BERRY = this.create("potted_mago_berry", BlocksInvoker.createFlowerPotBlock(MAGO_BERRY))
@JvmField
val POTTED_MAGOST_BERRY = this.create("potted_magost_berry", BlocksInvoker.createFlowerPotBlock(MAGOST_BERRY))
@JvmField
val POTTED_MARANGA_BERRY = this.create("potted_maranga_berry", BlocksInvoker.createFlowerPotBlock(MARANGA_BERRY))
@JvmField
val POTTED_MICLE_BERRY = this.create("potted_micle_berry", BlocksInvoker.createFlowerPotBlock(MICLE_BERRY))
@JvmField
val POTTED_NANAB_BERRY = this.create("potted_nanab_berry", BlocksInvoker.createFlowerPotBlock(NANAB_BERRY))
@JvmField
val POTTED_NOMEL_BERRY = this.create("potted_nomel_berry", BlocksInvoker.createFlowerPotBlock(NOMEL_BERRY))
@JvmField
val POTTED_OCCA_BERRY = this.create("potted_occa_berry", BlocksInvoker.createFlowerPotBlock(OCCA_BERRY))
@JvmField
val POTTED_ORAN_BERRY = this.create("potted_oran_berry", BlocksInvoker.createFlowerPotBlock(ORAN_BERRY))
@JvmField
val POTTED_PAMTRE_BERRY = this.create("potted_pamtre_berry", BlocksInvoker.createFlowerPotBlock(PAMTRE_BERRY))
@JvmField
val POTTED_PASSHO_BERRY = this.create("potted_passho_berry", BlocksInvoker.createFlowerPotBlock(PASSHO_BERRY))
@JvmField
val POTTED_PAYAPA_BERRY = this.create("potted_payapa_berry", BlocksInvoker.createFlowerPotBlock(PAYAPA_BERRY))
@JvmField
val POTTED_PECHA_BERRY = this.create("potted_pecha_berry", BlocksInvoker.createFlowerPotBlock(PECHA_BERRY))
@JvmField
val POTTED_PERSIM_BERRY = this.create("potted_persim_berry", BlocksInvoker.createFlowerPotBlock(PERSIM_BERRY))
@JvmField
val POTTED_PETAYA_BERRY = this.create("potted_petaya_berry", BlocksInvoker.createFlowerPotBlock(PETAYA_BERRY))
@JvmField
val POTTED_PINAP_BERRY = this.create("potted_pinap_berry", BlocksInvoker.createFlowerPotBlock(PINAP_BERRY))
@JvmField
val POTTED_POMEG_BERRY = this.create("potted_pomeg_berry", BlocksInvoker.createFlowerPotBlock(POMEG_BERRY))
@JvmField
val POTTED_QUALOT_BERRY = this.create("potted_qualot_berry", BlocksInvoker.createFlowerPotBlock(QUALOT_BERRY))
@JvmField
val POTTED_RABUTA_BERRY = this.create("potted_rabuta_berry", BlocksInvoker.createFlowerPotBlock(RABUTA_BERRY))
@JvmField
val POTTED_RAWST_BERRY = this.create("potted_rawst_berry", BlocksInvoker.createFlowerPotBlock(RAWST_BERRY))
@JvmField
val POTTED_RAZZ_BERRY = this.create("potted_razz_berry", BlocksInvoker.createFlowerPotBlock(RAZZ_BERRY))
@JvmField
val POTTED_RINDO_BERRY = this.create("potted_rindo_berry", BlocksInvoker.createFlowerPotBlock(RINDO_BERRY))
@JvmField
val POTTED_ROSELI_BERRY = this.create("potted_roseli_berry", BlocksInvoker.createFlowerPotBlock(ROSELI_BERRY))
@JvmField
val POTTED_ROWAP_BERRY = this.create("potted_rowap_berry", BlocksInvoker.createFlowerPotBlock(ROWAP_BERRY))
@JvmField
val POTTED_SALAC_BERRY = this.create("potted_salac_berry", BlocksInvoker.createFlowerPotBlock(SALAC_BERRY))
@JvmField
val POTTED_SHUCA_BERRY = this.create("potted_shuca_berry", BlocksInvoker.createFlowerPotBlock(SHUCA_BERRY))
@JvmField
val POTTED_SITRUS_BERRY = this.create("potted_sitrus_berry", BlocksInvoker.createFlowerPotBlock(SITRUS_BERRY))
@JvmField
val POTTED_SPELON_BERRY = this.create("potted_spelon_berry", BlocksInvoker.createFlowerPotBlock(SPELON_BERRY))
@JvmField
val POTTED_STARF_BERRY = this.create("potted_starf_berry", BlocksInvoker.createFlowerPotBlock(STARF_BERRY))
@JvmField
val POTTED_TAMATO_BERRY = this.create("potted_tamato_berry", BlocksInvoker.createFlowerPotBlock(TAMATO_BERRY))
@JvmField
val POTTED_TANGA_BERRY = this.create("potted_tanga_berry", BlocksInvoker.createFlowerPotBlock(TANGA_BERRY))
@JvmField
val POTTED_TOUGA_BERRY = this.create("potted_touga_berry", BlocksInvoker.createFlowerPotBlock(TOUGA_BERRY))
@JvmField
val POTTED_WACAN_BERRY = this.create("potted_wacan_berry", BlocksInvoker.createFlowerPotBlock(WACAN_BERRY))
@JvmField
val POTTED_WATMEL_BERRY = this.create("potted_watmel_berry", BlocksInvoker.createFlowerPotBlock(WATMEL_BERRY))
@JvmField
val POTTED_WEPEAR_BERRY = this.create("potted_wepear_berry", BlocksInvoker.createFlowerPotBlock(WEPEAR_BERRY))
@JvmField
val POTTED_WIKI_BERRY = this.create("potted_wiki_berry", BlocksInvoker.createFlowerPotBlock(WIKI_BERRY))
@JvmField
val POTTED_YACHE_BERRY = this.create("potted_yache_berry", BlocksInvoker.createFlowerPotBlock(YACHE_BERRY))
init {
/**
* Makes all blocks in array flammable by adding them to FireBlock's flammableRegistry.
@@ -1134,4 +1345,48 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
}
fun typeGemClusters() = typeGemClusters.toMap()
private fun pottedPlant(name: String, flower: Block) = this.create(name, BlocksInvoker.createFlowerPotBlock(flower))
private fun mintLitter(name: String, color: MapColor) = this.create(
name,
MintLitterBlock(
BlockBehaviour.Properties.of()
.sound(CobblemonSounds.MINT_SOUNDS)
.mapColor(color)
.noOcclusion()
.noCollission()
.pushReaction(PushReaction.DESTROY)
)
)
private fun terrainSeed(name: String, type: TerrainSeedBlock.TerrainSeedType) = this.create(
name,
TerrainSeedBlock(
type,
BlockBehaviour.Properties.of()
.pushReaction(PushReaction.DESTROY)
.ignitedByLava()
.mapColor(MapColor.PLANT)
.offsetType(BlockBehaviour.OffsetType.XZ)
.noCollission()
.randomTicks()
.instabreak()
)
)
private fun terrainPlant(name: String, lightLevel: (BlockState) -> Int) = this.create(
name,
TerrainPlantBlock(
BlockBehaviour.Properties.of()
.mapColor(MapColor.PLANT)
.noCollission()
.instabreak()
.sound(SoundType.GRASS)
.offsetType(BlockBehaviour.OffsetType.XZ)
.ignitedByLava()
.pushReaction(PushReaction.DESTROY)
.lightLevel(lightLevel)
)
)
}
@@ -477,8 +477,8 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField val DUBIOUS_DISC = itemNameBlockItem("dubious_disc", CobblemonBlocks.DUBIOUS_DISC)
@JvmField val DEEP_SEA_SCALE = noSettingsItem("deep_sea_scale")
@JvmField val DEEP_SEA_TOOTH = noSettingsItem("deep_sea_tooth")
@JvmField val ELECTIRIZER = noSettingsItem("electirizer")
@JvmField val MAGMARIZER = noSettingsItem("magmarizer")
@JvmField val ELECTIRIZER = itemNameBlockItem("electirizer", CobblemonBlocks.ELECTIRIZER)
@JvmField val MAGMARIZER = itemNameBlockItem("magmarizer", CobblemonBlocks.MAGMARIZER)
@JvmField val OVAL_STONE = noSettingsItem("oval_stone")
@JvmField val PROTECTOR = noSettingsItem("protector")
@JvmField val REAPER_CLOTH = noSettingsItem("reaper_cloth")
@@ -711,7 +711,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val PEP_UP_FLOWER = compostableBlockItem("pep_up_flower", CobblemonBlocks.PEP_UP_FLOWER)
@JvmField
val MEDICINAL_BREW = noSettingsItem("medicinal_brew")
val MEDICINAL_BREW = itemNameBlockItem("medicinal_brew", CobblemonBlocks.MEDICINAL_BREW)
@JvmField
val REMEDY = compostableItem("remedy", RemedyItem(RemedyItem.NORMAL), 0.65f)
@JvmField
@@ -719,7 +719,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val SUPERB_REMEDY = compostableItem("superb_remedy", RemedyItem(RemedyItem.SUPERB), 1f)
@JvmField
val MOOMOO_MILK = create("moomoo_milk", MoomooMilk(Item.Properties().craftRemainder(Items.GLASS_BOTTLE).stacksTo((16))))
val MOOMOO_MILK = create("moomoo_milk", MoomooMilk(Item.Properties().craftRemainder(Items.GLASS_BOTTLE).stacksTo((16)), CobblemonBlocks.MOOMOO_MILK))
@JvmField
val POTION = create("potion", PotionItem(PotionType.POTION, CobblemonBlocks.POTION))
@@ -748,27 +748,27 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val RED_MINT_SEEDS = mintSeed("red", MintType.RED.getCropBlock())
@JvmField
val RED_MINT_LEAF = mintLeaf("red", MintLeafItem(MintType.RED))
val RED_MINT_LEAF = compostableItemNameBlockItem("red_mint_leaf", CobblemonBlocks.RED_MINT_LEAF_LITTER, 0.50f)
@JvmField
val BLUE_MINT_SEEDS = mintSeed("blue", MintType.BLUE.getCropBlock())
@JvmField
val BLUE_MINT_LEAF = mintLeaf("blue", MintLeafItem(MintType.BLUE))
val BLUE_MINT_LEAF = compostableItemNameBlockItem("blue_mint_leaf", CobblemonBlocks.BLUE_MINT_LEAF_LITTER, 0.50f)
@JvmField
val CYAN_MINT_SEEDS = mintSeed("cyan", MintType.CYAN.getCropBlock())
@JvmField
val CYAN_MINT_LEAF = mintLeaf("cyan", MintLeafItem(MintType.CYAN))
val CYAN_MINT_LEAF = compostableItemNameBlockItem("cyan_mint_leaf", CobblemonBlocks.CYAN_MINT_LEAF_LITTER, 0.50f)
@JvmField
val PINK_MINT_SEEDS = mintSeed("pink", MintType.PINK.getCropBlock())
@JvmField
val PINK_MINT_LEAF = mintLeaf("pink", MintLeafItem(MintType.PINK))
val PINK_MINT_LEAF = compostableItemNameBlockItem("pink_mint_leaf", CobblemonBlocks.PINK_MINT_LEAF_LITTER, 0.50f)
@JvmField
val GREEN_MINT_SEEDS = mintSeed("green", MintType.GREEN.getCropBlock())
@JvmField
val GREEN_MINT_LEAF = mintLeaf("green", MintLeafItem(MintType.GREEN))
val GREEN_MINT_LEAF = compostableItemNameBlockItem("green_mint_leaf", CobblemonBlocks.GREEN_MINT_LEAF_LITTER, 0.50f)
@JvmField
val WHITE_MINT_SEEDS = mintSeed("white", MintType.WHITE.getCropBlock())
@JvmField
val WHITE_MINT_LEAF = mintLeaf("white", MintLeafItem(MintType.WHITE))
val WHITE_MINT_LEAF = compostableItemNameBlockItem("white_mint_leaf", CobblemonBlocks.WHITE_MINT_LEAF_LITTER, 0.50f)
val mints = mutableMapOf<String, MintItem>()
@@ -969,7 +969,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val EJECT_PACK = heldItem("eject_pack")
@JvmField
val ELECTRIC_SEED = heldItem("electric_seed")
val ELECTRIC_SEED = itemNameBlockItem("electric_seed", CobblemonBlocks.ELECTRIC_SEED)
@JvmField
val EVERSTONE = heldItem("everstone")
@JvmField
@@ -985,7 +985,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val FOCUS_SASH = heldItem("focus_sash")
@JvmField
val GRASSY_SEED = heldItem("grassy_seed")
val GRASSY_SEED = itemNameBlockItem("grassy_seed", CobblemonBlocks.GRASSY_SEED)
@JvmField
val GRIP_CLAW = heldItem("grip_claw")
@JvmField
@@ -1033,7 +1033,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val POWER_WEIGHT = heldItem("power_weight")
@JvmField
val PSYCHIC_SEED = heldItem("psychic_seed")
val PSYCHIC_SEED = itemNameBlockItem("psychic_seed", CobblemonBlocks.PSYCHIC_SEED)
@JvmField
val PROTECTIVE_PADS = heldItem("protective_pads")
@JvmField
@@ -1057,7 +1057,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val SILVER_POWDER = heldItem("silver_powder")
@JvmField
val SOFT_SAND = heldItem("soft_sand")
val SOFT_SAND = itemNameBlockItem("soft_sand", CobblemonBlocks.SOFT_SAND)
@JvmField
val SMOKE_BALL = itemNameBlockItem("smoke_ball", CobblemonBlocks.SMOKE_BALL)
@JvmField
@@ -1065,7 +1065,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val STICKY_BARB = heldItem("sticky_barb")
@JvmField
val TERRAIN_EXTENDER = heldItem("terrain_extender")
val TERRAIN_EXTENDER = itemNameBlockItem("terrain_extender", CobblemonBlocks.TERRAIN_EXTENDER)
@JvmField
val THROAT_SPRAY = itemNameBlockItem("throat_spray", CobblemonBlocks.THROAT_SPRAY)
@JvmField
@@ -1085,13 +1085,13 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val MIRROR_HERB = compostableHeldItem("mirror_herb", null, 0.85F)
@JvmField
val MISTY_SEED = heldItem("misty_seed")
val MISTY_SEED = itemNameBlockItem("misty_seed", CobblemonBlocks.MISTY_SEED)
@JvmField
val POWER_HERB = compostableHeldItem("power_herb", null, 0.85F)
@JvmField
val WHITE_HERB = compostableHeldItem("white_herb", null, 0.85F)
@JvmField
val BRIGHT_POWDER = itemNameBlockItem("bright_powder", CobblemonBlocks.BRIGHT_POWDER)
val BRIGHT_POWDER = glowItem("bright_powder", CobblemonBlocks.BRIGHT_POWDER)
@JvmField
val METAL_POWDER = itemNameBlockItem("metal_powder", CobblemonBlocks.METAL_POWDER)
@JvmField
@@ -1103,7 +1103,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val SMOOTH_ROCK = itemNameBlockItem("smooth_rock", CobblemonBlocks.SMOOTH_ROCK)
@JvmField
val ICY_ROCK = itemNameBlockItem("icy_rock", CobblemonBlocks.ICY_ROCK)
val ICY_ROCK = heldItem("icy_rock")
// Mulch
@JvmField
@@ -1558,6 +1558,25 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
//@JvmField
//val BINDING_SOIL = blockItem("binding_soil", CobblemonBlocks.BINDING_SOIL)
// Plants, Flora, and Flowers
@JvmField
val LOBAILA = compostableBlockItem("lobaila", CobblemonBlocks.LOBAILA, 0.65f)
@JvmField
val POMOHANA = compostableBlockItem("pomohana", CobblemonBlocks.POMOHANA, 0.3f)
@JvmField
val PAUVILLEA = compostableBlockItem("pauvillea", CobblemonBlocks.PAUVILLEA, 0.65f)
@JvmField
val WISTENSU = compostableBlockItem("wistensu", CobblemonBlocks.WISTENSU, 0.5f)
@JvmField
val VOLTACONIA = compostableBlockItem("voltaconia", CobblemonBlocks.VOLTACONIA, 0.85f)
@JvmField
val VERDROOT = compostableBlockItem("verdroot", CobblemonBlocks.VERDROOT, 0.85f)
@JvmField
val MISTLEAF = compostableBlockItem("mistleaf", CobblemonBlocks.MISTLEAF, 0.85f)
@JvmField
val LUCIBELL = compostableBlockItem("lucibell", CobblemonBlocks.LUCIBELL, 0.85f)
private fun blockItem(name: String, block: Block, rarity: Rarity = Rarity.COMMON): BlockItem = this.create(name, BlockItem(block, Item.Properties().rarity(rarity)))
private fun itemNameBlockItem(name: String, block: Block, rarity: Rarity = Rarity.COMMON): ItemNameBlockItem = this.create(name, ItemNameBlockItem(block, Item.Properties().rarity(rarity)))
@@ -1760,4 +1779,6 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
private fun foodItem(nutrition: Int, saturationModifier: Float): Item {
return CobblemonItem(Item.Properties().food(FoodProperties.Builder().nutrition(nutrition).saturationModifier(saturationModifier).build()))
}
private fun glowItem(name: String, block: Block): GlowItem = this.create(name, GlowItem(block, Item.Properties()))
}
@@ -22,6 +22,7 @@ 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.RideSettingsSyncHandler
import com.cobblemon.mod.common.client.net.debug.OpenRidingStatsDebugGUIHandler
import com.cobblemon.mod.common.client.net.debug.OpenSpeedTunerHandler
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
@@ -83,6 +84,7 @@ import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyMoveCallba
import com.cobblemon.mod.common.net.messages.client.cooking.SeasoningRegistrySyncPacket
import com.cobblemon.mod.common.net.messages.client.cooking.ToggleCookingPotLidPacket
import com.cobblemon.mod.common.net.messages.client.data.*
import com.cobblemon.mod.common.net.messages.client.debug.OpenSpeedTunerPacket
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
@@ -173,6 +175,9 @@ import com.cobblemon.mod.common.net.messages.server.callback.party.PartySelectCa
import com.cobblemon.mod.common.net.messages.server.callback.partymove.PartyMoveSelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.partymove.PartyPokemonMoveSelectedPacket
import com.cobblemon.mod.common.net.messages.server.debug.OpenRidingStatsDebugGUIPacket
import com.cobblemon.mod.common.net.messages.server.debug.RequestOpenSpeedTunerPacket
import com.cobblemon.mod.common.net.messages.server.debug.SetSpeedTunerSprintSpeedPacket
import com.cobblemon.mod.common.net.messages.server.debug.SetSpeedTunerWalkSpeedPacket
import com.cobblemon.mod.common.net.messages.server.debug.ServerboundUpdateRidingSettingsPacket
import com.cobblemon.mod.common.net.messages.server.debug.ServerboundUpdateRidingStatRangePacket
import com.cobblemon.mod.common.net.messages.server.debug.ServerboundUpdateRidingStatsPacket
@@ -231,6 +236,9 @@ import com.cobblemon.mod.common.net.serverhandling.callback.party.PartySelectCan
import com.cobblemon.mod.common.net.serverhandling.callback.partymove.PartyMoveSelectCancelledHandler
import com.cobblemon.mod.common.net.serverhandling.callback.partymove.PartyPokemonMoveSelectedHandler
import com.cobblemon.mod.common.net.serverhandling.debug.RequestOpenRidingStatsDebugGUIHandler
import com.cobblemon.mod.common.net.serverhandling.debug.RequestOpenSpeedTunerHandler
import com.cobblemon.mod.common.net.serverhandling.debug.SetSpeedTunerSprintSpeedHandler
import com.cobblemon.mod.common.net.serverhandling.debug.SetSpeedTunerWalkSpeedHandler
import com.cobblemon.mod.common.net.serverhandling.debug.ServerboundUpdateRidingSettingsHandler
import com.cobblemon.mod.common.net.serverhandling.debug.ServerboundUpdateRidingStatRangeHandler
import com.cobblemon.mod.common.net.serverhandling.debug.ServerboundUpdateRidingStatsHandler
@@ -517,6 +525,7 @@ object CobblemonNetwork {
// Debug
list.add(PacketRegisterInfo(OpenRidingStatsDebugGUIPacket.ID, OpenRidingStatsDebugGUIPacket::decode, OpenRidingStatsDebugGUIHandler))
list.add(PacketRegisterInfo(OpenSpeedTunerPacket.ID, OpenSpeedTunerPacket::decode, OpenSpeedTunerHandler))
return list
@@ -649,6 +658,9 @@ object CobblemonNetwork {
// Debug / cheats
list.add(PacketRegisterInfo(RequestOpenRidingStatsDebugGUIPacket.ID, RequestOpenRidingStatsDebugGUIPacket::decode, RequestOpenRidingStatsDebugGUIHandler))
list.add(PacketRegisterInfo(RequestOpenSpeedTunerPacket.ID, RequestOpenSpeedTunerPacket::decode, RequestOpenSpeedTunerHandler))
list.add(PacketRegisterInfo(SetSpeedTunerSprintSpeedPacket.ID, SetSpeedTunerSprintSpeedPacket::decode, SetSpeedTunerSprintSpeedHandler))
list.add(PacketRegisterInfo(SetSpeedTunerWalkSpeedPacket.ID, SetSpeedTunerWalkSpeedPacket::decode, SetSpeedTunerWalkSpeedHandler))
return list
@@ -148,6 +148,9 @@ object CobblemonSounds : PlatformRegistry<Registry<SoundEvent>, ResourceKey<Regi
@JvmField
val MONITOR_GLITCHING = this.create("block.monitor.glitching")
@JvmField
val BRIGHT_POWDER_USE = this.create("block.bright_powder.use")
@JvmField
val FOSSIL_MACHINE_ACTIVATE = this.create("block.fossil_machine.activate")
@JvmField
@@ -22,17 +22,22 @@ import net.minecraft.world.entity.ai.memory.MemoryModuleType
class MoveToOwnerTaskConfig : SingleTaskConfig {
val condition: ExpressionOrEntityVariable = Either.left("true".asExpression())
var completionRange: ExpressionOrEntityVariable = Either.left("4.0".asExpression())
// Gap between the pokemon's hitbox and the owner's where it stops
var completionRange: ExpressionOrEntityVariable = Either.left("2.0".asExpression())
var speedMultiplier: ExpressionOrEntityVariable = Either.left("0.4".asExpression())
var teleportDistance: ExpressionOrEntityVariable = Either.left("24.0".asExpression())
var maxDistance: ExpressionOrEntityVariable = Either.left("14.0".asExpression())
// Gap between the pokemon's hitbox and the owner's before it sprints to catch up. Past maxDistance so short
// catch ups are walked
var sprintDistance: ExpressionOrEntityVariable = Either.left("14.0".asExpression())
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) = listOf(
condition,
completionRange,
speedMultiplier,
teleportDistance,
maxDistance
maxDistance,
sprintDistance
).asVariables()
override fun createTask(
@@ -49,7 +54,8 @@ class MoveToOwnerTaskConfig : SingleTaskConfig {
completionRange = completionRange.asExpression(),
speedMultiplier = speedMultiplier.asExpression(),
teleportDistance = teleportDistance.asExpression(),
maxDistance = maxDistance.asExpression()
maxDistance = maxDistance.asExpression(),
sprintDistance = sprintDistance.asExpression()
),
PokemonEntity::class.java
)
@@ -54,10 +54,15 @@ object PokemonEntityMoLangFunctions : AbstractMoLangFunctionHolder<PokemonEntity
}
}
functions["is_sprinting"] = {
pokemonEntity.ifRidingAvailableSupply(DoubleValue.ZERO) { behaviour, settings, state ->
val moValue = behaviour.asMoLangValue(settings, state, pokemonEntity)
return@ifRidingAvailableSupply moValue.functions["sprinting"]?.apply(MoParams.EMPTY) as? MoValue
?: DoubleValue.ZERO
// Ridden pokemon sprint from the ride state, otherwise from the AI
if (!pokemonEntity.hasControllingPassenger()) {
DoubleValue(pokemonEntity.isPokemonSprinting)
} else {
pokemonEntity.ifRidingAvailableSupply(DoubleValue.ZERO) { behaviour, settings, state ->
val moValue = behaviour.asMoLangValue(settings, state, pokemonEntity)
return@ifRidingAvailableSupply moValue.functions["sprinting"]?.apply(MoParams.EMPTY) as? MoValue
?: DoubleValue.ZERO
}
}
}
functions["is_drifting"] = {
@@ -151,6 +151,7 @@ object CobblemonPermissions {
@JvmStatic
val USE_RIDING_STATS_DEBUG = this.create("useridingstatsdebug", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
val USE_SPEED_TUNER = this.create("usespeedtuner", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
fun all(): Iterable<Permission> = this.permissions
@@ -18,50 +18,49 @@ import org.joml.Quaternionf
import org.joml.Vector3f
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.sign
import kotlin.math.sin
/**
* Class to handle the updating and containment of ride data used in animation.
* Class to handle the updating and holding of motion data used in animation. Motion values accumulate for every
* pokemon on the client, some only while ridden.
*
* @author Jackowes
* @since April 7th, 2025
*/
class RidingAnimationData(val ride: PokemonEntity)
class DynamicAnimationData(val ride: PokemonEntity)
{
companion object {
// Blend time for motion values, they catch up within 1.5x this. Longer is smoother but lags more
const val MOTION_BLEND_SECONDS = 0.8F
// Rider input comes straight from key presses so it catches up quicker
const val INPUT_BLEND_SECONDS = 0.3F
}
var prevOrientation: Matrix3f = Matrix3f()
var prevRot: Vec3 = Vec3.ZERO
var velocitySpring: Vec3Spring = Vec3Spring()
var rotSpring: Vec3Spring = Vec3Spring()
var rotDeltaSpring: Vec3Spring = Vec3Spring()
var localVelocitySpring: Vec3Spring = Vec3Spring()
var driverInputSpring: Vec3Spring = Vec3Spring()
var diveSpring: Vec3Spring = Vec3Spring()
var velocitySpring = Vec3Ease(MOTION_BLEND_SECONDS)
var rotSpring = Vec3Ease(MOTION_BLEND_SECONDS, isAngle = true)
var rotDeltaSpring = Vec3Ease(MOTION_BLEND_SECONDS)
var localVelocitySpring = Vec3Ease(MOTION_BLEND_SECONDS)
var driverInputSpring = Vec3Ease(INPUT_BLEND_SECONDS)
var diveSpring = Vec3Ease(MOTION_BLEND_SECONDS)
private var initialized = false
private var dismounted = false
/** Called on dismount. Motion keeps easing on its own, only the ride orientation needs to start fresh. */
fun clear() {
prevOrientation = Matrix3f()
prevRot = Vec3.ZERO
velocitySpring = Vec3Spring()
rotSpring = Vec3Spring()
rotDeltaSpring = Vec3Spring()
localVelocitySpring = Vec3Spring()
driverInputSpring = Vec3Spring()
diveSpring = Vec3Spring()
dismounted = true
}
fun update() {
val activeRide = ride.hasControllingPassenger()
if (!activeRide) return
var currRot = Vec3.ZERO
var angDelta = Vec3.ZERO
val stiffness = 90.0
val damping = 18.0
// Update velocity first
val currentVelocity = Vec3(
ride.x - ride.xOld,
@@ -69,7 +68,7 @@ class RidingAnimationData(val ride: PokemonEntity)
ride.z - ride.zOld
)
val r = ride.ifRidingAvailableSupply(false) { behaviour, settings, state ->
val r = activeRide && ride.ifRidingAvailableSupply(false) { behaviour, settings, state ->
behaviour.shouldRoll(settings, state, ride);
}
if( r ) {
@@ -118,22 +117,39 @@ class RidingAnimationData(val ride: PokemonEntity)
val localVelocityF = Matrix3f(orientation).transpose().transform(velocityWorldF)
val localVelocity = Vec3(localVelocityF.x.toDouble(), localVelocityF.y.toDouble(), localVelocityF.z.toDouble())
localVelocitySpring.update(localVelocity, stiffness, damping)
localVelocitySpring.update(localVelocity)
// Update previous values
prevOrientation.set(currOrientation)
} else {
val currPitch = ride.xRot
// Pitch only means something while ridden
val currPitch = if (activeRide) ride.xRot else 0.0f
val currYaw = ride.yRot
val currRoll = 0.0f
currRot = Vec3(currPitch.toDouble(), currYaw.toDouble(), currRoll.toDouble())
// Start from where the pokemon already is instead of turning in from zero
if (!initialized) {
prevRot = currRot
rotSpring.snap(currRot)
initialized = true
}
// measure turning from here rather than as one big turn
if (dismounted) {
prevRot = currRot
dismounted = false
}
//Scale rotation delta by 20 to represent degree per second instead of tick
angDelta = Vec3(
(currRot.x - prevRot.x),
Mth.degreesDifference(currRot.y.toFloat(), prevRot.y.toFloat()).toDouble(),
Mth.degreesDifference(currRot.z.toFloat(), prevRot.z.toFloat()).toDouble()
).scale(20.0)
// Unridden turning comes straight from the heading ease, it's already smooth
if (!activeRide) {
angDelta = Vec3(angDelta.x, ride.heading.yaw.velocity().toDouble() * -0.5, angDelta.z)
}
/******************************************************
* Calculate ride local velocity
@@ -147,7 +163,7 @@ class RidingAnimationData(val ride: PokemonEntity)
val localVelocityF = Matrix3f(orientationNonRollable).transpose().transform(velocityWorldF)
val localVelocity = Vec3(localVelocityF.x.toDouble(), localVelocityF.y.toDouble(), localVelocityF.z.toDouble())
localVelocitySpring.update(localVelocity, stiffness, damping)
localVelocitySpring.update(localVelocity)
}
/******************************************************
@@ -159,14 +175,14 @@ class RidingAnimationData(val ride: PokemonEntity)
driver != null && driver.isShiftKeyDown -> -1.0
else -> 0.0
}
val input = if(driver != null) Vec3(driver.xxa.toDouble() * -1, vertInput, driver.zza.toDouble()) else Vec3.ZERO
driverInputSpring.update(input, stiffness * 2, damping)
rotDeltaSpring.update(angDelta, stiffness, damping)
velocitySpring.update(currentVelocity, stiffness, damping)
rotSpring.update(currRot, stiffness, damping)
val input = if(driver != null && activeRide) Vec3(driver.xxa.toDouble() * -1, vertInput, driver.zza.toDouble()) else Vec3.ZERO
driverInputSpring.update(input)
if (activeRide) rotDeltaSpring.update(angDelta) else rotDeltaSpring.follow(angDelta)
velocitySpring.update(currentVelocity)
rotSpring.update(currRot)
val diving = abs(max(sin(currRot.x.toRadians()),0f))
diveSpring.update(Vec3(0.0,diving.toDouble(),0.0), stiffness, damping)
diveSpring.update(Vec3(0.0,diving.toDouble(),0.0))
prevRot = currRot
}
}
@@ -0,0 +1,109 @@
/*
* 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.riding.util
import com.cobblemon.mod.common.util.math.InertialVals
import com.cobblemon.mod.common.util.math.smoothstep
import com.cobblemon.mod.common.util.math.smoothstepDt
import com.cobblemon.mod.common.util.math.smoothstepSolve
import net.minecraft.util.Mth
import net.minecraft.world.phys.Vec3
/**
* Eases a Vec3 towards a target with inertial easing, see SpringUtils and https://theorangeduck.com/page/inertial-easing
* When the target changes each component keeps its current velocity
*
* With [isAngle] the components are treated as degrees and always ease the short way round.
*/
class Vec3Ease(
private val blendTime: Float,
private val isAngle: Boolean = false
) {
companion object {
const val SECONDS_PER_TICK = 0.05F
}
var value: Vec3 = Vec3.ZERO
private set
private val axes = Array(3) { InertialVals() }
// Last tick's value while following a value that's already smooth, null while easing
private var followedFrom: Vec3? = null
// How far along the curve one tick is
private val step = SECONDS_PER_TICK / blendTime
private var target = Vec3.ZERO
/** Moves one tick towards [newTarget] */
fun update(newTarget: Vec3) {
followedFrom = null
val current = doubleArrayOf(value.x, value.y, value.z)
val oldGoal = doubleArrayOf(target.x, target.y, target.z)
val goal = doubleArrayOf(newTarget.x, newTarget.y, newTarget.z)
for (i in 0..2) {
val vals = axes[i]
if (goal[i] != oldGoal[i]) {
val difference = if (isAngle) Mth.wrapDegrees(goal[i] - current[i]) else goal[i] - current[i]
// Velocity along the current curve, carried into the new one so there's no sudden change
smoothstepSolve(vals, difference.toFloat(), smoothstepDt(vals.t, vals.s))
vals.offset = current[i].toFloat() - smoothstep(vals.t, vals.s)
}
vals.t += step
current[i] = (vals.offset + smoothstep(vals.t, vals.s)).toDouble()
if (isAngle) {
// Keep turning angles in range so they don't grow forever
val turns = current[i] - Mth.wrapDegrees(current[i])
if (turns != 0.0) {
vals.offset -= turns.toFloat()
current[i] -= turns
}
}
}
target = newTarget
value = Vec3(current[0], current[1], current[2])
}
/** Takes [to] as is for something already smooth. Still blends from last tick's value between ticks */
fun follow(to: Vec3) {
val from = value
snap(to)
followedFrom = from
}
/** Jumps straight to [to] with no easing */
fun snap(to: Vec3) {
followedFrom = null
val components = floatArrayOf(to.x.toFloat(), to.y.toFloat(), to.z.toFloat())
for (i in 0..2) {
axes[i].t = 1F
axes[i].s = 0F
axes[i].offset = components[i]
}
target = to
value = to
}
/** Value between last tick and this one read straight off the curve. [lookBackTick] isn't used anymore */
fun getInterpolated(partialTick: Double, @Suppress("UNUSED_PARAMETER") lookBackTick: Int = 0): Vec3 {
followedFrom?.let { from ->
if (!isAngle) return from.add(value.subtract(from).scale(partialTick))
// Short way around for angles
return Vec3(
Mth.wrapDegrees(from.x + Mth.wrapDegrees(value.x - from.x) * partialTick),
Mth.wrapDegrees(from.y + Mth.wrapDegrees(value.y - from.y) * partialTick),
Mth.wrapDegrees(from.z + Mth.wrapDegrees(value.z - from.z) * partialTick)
)
}
val back = step * (1 - partialTick.toFloat())
val x = axes[0].let { it.offset + smoothstep(it.t - back, it.s) }.toDouble()
val y = axes[1].let { it.offset + smoothstep(it.t - back, it.s) }.toDouble()
val z = axes[2].let { it.offset + smoothstep(it.t - back, it.s) }.toDouble()
// Can land just past ±180 partway through a tick
return if (isAngle) Vec3(Mth.wrapDegrees(x), Mth.wrapDegrees(y), Mth.wrapDegrees(z)) else Vec3(x, y, z)
}
}
@@ -1,68 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.riding.util
import net.minecraft.world.phys.Vec3
import kotlin.math.min
/**
* A spring-based smoother for Vec3 values, designed for applying damped spring motion to riding data.
* Stores the previous and current values, along with a history buffer for lookback-based interpolation.
*
* @author Jackowes
* @since March 27th, 2025
*/
class Vec3Spring(
var value: Vec3 = Vec3.ZERO,
var velocity: Vec3 = Vec3.ZERO
) {
val history = ArrayDeque<Vec3>()
//Hold up to a seconds worth of values
val lookBackSize = 20
fun update(target: Vec3, stiffness: Double, damping: Double) {
//Hooke's Law f = kx where:
//f is force
//k is the spring constant or stiffness of this spring
//x is the distance from rest and in this case the difference between the most recent
//rotation delta ( the target ) and the springs current value for delta rot
val force = target.subtract(value).scale(stiffness)
//Now calculate acceleration and apply damping
val accel = force.subtract(velocity.scale(damping))
velocity = velocity.add(accel.scale(1.0/20.0))
value = value.add(velocity.scale(1.0/20.0))
history.addFirst(value)
//Adding one since if we want to check max look back point it needs something to interp with.
if( history.size > lookBackSize + 1) {
history.removeLast()
}
}
fun getInterpolated(partialTick: Double, lookBackTick: Int): Vec3 {
if (history.size < 2) {
return value // or Vec3.ZERO if you prefer
}
val tick = min(lookBackTick, history.size - 2)
var ret = Vec3.ZERO
for( i in 0..tick) {
val older = history.get(i + 1).scale(1.0)
val newer = history.get(i).scale(1.0)
ret = ret.add(older.add(newer.subtract(older).scale(partialTick)))
}
return ret.scale(1.0/(tick+1))
}
}
@@ -34,6 +34,7 @@ class ElementalType(
val resourceLocation: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Cobblemon.MODID, "ui/types.png"),
val showdownId: String = ShowdownIdentifiable.REGEX.replace(name.lowercase(), ""),
val typeGem: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Cobblemon.MODID, showdownId + "_gem"),
val order: Int = 1000
) : ShowdownIdentifiable {
override fun showdownId(): String {
@@ -25,7 +25,8 @@ object ElementalTypes {
hue = 0xE8E8DA,
textureXMultiplier = 0,
primaryColor = 0x8FA689,
secondaryColor = 0xFFFBE2
secondaryColor = 0xFFFBE2,
order = 0
)
@JvmField
@@ -35,7 +36,8 @@ object ElementalTypes {
hue = 0xFF6E21,
textureXMultiplier = 1,
primaryColor = 0xF92D2D,
secondaryColor = 0xF99B1D
secondaryColor = 0xF99B1D,
order = 1
)
@JvmField
@@ -45,7 +47,8 @@ object ElementalTypes {
hue = 0x3FA5FF,
textureXMultiplier = 2,
primaryColor = 0x377AF6,
secondaryColor = 0x50DDF9
secondaryColor = 0x50DDF9,
order = 2
)
@JvmField
@@ -55,7 +58,8 @@ object ElementalTypes {
hue = 0x62D14F,
textureXMultiplier = 3,
primaryColor = 0x0EAE95,
secondaryColor = 0xC6F554
secondaryColor = 0xC6F554,
order = 3
)
@JvmField
@@ -65,7 +69,8 @@ object ElementalTypes {
hue = 0xFFD314,
textureXMultiplier = 4,
primaryColor = 0xF7AB19,
secondaryColor = 0xFFF257
secondaryColor = 0xFFF257,
order = 4
)
@JvmField
@@ -75,7 +80,8 @@ object ElementalTypes {
hue = 0x54F2F2,
textureXMultiplier = 5,
primaryColor = 0x41BAED,
secondaryColor = 0x91F4F9
secondaryColor = 0x91F4F9,
order = 5
)
@JvmField
@@ -85,7 +91,8 @@ object ElementalTypes {
hue = 0xEF565D,
textureXMultiplier = 6,
primaryColor = 0xC8356B,
secondaryColor = 0xF36A3F
secondaryColor = 0xF36A3F,
order = 6
)
@JvmField
@@ -95,7 +102,8 @@ object ElementalTypes {
hue = 0xD651FF,
textureXMultiplier = 7,
primaryColor = 0x7B41DD,
secondaryColor = 0xE568F4
secondaryColor = 0xE568F4,
order = 7
)
@JvmField
@@ -105,7 +113,8 @@ object ElementalTypes {
hue = 0xF4A453,
textureXMultiplier = 8,
primaryColor = 0xE08835,
secondaryColor = 0xFFD057
secondaryColor = 0xFFD057,
order = 8
)
@JvmField
@@ -115,7 +124,8 @@ object ElementalTypes {
hue = 0xB8B2FF,
textureXMultiplier = 9,
primaryColor = 0x6083F6,
secondaryColor = 0xD5CCFA
secondaryColor = 0xD5CCFA,
order = 9
)
@JvmField
@@ -125,7 +135,8 @@ object ElementalTypes {
hue = 0xFF5E9E,
textureXMultiplier = 10,
primaryColor = 0xE553CE,
secondaryColor = 0xF69DD5
secondaryColor = 0xF69DD5,
order = 10
)
@JvmField
@@ -135,7 +146,8 @@ object ElementalTypes {
hue = 0xD3D319,
textureXMultiplier = 11,
primaryColor = 0x9FBE1B,
secondaryColor = 0xE9FB5B
secondaryColor = 0xE9FB5B,
order = 11
)
@JvmField
@@ -145,7 +157,8 @@ object ElementalTypes {
hue = 0xB7A16E,
textureXMultiplier = 12,
primaryColor = 0x9F814A,
secondaryColor = 0xEECB7A
secondaryColor = 0xEECB7A,
order = 12
)
@JvmField
@@ -155,7 +168,8 @@ object ElementalTypes {
hue = 0x9C80F7,
textureXMultiplier = 13,
primaryColor = 0x4F57DD,
secondaryColor = 0xC182F1
secondaryColor = 0xC182F1,
order = 13
)
@JvmField
@@ -165,7 +179,8 @@ object ElementalTypes {
hue = 0x7580FF,
textureXMultiplier = 14,
primaryColor = 0x4E68E2,
secondaryColor = 0x47C7F7
secondaryColor = 0x47C7F7,
order = 14
)
@JvmField
@@ -175,7 +190,8 @@ object ElementalTypes {
hue = 0x587DA0,
textureXMultiplier = 15,
primaryColor = 0x465B99,
secondaryColor = 0x689EBF
secondaryColor = 0x689EBF,
order = 15
)
@JvmField
@@ -185,7 +201,8 @@ object ElementalTypes {
hue = 0xABD1F4,
textureXMultiplier = 16,
primaryColor = 0x628CB9,
secondaryColor = 0xD6F7F3
secondaryColor = 0xD6F7F3,
order = 16
)
@JvmField
@@ -195,18 +212,21 @@ object ElementalTypes {
hue = 0xFF7FE5,
textureXMultiplier = 17,
primaryColor = 0xE2678C,
secondaryColor = 0xF5A7B7
secondaryColor = 0xF5A7B7,
order = 17
)
@JvmStatic
fun register(name: String, displayName: MutableComponent, hue: Int, textureXMultiplier: Int, primaryColor: Int, secondaryColor: Int): ElementalType {
@JvmOverloads
fun register(name: String, displayName: MutableComponent, hue: Int, textureXMultiplier: Int, primaryColor: Int, secondaryColor: Int, order: Int = 1000): ElementalType {
return ElementalType(
name = name,
displayName = displayName,
hue = hue,
textureXMultiplier = textureXMultiplier,
primaryColor = primaryColor,
secondaryColor = secondaryColor
secondaryColor = secondaryColor,
order = order
).also {
allTypes.add(it)
}
@@ -48,7 +48,8 @@ class DecorativeItemBlock(settings: Properties, val type: DecorativeItemBlockTyp
NEVERMELT,
SCOPE,
WIDE,
ZOOM;
ZOOM,
IZER;
override fun getSerializedName(): String = this.name.lowercase()
@@ -76,6 +77,7 @@ class DecorativeItemBlock(settings: Properties, val type: DecorativeItemBlockTyp
val SCOPE_HITBOX = Shapes.box(0.625, 0.0, 0.4375, 0.9375, 0.125, 0.75)
val WIDE_HITBOX = Shapes.box(0.625, 0.0, 0.125, 0.9375, 0.0625, 0.625)
val ZOOM_HITBOX = Shapes.box(0.125, 0.0, 0.1875, 0.375, 0.1875, 0.5)
val IZER_HITBOX = Shapes.box(0.25, 0.0, 0.25, 0.75, 0.375, 0.75)
fun getShape(type: DecorativeItemBlockType): VoxelShape {
return when (type) {
@@ -87,6 +89,7 @@ class DecorativeItemBlock(settings: Properties, val type: DecorativeItemBlockTyp
DecorativeItemBlockType.SCOPE -> SCOPE_HITBOX
DecorativeItemBlockType.WIDE -> WIDE_HITBOX
DecorativeItemBlockType.ZOOM -> ZOOM_HITBOX
DecorativeItemBlockType.IZER -> IZER_HITBOX
DecorativeItemBlockType.FLATTENED_10x4 -> SMOOSHED4_HITBOX
DecorativeItemBlockType.FLATTENED_8x4 -> FLAT4_HITBOX
DecorativeItemBlockType.FLATTENED_8x6 -> FLAT6_HITBOX
@@ -103,15 +103,15 @@ class MintBlock(private val mintType: MintType, settings: Properties) : CropBloc
GREEN -> CobblemonItems.GREEN_MINT_SEEDS
WHITE -> CobblemonItems.WHITE_MINT_SEEDS
}
fun getLeaf(): MintLeafItem = when (this) {
RED -> CobblemonItems.RED_MINT_LEAF
BLUE -> CobblemonItems.BLUE_MINT_LEAF
CYAN -> CobblemonItems.CYAN_MINT_LEAF
PINK -> CobblemonItems.PINK_MINT_LEAF
GREEN -> CobblemonItems.GREEN_MINT_LEAF
WHITE -> CobblemonItems.WHITE_MINT_LEAF
}
// These don't seem to get called to anywhere besides the direct item, and since I'm trying to do funny stuff there, just commenting this out
// fun getLeaf(): MintLeafItem = when (this) {
// RED -> CobblemonItems.RED_MINT_LEAF
// BLUE -> CobblemonItems.BLUE_MINT_LEAF
// CYAN -> CobblemonItems.CYAN_MINT_LEAF
// PINK -> CobblemonItems.PINK_MINT_LEAF
// GREEN -> CobblemonItems.GREEN_MINT_LEAF
// WHITE -> CobblemonItems.WHITE_MINT_LEAF
// }
fun getCropBlock(): MintBlock = when (this) {
RED -> CobblemonBlocks.RED_MINT
@@ -45,7 +45,9 @@ class StackableItemBlock(settings: Properties, val type: StackableItemBlockType)
IRON_BALL,
THROAT_SPRAY,
UPGRADE,
DICE;
DICE,
GLASS_BOTTLE,
EXTENDER;
override fun getSerializedName(): String = this.name.lowercase()
@@ -133,6 +135,18 @@ class StackableItemBlock(settings: Properties, val type: StackableItemBlockType)
Shapes.box(0.125, 0.0, 0.625, 0.5625, 0.1875, 0.875),
Shapes.box(0.125, 0.0, 0.4375, 0.5625, 0.1875, 0.875),
Shapes.box(0.125, 0.0, 0.4375, 0.5625, 0.375, 0.875)
),
StackableItemBlockType.GLASS_BOTTLE to listOf(
Shapes.box(0.375, 0.0, 0.375, 0.625, 0.375, 0.625),
Shapes.box(0.1875, 0.0, 0.1875, 0.8125, 0.375, 0.6875),
Shapes.box(0.125, 0.0, 0.125, 0.8125, 0.375, 0.875),
Shapes.box(0.125, 0.0, 0.125, 0.875, 0.375, 0.875)
),
StackableItemBlockType.EXTENDER to listOf(
Shapes.box(0.25, 0.0, 0.375, 0.625, 0.5625, 0.625),
Shapes.box(0.125, 0.0, 0.25, 0.875, 0.5625, 0.75),
Shapes.box(0.0625, 0.0, 0.0625, 0.875, 0.5625, 0.875),
Shapes.box(0.0625, 0.0, 0.0625, 0.9375, 0.5625, 0.9375)
)
)
@@ -0,0 +1,59 @@
/*
* 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.decor
import net.minecraft.core.BlockPos
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.SimpleWaterloggedBlock
import net.minecraft.world.level.block.TallFlowerBlock
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf
import net.minecraft.world.level.material.FluidState
import net.minecraft.world.level.material.Fluids
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
class LobailaFlowerBlock(properties: Properties?) : TallFlowerBlock(properties), SimpleWaterloggedBlock {
init {
registerDefaultState(stateDefinition.any()
.setValue(HALF, DoubleBlockHalf.LOWER)
.setValue(WATERLOGGED, false))
}
override fun getFluidState(blockState: BlockState): FluidState? {
return if (blockState.getValue(WATERLOGGED)) Fluids.WATER.getSource(false) else super.getFluidState(blockState);
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(HALF, WATERLOGGED)
}
override fun getStateForPlacement(context: BlockPlaceContext): BlockState? {
val fluidState = context.level.getFluidState(context.clickedPos)
return this.defaultBlockState()
.setValue(
WATERLOGGED,
fluidState.type === Fluids.WATER
)
}
override fun getShape(state: BlockState, world: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape = AABB
companion object {
private val AABB = Shapes.box(0.125, 0.0, 0.125, 0.875, 1.0, 0.875)
}
}
@@ -0,0 +1,87 @@
/*
* 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.decor
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.block.entity.MistleafFlowerBlockEntity
import net.minecraft.core.BlockPos
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.Level
import net.minecraft.world.level.LevelReader
import net.minecraft.world.level.block.BaseEntityBlock
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.EntityBlock
import net.minecraft.world.level.block.SoundType
import net.minecraft.world.level.block.TallFlowerBlock
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityTicker
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.state.BlockBehaviour
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf
import net.minecraft.world.level.block.state.properties.IntegerProperty
import net.minecraft.world.level.material.MapColor
import net.minecraft.world.level.material.PushReaction
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
class MistleafFlowerBlock(lightLevel: (BlockState) -> Int) :
EntityBlock,
TallFlowerBlock(
Properties.of()
.mapColor(MapColor.PLANT)
.noCollission()
.instabreak()
.sound(SoundType.GRASS)
.offsetType(OffsetType.XZ)
.ignitedByLava()
.pushReaction(PushReaction.DESTROY)
.lightLevel(lightLevel)
) {
init {
registerDefaultState(stateDefinition.any()
.setValue(HALF, DoubleBlockHalf.LOWER)
.setValue(GLOWING, 0))
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(HALF, GLOWING)
}
override fun isValidBonemealTarget(level: LevelReader, pos: BlockPos, state: BlockState): Boolean {
return false
}
override fun getShape(state: BlockState, world: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape = AABB
override fun newBlockEntity(pos: BlockPos, state: BlockState): BlockEntity? {
return MistleafFlowerBlockEntity(pos, state)
}
override fun <T : BlockEntity?> getTicker(
level: Level,
state: BlockState,
blockEntityType: BlockEntityType<T?>
): BlockEntityTicker<T?>? {
return if (level.isClientSide) null else BaseEntityBlock.createTickerHelper(blockEntityType as BlockEntityType<*>,
CobblemonBlockEntities.MISTLEAF_FLOWER, MistleafFlowerBlockEntity::serverTick) as BlockEntityTicker<T?>?
}
companion object {
private val AABB = Shapes.box(0.125, 0.0, 0.125, 0.875, 1.0, 0.875)
val GLOWING = IntegerProperty.create("glowing", 0, 8)
}
override fun hasAnalogOutputSignal(state: BlockState): Boolean { return true }
override fun getAnalogOutputSignal(state: BlockState, level: Level, pos: BlockPos): Int {
return state.getValue(GLOWING) as Int
}
}
@@ -0,0 +1,104 @@
/*
* 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.decor
import com.mojang.serialization.MapCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.server.level.ServerLevel
import net.minecraft.util.RandomSource
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.Level
import net.minecraft.world.level.LevelAccessor
import net.minecraft.world.level.LevelReader
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.BonemealableBlock
import net.minecraft.world.level.block.DirectionalBlock
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
class PauvilleaFlowerBlock(properties: Properties?) : DirectionalBlock(properties), BonemealableBlock {
init {
registerDefaultState(stateDefinition.any()
.setValue(FACING, Direction.DOWN))
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(FACING)
}
override fun getStateForPlacement(ctx: BlockPlaceContext): BlockState? {
var blockState = defaultBlockState()
val worldView = ctx.level
val blockPos = ctx.clickedPos
blockState = blockState.setValue(FACING, ctx.clickedFace) as BlockState
if (blockState.canSurvive(worldView, blockPos)) {
return blockState
}
return null
}
override fun updateShape(state: BlockState, direction: Direction, neighborState: BlockState, level: LevelAccessor, pos: BlockPos, neighborPos: BlockPos): BlockState {
val facing = state.getValue(FACING)
if (direction == facing.opposite && !canSurvive(state, level, pos)) {
return Blocks.AIR.defaultBlockState()
}
return super.updateShape(state, direction, neighborState, level, pos, neighborPos)
}
override fun canSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
val direction = state.getValue(FACING) as Direction
val blockState = level.getBlockState(pos.relative(direction.opposite))
return blockState.isCollisionShapeFullBlock(level, pos) or blockState.canOcclude()
}
override fun codec(): MapCodec<PauvilleaFlowerBlock> = CODEC
override fun getShape(state: BlockState, blockGetter: BlockGetter, pos: BlockPos, collisionContext: CollisionContext): VoxelShape {
return when (state.getValue(FACING)) {
Direction.DOWN -> CEILING
Direction.NORTH -> WALL_NORTH
Direction.EAST -> WALL_EAST
Direction.SOUTH -> WALL_SOUTH
Direction.WEST -> WALL_WEST
Direction.UP -> FLOOR
}
}
companion object {
val CODEC: MapCodec<PauvilleaFlowerBlock> = RecordCodecBuilder.mapCodec {
it.group(propertiesCodec()).apply(it, ::PauvilleaFlowerBlock)
}
val FLOOR = Shapes.box(0.075, 0.0, 0.075, 0.925, 0.4375, 0.925)
val WALL_NORTH = Shapes.box(0.0, 0.0, 0.8125, 1.0, 1.0, 1.0)
val WALL_EAST = Shapes.box(0.0, 0.0, 0.0, 0.1875, 1.0, 1.0)
val WALL_SOUTH = Shapes.box(0.0, 0.0, 0.0, 1.0, 1.0, 0.1875)
val WALL_WEST = Shapes.box(0.8125, 0.0, 0.0, 1.0, 1.0, 1.0)
val CEILING = Shapes.box(0.075, 0.0, 0.075, 0.925, 1.0, 0.925)
}
override fun isValidBonemealTarget(level: LevelReader?, pos: BlockPos?, state: BlockState?): Boolean {
return true
}
override fun isBonemealSuccess(level: Level?, random: RandomSource?, pos: BlockPos?, state: BlockState?): Boolean {
return true
}
override fun performBonemeal(level: ServerLevel, random: RandomSource?, pos: BlockPos, state: BlockState?) {
popResource(level, pos, ItemStack(this))
}
}
@@ -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.block.decor
import net.minecraft.core.BlockPos
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.block.PinkPetalsBlock
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
class PomohanaFlowerBlock(properties: Properties?) : PinkPetalsBlock(properties) {
override fun getShape(state: BlockState, world: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape = AABB
companion object {
private val AABB = Shapes.box(0.125, 0.0, 0.125, 0.875, 1.0, 0.875)
}
}
@@ -0,0 +1,105 @@
/*
* 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.decor
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.block.entity.TerrainPlantBlockEntity
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
import net.minecraft.util.RandomSource
import net.minecraft.world.entity.Entity
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.Level
import net.minecraft.world.level.LevelReader
import net.minecraft.world.level.block.BaseEntityBlock
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.EntityBlock
import net.minecraft.world.level.block.TallFlowerBlock
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityTicker
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BooleanProperty
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
class TerrainPlantBlock(properties: Properties) : TallFlowerBlock(properties), EntityBlock {
init {
registerDefaultState(stateDefinition.any()
.setValue(HALF, DoubleBlockHalf.LOWER)
.setValue(GLOWING, false))
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(HALF, GLOWING)
}
override fun isValidBonemealTarget(level: LevelReader, pos: BlockPos, state: BlockState): Boolean {
return false
}
override fun tick(state: BlockState, level: ServerLevel, pos: BlockPos, random: RandomSource) {
if (state.`is`(CobblemonBlocks.LUCIBELL)) { this.checkInside(level, pos, state) }
}
private fun checkInside(level: Level, pos: BlockPos, state: BlockState) {
// Only lower will handle this
val upperState = level.getBlockState(pos.above()).takeIf { it.`is`(CobblemonBlocks.LUCIBELL) && it.getValue(HALF) == DoubleBlockHalf.UPPER }
val entityInside = level.getEntities(null, state.getShape(level, pos).bounds().move(pos)).isNotEmpty()
|| upperState?.let { level.getEntities(null, it.getShape(level, pos.above()).bounds().move(pos.above())).isNotEmpty() } ?: false
val glowingState = state.getValue(GLOWING)
if (glowingState != entityInside) {
level.setBlock(pos, state.setValue(GLOWING, entityInside), UPDATE_CLIENTS)
upperState?.let { level.setBlock(pos.above(), it.setValue(GLOWING, entityInside), UPDATE_CLIENTS) }
}
if (entityInside) level.scheduleTick(pos, this, 30) // tick occasionally to check if player inside
}
override fun entityInside(state: BlockState, level: Level, pos: BlockPos, entity: Entity) {
// Lucibell lower block will be our 'driver'
if (!level.isClientSide && !state.getValue(GLOWING) && state.`is`(CobblemonBlocks.LUCIBELL)) {
val halfState = state.getValue(HALF)
if (halfState == DoubleBlockHalf.UPPER) {
val lowerState = level.getBlockState(pos.below())
.takeIf { it.`is`(CobblemonBlocks.LUCIBELL) && it.getValue(HALF) == DoubleBlockHalf.LOWER }
?: return
(lowerState.block as TerrainPlantBlock).checkInside(level, pos.below(), lowerState)
} else {
checkInside(level, pos, state)
}
}
}
override fun getShape(state: BlockState, world: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape = AABB
override fun newBlockEntity(pos: BlockPos, state: BlockState): BlockEntity {
return TerrainPlantBlockEntity(pos, state)
}
override fun <T : BlockEntity> getTicker(
level: Level,
state: BlockState,
blockEntityType: BlockEntityType<T?>
): BlockEntityTicker<T?>? {
return if (level.isClientSide) null else BaseEntityBlock.createTickerHelper(blockEntityType as BlockEntityType<*>,
CobblemonBlockEntities.TERRAIN_PLANT, TerrainPlantBlockEntity::serverTick) as BlockEntityTicker<T?>?
}
companion object {
private val AABB = Shapes.box(0.125, 0.0, 0.125, 0.875, 1.0, 0.875)
val GLOWING = BooleanProperty.create("glowing")
}
}
@@ -0,0 +1,273 @@
/*
* 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.decor
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.CobblemonSounds
import com.mojang.serialization.MapCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.server.level.ServerLevel
import net.minecraft.util.RandomSource
import net.minecraft.util.StringRepresentable
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.*
import net.minecraft.world.level.block.*
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BlockStateProperties
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf
import net.minecraft.world.level.block.state.properties.EnumProperty
import net.minecraft.world.level.block.state.properties.IntegerProperty
import net.minecraft.world.level.material.Fluids
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.VoxelShape
@Suppress("OVERRIDE_DEPRECATION")
class TerrainSeedBlock(private val terrainSeedType: TerrainSeedType, settings: Properties) : CropBlock(settings) {
companion object {
val CODEC: MapCodec<TerrainSeedBlock> = RecordCodecBuilder.mapCodec { it.group(
TerrainSeedType.CODEC.fieldOf("terrainSeedType").forGetter(TerrainSeedBlock::terrainSeedType),
propertiesCodec()
).apply(it, ::TerrainSeedBlock) }
const val MATURE_AGE = 4
val AGE: IntegerProperty = IntegerProperty.create("age", 0, MATURE_AGE)
val HALF: EnumProperty<DoubleBlockHalf> = BlockStateProperties.DOUBLE_BLOCK_HALF
val AGE_TO_SHAPE = arrayOf(
box(2.0, 0.0, 2.0, 14.0, 6.0, 14.0), // Stage 0
box(2.0, 0.0, 2.0, 14.0, 16.0, 14.0), // Stage 1
box(2.0, 0.0, 2.0, 14.0, 16.0, 14.0), // Stage 2
box(2.0, 0.0, 2.0, 14.0, 16.0, 14.0), // Stage 3
box(2.0, 0.0, 2.0, 14.0, 16.0, 14.0), // Stage 4
)
val AGE_TO_SHAPE_TOP = arrayOf(
box(2.0, 0.0, 2.0, 14.0, 1.0, 14.0), // Stage 0
box(2.0, 0.0, 2.0, 14.0, 1.0, 14.0), // Stage 1
box(2.0, 0.0, 2.0, 14.0, 8.0, 14.0), // Stage 2
box(2.0, 0.0, 2.0, 14.0, 16.0, 14.0), // Stage 3
box(2.0, 0.0, 2.0, 14.0, 16.0, 14.0), // Stage 4
)
}
override fun codec(): MapCodec<out CropBlock> {
return CODEC
}
init {
this.registerDefaultState(
this.stateDefinition.any()
.setValue(AGE, 0)
.setValue(HALF, DoubleBlockHalf.LOWER)
)
}
override fun getAgeProperty(): IntegerProperty = AGE
override fun getMaxAge(): Int = MATURE_AGE
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(AGE, HALF)
}
override fun getBaseSeedId(): ItemLike = this.terrainSeedType.getSeed()
override fun getShape(state: BlockState, world: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape =
if (state.getValue(HALF) == DoubleBlockHalf.UPPER) AGE_TO_SHAPE_TOP[getAge(state)] else AGE_TO_SHAPE[this.getAge(state)]
override fun randomTick(state: BlockState, world: ServerLevel, pos: BlockPos, random: RandomSource) {
if (state.getValue(HALF) == DoubleBlockHalf.UPPER || !canGrow(world, pos, state)) return
if (hasSufficientLight(world, pos) && random.nextInt(32) == 0) {
grow(world, pos, state, 1)
}
}
private fun canGrow(world: LevelReader, pos: BlockPos, state: BlockState): Boolean {
return !this.isMaxAge(state) && canSurvive(state, world, pos) && !(!canGrowInto(world, pos.above()) && state.getValue(AGE) >= 4)
}
private fun canGrowInto(world: LevelReader, pos: BlockPos): Boolean {
val blockState = world.getBlockState(pos)
return blockState.isAir || blockState.`is`(this.terrainSeedType.getCropBlock())
}
override fun updateShape(state: BlockState, direction: Direction, neighborState: BlockState, world: LevelAccessor, pos: BlockPos, neighborPos: BlockPos): BlockState {
return if (state.getValue(AGE) >= 2) {
super.updateShape(state, direction, neighborState, world, pos, neighborPos)
} else {
if (state.canSurvive(world, pos)) state else Blocks.AIR.defaultBlockState()
}
}
override fun isValidBonemealTarget(level: LevelReader, pos: BlockPos, state: BlockState): Boolean {
val posAndState = getLowerHalf(level, pos, state)
return if (posAndState == null) false else this.canGrow(
level,
posAndState.first,
posAndState.second
)
}
override fun performBonemeal(level: ServerLevel, random: RandomSource, pos: BlockPos, state: BlockState) {
val posAndState = getLowerHalf(level, pos, state)
if (posAndState != null) {
grow(level, posAndState.first, posAndState.second, getBonemealAgeIncrease(level))
}
}
fun grow(world: Level, pos: BlockPos, state: BlockState, increment: Int) {
if (!canGrow(world, pos, state)) return
val newAge = (this.getAge(state) + increment).coerceAtMost(this.maxAge)
world.setBlock(pos, state.setValue(this.ageProperty, newAge), UPDATE_CLIENTS)
val abovePos = pos.above()
val stateAbove = world.getBlockState(abovePos)
// Set top half depending on age
if (newAge >= (2)) {
if (stateAbove.isAir) {
world.setBlock(
abovePos,
this.defaultBlockState().setValue(AGE, newAge).setValue(HALF, DoubleBlockHalf.UPPER),
UPDATE_CLIENTS
)
}
else if (stateAbove.hasProperty(HALF) && stateAbove.getValue(HALF) == DoubleBlockHalf.UPPER) {
world.setBlock(
abovePos,
stateAbove.setValue(AGE, newAge),
UPDATE_CLIENTS
)
}
}
// Swaps blocks to the actual Flower when fully grown
if (newAge == this.maxAge) {
val nextStateLower = this.terrainSeedType.getFlowerBlock().defaultBlockState().setValue(HALF, DoubleBlockHalf.LOWER)
val nextStateUpper = this.terrainSeedType.getFlowerBlock().defaultBlockState().setValue(HALF, DoubleBlockHalf.UPPER)
world.setBlock(abovePos, nextStateUpper, UPDATE_CLIENTS)
world.setBlock(pos, nextStateLower, UPDATE_CLIENTS)
}
}
override fun getBonemealAgeIncrease(level: Level): Int {
return 1
}
override fun canSurvive(state: BlockState, world: LevelReader, pos: BlockPos): Boolean {
return when (state.getValue(HALF)) {
DoubleBlockHalf.LOWER -> {
val floorBlock = pos.below()
val floor = world.getBlockState(floorBlock)
floor.`is`(Blocks.FARMLAND)
}
DoubleBlockHalf.UPPER -> {
getLowerHalf(world, pos, state) != null
// Upper part survives only if the lower part is valid
}
}
}
private fun getLowerHalf(level: LevelReader, pos: BlockPos, state: BlockState): Pair<BlockPos, BlockState>? {
if (isLower(state)) {
return Pair(pos, state)
} else {
val blockPos = pos.below()
val blockState = level.getBlockState(blockPos)
return if (isLower(blockState)) Pair(
blockPos,
blockState
) else null
}
}
private fun isLower(state: BlockState): Boolean =
state.`is`(this.terrainSeedType.getCropBlock()) && state.getValue(HALF) == DoubleBlockHalf.LOWER
override fun getSoundType(state: BlockState): SoundType {
return CobblemonSounds.HEARTY_GRAINS_SOUNDS
}
@Suppress("unused")
enum class TerrainSeedType : StringRepresentable {
ELECTRIC,
PSYCHIC,
MISTY,
GRASSY;
fun getSeed(): Item = when (this) {
ELECTRIC -> CobblemonItems.ELECTRIC_SEED
PSYCHIC -> CobblemonItems.PSYCHIC_SEED
MISTY -> CobblemonItems.MISTY_SEED
GRASSY -> CobblemonItems.GRASSY_SEED
}
fun getCropBlock(): TerrainSeedBlock = when (this) {
ELECTRIC -> CobblemonBlocks.ELECTRIC_SEED
PSYCHIC -> CobblemonBlocks.PSYCHIC_SEED
MISTY -> CobblemonBlocks.MISTY_SEED
GRASSY -> CobblemonBlocks.GRASSY_SEED
}
fun getFlowerBlock(): Block = when (this) {
ELECTRIC -> CobblemonBlocks.VOLTACONIA
PSYCHIC -> CobblemonBlocks.LUCIBELL
MISTY -> CobblemonBlocks.MISTLEAF
GRASSY -> CobblemonBlocks.VERDROOT
}
override fun getSerializedName(): String = this.name.lowercase()
companion object {
val CODEC = StringRepresentable.fromEnum(::values)
}
}
override fun playerWillDestroy(level: Level, pos: BlockPos, state: BlockState, player: Player): BlockState {
if (!level.isClientSide) {
if (player.isCreative()) {
preventDropFromBottomPart(level, pos, state, player)
} else {
dropResources(state, level, pos, null as BlockEntity?, player, player.getMainHandItem())
}
}
return super.playerWillDestroy(level, pos, state, player)
}
override fun playerDestroy(
level: Level,
player: Player,
pos: BlockPos,
state: BlockState,
blockEntity: BlockEntity?,
tool: ItemStack
) {
super.playerDestroy(level, player, pos, Blocks.AIR.defaultBlockState(), blockEntity, tool)
}
fun preventDropFromBottomPart(level: Level, pos: BlockPos, state: BlockState, player: Player?) {
val doubleblockhalf = state.getValue<DoubleBlockHalf?>(DoublePlantBlock.HALF) as DoubleBlockHalf
if (doubleblockhalf == DoubleBlockHalf.UPPER) {
val blockpos = pos.below()
val blockstate = level.getBlockState(blockpos)
if (blockstate.`is`(state.getBlock()) && blockstate.getValue<DoubleBlockHalf?>(DoublePlantBlock.HALF) == DoubleBlockHalf.LOWER) {
val blockstate1 = if (blockstate.getFluidState()
.`is`(Fluids.WATER)
) Blocks.WATER.defaultBlockState() else Blocks.AIR.defaultBlockState()
level.setBlock(blockpos, blockstate1, 35)
level.levelEvent(player, 2001, blockpos, getId(blockstate))
}
}
}
}
@@ -0,0 +1,80 @@
/*
* 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.decor
import com.cobblemon.mod.common.CobblemonBlocks
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.LevelAccessor
import net.minecraft.world.level.LevelReader
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BlockStateProperties
import net.minecraft.world.level.block.state.properties.BooleanProperty
interface WistensuFlowerBlock {
fun additionalBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(BlockStateProperties.FACING)
builder.add(ROOT)
}
fun adjustStateForPlacement(context: BlockPlaceContext, defaultState: BlockState): BlockState {
val upperBlock = context.level.getBlockState(context.clickedPos.relative(Direction.UP))
if (upperBlock.`is`(CobblemonBlocks.WISTENSU) || upperBlock.`is`(CobblemonBlocks.WISTENSU_VINE)) {
return defaultState
.setValue(BlockStateProperties.FACING, Direction.DOWN)
.setValue(ROOT, false)
} else {
var placeDirection = context.clickedFace
if (placeDirection == Direction.UP) placeDirection = Direction.DOWN
return defaultState
.setValue(BlockStateProperties.FACING, placeDirection)
.setValue(ROOT, true)
}
}
fun copyStateValues(target: BlockState, source: BlockState): BlockState {
return target
.setValue(ROOT, source.getValue(ROOT))
.setValue(BlockStateProperties.FACING, source.getValue(BlockStateProperties.FACING))
}
fun wisternCanSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
if (state.getValue(ROOT)) {
val facing = state.getValue(BlockStateProperties.FACING)
val targetPos = pos.relative(facing.opposite)
val directionState = level.getBlockState(targetPos)
return directionState.isFaceSturdy(level, targetPos, facing)
} else {
val topState = level.getBlockState(pos.relative(Direction.UP))
return topState.`is`(CobblemonBlocks.WISTENSU) || topState.`is`(CobblemonBlocks.WISTENSU_VINE)
}
}
fun wisternUpdateShape(state: BlockState, direction: Direction, neighborState: BlockState, level: LevelAccessor, pos: BlockPos, neighborPos: BlockPos, block: Block): BlockState? {
if (!wisternCanSurvive(state, level, pos)) {
level.scheduleTick(pos, block, 1)
}
if (direction == Direction.UP &&
(neighborState.`is`(CobblemonBlocks.WISTENSU) || neighborState.`is`(CobblemonBlocks.WISTENSU_VINE))) {
return state
.setValue(ROOT, false)
.setValue(BlockStateProperties.FACING, Direction.DOWN)
}
return null
}
companion object {
val ROOT = BooleanProperty.create("root")
}
}
@@ -0,0 +1,114 @@
/*
* 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.decor
import com.cobblemon.mod.common.CobblemonBlocks
import com.mojang.serialization.MapCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.server.level.ServerLevel
import net.minecraft.util.RandomSource
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.LevelAccessor
import net.minecraft.world.level.LevelReader
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.GrowingPlantHeadBlock
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BlockStateProperties
import net.minecraft.world.phys.shapes.VoxelShape
import kotlin.math.min
//TODO: Root/connection shit
class WistensuFlowerHeadBlock(properties: Properties?) : GrowingPlantHeadBlock(properties, Direction.DOWN, SHAPE, false, 0.1), WistensuFlowerBlock {
companion object {
val CODEC: MapCodec<WistensuFlowerHeadBlock> = RecordCodecBuilder.mapCodec {
it.group(propertiesCodec()).apply(it, ::WistensuFlowerHeadBlock)
}
val SHAPE: VoxelShape = box(1.0, 2.0, 1.0, 15.0, 16.0, 15.0)
}
override fun codec(): MapCodec<WistensuFlowerHeadBlock> = CODEC
override fun getBlocksToGrowWhenBonemealed(random: RandomSource): Int {
var d = 1.0
var i = 0
while (random.nextDouble() < d) {
d *= 0.826
++i
}
return i
}
override fun canGrowInto(state: BlockState): Boolean {
return state.isAir
}
override fun getBodyBlock(): Block {
return CobblemonBlocks.WISTENSU_VINE
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
super.createBlockStateDefinition(builder)
additionalBlockStateDefinition(builder)
}
override fun updateShape(
state: BlockState,
direction: Direction,
neighborState: BlockState,
level: LevelAccessor,
pos: BlockPos,
neighborPos: BlockPos
): BlockState {
val updateState = wisternUpdateShape(state, direction, neighborState, level, pos, neighborPos, this)
if (updateState != null) return updateState
if (direction == Direction.DOWN &&
(neighborState.`is`(CobblemonBlocks.WISTENSU) || neighborState.`is`(CobblemonBlocks.WISTENSU_VINE))) {
return updateBodyAfterConvertedFromHead(state, bodyBlock.defaultBlockState())
}
return state
}
override fun getStateForPlacement(context: BlockPlaceContext): BlockState {
val defaultState = super.getStateForPlacement(context)!!
return adjustStateForPlacement(context, defaultState)
}
override fun getGrowIntoState(state: BlockState, random: RandomSource): BlockState {
return super.getGrowIntoState(state, random)
.setValue(WistensuFlowerBlock.ROOT, false)
.setValue(BlockStateProperties.FACING, Direction.DOWN)
}
override fun updateBodyAfterConvertedFromHead(head: BlockState, body: BlockState): BlockState {
return copyStateValues(body, head)
}
override fun performBonemeal(level: ServerLevel, random: RandomSource, pos: BlockPos, state: BlockState) {
var blockPos = pos.relative(growthDirection)
var i = min(state.getValue(AGE), MAX_AGE)
val j = getBlocksToGrowWhenBonemealed(random)
var currentState = state
for (k in 0..<j) {
if (!canGrowInto(level.getBlockState(blockPos))) break
level.setBlockAndUpdate(blockPos, getGrowIntoState(currentState, random))
currentState = level.getBlockState(blockPos)
blockPos = blockPos.relative(growthDirection)
i = min(i + 1, MAX_AGE)
}
}
override fun canSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
return wisternCanSurvive(state, level, pos)
}
}
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.block.decor
import com.cobblemon.mod.common.CobblemonBlocks
import com.mojang.serialization.MapCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.LevelAccessor
import net.minecraft.world.level.LevelReader
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.GrowingPlantBodyBlock
import net.minecraft.world.level.block.GrowingPlantHeadBlock
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BlockStateProperties
import net.minecraft.world.phys.shapes.VoxelShape
//TODO: Root/connection shit
class WistensuFlowerVineBlock(properties: Properties?) : GrowingPlantBodyBlock(properties, Direction.DOWN, SHAPE, false), WistensuFlowerBlock
{
companion object {
val CODEC: MapCodec<WistensuFlowerVineBlock> = RecordCodecBuilder.mapCodec {
it.group(propertiesCodec()).apply(it, ::WistensuFlowerVineBlock)
}
val SHAPE: VoxelShape = box(1.0, 0.0, 1.0, 15.0, 16.0, 15.0)
}
override fun codec(): MapCodec<WistensuFlowerVineBlock> = CODEC
override fun getHeadBlock(): GrowingPlantHeadBlock {
return CobblemonBlocks.WISTENSU
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
super.createBlockStateDefinition(builder)
additionalBlockStateDefinition(builder)
}
override fun updateShape(
state: BlockState,
direction: Direction,
neighborState: BlockState,
level: LevelAccessor,
pos: BlockPos,
neighborPos: BlockPos
): BlockState {
val updateState = wisternUpdateShape(state, direction, neighborState, level, pos, neighborPos, this)
if (updateState != null) return updateState
if (direction == Direction.DOWN &&
!neighborState.`is`(CobblemonBlocks.WISTENSU) && !neighborState.`is`(CobblemonBlocks.WISTENSU_VINE)) {
return updateHeadAfterConvertedFromBody(state, headBlock.getStateForPlacement(level))
}
return state
}
override fun getStateForPlacement(context: BlockPlaceContext): BlockState {
val defaultState = super.getStateForPlacement(context)!!
return adjustStateForPlacement(context, defaultState)
}
override fun updateHeadAfterConvertedFromBody(head: BlockState, body: BlockState): BlockState {
return copyStateValues(body, head)
}
override fun canSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
return wisternCanSurvive(state, level, pos)
}
}
@@ -0,0 +1,46 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.block.entity
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.block.decor.MistleafFlowerBlock
import net.minecraft.core.BlockPos
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.state.BlockState
class MistleafFlowerBlockEntity(pos: BlockPos, state: BlockState): BlockEntity(CobblemonBlockEntities.MISTLEAF_FLOWER, pos, state) {
companion object {
fun serverTick(level: Level, pos: BlockPos, state: BlockState, mistleafFlowerBlockEntity: MistleafFlowerBlockEntity) {
if (level.isClientSide) return
if (state.block !is MistleafFlowerBlock) return
val glowing = state.getValue(MistleafFlowerBlock.GLOWING)
val wantedState = when (state.`is`(CobblemonBlocks.MISTLEAF)) {
//The light level and comparator output increases on odds from New to Full, and decreases on the evens from Full to New
((level.dayTime().toInt().mod(24000) in 12542..23459) && level.moonPhase == 4) -> 1 //New Moon
((level.dayTime().toInt().mod(24000) in 12542..23459) && level.moonPhase == 5) -> 3 //Waxing Crescent
((level.dayTime().toInt().mod(24000) in 12542..23459) && level.moonPhase == 6) -> 5 //First Quarter
((level.dayTime().toInt().mod(24000) in 12542..23459) && level.moonPhase == 7) -> 7 //Waxing Gibbous
((level.dayTime().toInt().mod(24000) in 12542..23459) && level.moonPhase == 0) -> 8 //Full Moon
((level.dayTime().toInt().mod(24000) in 12542..23459) && level.moonPhase == 1) -> 6 //Waning Gibbous
((level.dayTime().toInt().mod(24000) in 12542..23459) && level.moonPhase == 2) -> 4 // Third Quarter
((level.dayTime().toInt().mod(24000) in 12542..23459) && level.moonPhase == 3) -> 2 //Waning Crescent
else -> 0 //Daytime, and likely dimensions with no concept of day/night cycle or moon phase
}
if (glowing != wantedState) {
level.setBlock(pos, state.setValue(MistleafFlowerBlock.GLOWING, wantedState), Block.UPDATE_ALL)
}
}
}
}
@@ -0,0 +1,37 @@
/*
* 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.entity
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.block.decor.TerrainPlantBlock
import net.minecraft.core.BlockPos
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.state.BlockState
class TerrainPlantBlockEntity(pos: BlockPos, state: BlockState): BlockEntity(CobblemonBlockEntities.TERRAIN_PLANT, pos, state) {
companion object {
fun serverTick(level: Level, pos: BlockPos, state: BlockState, terrainPlantBlockEntity: TerrainPlantBlockEntity) {
if (level.isClientSide) return
if (state.block !is TerrainPlantBlock) return
val glowing = state.getValue(TerrainPlantBlock.GLOWING)
val wantedState = if (state.`is`(CobblemonBlocks.VOLTACONIA)) level.isThundering
else if (state.`is`(CobblemonBlocks.VERDROOT)) level.isRaining && !level.isThundering
else null // We dont control glowing here
if (wantedState != null && glowing != wantedState) {
level.setBlock(pos, state.setValue(TerrainPlantBlock.GLOWING, wantedState), Block.UPDATE_CLIENTS)
}
}
}
}
@@ -0,0 +1,133 @@
/*
* 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.general
import com.cobblemon.mod.common.util.rotateShape
import com.mojang.serialization.MapCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.LevelAccessor
import net.minecraft.world.level.LevelReader
import net.minecraft.world.level.block.*
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.HorizontalDirectionalBlock.FACING
import net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_FACING
import net.minecraft.world.level.block.state.properties.IntegerProperty
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
import kotlin.math.min
class MintLitterBlock(properties: Properties?) : Block(properties) {
init {
registerDefaultState(stateDefinition.any()
.setValue(FACING, Direction.NORTH)
.setValue(AMOUNT, 1))
}
override fun getStateForPlacement(context: BlockPlaceContext): BlockState? {
val blockState = context.getLevel().getBlockState(context.getClickedPos())
return if (blockState.`is`(this)) blockState.setValue<Int?, Int?>(
AMOUNT,
min(4, blockState.getValue<Int?>(AMOUNT) as Int + 1)
) as BlockState else this.defaultBlockState().setValue<Direction?, Direction?>(
FACING,
context.getHorizontalDirection().getOpposite()
) as BlockState
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(FACING)
builder.add(AMOUNT)
}
override fun updateShape(
state: BlockState,
direction: Direction?,
neighborState: BlockState?,
level: LevelAccessor?,
pos: BlockPos?,
neighborPos: BlockPos?
): BlockState {
return if (!state.canSurvive(level, pos)) Blocks.AIR.defaultBlockState() else super.updateShape(
state,
direction,
neighborState,
level,
pos,
neighborPos
)
}
override fun canSurvive(state: BlockState?, level: LevelReader, pos: BlockPos): Boolean {
val posBelow = pos.below()
return level.getBlockState(posBelow).isFaceSturdy(level, posBelow, Direction.UP)
}
override fun propagatesSkylightDown(state: BlockState, level: BlockGetter?, pos: BlockPos?): Boolean {
return state.getFluidState().isEmpty()
}
public override fun canBeReplaced(state: BlockState, useContext: BlockPlaceContext): Boolean {
return if (!useContext.isSecondaryUseActive && useContext.itemInHand.`is`(this.asItem()) && state.getValue(
AMOUNT
) <= MAX_AMOUNT
) true else super.canBeReplaced(state, useContext)
}
@Deprecated("Deprecated in Java")
override fun rotate(state: BlockState, rotation: Rotation): BlockState {
return state.setValue(HORIZONTAL_FACING, rotation.rotate(state.getValue(HORIZONTAL_FACING)))
}
override fun getShape(state: BlockState, level: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape? {
val litterBoxes = when (state.getValue(FACING)) {
Direction.SOUTH -> SOUTH_SHAPES
Direction.WEST -> WEST_SHAPES
Direction.EAST -> EAST_SHAPES
else -> NORTH_SHAPES
}
var litterBox = litterBoxes[state.getValue(AMOUNT)]
return litterBox
}
companion object {
val CODEC: MapCodec<MintLitterBlock> = RecordCodecBuilder.mapCodec {
it.group(
propertiesCodec()
).apply(it, ::MintLitterBlock) }
val AMOUNT = IntegerProperty.create("amount", 1, 4)
const val MAX_AMOUNT: Int = 4
private val NORTH_SHAPES = listOf(
Shapes.box(0.0, 0.0, 0.0, 1.0, 0.125, 1.0),
Shapes.box(0.0, 0.0, 0.0, 0.5, 0.0625, 0.5),
Shapes.box(0.0, 0.0, 0.0, 0.5, 0.0625, 1.0),
Shapes.or(
Shapes.box(0.0, 0.0, 0.0, 0.5, 0.0625, 1.0),
Shapes.box(0.5, 0.0, 0.5, 1.0, 0.0625, 1.0)
),
Shapes.box(0.0, 0.0, 0.0, 1.0, 0.0625, 1.0)
)
private val SOUTH_SHAPES = createShapes(Direction.SOUTH)
private val WEST_SHAPES = createShapes(Direction.WEST)
private val EAST_SHAPES = createShapes(Direction.EAST)
private fun createShapes(to: Direction): List<VoxelShape> {
return NORTH_SHAPES.map { rotateShape(Direction.NORTH, to, it) }
}
}
}
@@ -391,7 +391,7 @@ object CobblemonClient {
CobblemonBlocks.X_SP_ATK,
CobblemonBlocks.X_SP_DEF,
CobblemonBlocks.X_SPEED,
CobblemonBlocks.TM_MACHINE,
CobblemonBlocks.TM_MACHINE,
CobblemonBlocks.GALARICA_CUFF,
CobblemonBlocks.GALARICA_WREATH,
@@ -415,7 +415,108 @@ object CobblemonClient {
CobblemonBlocks.NEVER_MELT_ICE,
CobblemonBlocks.WIDE_LENS,
CobblemonBlocks.ZOOM_LENS,
CobblemonBlocks.SCOPE_LENS
CobblemonBlocks.SCOPE_LENS,
CobblemonBlocks.MOOMOO_MILK,
CobblemonBlocks.MEDICINAL_BREW,
CobblemonBlocks.TERRAIN_EXTENDER,
CobblemonBlocks.RED_MINT_LEAF_LITTER,
CobblemonBlocks.POTTED_RED_MINT,
CobblemonBlocks.BLUE_MINT_LEAF_LITTER,
CobblemonBlocks.POTTED_BLUE_MINT,
CobblemonBlocks.CYAN_MINT_LEAF_LITTER,
CobblemonBlocks.POTTED_CYAN_MINT,
CobblemonBlocks.PINK_MINT_LEAF_LITTER,
CobblemonBlocks.POTTED_PINK_MINT,
CobblemonBlocks.GREEN_MINT_LEAF_LITTER,
CobblemonBlocks.POTTED_GREEN_MINT,
CobblemonBlocks.WHITE_MINT_LEAF_LITTER,
CobblemonBlocks.POTTED_WHITE_MINT,
CobblemonBlocks.LOBAILA,
CobblemonBlocks.POMOHANA,
CobblemonBlocks.PAUVILLEA,
CobblemonBlocks.WISTENSU,
CobblemonBlocks.WISTENSU_VINE,
CobblemonBlocks.ELECTRIC_SEED,
CobblemonBlocks.VOLTACONIA,
CobblemonBlocks.GRASSY_SEED,
CobblemonBlocks.VERDROOT,
CobblemonBlocks.MISTY_SEED,
CobblemonBlocks.MISTLEAF,
CobblemonBlocks.PSYCHIC_SEED,
CobblemonBlocks.LUCIBELL,
CobblemonBlocks.POTTED_AGUAV_BERRY,
CobblemonBlocks.POTTED_APICOT_BERRY,
CobblemonBlocks.POTTED_ASPEAR_BERRY,
CobblemonBlocks.POTTED_BABIRI_BERRY,
CobblemonBlocks.POTTED_BELUE_BERRY,
CobblemonBlocks.POTTED_BLUK_BERRY,
CobblemonBlocks.POTTED_CHARTI_BERRY,
CobblemonBlocks.POTTED_CHERI_BERRY,
CobblemonBlocks.POTTED_CHESTO_BERRY,
CobblemonBlocks.POTTED_CHILAN_BERRY,
CobblemonBlocks.POTTED_CHOPLE_BERRY,
CobblemonBlocks.POTTED_COBA_BERRY,
CobblemonBlocks.POTTED_COLBUR_BERRY,
CobblemonBlocks.POTTED_CORNN_BERRY,
CobblemonBlocks.POTTED_CUSTAP_BERRY,
CobblemonBlocks.POTTED_DURIN_BERRY,
CobblemonBlocks.POTTED_EGGANT_BERRY,
CobblemonBlocks.POTTED_ENIGMA_BERRY,
CobblemonBlocks.POTTED_FIGY_BERRY,
CobblemonBlocks.POTTED_GANLON_BERRY,
CobblemonBlocks.POTTED_GREPA_BERRY,
CobblemonBlocks.POTTED_HABAN_BERRY,
CobblemonBlocks.POTTED_HONDEW_BERRY,
CobblemonBlocks.POTTED_HOPO_BERRY,
CobblemonBlocks.POTTED_IAPAPA_BERRY,
CobblemonBlocks.POTTED_JABOCA_BERRY,
CobblemonBlocks.POTTED_KASIB_BERRY,
CobblemonBlocks.POTTED_KEBIA_BERRY,
CobblemonBlocks.POTTED_KEE_BERRY,
CobblemonBlocks.POTTED_KELPSY_BERRY,
CobblemonBlocks.POTTED_LANSAT_BERRY,
CobblemonBlocks.POTTED_LEPPA_BERRY,
CobblemonBlocks.POTTED_LIECHI_BERRY,
CobblemonBlocks.POTTED_LUM_BERRY,
CobblemonBlocks.POTTED_MAGOST_BERRY,
CobblemonBlocks.POTTED_MAGO_BERRY,
CobblemonBlocks.POTTED_MARANGA_BERRY,
CobblemonBlocks.POTTED_MICLE_BERRY,
CobblemonBlocks.POTTED_NANAB_BERRY,
CobblemonBlocks.POTTED_NOMEL_BERRY,
CobblemonBlocks.POTTED_OCCA_BERRY,
CobblemonBlocks.POTTED_ORAN_BERRY,
CobblemonBlocks.POTTED_PAMTRE_BERRY,
CobblemonBlocks.POTTED_PASSHO_BERRY,
CobblemonBlocks.POTTED_PAYAPA_BERRY,
CobblemonBlocks.POTTED_PECHA_BERRY,
CobblemonBlocks.POTTED_PERSIM_BERRY,
CobblemonBlocks.POTTED_PETAYA_BERRY,
CobblemonBlocks.POTTED_PINAP_BERRY,
CobblemonBlocks.POTTED_POMEG_BERRY,
CobblemonBlocks.POTTED_QUALOT_BERRY,
CobblemonBlocks.POTTED_RABUTA_BERRY,
CobblemonBlocks.POTTED_RAWST_BERRY,
CobblemonBlocks.POTTED_RAZZ_BERRY,
CobblemonBlocks.POTTED_RINDO_BERRY,
CobblemonBlocks.POTTED_ROSELI_BERRY,
CobblemonBlocks.POTTED_ROWAP_BERRY,
CobblemonBlocks.POTTED_SALAC_BERRY,
CobblemonBlocks.POTTED_SHUCA_BERRY,
CobblemonBlocks.POTTED_SITRUS_BERRY,
CobblemonBlocks.POTTED_SPELON_BERRY,
CobblemonBlocks.POTTED_STARF_BERRY,
CobblemonBlocks.POTTED_TAMATO_BERRY,
CobblemonBlocks.POTTED_TANGA_BERRY,
CobblemonBlocks.POTTED_TOUGA_BERRY,
CobblemonBlocks.POTTED_WACAN_BERRY,
CobblemonBlocks.POTTED_WATMEL_BERRY,
CobblemonBlocks.POTTED_WEPEAR_BERRY,
CobblemonBlocks.POTTED_WIKI_BERRY,
CobblemonBlocks.POTTED_YACHE_BERRY
)
this.createBoatModelLayers()
@@ -0,0 +1,466 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.debug.speed
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.net.messages.client.debug.SpeedTunerEntry
import com.cobblemon.mod.common.net.messages.server.debug.SetSpeedTunerSprintSpeedPacket
import com.cobblemon.mod.common.net.messages.server.debug.SetSpeedTunerWalkSpeedPacket
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.platform.events.RenderEvent
import com.cobblemon.mod.common.pokemon.Pokemon
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import java.nio.file.Files
import java.nio.file.Path
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.floor
import kotlin.math.sin
import kotlin.math.sqrt
import net.minecraft.client.Minecraft
import net.minecraft.client.multiplayer.ClientLevel
import net.minecraft.client.renderer.LevelRenderer
import net.minecraft.core.particles.ParticleTypes
import net.minecraft.ChatFormatting
import net.minecraft.network.chat.ClickEvent
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.HoverEvent
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.Mth
import net.minecraft.world.entity.ai.attributes.Attributes
import net.minecraft.world.phys.Vec3
/**
* Client side session for tuning species walk and sprint speeds. A display only pokemon runs a circle or a line at
* exactly the speed being tuned, and edits are kept in blocks per second and saved to a file for applying to the
* species data later.
*/
object SpeedTuner {
enum class Gait(val jsonKey: String) { WALK("walkSpeed"), SPRINT("sprintSpeed") }
enum class Track { CIRCLE, LINE }
// Vanilla player top speeds in blocks per second, shown and run alongside for comparison
const val PLAYER_WALK_SPEED = 4.317
const val PLAYER_SPRINT_SPEED = 5.612
// Sprint speed shown for species without one yet, matches the data script default
const val DEFAULT_SPRINT_SPEED = 5.6
// Walk speed values are multiplied by this in PokemonMoveControl
const val WALK_SPEED_SCALE = 2.5
// Friction of ordinary blocks, walk estimates assume flat normal ground
const val VANILLA_BLOCK_FRICTION = 0.6
const val TICKS_PER_SECOND = 20.0
const val DEFAULT_CIRCLE_RADIUS = 6.0
const val DEFAULT_LINE_LENGTH = 16.0
// How often the track gets outlined, and the gap between outline markers in blocks
const val TRACK_MARKER_INTERVAL_TICKS = 10
const val TRACK_MARKER_SPACING = 1.0
// Markers sit this far above the start height so they aren't inside the ground
const val MARKER_HEIGHT = 0.1
// Edits made while dragging a slider are written out at most this often
const val AUTOSAVE_INTERVAL_TICKS = 40
private const val UNITS = "blocks per second"
private val gson = GsonBuilder().setPrettyPrinting().create()
private val sessionFile: Path
get() = Minecraft.getInstance().gameDirectory.toPath().resolve("cobblemon-dev").resolve("speed_tuning_session.json")
var active = false
private set
var entries: List<SpeedTunerEntry> = emptyList()
private set
var selected: SpeedTunerEntry? = null
private set
var gait = Gait.SPRINT
set(value) {
field = value
ensureSelectionVisible()
restartRun()
}
var track = Track.CIRCLE
set(value) {
field = value
restartRun()
}
var radius = DEFAULT_CIRCLE_RADIUS
var lineLength = DEFAULT_LINE_LENGTH
var paused = false
// How fast the display pokemon plays, movement and animation together. 1 is real time
var playbackSpeed = 1.0
// Species id to edited speeds by gait json key, all in blocks per second
private val edits = mutableMapOf<ResourceLocation, MutableMap<String, Double>>()
private val reviewed = mutableSetOf<ResourceLocation>()
private var dirty = false
private var subscribed = false
private var origin = Vec3.ZERO
private var lineDirection = Vec3(0.0, 0.0, 1.0)
private var distance = 0.0
private var referenceDistance = 0.0
private var ticks = 0L
// The display pokemon's own clock in ticks, so it can run slower than the game or stand still
private var displayTime = 0.0
// Game ticks since the display pokemon last stepped, for how far between its steps it is
private var gameTicksSinceStep = 0
// Partial tick it was drawn at last, held while paused so it freezes exactly where it was
private var lastPartialTick = 0F
private var puppet: PokemonEntity? = null
fun open(entries: List<SpeedTunerEntry>) {
val player = Minecraft.getInstance().player ?: return
this.entries = entries.sortedBy { it.dex }
if (!active) {
origin = player.position()
val look = Vec3(player.lookAngle.x, 0.0, player.lookAngle.z)
lineDirection = if (look.lengthSqr() > 0) look.normalize() else Vec3(0.0, 0.0, 1.0)
load()
active = true
// The server starts from the species data, so bring it up to date with the loaded session
edits.forEach { (id, speeds) ->
speeds[Gait.SPRINT.jsonKey]?.let { sendSprintSpeed(id, it.toFloat()) }
speeds[Gait.WALK.jsonKey]?.let { sendWalkSpeed(id, walkSpeedFor(it)) }
}
}
subscribe()
ensureSelectionVisible()
Minecraft.getInstance().setScreen(SpeedTunerScreen())
}
fun stop() {
save()
active = false
puppet = null
selected = null
}
/** Species that make sense for the current gait. Only species with a sprint speed can sprint. */
fun visibleEntries(): List<SpeedTunerEntry> {
return if (gait == Gait.SPRINT) entries.filter { it.sprintSpeed != null } else entries
}
fun select(entry: SpeedTunerEntry) {
selected = entry
puppet = null
restartRun()
}
fun step(offset: Int) {
val visible = visibleEntries().ifEmpty { return }
val index = visible.indexOf(selected).coerceAtLeast(0)
select(visible[Math.floorMod(index + offset, visible.size)])
}
/** Finds a species by dex number or by name, preferring names that start with the search. */
fun search(text: String): Boolean {
val query = text.trim().lowercase().ifEmpty { return false }
val visible = visibleEntries()
val match = query.toIntOrNull()?.let { dex -> visible.firstOrNull { it.dex == dex } }
?: visible.firstOrNull { it.speciesId.path.startsWith(query) }
?: visible.firstOrNull { it.speciesId.path.contains(query) }
?: return false
select(match)
return true
}
fun speed(entry: SpeedTunerEntry, gait: Gait = this.gait): Double {
return edits[entry.speciesId]?.get(gait.jsonKey) ?: originalSpeed(entry, gait)
}
fun originalSpeed(entry: SpeedTunerEntry, gait: Gait = this.gait): Double {
return when (gait) {
Gait.SPRINT -> entry.sprintSpeed?.toDouble() ?: DEFAULT_SPRINT_SPEED
Gait.WALK -> estimatedWalkSpeed(entry)
}
}
/**
* Walk speed is still relative in the species data, so this works out what it comes to in blocks per second on
* normal ground. Walkers always use the same speed modifier so this is exact.
*/
fun estimatedWalkSpeed(entry: SpeedTunerEntry): Double {
val input = walkInputPerWalkSpeed() * entry.walkSpeed
return PokemonMoveControl.vanillaGroundTopSpeed(input, VANILLA_BLOCK_FRICTION) * TICKS_PER_SECOND
}
/** The other way round, walkSpeed that comes out at [blocksPerSecond]. Top speed grows with the square of the input */
fun walkSpeedFor(blocksPerSecond: Double): Float {
val topSpeedPerInputSquared = PokemonMoveControl.vanillaGroundTopSpeed(1.0, VANILLA_BLOCK_FRICTION)
val input = sqrt(blocksPerSecond / TICKS_PER_SECOND / topSpeedPerInputSquared)
return (input / walkInputPerWalkSpeed()).toFloat()
}
// Move control input for each unit of walkSpeed
private fun walkInputPerWalkSpeed(): Double {
return Attributes.MOVEMENT_SPEED.value().defaultValue * PokemonMoveControl.GROUND_SPEED_MODIFIER * WALK_SPEED_SCALE
}
fun referenceSpeed() = if (gait == Gait.SPRINT) PLAYER_SPRINT_SPEED else PLAYER_WALK_SPEED
fun setSpeed(value: Double) {
val entry = selected ?: return
edits.getOrPut(entry.speciesId) { mutableMapOf() }[gait.jsonKey] = value
dirty = true
when (gait) {
Gait.SPRINT -> sendSprintSpeed(entry.speciesId, value.toFloat())
Gait.WALK -> sendWalkSpeed(entry.speciesId, walkSpeedFor(value))
}
}
fun resetSpeed() {
val entry = selected ?: return
edits[entry.speciesId]?.let {
it.remove(gait.jsonKey)
if (it.isEmpty()) edits.remove(entry.speciesId)
}
dirty = true
when (gait) {
Gait.SPRINT -> sendSprintSpeed(entry.speciesId, entry.sprintSpeed)
Gait.WALK -> sendWalkSpeed(entry.speciesId, entry.walkSpeed)
}
}
private fun sendSprintSpeed(speciesId: ResourceLocation, sprintSpeed: Float?) {
SetSpeedTunerSprintSpeedPacket(speciesId, sprintSpeed).sendToServer()
}
/** Walk speeds go over in the species data's own units, converted from the tuner's blocks per second */
private fun sendWalkSpeed(speciesId: ResourceLocation, walkSpeed: Float) {
SetSpeedTunerWalkSpeedPacket(speciesId, walkSpeed).sendToServer()
}
fun isEdited(entry: SpeedTunerEntry) = edits[entry.speciesId]?.containsKey(gait.jsonKey) == true
fun isReviewed(entry: SpeedTunerEntry) = entry.speciesId in reviewed
fun toggleReviewed() {
val entry = selected ?: return
if (!reviewed.remove(entry.speciesId)) reviewed.add(entry.speciesId)
dirty = true
}
fun editedCount() = edits.size
fun reviewedCount() = reviewed.size
fun restartRun() {
distance = 0.0
referenceDistance = 0.0
}
private fun ensureSelectionVisible() {
val visible = visibleEntries()
if (selected !in visible) {
visible.firstOrNull()?.let { select(it) } ?: run { selected = null }
}
}
private fun subscribe() {
if (subscribed) return
subscribed = true
PlatformEvents.CLIENT_TICK_POST.subscribe { tick() }
PlatformEvents.RENDER.subscribe { render(it) }
PlatformEvents.CLIENT_PLAYER_LOGOUT.subscribe { if (active) stop() }
}
private fun tick() {
if (!active) return
ticks++
if (dirty && ticks % AUTOSAVE_INTERVAL_TICKS == 0L) save()
val level = Minecraft.getInstance().level ?: return
val entry = selected ?: return
val display = puppet?.takeIf { it.level() === level } ?: createPuppet(level, entry)?.also { puppet = it } ?: return
val rate = if (paused) 0.0 else playbackSpeed
val before = displayTime
displayTime += rate
// One display tick each time its clock passes a whole tick, so slow motion moves and animates less often
val steps = (floor(displayTime) - floor(before)).toInt()
repeat(steps) { stepDisplay(display, entry) }
// Hold while paused so it picks up from the same spot when resumed
if (steps > 0) gameTicksSinceStep = 0 else if (!paused) gameTicksSinceStep++
display.isPokemonSprinting = gait == Gait.SPRINT
display.entityData.set(PokemonEntity.POSE_TYPE, PoseType.WALK)
display.entityData.set(PokemonEntity.MOVING, !paused)
val reference = trackPosition(referenceDistance)
level.addParticle(ParticleTypes.ELECTRIC_SPARK, reference.x, reference.y + MARKER_HEIGHT, reference.z, 0.0, 0.0, 0.0)
if (ticks % TRACK_MARKER_INTERVAL_TICKS == 0L) outlineTrack(level)
}
/** One tick of the display pokemon's own clock, moving it along the track and ticking its animations */
private fun stepDisplay(display: PokemonEntity, entry: SpeedTunerEntry) {
distance += speed(entry) / TICKS_PER_SECOND
referenceDistance += referenceSpeed() / TICKS_PER_SECOND
display.tickCount++
val lapLength = lapLength()
// Line runs start over from the beginning, so jump without smoothing between the two ends
val wrapped = distance >= lapLength
distance %= lapLength
referenceDistance %= lapLength
display.setOldPosAndRot()
display.yBodyRotO = display.yBodyRot
display.yHeadRotO = display.yHeadRot
val position = trackPosition(distance)
// Keep yaw continuous with last tick so nothing lerping it spins round at the wrap
val yaw = display.yBodyRot + Mth.wrapDegrees(trackYaw(distance) - display.yBodyRot)
display.setPos(position.x, position.y, position.z)
display.yRot = yaw
display.yBodyRot = yaw
display.yHeadRot = yaw
if (wrapped && track == Track.LINE) {
display.setOldPosAndRot()
display.yBodyRotO = yaw
display.yHeadRotO = yaw
}
// Animation age and pose changes advance in the client delegate's tick, which normally runs from the
// entity's own tick. The display pokemon is never ticked so do it here
display.delegate.tick(display)
}
private fun render(event: RenderEvent) {
if (!active) return
val puppet = puppet ?: return
val level = Minecraft.getInstance().level ?: return
if (puppet.level() !== level) return
val camera = event.camera.position
// How far between its own steps the display pokemon is, so slow motion stays smooth and paused really stops
val partialTick = if (paused) {
lastPartialTick
} else {
val gamePartialTick = event.tickCounter.getGameTimeDeltaPartialTick(false)
((gameTicksSinceStep + gamePartialTick) * playbackSpeed).coerceAtMost(1.0).toFloat()
}
lastPartialTick = partialTick
val x = Mth.lerp(partialTick.toDouble(), puppet.xo, puppet.x) - camera.x
val y = Mth.lerp(partialTick.toDouble(), puppet.yo, puppet.y) - camera.y
val z = Mth.lerp(partialTick.toDouble(), puppet.zo, puppet.z) - camera.z
val light = LevelRenderer.getLightColor(level, puppet.blockPosition())
val buffers = Minecraft.getInstance().renderBuffers().bufferSource()
Minecraft.getInstance().entityRenderDispatcher.render(puppet, x, y, z, puppet.yRot, partialTick, event.poseStack, buffers, light)
buffers.endBatch()
}
/** Same setup as HabitatBlockRenderer, a pokemon that only exists to be drawn. */
private fun createPuppet(level: ClientLevel, entry: SpeedTunerEntry): PokemonEntity? {
val species = PokemonSpecies.getByIdentifier(entry.speciesId) ?: return null
val pokemon = Pokemon().apply {
isClient = true
this.species = species
initialize()
}
return PokemonEntity(level, pokemon).apply {
noPhysics = true
setNoGravity(true)
val start = trackPosition(distance)
setPos(start.x, start.y, start.z)
setOldPosAndRot()
}
}
private fun lapLength() = if (track == Track.CIRCLE) 2 * PI * radius else lineLength
private fun trackPosition(along: Double): Vec3 {
return when (track) {
Track.CIRCLE -> {
val angle = along / radius
origin.add(cos(angle) * radius, 0.0, sin(angle) * radius)
}
Track.LINE -> origin.add(lineDirection.scale(along))
}
}
private fun trackYaw(along: Double): Float {
val direction = when (track) {
// Tangent of the circle in the direction of travel
Track.CIRCLE -> (along / radius).let { Vec3(-sin(it), 0.0, cos(it)) }
Track.LINE -> lineDirection
}
return (Mth.atan2(direction.z, direction.x) * Mth.RAD_TO_DEG).toFloat() - 90F
}
private fun outlineTrack(level: ClientLevel) {
val markers = (lapLength() / TRACK_MARKER_SPACING).toInt()
for (i in 0..markers) {
val marker = trackPosition(i * TRACK_MARKER_SPACING)
level.addParticle(ParticleTypes.COMPOSTER, marker.x, marker.y + MARKER_HEIGHT, marker.z, 0.0, 0.0, 0.0)
}
}
private fun toJson(): JsonObject {
val species = JsonObject()
edits.toSortedMap(compareBy { it.toString() }).forEach { (id, speeds) ->
species.add(id.toString(), JsonObject().also { speedsJson -> speeds.forEach { (key, value) -> speedsJson.addProperty(key, value) } })
}
return JsonObject().apply {
addProperty("units", UNITS)
add("species", species)
add("reviewed", JsonArray().also { array -> reviewed.map { it.toString() }.sorted().forEach { array.add(it) } })
}
}
fun save() {
runCatching {
Files.createDirectories(sessionFile.parent)
Files.writeString(sessionFile, gson.toJson(toJson()))
dirty = false
}.onFailure { Cobblemon.LOGGER.error("Couldn't save the speed tuning session", it) }
}
/** Writes a timestamped copy of the session next to it and posts links to it in chat. */
fun export() {
save()
val stamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"))
val file = sessionFile.resolveSibling("speed_tuning_$stamp.json").toAbsolutePath()
runCatching {
Files.writeString(file, gson.toJson(toJson()))
val message = Component.literal("Speed tuning exported: ")
.append(chatLink("[open file]", ClickEvent.Action.OPEN_FILE, file.toString()))
.append(" ")
.append(chatLink("[open folder]", ClickEvent.Action.OPEN_FILE, file.parent.toString()))
.append(" ")
.append(chatLink("[copy path]", ClickEvent.Action.COPY_TO_CLIPBOARD, file.toString()))
Minecraft.getInstance().player?.displayClientMessage(message, false)
}.onFailure { Cobblemon.LOGGER.error("Couldn't export the speed tuning session", it) }
}
private fun chatLink(label: String, action: ClickEvent.Action, value: String): Component {
return Component.literal(label).withStyle { style ->
style.withColor(ChatFormatting.AQUA)
.withUnderlined(true)
.withClickEvent(ClickEvent(action, value))
.withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal(value)))
}
}
private fun load() {
edits.clear()
reviewed.clear()
if (!Files.exists(sessionFile)) return
runCatching {
val json = JsonParser.parseString(Files.readString(sessionFile)).asJsonObject
json.getAsJsonObject("species")?.entrySet()?.forEach { (id, speeds) ->
val map = edits.getOrPut(ResourceLocation.parse(id)) { mutableMapOf() }
speeds.asJsonObject.entrySet().forEach { (key, value) -> map[key] = value.asDouble }
}
json.getAsJsonArray("reviewed")?.forEach { reviewed.add(ResourceLocation.parse(it.asString)) }
}.onFailure { Cobblemon.LOGGER.error("Couldn't load the speed tuning session", it) }
}
}
@@ -0,0 +1,240 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.debug.speed
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.text.text
import java.awt.Color
import java.util.Locale
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractSliderButton
import net.minecraft.client.gui.components.Button
import net.minecraft.client.gui.components.EditBox
import net.minecraft.client.gui.screens.Screen
import net.minecraft.network.chat.Component
import net.minecraft.util.Mth
import org.lwjgl.glfw.GLFW
/**
* Panel for [SpeedTuner] down the left side of the screen. Doesn't pause and leaves the world visible so the display
* pokemon can be watched from anywhere. Closing it leaves the session running, the debug keybind brings it back.
*/
class SpeedTunerScreen : Screen("Speed Tuner".text()) {
companion object {
const val PANEL_WIDTH = 180
const val MARGIN = 6
const val ROW_HEIGHT = 20
const val ROW_GAP = 4
const val TEXT_LINE_HEIGHT = 11
// Typed speeds can go up to this, the slider only covers the usual range
const val MAX_SPEED = 20.0
const val SLIDER_MAX_SPEED = 8.0
// Blocks per second the - and + buttons change the speed by
const val SPEED_STEP = 0.05
val PLAYBACK_SPEEDS = listOf(1.0, 0.5, 0.25, 0.1)
const val MIN_RADIUS = 2.0
const val MAX_RADIUS = 24.0
const val MIN_LINE_LENGTH = 4.0
const val MAX_LINE_LENGTH = 48.0
val PANEL_COLOUR = Color(0, 0, 0, 150).rgb
val EDITED_COLOUR = Color(255, 200, 80).rgb
val TEXT_COLOUR = Color(255, 255, 255).rgb
}
private lateinit var searchBox: EditBox
private lateinit var speedBox: EditBox
private lateinit var speedSlider: DecimalSlider
private var infoTop = 0
override fun isPauseScreen() = false
override fun init() {
super.init()
val fullWidth = PANEL_WIDTH - MARGIN * 2
val halfWidth = (fullWidth - ROW_GAP) / 2
var y = MARGIN
fun nextRow(): Int = y.also { y += ROW_HEIGHT + ROW_GAP }
nextRow().let { row ->
addRenderableWidget(Button.builder("<".text()) { step(-1) }.bounds(MARGIN, row, ROW_HEIGHT, ROW_HEIGHT).build())
addRenderableWidget(Button.builder(">".text()) { step(1) }.bounds(PANEL_WIDTH - MARGIN - ROW_HEIGHT, row, ROW_HEIGHT, ROW_HEIGHT).build())
}
searchBox = addRenderableWidget(EditBox(font, MARGIN, nextRow(), fullWidth, ROW_HEIGHT, "Search".text())).apply {
setHint("Dex number or name, Enter".text())
}
nextRow().let { row ->
addRenderableWidget(Button.builder("Gait: ${SpeedTuner.gait.name.lowercase()}".text()) {
SpeedTuner.gait = if (SpeedTuner.gait == SpeedTuner.Gait.SPRINT) SpeedTuner.Gait.WALK else SpeedTuner.Gait.SPRINT
rebuildWidgets()
}.bounds(MARGIN, row, halfWidth, ROW_HEIGHT).build())
addRenderableWidget(Button.builder("Track: ${SpeedTuner.track.name.lowercase()}".text()) {
SpeedTuner.track = if (SpeedTuner.track == SpeedTuner.Track.CIRCLE) SpeedTuner.Track.LINE else SpeedTuner.Track.CIRCLE
rebuildWidgets()
}.bounds(MARGIN + halfWidth + ROW_GAP, row, halfWidth, ROW_HEIGHT).build())
}
val entry = SpeedTuner.selected
nextRow().let { row ->
addRenderableWidget(Button.builder("-".text()) { nudgeSpeed(-SPEED_STEP) }.bounds(MARGIN, row, ROW_HEIGHT, ROW_HEIGHT).build())
speedSlider = addRenderableWidget(
DecimalSlider(MARGIN + ROW_HEIGHT + ROW_GAP, row, fullWidth - 2 * (ROW_HEIGHT + ROW_GAP), "Speed", 0.0, SLIDER_MAX_SPEED, entry?.let { SpeedTuner.speed(it) } ?: 0.0) {
SpeedTuner.setSpeed(it)
speedBox.value = format(it)
}
)
addRenderableWidget(Button.builder("+".text()) { nudgeSpeed(SPEED_STEP) }.bounds(PANEL_WIDTH - MARGIN - ROW_HEIGHT, row, ROW_HEIGHT, ROW_HEIGHT).build())
}
nextRow().let { row ->
speedBox = addRenderableWidget(EditBox(font, MARGIN, row, halfWidth, ROW_HEIGHT, "Speed".text())).apply {
value = entry?.let { format(SpeedTuner.speed(it)) } ?: ""
}
addRenderableWidget(Button.builder("Set".text()) { applyTypedSpeed() }.bounds(MARGIN + halfWidth + ROW_GAP, row, halfWidth, ROW_HEIGHT).build())
}
if (SpeedTuner.track == SpeedTuner.Track.CIRCLE) {
addRenderableWidget(DecimalSlider(MARGIN, nextRow(), fullWidth, "Radius", MIN_RADIUS, MAX_RADIUS, SpeedTuner.radius) {
SpeedTuner.radius = it
})
} else {
addRenderableWidget(DecimalSlider(MARGIN, nextRow(), fullWidth, "Length", MIN_LINE_LENGTH, MAX_LINE_LENGTH, SpeedTuner.lineLength) {
SpeedTuner.lineLength = it
})
}
nextRow().let { row ->
addRenderableWidget(Button.builder((if (SpeedTuner.paused) "Resume" else "Pause").text()) {
SpeedTuner.paused = !SpeedTuner.paused
rebuildWidgets()
}.bounds(MARGIN, row, halfWidth, ROW_HEIGHT).build())
addRenderableWidget(Button.builder("Reset speed".text()) {
SpeedTuner.resetSpeed()
rebuildWidgets()
}.bounds(MARGIN + halfWidth + ROW_GAP, row, halfWidth, ROW_HEIGHT).build())
}
addRenderableWidget(Button.builder("Playback: ${format(SpeedTuner.playbackSpeed)}x".text()) {
val next = (PLAYBACK_SPEEDS.indexOf(SpeedTuner.playbackSpeed) + 1) % PLAYBACK_SPEEDS.size
SpeedTuner.playbackSpeed = PLAYBACK_SPEEDS[next]
rebuildWidgets()
}.bounds(MARGIN, nextRow(), fullWidth, ROW_HEIGHT).build())
nextRow().let { row ->
val reviewed = entry?.let { SpeedTuner.isReviewed(it) } == true
addRenderableWidget(Button.builder((if (reviewed) "Reviewed ✔" else "Mark reviewed").text()) {
SpeedTuner.toggleReviewed()
rebuildWidgets()
}.bounds(MARGIN, row, halfWidth, ROW_HEIGHT).build())
addRenderableWidget(Button.builder("Export".text()) { SpeedTuner.export() }
.bounds(MARGIN + halfWidth + ROW_GAP, row, halfWidth, ROW_HEIGHT).build())
}
addRenderableWidget(Button.builder("End session".text()) {
SpeedTuner.stop()
onClose()
}.bounds(MARGIN, nextRow(), fullWidth, ROW_HEIGHT).build())
infoTop = y
}
private fun step(offset: Int) {
SpeedTuner.step(offset)
rebuildWidgets()
}
private fun nudgeSpeed(change: Double) {
val entry = SpeedTuner.selected ?: return
SpeedTuner.setSpeed((SpeedTuner.speed(entry) + change).coerceIn(0.0, MAX_SPEED))
rebuildWidgets()
}
private fun applyTypedSpeed() {
val typed = speedBox.value.toDoubleOrNull() ?: return
SpeedTuner.setSpeed(typed.coerceIn(0.0, MAX_SPEED))
rebuildWidgets()
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) {
if (searchBox.isFocused) {
if (SpeedTuner.search(searchBox.value)) rebuildWidgets()
return true
}
if (speedBox.isFocused) {
applyTypedSpeed()
return true
}
}
return super.keyPressed(keyCode, scanCode, modifiers)
}
override fun onClose() {
SpeedTuner.save()
super.onClose()
}
// Only darken the panel so the world stays visible
override fun renderBackground(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
guiGraphics.fill(0, 0, PANEL_WIDTH, height, PANEL_COLOUR)
}
override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
super.render(guiGraphics, mouseX, mouseY, partialTick)
val entry = SpeedTuner.selected
val title = entry?.let { selected ->
val name = PokemonSpecies.getByIdentifier(selected.speciesId)?.translatedName ?: selected.speciesId.path.text()
Component.literal("#${selected.dex} ").append(name)
} ?: "No species".text()
guiGraphics.drawCenteredString(font, title, PANEL_WIDTH / 2, MARGIN + (ROW_HEIGHT - font.lineHeight) / 2, TEXT_COLOUR)
var y = infoTop
fun line(text: String, colour: Int = TEXT_COLOUR) {
guiGraphics.drawString(font, text, MARGIN, y, colour)
y += TEXT_LINE_HEIGHT
}
if (entry != null) {
val speed = SpeedTuner.speed(entry)
line("Speed: ${format(speed)} blocks/s", if (SpeedTuner.isEdited(entry)) EDITED_COLOUR else TEXT_COLOUR)
line("Original: ${format(SpeedTuner.originalSpeed(entry))} blocks/s")
if (SpeedTuner.gait == SpeedTuner.Gait.WALK) {
line("(estimated, raw walkSpeed ${format(entry.walkSpeed.toDouble())})")
}
}
line("Sparks: player ${SpeedTuner.gait.name.lowercase()} ${format(SpeedTuner.referenceSpeed())} blocks/s")
line("Edited: ${SpeedTuner.editedCount()} Reviewed: ${SpeedTuner.reviewedCount()}")
line("Species: ${SpeedTuner.visibleEntries().size}")
}
private fun format(value: Double) = String.format(Locale.ROOT, "%.2f", value)
/** Slider over a decimal range that reports every change. */
class DecimalSlider(
x: Int,
y: Int,
width: Int,
private val label: String,
private val min: Double,
private val max: Double,
initial: Double,
private val onChange: (Double) -> Unit
) : AbstractSliderButton(x, y, width, ROW_HEIGHT, Component.empty(), 0.0) {
private val current: Double
get() = Mth.lerp(value, min, max)
init {
value = ((initial - min) / (max - min)).coerceIn(0.0, 1.0)
updateMessage()
}
override fun updateMessage() {
message = "$label: ${String.format(Locale.ROOT, "%.2f", current)}".text()
}
override fun applyValue() {
onChange(current)
}
}
}
@@ -11,11 +11,14 @@ package com.cobblemon.mod.common.client.keybind.keybinds
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.keybind.CobblemonKeyBinding
import com.cobblemon.mod.common.client.gui.debug.speed.SpeedTuner
import com.cobblemon.mod.common.client.gui.debug.speed.SpeedTunerScreen
import com.cobblemon.mod.common.client.keybind.KeybindCategories
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.debug.RequestOpenRidingStatsDebugGUIPacket
import com.cobblemon.mod.common.net.messages.server.debug.RequestOpenSpeedTunerPacket
import com.mojang.blaze3d.platform.InputConstants
import net.minecraft.client.Minecraft
import net.minecraft.network.chat.Component
@@ -30,6 +33,7 @@ object DebugKeybindings {
TranslateDownKeybinding(),
PrintModelSettingsKeybinding(),
ToggleRidingStatsDebugGUIKeybinding(),
ToggleSpeedTunerKeybinding(),
)
class ScaleUpKeybinding: CobblemonKeyBinding(
@@ -163,4 +167,20 @@ object DebugKeybindings {
}
}
}
class ToggleSpeedTunerKeybinding : CobblemonKeyBinding(
"key.cobblemon.togglespeedtuner",
InputConstants.Type.KEYSYM,
InputConstants.KEY_APOSTROPHE,
KeybindCategories.COBBLEMON_DEBUG_CATEGORY
) {
override fun onPress() {
// A running session just needs its panel back, otherwise the server checks permission and sends speeds
if (SpeedTuner.active) {
Minecraft.getInstance().setScreen(SpeedTunerScreen())
} else {
RequestOpenSpeedTunerPacket().sendToServer()
}
}
}
}
@@ -0,0 +1,20 @@
/*
* 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.debug
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.gui.debug.speed.SpeedTuner
import com.cobblemon.mod.common.net.messages.client.debug.OpenSpeedTunerPacket
import net.minecraft.client.Minecraft
object OpenSpeedTunerHandler : ClientNetworkPacketHandler<OpenSpeedTunerPacket> {
override fun handle(packet: OpenSpeedTunerPacket, client: Minecraft) {
SpeedTuner.open(packet.entries)
}
}
@@ -48,6 +48,7 @@ import com.mojang.blaze3d.vertex.DefaultVertexFormat
import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.blaze3d.vertex.VertexConsumer
import com.mojang.blaze3d.vertex.VertexFormat
import com.cobblemon.mod.common.client.render.pokemon.ModelPitch
import com.mojang.math.Axis
import net.minecraft.Util
import net.minecraft.client.model.geom.ModelPart
@@ -776,7 +777,14 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
matrixStack.mulPose(transformationMatrix)
} else {
yRot = Mth.lerp(state.getPartialTicks(), entity.yBodyRotO, entity.yBodyRot)
yRot = 180f - Mth.wrapDegrees(yRot)
val tilt = ModelPitch.tiltDegrees(entity, state.getPartialTicks())
if (tilt != 0F) {
// Same as the model tilt in PokemonRenderer
matrixStack.mulPose(ModelPitch.aroundCentre(entity, ModelPitch.orientation(yRot, tilt)))
yRot = 0F
} else {
yRot = 180f - Mth.wrapDegrees(yRot)
}
}
} else if (entity is EmptyPokeBallEntity) {
scale = 0.7F
@@ -19,7 +19,7 @@ import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.riding.util.RidingAnimationData
import com.cobblemon.mod.common.api.riding.util.DynamicAnimationData
import com.cobblemon.mod.common.api.scheduling.Schedulable
import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.client.particle.BedrockParticleOptionsRepository
@@ -141,7 +141,7 @@ abstract class PosableState : Schedulable {
*/
val renderMarkers = mutableSetOf<String>()
/** The MoLang functions that expose current [RidingAnimationData] data.
/** The MoLang functions that expose current [DynamicAnimationData] data.
* They are all divided by a reference value and coerece into a float range useful in animation
*/
val ridingFunctions = QueryStruct(hashMapOf())
@@ -10,23 +10,20 @@ package com.cobblemon.mod.common.client.render.models.blockbench.animation
import com.cobblemon.mod.common.client.render.models.blockbench.PosableModel
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
import com.cobblemon.mod.common.client.render.models.blockbench.addRotation
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Bone
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
import com.cobblemon.mod.common.util.math.geometry.toDegrees
import com.cobblemon.mod.common.util.math.geometry.toRadians
import net.minecraft.util.Mth
/**
* Animation used to tilt the targeted bone of the entity by a pitch that is proportionate
* to the direction the entity is moving. This is used to make birds and fish tilt upwards
* or downwards based on their motion.
* or downwards based on their motion. The pitch comes from the eased heading locomotion travels on.
*
* @author Hiroku
* @since August 9th, 2025
*/
class PitchTiltAnimation(
val bone: Bone,
// Not used anymore since the heading is already eased, kept so existing posers still load
val maxChangePerTick: Float = 1F,
val minPitch: Float = -45F,
val maxPitch: Float = 45F,
@@ -35,6 +32,8 @@ class PitchTiltAnimation(
const val PREVIOUS_ANGLE = "previous_pitch_tilt_angle"
const val CORRECTED_ANGLE = "corrected_pitch_tilt_angle"
const val PITCHED_TILT = "has_pitched_tilt"
// How much of the tilt has been applied this render, so poses blending together add up to one full tilt
const val APPLIED_WEIGHT = "applied_pitch_tilt_weight"
}
override fun setupAnim(
@@ -48,36 +47,7 @@ class PitchTiltAnimation(
headPitch: Float,
intensity: Float
) {
val entity = context.entity ?: return
if (entity.passengers.isNotEmpty()) {
return
}
val delta = entity.deltaMovement
val vertical = delta.y
val horizontal = delta.horizontalDistance()
// It should be the angle in degrees that becomes higher as the entity's motion becomes more upwards, and negative as the entity's motion becomes more downwards
val angleOfMotion = if (horizontal == 0.0) {
when {
vertical > 0 -> maxPitch
vertical < 0 -> minPitch
else -> 0F
}
} else {
Mth.atan2(vertical, horizontal).toDegrees()
}.coerceIn(minPitch, maxPitch) * -1
val lastAngle = state.numbers[PREVIOUS_ANGLE] ?: 0F
val change = (angleOfMotion - lastAngle).coerceIn(-maxChangePerTick, maxChangePerTick)
if (PITCHED_TILT !in state.renderMarkers) {
state.numbers.remove(CORRECTED_ANGLE)
val correctedAngle = (lastAngle + change) * intensity
bone.addRotation(0, correctedAngle.toRadians())
state.numbers[PREVIOUS_ANGLE] = correctedAngle
} else {
// It was already pitched, which means the body bone already has a rotation, so only rotate it a pinch more.
bone.addRotation(0, change.toRadians() * intensity)
state.numbers[PREVIOUS_ANGLE] = lastAngle + change * intensity
}
state.renderMarkers += PITCHED_TILT
// The whole model is tilted by ModelPitch now so this does nothing\
//TODO: remove posers with this anim if we like ModelPitch
}
}
@@ -0,0 +1,43 @@
/*
* 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.pokemon
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.math.geometry.toRadians
import net.minecraft.util.Mth
import org.joml.Matrix4f
import org.joml.Quaternionf
import org.joml.Vector3f
/**
* Tilts an unridden pokemon's whole model with how it's climbing or diving, the same way ridden flyers are turned
* by their orientation. Walkers always travel level so they never tilt.
*/
object ModelPitch {
// Most the model tilts either way
const val MAX_TILT_DEGREES = 45F
/** Minecraft pitch to tilt by. Only as much as travel is horizontal */
fun tiltDegrees(entity: PokemonEntity, partialTick: Float): Float {
if (entity.passengers.isNotEmpty()) return 0F
val pitch = entity.heading.pitch.valueAt(partialTick)
return (pitch * Mth.cos(pitch.toRadians())).coerceIn(-MAX_TILT_DEGREES, MAX_TILT_DEGREES)
}
/** Built the same way as OrientationController.rotate so it lines up with ridden orientation */
fun orientation(yaw: Float, tilt: Float): Quaternionf {
return Quaternionf().rotateY(-(yaw - 180F).toRadians()).rotateX(-tilt.toRadians())
}
/** Pivots orientation around the middle of the hitbox */
fun aroundCentre(entity: PokemonEntity, orientation: Quaternionf): Matrix4f {
val centre = Vector3f(0F, entity.bbHeight / 2, 0F)
return Matrix4f().translate(centre).rotate(orientation).translate(centre.negate(Vector3f()))
}
}
@@ -149,7 +149,19 @@ class PokemonRenderer(
if (entity.passengers.isNotEmpty()) {
renderRiding(entity, entityYaw, partialTicks, poseMatrix, buffer, packedLight)
} else {
super.render(entity, entityYaw, partialTicks, poseMatrix, buffer, packedLight)
val tilt = ModelPitch.tiltDegrees(entity, partialTicks)
if (tilt != 0F) {
poseMatrix.pushPose()
val yaw = Mth.rotLerp(partialTicks, entity.yBodyRotO, entity.yBodyRot)
val transformationMatrix = ModelPitch.aroundCentre(entity, ModelPitch.orientation(yaw, tilt))
// Pre-undo yaw rotation, the orientation already has it
transformationMatrix.rotate(Axis.YP.rotationDegrees(yaw + 180F))
poseMatrix.last().pose().mul(transformationMatrix)
super.render(entity, entityYaw, partialTicks, poseMatrix, buffer, packedLight)
poseMatrix.popPose()
} else {
super.render(entity, entityYaw, partialTicks, poseMatrix, buffer, packedLight)
}
}
// Call rendering for alpha eye trail
@@ -10,24 +10,41 @@ package com.cobblemon.mod.common.entity.ai
import net.minecraft.core.BlockPos
import net.minecraft.world.entity.ai.behavior.BlockPosTracker
import net.minecraft.world.entity.ai.behavior.PositionTracker
import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.level.pathfinder.PathType
/**
* This is a trick to allow us to instruct the node-maker to only consider specific node types,
* even if the entity can, in a pinch, use more.
* even if the entity can, in a pinch, use more. Can also have the entity sprint while it is further
* than [sprintDistance] from the target, and count as arrived once within [stopDistance]. Both are measured
* from the edge of its hitbox so big mons keep the same gap as small ones. With [sprintWithTarget] it also sprints
* while the entity it's following is sprinting.
*
* @author Hiroku
* @since March 24th, 2025
*/
class CobblemonWalkTarget(
pos: BlockPos,
target: PositionTracker,
speedModifier: Float,
completionRange: Int,
val nodeTypeFilter: (PathType) -> Boolean = { true },
val destinationNodeTypeFilter: (PathType) -> Boolean = { true }
val destinationNodeTypeFilter: (PathType) -> Boolean = { true },
val sprintDistance: Float? = null,
val stopDistance: Float? = null,
val sprintWithTarget: Boolean = false
) : WalkTarget(
BlockPosTracker(pos),
target,
speedModifier,
completionRange
)
) {
constructor(
pos: BlockPos,
speedModifier: Float,
completionRange: Int,
nodeTypeFilter: (PathType) -> Boolean = { true },
destinationNodeTypeFilter: (PathType) -> Boolean = { true },
sprintDistance: Float? = null,
stopDistance: Float? = null
) : this(BlockPosTracker(pos), speedModifier, completionRange, nodeTypeFilter, destinationNodeTypeFilter, sprintDistance, stopDistance)
}
@@ -38,6 +38,7 @@ class FollowWalkTargetTask(
private var path: Path? = null
private var lookTargetPos: BlockPos? = null
private var speed = 0f
private var sprinting = false
override fun checkExtraStartConditions(level: ServerLevel, entity: PathfinderMob): Boolean {
if (this.pathUpdateCountdownTicks > 0) {
@@ -87,14 +88,17 @@ class FollowWalkTargetTask(
entity.brain.eraseMemory(MemoryModuleType.WALK_TARGET)
entity.brain.eraseMemory(MemoryModuleType.PATH)
this.path = null
this.sprinting = false
}
override fun start(arg: ServerLevel, entity: PathfinderMob, time: Long) {
entity.brain.setMemory(MemoryModuleType.PATH, this.path)
entity.navigation.moveTo(this.path, speed.toDouble())
updateSprinting(entity)
}
override fun tick(world: ServerLevel, entity: PathfinderMob, l: Long) {
updateSprinting(entity)
val path = entity.navigation.path
val brain = entity.brain
if (this.path !== path) {
@@ -196,11 +200,42 @@ class FollowWalkTargetTask(
return path
}
/**
* Sprints while far from the walk target, stopping a bit closer than it started so it doesn't flicker.
* Can also sprint while the entity being followed is sprinting.
*/
private fun updateSprinting(entity: PathfinderMob) {
val navigation = entity.navigation as? OmniPathNavigation ?: return
val walkTarget = entity.brain.getMemory(MemoryModuleType.WALK_TARGET).orElse(null) as? CobblemonWalkTarget
val sprintDistance = walkTarget?.sprintDistance
sprinting = if (walkTarget == null || sprintDistance == null) {
false
} else {
val gap = edgeGap(entity, walkTarget)
val targetSprinting = walkTarget.sprintWithTarget && (walkTarget.target as? EntityTracker)?.entity?.isSprinting == true
targetSprinting || gap > if (sprinting) sprintDistance * SPRINT_STOP_FRACTION else sprintDistance
}
navigation.sprinting = sprinting
}
private fun hasReached(entity: PathfinderMob, walkTarget: WalkTarget): Boolean {
val stopDistance = (walkTarget as? CobblemonWalkTarget)?.stopDistance
if (stopDistance != null) {
return edgeGap(entity, walkTarget) <= stopDistance
}
return walkTarget.target.currentBlockPosition().distManhattan(entity.blockPosition()) <= walkTarget.closeEnoughDist
}
/** Straight line distance between hitbox edges, or to the target position when it isn't an entity. */
private fun edgeGap(entity: PathfinderMob, walkTarget: WalkTarget): Double {
val targetRadius = (walkTarget.target as? EntityTracker)?.entity?.let { it.bbWidth / 2.0 } ?: 0.0
return walkTarget.target.currentPosition().distanceTo(entity.position()) - entity.bbWidth / 2.0 - targetRadius
}
companion object {
// Once sprinting, keep going until this fraction of the sprint distance
const val SPRINT_STOP_FRACTION = 0.5F
private fun isTargetSpectator(target: WalkTarget): Boolean {
val lookTarget = target.target
return if (lookTarget is EntityTracker) {
@@ -16,16 +16,18 @@ import com.cobblemon.mod.common.util.resolveFloat
import com.cobblemon.mod.common.util.resolveInt
import com.cobblemon.mod.common.util.withQueryValue
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.EntityTracker
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.WalkTarget
object MoveToAttackTargetTask {
fun create(
speedMultiplier: Expression = "0.5".asExpression(),
closeEnoughDistance: Expression = "0".asExpression()
closeEnoughDistance: Expression = "0".asExpression(),
// Gap between the pokemon's hitbox and the target's before it sprints. At 0 it sprints until they touch
sprintDistance: Expression = "0.0".asExpression()
): OneShot<LivingEntity> = BehaviorBuilder.create {
it.group(
it.present(MemoryModuleType.ATTACK_TARGET),
@@ -35,12 +37,22 @@ object MoveToAttackTargetTask {
mainThreadRuntime.withQueryValue("entity", entity.asMostSpecificMoLangValue())
val speedMultiplier = mainThreadRuntime.resolveFloat(speedMultiplier)
val closeEnoughDistance = mainThreadRuntime.resolveInt(closeEnoughDistance)
val sprintDistance = mainThreadRuntime.resolveFloat(sprintDistance)
val attackTarget = it.get(attackTarget)
val position = attackTarget.position()
val walkTarget = it.tryGet(walkTarget).orElse(null)
if (walkTarget == null || walkTarget.target.currentPosition().distanceToSqr(position) > closeEnoughDistance) {
entity.brain.setMemory(MemoryModuleType.WALK_TARGET, WalkTarget(attackTarget, speedMultiplier, closeEnoughDistance))
entity.brain.setMemory(
MemoryModuleType.WALK_TARGET,
CobblemonWalkTarget(
target = EntityTracker(attackTarget, false),
speedModifier = speedMultiplier,
completionRange = closeEnoughDistance,
sprintDistance = sprintDistance,
stopDistance = 0F
)
)
true
} else {
false
@@ -31,10 +31,16 @@ import net.minecraft.world.level.pathfinder.Path
import net.minecraft.world.level.pathfinder.PathComputationType
import net.minecraft.world.level.pathfinder.PathFinder
import net.minecraft.world.level.pathfinder.PathType
import net.minecraft.world.level.pathfinder.WalkNodeEvaluator
import net.minecraft.world.phys.Vec3
import com.cobblemon.mod.common.util.math.arriveSpeed
import com.cobblemon.mod.common.util.math.slowDownSpeed
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.acos
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
/**
* A navigator designed to work with the [OmniPathNodeMaker], allowing a path that can cross land, water, and air
@@ -48,6 +54,12 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
val pather = entity as OmniPathingEntity
var cachedCurrentNode: Node? = null
// Per node steering results for the path they were worked out for, by node index
private var cachedSteeringPath: Path? = null
private val tightCorners = mutableMapOf<Int, Boolean>()
private val narrowNodes = mutableMapOf<Int, Boolean>()
private val stepNodes = mutableMapOf<Int, Boolean>()
private val steerPositions = mutableMapOf<Int, Vec3>()
var currentNodeDistance = 0F
data class NavigationContext(
@@ -61,9 +73,35 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
)
var navigationContext = NavigationContext()
// How far a steered mover travels while finishing a turn, given by its move control each tick
private var turnDistance = 0.0
// Whether whatever started the current path wants it sprinted. Cleared on every new path and on stop
var sprinting = false
/**
* Where a steered mover should head this tick. [isFinal] means it should slow down and stop there.
* [maxSpeed] is how fast it can go right now while still slowing in time for the corners ahead.
*/
data class SteerTarget(
val position: Vec3,
val isFinal: Boolean,
val maxSpeed: Double,
val narrowAhead: Boolean = false,
// Which way the path carries on from a node the target is waiting on, null when it isn't waiting on one
val afterDirection: Vec3? = null
)
companion object {
val verticallyPreciseNodeTypes = setOf(PathType.WATER, PathType.OPEN, PathType.LAVA)
// Corners sharper than this start slowing the mob down
const val CORNER_FREE_ANGLE_DEGREES = 45.0
// Fraction of top speed kept going round a full reversal
const val CORNER_MIN_SPEED_FRACTION = 0.5
// Fraction of top speed kept going round a corner that can't be cut
const val TIGHT_CORNER_SPEED_FRACTION = 0.3
// Largest distance from a tight corner that still counts as reaching it, so big mobs can't turn early
const val TIGHT_CORNER_REACH_DISTANCE = 0.5
}
override fun createPathFinder(range: Int): PathFinder {
@@ -137,8 +175,11 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
&& f < this.maxDistanceToWaypoint.toDouble()
&& e < (if (currentNode.type in verticallyPreciseNodeTypes && (mob.isUnderWater || pather.isFlying())) maxDistanceToWaypoint else 1.0).toDouble()
val reachedTightCorner = !mustTouch(path!!, path!!.nextNodeIndex) ||
horizontalDistance(mob.position(), steerPosition(path!!, path!!.nextNodeIndex)) <= min(maxDistanceToWaypoint.toDouble(), TIGHT_CORNER_REACH_DISTANCE)
// Corner cutting is commented out because it makes pokemon and NPCs 'cut' the corner and fall into water or lava
if (closeEnough) {// || mob.navigation.canCutCorner(path!!.nextNode.type) && shouldTargetNextNodeInDirection(vec3d)) {
if ((closeEnough && reachedTightCorner) || hasPassedNextNode(path!!)) {// || mob.navigation.canCutCorner(path!!.nextNode.type) && shouldTargetNextNodeInDirection(vec3d)) {
path!!.advance()
if (path!!.isDone) {
path = null
@@ -162,6 +203,212 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
doStuckDetection(vec3d)
}
/**
* Pure pursuit path following. The target is a point a lookahead distance further along the path than the
* mob's closest point on it, so the mob eases back onto the path and rounds corners instead of zigzagging.
* Also scans ahead for corners and the path end to work out how fast the mob can go and still slow in time.
*/
fun getSteerTarget(maxSpeed: Double, speedBlendTicks: Double, turnDistance: Double): SteerTarget? {
this.turnDistance = turnDistance
val currentPath = path?.takeUnless { it.isDone } ?: return null
val index = currentPath.nextNodeIndex
val lastIndex = currentPath.nodeCount - 1
if (lastIndex == 0) return SteerTarget(steerPosition(currentPath, 0), true, maxSpeed)
// A fresh path starts next to the mob, so follow its first segment rather than seeking node 0 itself
val segmentEnd = max(index, 1)
val lookAhead = lookAheadDistance()
// Far enough to see anything we'd need to start slowing for, stopping from top speed covers this much
val scanDistance = max(lookAhead, maxSpeed * speedBlendTicks / 2)
var point = closestPointOnSegment(mob.position(), steerPosition(currentPath, segmentEnd - 1), steerPosition(currentPath, segmentEnd))
var distance = 0.0
var carrot: Vec3? = null
var speedLimit = maxSpeed
var narrowAhead = isNarrow(currentPath, segmentEnd - 1)
var afterDirection: Vec3? = null
for (i in segmentEnd..lastIndex) {
narrowAhead = narrowAhead || isNarrow(currentPath, i)
val node = steerPosition(currentPath, i)
val segmentLength = node.distanceTo(point)
if (carrot == null && distance + segmentLength >= lookAhead) {
carrot = point.add(node.subtract(point).normalize().scale(lookAhead - distance))
}
distance += segmentLength
if (i == lastIndex) {
// Brake to a stop on the end
speedLimit = min(speedLimit, arriveSpeed(distance, maxSpeed, speedBlendTicks))
break
}
if (distance > scanDistance) break
val tight = mustTouch(currentPath, i)
val cornerSpeed = maxSpeed * if (tight) TIGHT_CORNER_SPEED_FRACTION else cornerSpeedFraction(currentPath, i)
speedLimit = min(speedLimit, slowDownSpeed(distance, cornerSpeed, speedBlendTicks))
// Rounding this corner would leave the path, so the carrot waits on it until it's reached
if (tight && carrot == null) {
carrot = node
afterDirection = steerPosition(currentPath, i + 1).subtract(node)
}
point = node
}
// Lookahead ran off the end so arrive on the last node
return if (carrot == null) {
SteerTarget(steerPosition(currentPath, lastIndex), true, speedLimit, narrowAhead)
} else {
SteerTarget(carrot, false, speedLimit, narrowAhead, afterDirection)
}
}
/**
* Aim at least as far ahead as the mob travels while finishing a turn otherwise it reaches the path before the
* correction is done and crosses over it.
*/
private fun lookAheadDistance() = max(turnDistance, mob.bbWidth.toDouble()) + mob.bbWidth / 2.0
private fun closestPointOnSegment(pos: Vec3, start: Vec3, end: Vec3): Vec3 {
val segment = end.subtract(start)
val lengthSqr = segment.lengthSqr()
if (lengthSqr == 0.0) return start
val t = (pos.subtract(start).dot(segment) / lengthSqr).coerceIn(0.0, 1.0)
return start.add(segment.scale(t))
}
/**
* A walking corner where cutting the inside would cross a cell that isn't walkable, like a turn on a
* narrow ledge or around a hole.
*/
/** Nodes the mob has to properly reach rather than round off, tight corners and steps */
private fun mustTouch(currentPath: Path, i: Int) = isTightCorner(currentPath, i) || isStep(currentPath, i)
/**
* A walking node next to a change in height
*/
private fun isStep(currentPath: Path, i: Int): Boolean {
refreshSteeringCache(currentPath)
return stepNodes.getOrPut(i) {
val node = currentPath.getNode(i)
if (node.type != PathType.WALKABLE) return@getOrPut false
val previous = if (i > 0) currentPath.getNode(i - 1) else null
val next = if (i < currentPath.nodeCount - 1) currentPath.getNode(i + 1) else null
(previous != null && previous.y != node.y) || (next != null && next.y != node.y)
}
}
private fun isTightCorner(currentPath: Path, i: Int): Boolean {
if (i <= 0 || i >= currentPath.nodeCount - 1) return false
refreshSteeringCache(currentPath)
return tightCorners.getOrPut(i) {
val node = currentPath.getNode(i)
if (node.type != PathType.WALKABLE) return@getOrPut false
val position = steerPosition(currentPath, i)
// The cell diagonally inside the turn. For a straight line this is the node's own cell
val insidePosition = steerPosition(currentPath, i - 1).add(steerPosition(currentPath, i + 1)).subtract(position)
val inside = BlockPos.containing(insidePosition.x, node.y.toDouble(), insidePosition.z)
val cell = BlockPos.containing(position.x, node.y.toDouble(), position.z)
inside != cell && WalkNodeEvaluator.getPathTypeStatic(mob, inside.mutable()) != PathType.WALKABLE
}
}
/** A walking node with nothing walkable on either side of the path, like a 1 wide ledge */
private fun isNarrow(currentPath: Path, i: Int): Boolean {
refreshSteeringCache(currentPath)
return narrowNodes.getOrPut(i) {
val node = currentPath.getNode(i)
if (node.type != PathType.WALKABLE || currentPath.nodeCount < 2) return@getOrPut false
val position = steerPosition(currentPath, i)
// Direction of the path through this node, from whichever neighbours it has
val from = steerPosition(currentPath, max(i - 1, 0))
val to = steerPosition(currentPath, min(i + 1, currentPath.nodeCount - 1))
val along = Vec3(to.x - from.x, 0.0, to.z - from.z)
if (along.lengthSqr() == 0.0) return@getOrPut false
val side = Vec3(-along.z, 0.0, along.x).normalize()
val left = BlockPos.containing(position.x + side.x, node.y.toDouble(), position.z + side.z)
val right = BlockPos.containing(position.x - side.x, node.y.toDouble(), position.z - side.z)
WalkNodeEvaluator.getPathTypeStatic(mob, left.mutable()) != PathType.WALKABLE &&
WalkNodeEvaluator.getPathTypeStatic(mob, right.mutable()) != PathType.WALKABLE
}
}
/**
* Where the mob's centre should be at node [i]. Big mobs are checked over a footprint of blocks and a node counts
* as walkable with only part of that footprint over ground, so they're centred over the walkable part instead.
*/
private fun steerPosition(currentPath: Path, i: Int): Vec3 {
refreshSteeringCache(currentPath)
return steerPositions.getOrPut(i) {
val default = currentPath.getEntityPosAtNode(mob, i)
val node = currentPath.getNode(i)
// Same footprint as OmniPathNodeMaker.getPathTypeOfMob
val sizeX = (mob.boundingBox.xsize.toInt() + 1).coerceAtMost(2)
val sizeZ = (mob.boundingBox.zsize.toInt() + 1).coerceAtMost(2)
if (node.type != PathType.WALKABLE || (sizeX == 1 && sizeZ == 1)) return@getOrPut default
val walkable = mutableListOf<BlockPos>()
for (dx in 0 until sizeX) {
for (dz in 0 until sizeZ) {
val pos = BlockPos(node.x + dx, node.y, node.z + dz)
if (WalkNodeEvaluator.getPathTypeStatic(mob, pos.mutable()) == PathType.WALKABLE) {
walkable.add(pos)
}
}
}
if (walkable.isEmpty() || walkable.size == sizeX * sizeZ) {
default
} else {
Vec3(walkable.map { it.x }.average() + 0.5, default.y, walkable.map { it.z }.average() + 0.5)
}
}
}
private fun refreshSteeringCache(currentPath: Path) {
if (cachedSteeringPath !== currentPath) {
cachedSteeringPath = currentPath
tightCorners.clear()
narrowNodes.clear()
stepNodes.clear()
steerPositions.clear()
}
}
private fun horizontalDistance(a: Vec3, b: Vec3) = sqrt((a.x - b.x) * (a.x - b.x) + (a.z - b.z) * (a.z - b.z))
/** 1 for gentle turns, falling to [CORNER_MIN_SPEED_FRACTION] for a full reversal at node [i]. */
private fun cornerSpeedFraction(currentPath: Path, i: Int): Double {
val node = steerPosition(currentPath, i)
val incoming = node.subtract(steerPosition(currentPath, i - 1))
val outgoing = steerPosition(currentPath, i + 1).subtract(node)
if (incoming.lengthSqr() == 0.0 || outgoing.lengthSqr() == 0.0) return 1.0
val cosAngle = incoming.normalize().dot(outgoing.normalize()).coerceIn(-1.0, 1.0)
val angle = Math.toDegrees(acos(cosAngle))
if (angle <= CORNER_FREE_ANGLE_DEGREES) return 1.0
val sharpness = (angle - CORNER_FREE_ANGLE_DEGREES) / (180.0 - CORNER_FREE_ANGLE_DEGREES)
return Mth.lerp(sharpness, 1.0, CORNER_MIN_SPEED_FRACTION)
}
/**
* Steering rounds corners instead of touching each node, so a node counts as reached once the mob is past it
* along its segment. Node type changes and the final node still need a real touch.
*/
private fun hasPassedNextNode(currentPath: Path): Boolean {
val index = currentPath.nextNodeIndex
if (index >= currentPath.nodeCount - 1) return false
if (currentPath.getNode(index).type != currentPath.getNode(index + 1).type) return false
if (mustTouch(currentPath, index)) return false
if (index == 0) {
// Nothing behind the first node, so it's passed once the mob is on the far side of it
val first = steerPosition(currentPath, 0)
val outgoing = steerPosition(currentPath, 1).subtract(first)
return mob.position().subtract(first).dot(outgoing) >= 0 && mob.position().distanceTo(first) < maxDistanceToWaypoint + lookAheadDistance()
}
val start = steerPosition(currentPath, index - 1)
val end = steerPosition(currentPath, index)
val along = mob.position().subtract(start).dot(end.subtract(start).normalize())
return along >= end.distanceTo(start) && mob.position().distanceTo(end) < maxDistanceToWaypoint + lookAheadDistance()
}
fun isAirborne(world: Level, pos: BlockPos) =
world.getBlockState(pos).isPathfindable(PathComputationType.AIR)
&& world.getBlockState(pos.below(1)).isPathfindable(PathComputationType.AIR)
@@ -224,6 +471,7 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
fun moveTo(x: Double, y: Double, z: Double, speed: Double = 1.0, navigationContext: NavigationContext) {
this.navigationContext = navigationContext
this.moveTo(x, y, z, speed)
this.sprinting = navigationContext.sprinting
}
override fun isStableDestination(pos: BlockPos): Boolean {
@@ -277,6 +525,7 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
}
override fun moveTo(path: Path?, speed: Double): Boolean {
sprinting = false
if (path != null && path.nodeCount > 0) {
val node = path.getNode(0)!!
// pokemonEntity.discard()
@@ -395,6 +644,7 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
override fun stop() {
super.stop()
sprinting = false
this.currentNodeDistance = -1F
this.cachedCurrentNode = null
path = null
@@ -56,7 +56,7 @@ import com.cobblemon.mod.common.api.riding.behaviour.RidingController
import com.cobblemon.mod.common.api.riding.events.SelectDriverEvent
import com.cobblemon.mod.common.api.riding.sound.RideSoundManager
import com.cobblemon.mod.common.api.riding.stats.RidingStat
import com.cobblemon.mod.common.api.riding.util.RidingAnimationData
import com.cobblemon.mod.common.api.riding.util.DynamicAnimationData
import com.cobblemon.mod.common.api.scheduling.Schedulable
import com.cobblemon.mod.common.api.scheduling.SchedulingTracker
import com.cobblemon.mod.common.api.scheduling.afterOnServer
@@ -77,6 +77,9 @@ import com.cobblemon.mod.common.entity.ai.OmniPathNavigation
import com.cobblemon.mod.common.entity.generic.GenericBedrockEntity
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.entity.pokeball.EmptyPokeBallEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonBodyRotationControl
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonLookControl
import com.cobblemon.mod.common.entity.pokemon.ai.HeadingEase
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.entity.pokemon.ai.sensors.PokemonItemSensor
import com.cobblemon.mod.common.entity.pokemon.effects.EffectTracker
@@ -150,6 +153,7 @@ import net.minecraft.world.entity.*
import net.minecraft.world.entity.ai.Brain
import net.minecraft.world.entity.ai.attributes.AttributeSupplier
import net.minecraft.world.entity.ai.attributes.Attributes
import net.minecraft.world.entity.ai.control.BodyRotationControl
import net.minecraft.world.entity.ai.control.MoveControl
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.MemoryStatus
@@ -175,6 +179,7 @@ import net.minecraft.world.level.material.FluidState
import net.minecraft.world.level.pathfinder.PathType
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import org.joml.Vector3f
import java.util.*
import java.util.concurrent.CompletableFuture
import kotlin.math.PI
@@ -217,6 +222,12 @@ open class PokemonEntity(
@JvmStatic var RIDE_STAMINA = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.FLOAT)
@JvmStatic var SCALE_MODIFIER = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.FLOAT)
@JvmStatic var IS_ALPHA = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.BOOLEAN)
@JvmStatic val IS_SPRINTING = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.BOOLEAN)
// Heading the server is easing towards as yaw, pitch and 0 when not steering, 1 when steering, 2 for a floating look turn
// and 3 for a standing look turn
@JvmStatic val HEADING_TARGET = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.VECTOR3)
// Blend time in seconds travel turns use for how the pokemon is moving
@JvmStatic val HEADING_TRAVEL_BLEND = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.FLOAT)
const val BATTLE_LOCK = "battle"
const val EVOLUTION_LOCK = "evolving"
@@ -298,6 +309,9 @@ open class PokemonEntity(
var shownItem: ItemStack
get() = entityData.get(SHOWN_HELD_ITEM)
set(value) = entityData.set(SHOWN_HELD_ITEM, value)
var isPokemonSprinting: Boolean
get() = entityData.get(IS_SPRINTING)
set(value) { entityData.set(IS_SPRINTING, value) }
var lastLightningBoltUUID: UUID? = null
@@ -310,7 +324,7 @@ open class PokemonEntity(
var enablePoseTypeRecalculation = true
val ridingAnimationData: RidingAnimationData = RidingAnimationData(this)
val ridingAnimationData: DynamicAnimationData = DynamicAnimationData(this)
var rideSoundManager: RideSoundManager = RideSoundManager(this)
private val rideStatOverrides = mutableMapOf<RidingStyle, MutableMap<RidingStat, Double>>()
@@ -320,6 +334,15 @@ open class PokemonEntity(
var previousRidingState: RidingBehaviourState? = null
private set
/** Push from PokemonMoveControl, added on the next travel right before vanilla moves the mon and applies friction */
var pendingThrust: Vec3 = Vec3.ZERO
/** Eased travel direction. Moved by PokemonMoveControl on the server and PokemonBodyRotationControl on the client */
val heading = HeadingEase()
/** Yaw a standing pokemon is turning its body to, set by PokemonBodyRotationControl and eased by PokemonMoveControl */
var stationaryTurnTarget: Float? = null
/** Whether that turn is a floating one, which uses the slower floating turn blend */
var stationaryTurnFloating = false
val runtime: MoLangRuntime by lazy {
MoLangRuntime()
.setup()
@@ -430,6 +453,7 @@ open class PokemonEntity(
delegate.changePokemon(pokemon)
addPosableFunctions(struct)
moveControl = PokemonMoveControl(this)
lookControl = PokemonLookControl(this)
if (!level().isClientSide) {
remakeBrain()
}
@@ -467,6 +491,9 @@ open class PokemonEntity(
builder.define(RIDE_STAMINA, 1F)
builder.define(SCALE_MODIFIER, 1F)
builder.define(IS_ALPHA, false)
builder.define(IS_SPRINTING, false)
builder.define(HEADING_TARGET, Vector3f())
builder.define(HEADING_TRAVEL_BLEND, PokemonMoveControl.GROUND_HEADING_BLEND_SECONDS)
}
override fun onSyncedDataUpdated(data: EntityDataAccessor<*>) {
@@ -653,11 +680,13 @@ open class PokemonEntity(
isPokemonFlying = flyDist - flyDistO > 0.005F
isPokemonWalking = walkDist - walkDistO > 0.005F
if (passengers.isNotEmpty() && level().isClientSide) {
rideSoundManager.tick()
if (level().isClientSide) {
ridingAnimationData.update()
} else if (!passengers.isNotEmpty() && level().isClientSide) {
rideSoundManager.stop()
if (passengers.isNotEmpty()) {
rideSoundManager.tick()
} else {
rideSoundManager.stop()
}
}
flyDistO = flyDist
@@ -708,6 +737,10 @@ open class PokemonEntity(
clearRestriction()
val spawnDirection = entityData.get(SPAWN_DIRECTION).takeIf { it.isFinite() } ?: 0F
yBodyRot = (spawnDirection * 1000F).toInt() / 1000F
yRot = yBodyRot
// hold the heading here too so turning starts from the spawn direction once this ends
heading.settle(yRot, snap = true)
stationaryTurnTarget = null
}
jumping = false
@@ -784,6 +817,10 @@ open class PokemonEntity(
this.moveControl = moveControl
}
override fun createBodyControl(): BodyRotationControl {
return PokemonBodyRotationControl(this)
}
/**
* Prevents water type Pokémon from taking drowning damage.
*/
@@ -1915,7 +1952,16 @@ open class PokemonEntity(
//Prevent current travel logic when riding a pokemon.
if (!this.hasRider() || this.controllingPassenger == null) {
// Same place vanilla adds walk input. Used up here so a second travel in a tick can't push twice
deltaMovement = deltaMovement.add(pendingThrust)
pendingThrust = Vec3.ZERO
// Powered flight uses its own drag instead of vanilla air friction
val poweredFlight = getBehaviourFlag(PokemonBehaviourFlag.FLYING) && !isInLiquid
setDiscardFriction(poweredFlight)
super.travel(movementInput)
if (poweredFlight) {
deltaMovement = deltaMovement.scale(PokemonMoveControl.FLYER_DRAG)
}
} else {
val inp = ifRidingAvailableSupply(fallback = Vec3.ZERO) { behaviour, settings, state ->
behaviour.velocity(settings, state, this, this.controllingPassenger as Player, deltaMovement)
@@ -26,6 +26,7 @@ import com.cobblemon.mod.common.api.spawning.fishing.FishingSpawnCause.Companion
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.activestate.ActivePokemonState
import com.cobblemon.mod.common.pokemon.activestate.SentOutState
@@ -244,11 +245,13 @@ class PokemonServerDelegate : PokemonSideDelegate {
entity.entityData.set(PokemonEntity.ASPECTS, trackedAspects)
entity.entityData.set(PokemonEntity.LABEL_LEVEL, entity.pokemon.level)
// Slow mons can travel less than the distance check each tick so steering somewhere counts too.
val travelling = entity.controllingPassenger == null && (entity.moveControl as? PokemonMoveControl)?.isTravelling == true
if (entity.getCurrentPoseType() in PoseType.FLYING_POSES) {
entity.entityData.set(PokemonEntity.MOVING, entity.isPokemonFlying)
entity.entityData.set(PokemonEntity.MOVING, entity.isPokemonFlying || travelling)
}
else {
entity.entityData.set(PokemonEntity.MOVING, entity.isPokemonWalking)
entity.entityData.set(PokemonEntity.MOVING, entity.isPokemonWalking || travelling)
}
entity.entityData.set(PokemonEntity.FRIENDSHIP, entity.pokemon.friendship)
@@ -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.entity.pokemon.ai
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl.Companion.STANDING_TURN_BLEND_SECONDS
import com.cobblemon.mod.common.util.math.InertialEase
import net.minecraft.world.phys.Vec3
/**
* The direction a pokemon is travelling, eased so it turns smoothly. The server moves with it and the client runs
* the same easing from the synced target so the body and animations see the same turn.
*/
class HeadingEase {
val yaw = InertialEase(STANDING_TURN_BLEND_SECONDS, isAngle = true)
// Minecraft pitch, negative is up
val pitch = InertialEase(STANDING_TURN_BLEND_SECONDS, isAngle = true)
/**
* Eases towards a direction taking [blendTime] for the turn. The blend only gets re-fit when it actually changes,
* so passing the same blend every tick keeps each turn's S curve.
*/
fun steer(targetYaw: Float, targetPitch: Float, blendTime: Float) {
yaw.setBlendTime(blendTime)
pitch.setBlendTime(blendTime)
yaw.update(targetYaw)
pitch.update(targetPitch)
}
/** Not steering so follow whatever else is turning the pokemon and level out */
fun settle(currentYaw: Float, snap: Boolean) {
if (snap) yaw.snap(currentYaw) else yaw.update(currentYaw)
// Level out at the standing turn pace rather than whatever travel was using
pitch.setBlendTime(STANDING_TURN_BLEND_SECONDS)
pitch.update(0F)
}
fun forward(): Vec3 = Vec3.directionFromRotation(pitch.value, yaw.value)
}
@@ -0,0 +1,97 @@
/*
* 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.entity.pokemon.ai
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import kotlin.math.abs
import net.minecraft.util.Mth
import net.minecraft.world.entity.ai.control.BodyRotationControl
/**
* Controls body movement of pokemon. Runs on both sides.
*
* Server: the body always faces yRot. When standing still it turns towards what it's looking at through the heading
* ease. Ground mons turn once, just far enough for the head to reach. Floating ones keep following the look.
* Client: the body follows the same heading ease as the server, run from the synced heading target.
*/
class PokemonBodyRotationControl(private val pokemon: PokemonEntity) : BodyRotationControl(pokemon) {
private var headingStarted = false
companion object {
// Share of the head's yaw limit a standing ground mon is happy to look across before turning its body.
// Posers usually limit the head well below the full limit so the body turns to keep things in that range
const val STANDING_TURN_HEAD_REACH_FRACTION = 0.25F
}
override fun clientTick() {
// Riding sets rotations itself
if (pokemon.controllingPassenger != null) {
// Keep the heading with the ride so it doesn't swing round on dismount
pokemon.heading.settle(pokemon.yRot, snap = true)
super.clientTick()
return
}
if (mob.level().isClientSide) {
smoothTowardsHeading()
} else {
followHeading()
}
}
private fun followHeading() {
if (!isMoving()) {
val lookYaw = (pokemon.lookControl as? PokemonLookControl)?.lookTargetYaw
val pose = pokemon.getCurrentPoseType()
val floating = pose in PoseType.FLYING_POSES || pose in PoseType.SWIMMING_POSES
if (floating && lookYaw != null) {
// Nothing to plant so keep easing towards whatever it's looking at
pokemon.stationaryTurnTarget = Mth.wrapDegrees(lookYaw)
pokemon.stationaryTurnFloating = true
} else if (lookYaw != null && pokemon.stationaryTurnTarget == null) {
val offset = Mth.wrapDegrees(lookYaw - pokemon.yRot)
val comfortableReach = pokemon.maxHeadYRot * STANDING_TURN_HEAD_REACH_FRACTION
if (abs(offset) > comfortableReach) {
// Turn the body in one go so the head is comfortable looking at it
val turn = offset - Math.copySign(comfortableReach, offset)
pokemon.stationaryTurnTarget = Mth.wrapDegrees(pokemon.yRot + turn)
pokemon.stationaryTurnFloating = false
}
}
}
pokemon.yBodyRot = pokemon.yRot
if (isMoving()) {
rotateHeadIfNecessary()
lastStableYHeadRot = mob.yHeadRot
headStableTime = 0
}
}
private fun smoothTowardsHeading() {
// Start facing where the pokemon already is instead of turning in from zero
if (!headingStarted) {
pokemon.heading.settle(pokemon.yRot, snap = true)
headingStarted = true
}
val target = pokemon.entityData.get(PokemonEntity.HEADING_TARGET)
if (target.z > 2.5F) {
pokemon.heading.steer(target.x, target.y, PokemonMoveControl.STANDING_TURN_BLEND_SECONDS)
} else if (target.z > 1.5F) {
pokemon.heading.steer(target.x, target.y, PokemonMoveControl.FLOATING_TURN_BLEND_SECONDS)
} else if (target.z > 0F) {
pokemon.heading.steer(target.x, target.y, pokemon.entityData.get(PokemonEntity.HEADING_TRAVEL_BLEND))
} else {
// Ease rather than snap so stationary turns from the server don't jump
pokemon.heading.settle(pokemon.yRot, snap = false)
}
// Keep continuous with last tick so plain lerps between yBodyRotO and yBodyRot don't spin at the wrap
pokemon.yBodyRot = pokemon.yBodyRotO + Mth.wrapDegrees(pokemon.heading.yaw.value - pokemon.yBodyRotO)
}
}
@@ -0,0 +1,66 @@
/*
* 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.entity.pokemon.ai
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.math.InertialEase
import net.minecraft.util.Mth
import net.minecraft.world.entity.ai.control.LookControl
/**
* Controls head movement of pokemon. The head is clamped to the body within the look itself, and
* [PokemonBodyRotationControl] turns the body using [lookTargetYaw].
*/
class PokemonLookControl(private val pokemon: PokemonEntity) : LookControl(pokemon) {
companion object {
const val HEAD_BLEND_SECONDS = 0.5F
}
private val headYaw = InertialEase(HEAD_BLEND_SECONDS, isAngle = true)
private val headPitch = InertialEase(HEAD_BLEND_SECONDS, isAngle = true)
private var headStarted = false
/** Yaw towards what the pokemon is looking at, null when it isn't looking at anything */
var lookTargetYaw: Float? = null
private set
override fun tick() {
lookTargetYaw = null
// Start from where the head already is instead of turning in from zero
if (!headStarted) {
headYaw.snap(mob.yHeadRot)
headPitch.snap(mob.xRot)
headStarted = true
}
// Focus on where it's going while travelling looking starts back up once it stops
if (!pokemon.navigation.isDone || pokemon.moveControl.hasWanted()) {
this.lookAtCooldown = 0
}
// nothing to look at so face forward and level out
var targetYaw = mob.yBodyRot
var targetPitch = 0F
val maxHeadTurn = mob.maxHeadYRot.toFloat()
if (this.lookAtCooldown > 0) {
--this.lookAtCooldown
getYRotD().ifPresent { lookYaw ->
lookTargetYaw = lookYaw
val offset = Mth.wrapDegrees(lookYaw - mob.yBodyRot)
targetYaw = mob.yBodyRot + Mth.clamp(offset, -maxHeadTurn, maxHeadTurn)
}
getXRotD().ifPresent { lookPitch -> targetPitch = lookPitch }
}
headYaw.update(Mth.wrapDegrees(targetYaw))
headPitch.update(targetPitch)
mob.yHeadRot = mob.yBodyRot + Mth.clamp(Mth.wrapDegrees(headYaw.value - mob.yBodyRot), -maxHeadTurn, maxHeadTurn)
mob.setXRot(headPitch.value)
}
}
@@ -16,7 +16,8 @@ import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.pokemon.PokemonBehaviourFlag
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.getWaterAndLavaIn
import com.cobblemon.mod.common.util.math.geometry.toDegrees
import com.cobblemon.mod.common.util.math.InertialEase
import com.cobblemon.mod.common.util.math.arriveSpeed
import com.cobblemon.mod.common.util.math.geometry.toRadians
import com.cobblemon.mod.common.util.resolveFloat
import kotlin.math.abs
@@ -33,11 +34,41 @@ import net.minecraft.world.level.material.FluidState
import net.minecraft.world.level.pathfinder.PathType
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import org.joml.Vector3f
import kotlin.math.sqrt
class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemonEntity) {
companion object {
const val VERY_CLOSE = 2.500000277905201E-3
const val TICKS_PER_SECOND = 20.0
// Travel turns get to a new direction within these times
const val GROUND_HEADING_BLEND_SECONDS = 0.5F
const val WATER_HEADING_BLEND_SECONDS = 0.8F
const val AIR_HEADING_BLEND_SECONDS = 1.0F
const val FLOATING_TURN_BLEND_SECONDS = 1.0F
const val STANDING_TURN_BLEND_SECONDS = 0.8F
// Speed gets to a new target
const val SPEED_BLEND_SECONDS = 0.3F
// Velocity kept each tick in powered flight
const val FLYER_DRAG = 0.7
// walkers ignore the task's speed so walk animations always match
const val GROUND_SPEED_MODIFIER = 0.35F
const val ARRIVAL_TOLERANCE = 0.05
const val FLUID_EXIT_Y_OFFSET = 0.05
// Vanilla values from LivingEntity.travel and aiStep
const val VANILLA_INPUT_SCALE = 0.98
const val VANILLA_GROUND_SPEED_FACTOR = 0.21600002
const val VANILLA_AIR_FRICTION = 0.91
const val VANILLA_FLYING_SPEED = 0.02
/** Speed where vanilla ground friction on a block with [blockFriction] balances a walk input of [speed]. */
fun vanillaGroundTopSpeed(speed: Double, blockFriction: Double): Double {
val frictionInfluencedSpeed = speed * (VANILLA_GROUND_SPEED_FACTOR / (blockFriction * blockFriction * blockFriction))
val inputForce = speed * VANILLA_INPUT_SCALE * frictionInfluencedSpeed
return inputForce / (1 - blockFriction * VANILLA_AIR_FRICTION)
}
}
val runtime = MoLangRuntime().also {
@@ -46,7 +77,32 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
private var waterLevel : Double = 0.0
var banking = false
// Where path following is steering this tick, null when not following a path
private var currentSteerTarget: Vec3? = null
var bankDurationTicks = -1
// Speed along the heading in blocks per tick of travel, eased so speed changes are smooth. Heading lives on the entity
private val speedEase = InertialEase(SPEED_BLEND_SECONDS)
// What steering asked for this tick, null when it isn't steering
private var steerGoal: SteerGoal? = null
/** True while steering somewhere this tick, however slowly. Used for the walk and fly poses */
var isTravelling = false
private set
private class SteerGoal(
val yaw: Float,
val pitch: Float,
val direction: Vec3,
val speed: Double,
val topSpeed: Double,
val distance: Double,
val horizontalOnly: Boolean,
val headingBlend: Float,
// Close enough to slide straight onto the target instead of turning to it. Attempting to prevent spinning
val slide: Boolean
)
// Travel turn blend for the way the mon is moving this tick
private var headingBlend = GROUND_HEADING_BLEND_SECONDS
var bankForwardBlocksPerTick = 0.1F
var bankUpwardsBlocksPerTick = 0F
var bankRightDegreesPerTick = 1F
@@ -74,6 +130,14 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
}
override fun tick() {
steerGoal = null
tickOperation()
updateMotion()
}
private fun tickOperation() {
val followedPath = currentSteerTarget != null
currentSteerTarget = null
if (banking && operation != Operation.WAIT) {
banking = false
}
@@ -92,17 +156,29 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
}
val behaviour = pokemonEntity.behaviour
val mediumSpeed = runtime.resolveFloat(if (pokemonEntity.getCurrentPoseType() in setOf(PoseType.FLY, PoseType.HOVER)) {
val flying = pokemonEntity.getCurrentPoseType() in setOf(PoseType.FLY, PoseType.HOVER)
val swimming = pokemonEntity.isEyeInFluid(FluidTags.WATER) || pokemonEntity.isEyeInFluid(FluidTags.LAVA)
// Sprinting is ground only and has to be asked for by whatever started the path
val sprintSpeed = behaviour.moving.walk.sprintSpeed
?.takeIf { operation == Operation.MOVE_TO && !flying && !swimming && pokemonEntity.navigation.sprinting }
pokemonEntity.isPokemonSprinting = sprintSpeed != null
val mediumSpeed = runtime.resolveFloat(if (flying) {
behaviour.moving.fly.flySpeedHorizontal
} else if (pokemonEntity.isEyeInFluid(FluidTags.WATER) || pokemonEntity.isEyeInFluid(FluidTags.LAVA)) {
} else if (swimming) {
behaviour.moving.swim.swimSpeed
} else {
behaviour.moving.walk.walkSpeed
}) * 2.5F
val baseSpeed = mob.getAttributeValue(Attributes.MOVEMENT_SPEED).toFloat() * this.speedModifier.toFloat()
val taskSpeed = if (flying || swimming) this.speedModifier.toFloat() else GROUND_SPEED_MODIFIER
val baseSpeed = mob.getAttributeValue(Attributes.MOVEMENT_SPEED).toFloat() * taskSpeed
// Mult the baseSpeed by the square root of the effectivescale to prevent foot slide (alpha/baby scaling)
val adjustedSpeed = baseSpeed * mediumSpeed * sqrt(pokemonEntity.pokemon.effectiveScale)
// Sprint speed is an exact top speed in blocks per second, so it skips attributes, task speed and friction
val sprintTopSpeed = sprintSpeed?.let {
runtime.resolveFloat(it).toDouble() / TICKS_PER_SECOND * sqrt(pokemonEntity.pokemon.effectiveScale.toDouble())
}
if (banking) {
pokemonEntity.yRot += bankRightDegreesPerTick
@@ -144,9 +220,46 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
if (!pokemonEntity.isFlying() && !pokemonEntity.isInLiquid) {
operation = Operation.WAIT
}
var xDist = wantedX - mob.x
var zDist = wantedZ - mob.z
var yDist = wantedY - mob.y
val (inWater, inLava) = mob.level().getWaterAndLavaIn(mob.boundingBox)
val inFluid = inWater || inLava
val fluidMover = pokemonEntity.getBehaviourFlag(PokemonBehaviourFlag.FLYING) || inFluid
val walkTopSpeed = if (fluidMover) adjustedSpeed.toDouble() else groundTopSpeed(adjustedSpeed.toDouble())
var topSpeed = if (fluidMover) walkTopSpeed else sprintTopSpeed ?: walkTopSpeed
val speedBlendTicks = SPEED_BLEND_SECONDS * TICKS_PER_SECOND
headingBlend = when {
inFluid -> WATER_HEADING_BLEND_SECONDS
pokemonEntity.getBehaviourFlag(PokemonBehaviourFlag.FLYING) -> AIR_HEADING_BLEND_SECONDS
else -> GROUND_HEADING_BLEND_SECONDS
}
// The speed ease is the step size this is how far we go while a turn finishes
val turnDistance = (speedEase.value * headingBlend * TICKS_PER_SECOND).toDouble()
// Follow the path when there is one otherwise go straight for the wanted position
var steerTarget = pokemonEntity.navigation.getSteerTarget(topSpeed, speedBlendTicks, turnDistance)
if (steerTarget != null && steerTarget.narrowAhead && topSpeed > walkTopSpeed) {
// No room to sprint on a narrow path, walk through it instead
topSpeed = walkTopSpeed
pokemonEntity.isPokemonSprinting = false
steerTarget = pokemonEntity.navigation.getSteerTarget(topSpeed, speedBlendTicks, turnDistance)
}
currentSteerTarget = steerTarget?.position
if (steerTarget == null && followedPath && fluidMover) {
// The path just ended or was cancelled, so glide to a stop instead of carrying on to the last waypoint.
// walkers stop on friction already
val velocity = mob.deltaMovement
// The speed ease stops in its blend time at half the speed on average
val brakingDistance = velocity.length() * speedBlendTicks / 2
val stop = mob.position().add(velocity.normalize().scale(brakingDistance))
setWantedPosition(stop.x, stop.y, stop.z, speedModifier)
}
val target = steerTarget?.position ?: Vec3(wantedX, wantedY, wantedZ)
val arrive = steerTarget?.isFinal ?: true
val maxSpeed = steerTarget?.maxSpeed ?: topSpeed
val xDist = target.x - mob.x
val zDist = target.z - mob.z
var yDist = target.y - mob.y
if (xDist * xDist + yDist * yDist + zDist * zDist < VERY_CLOSE) {
// If we're close enough, pull up stumps here.
@@ -156,52 +269,34 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
if ((pokemonEntity.isFlying() || pokemonEntity.isInLiquid)) {
operation = Operation.WAIT
mob.deltaMovement = Vec3.ZERO
speedEase.snap(0F)
}
return
}
val horizontalDistanceFromTarget = xDist * xDist + zDist * zDist
val closeHorizontally = horizontalDistanceFromTarget < VERY_CLOSE
if (!closeHorizontally) {
val angleToTarget = Mth.atan2(zDist, xDist).toDegrees() - 90.0f
val currentMovingAngle = mob.yRot
val steppedAngle = Mth.approachDegrees(currentMovingAngle, angleToTarget, 100 * mediumSpeed)
mob.yRot = steppedAngle
}
val (inWater, inLava) = mob.level().getWaterAndLavaIn(mob.boundingBox)
val inFluid = inWater || inLava
var verticalHandled = false
val blockPos = mob.blockPosition()
val blockState = mob.level().getBlockState(blockPos)
val voxelShape = blockState.getCollisionShape(mob.level(), blockPos)
if (pokemonEntity.getBehaviourFlag(PokemonBehaviourFlag.FLYING) || inFluid) {
if (fluidMover) {
verticalHandled = true
mob.yya = 0F
mob.speed = 0F
// Refinement is to prevent the entity from spinning around trying to get to a super precise location.
val refine: (Double) -> Double = { if (abs(it) < 0.05) 0.0 else it }
val fluidTarget = if (inFluid) target.add(0.0, FLUID_EXIT_Y_OFFSET, 0.0) else target
yDist = fluidTarget.y - mob.y
val fullDistance = Vec3(
xDist,
refine(yDist + 0.05), // + 0.05 for dealing with swimming out of water, they otherwise get stuck on the lip
zDist
)
val direction = fullDistance.normalize()
val scale = min(adjustedSpeed.toDouble(), fullDistance.length())
mob.deltaMovement = direction.scale(scale)
xDist = fullDistance.x
zDist = fullDistance.z
yDist = fullDistance.y
if (arrive && fluidTarget.distanceTo(mob.position()) < ARRIVAL_TOLERANCE) {
operation = Operation.WAIT
mob.deltaMovement = Vec3.ZERO
speedEase.snap(0F)
stopInputs()
return
}
steerTowards(fluidTarget, maxSpeed, topSpeed, speedBlendTicks, arrive, steerTarget?.afterDirection, horizontalOnly = false)
} else {
// division is to slow the speed down a bit so they don't overshoot when they get there.
val forwardSpeed = min(adjustedSpeed, max(horizontalDistanceFromTarget.toFloat() / 2, 0.15F))
mob.speed = forwardSpeed
steerTowards(target, maxSpeed, topSpeed, speedBlendTicks, arrive, steerTarget?.afterDirection, horizontalOnly = true)
}
if (!verticalHandled) {
@@ -210,10 +305,17 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
val zComponent = Mth.cos(mob.yRot.toRadians()).toDouble()
val motion = Vec3(xComponent, 0.0, zComponent).normalize()
val offset = motion.scale(mob.speed.toDouble())
val offset = motion.scale(adjustedSpeed.toDouble())
val closeEnoughToJump = !mob.isFree(offset.x, 0.0, offset.z)// sqrt(xDist * xDist + zDist * zDist) - 0.5 < entity.width / 2 + entity.movementSpeed * 4
// Vanilla's jump check against the next node itself, doesn't depend on the wanted position or where we're facing
val nextNodeNeedsJump = pokemonEntity.navigation.path?.takeUnless { it.isDone }?.getNextEntityPos(mob)?.let { node ->
val dx = node.x - mob.x
val dz = node.z - mob.z
node.y - mob.y > pokemonEntity.behaviour.moving.stepHeight && dx * dx + dz * dz < max(1F, mob.bbWidth)
} ?: false
if (tooBigToStep &&
if (nextNodeNeedsJump ||
tooBigToStep &&
closeEnoughToJump ||
!voxelShape.isEmpty && mob.y < voxelShape.max(Direction.Axis.Y) + blockPos.y.toDouble() &&
!blockState.`is`(BlockTags.DOORS) &&
@@ -282,6 +384,122 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
}
}
/** Works out the heading and speed steering wants this tick. Locomotion turns them into thrust in [updateMotion] */
private fun steerTowards(
target: Vec3,
maxSpeed: Double,
topSpeed: Double,
speedBlendTicks: Double,
arrive: Boolean,
afterDirection: Vec3?,
horizontalOnly: Boolean
) {
stopInputs()
val offset = target.subtract(mob.position()).let { if (horizontalOnly) Vec3(it.x, 0.0, it.z) else it }
val distance = offset.length()
if (distance < VERY_CLOSE) return
val direction = offset.scale(1 / distance)
// Target is inside our own footprint so turning to it would just spin. Slide onto it and face where the
// path goes next instead, or keep facing the same way if there's nowhere next
val slide = distance < mob.bbWidth / 2.0
val onwards = afterDirection?.let { if (horizontalOnly) Vec3(it.x, 0.0, it.z) else it }?.takeIf { it.lengthSqr() > 0 }
val faceDirection = if (slide) onwards?.normalize() else direction
steerGoal = SteerGoal(
yaw = faceDirection?.let { (Mth.atan2(it.z, it.x) * Mth.RAD_TO_DEG).toFloat() - 90F } ?: mob.yRot,
// Minecraft pitch is negative looking up
pitch = faceDirection?.let { -(Mth.atan2(it.y, it.horizontalDistance()) * Mth.RAD_TO_DEG).toFloat() } ?: pokemonEntity.heading.pitch.value,
direction = direction,
speed = if (arrive || slide) arriveSpeed(distance, maxSpeed, speedBlendTicks) else maxSpeed,
topSpeed = topSpeed,
distance = distance,
horizontalOnly = horizontalOnly,
headingBlend = headingBlend,
slide = slide
)
}
/**
* Eases heading and speed towards this tick's goal and works out the push that makes this tick's step match.
* Vanilla moves by velocity plus push and then applies friction, so knockback, ice and falling all work like vanilla.
*/
private fun updateMotion() {
val goal = steerGoal
isTravelling = goal != null && !pokemonEntity.isDeadOrDying
val heading = pokemonEntity.heading
val turnTarget = pokemonEntity.stationaryTurnTarget
if (goal == null && turnTarget != null) {
// Standing still and turning to look at something
val floating = pokemonEntity.stationaryTurnFloating
heading.steer(turnTarget, 0F, if (floating) FLOATING_TURN_BLEND_SECONDS else STANDING_TURN_BLEND_SECONDS)
mob.yRot = heading.yaw.value
// 2 and 3 tell the client which look turn blend to use
pokemonEntity.entityData.set(PokemonEntity.HEADING_TARGET, Vector3f(turnTarget, 0F, if (floating) 2F else 3F))
if (heading.yaw.isSettled()) {
pokemonEntity.stationaryTurnTarget = null
}
} else if (goal == null) {
heading.settle(mob.yRot, snap = true)
pokemonEntity.entityData.set(PokemonEntity.HEADING_TARGET, Vector3f(mob.yRot, 0F, 0F))
} else {
pokemonEntity.stationaryTurnTarget = null
heading.steer(goal.yaw, goal.pitch, goal.headingBlend)
pokemonEntity.entityData.set(PokemonEntity.HEADING_TRAVEL_BLEND, goal.headingBlend)
mob.yRot = heading.yaw.value
pokemonEntity.entityData.set(PokemonEntity.HEADING_TARGET, Vector3f(goal.yaw, goal.pitch, 1F))
}
val forward = heading.forward()
val currentSpeed = mob.deltaMovement.dot(forward)
// Walkers can't push off the air so a jump or fall just coasts
val airborneWalker = goal != null && goal.horizontalOnly && !mob.onGround()
if (goal == null || airborneWalker || pokemonEntity.isDeadOrDying) {
// Not pushing, follow the real speed so steering picks up from it without a jump
speedEase.snap(currentSpeed.toFloat())
pokemonEntity.pendingThrust = Vec3.ZERO
return
}
if (goal.slide) {
// Push straight at the target whichever way we're facinge
val velocity = mob.deltaMovement.let { if (goal.horizontalOnly) Vec3(it.x, 0.0, it.z) else it }
val push = goal.direction.scale(goal.speed).subtract(velocity)
pokemonEntity.pendingThrust = if (push.length() > goal.topSpeed) push.normalize().scale(goal.topSpeed) else push
speedEase.snap(currentSpeed.toFloat())
return
}
// Only go as fast as we're facing the right way
val facing = forward.dot(goal.direction).coerceAtLeast(0.0)
speedEase.update((goal.speed * facing).toFloat())
// Vanilla steps velocity plus push
val push = (speedEase.value - currentSpeed).coerceIn(-goal.topSpeed, goal.topSpeed)
pokemonEntity.pendingThrust = forward.scale(push)
}
private fun blockFriction(): Double {
return mob.level().getBlockState(mob.getBlockPosBelowThatAffectsMyMovement()).block.getFriction().toDouble()
}
private fun stopInputs() {
mob.speed = 0F
mob.yya = 0F
mob.setZza(0F)
mob.setXxa(0F)
}
/**
* Speed where vanilla ground friction balances the walk input force
*/
private fun groundTopSpeed(speed: Double): Double {
val blockFriction = blockFriction()
if (mob.onGround()) {
return vanillaGroundTopSpeed(speed, blockFriction)
}
val inputForce = speed * VANILLA_INPUT_SCALE * VANILLA_FLYING_SPEED
return inputForce / (1 - blockFriction * VANILLA_AIR_FRICTION)
}
/*
* Returns the ideal water level y position of a given pokemon during battle.
* This is separate for the eye height check and is dependent on if the pokemon is rafted or swimming.
@@ -11,6 +11,7 @@ package com.cobblemon.mod.common.entity.pokemon.ai.tasks
import com.bedrockk.molang.Expression
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangValue
import com.cobblemon.mod.common.entity.ai.CobblemonWalkTarget
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.mainThreadRuntime
import com.cobblemon.mod.common.util.resolveBoolean
@@ -18,7 +19,7 @@ import com.cobblemon.mod.common.util.resolveFloat
import com.cobblemon.mod.common.util.resolveInt
import com.cobblemon.mod.common.util.withQueryValue
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.floor
import net.minecraft.core.BlockPos
import net.minecraft.util.RandomSource
import net.minecraft.world.entity.Entity
@@ -27,12 +28,18 @@ 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.WalkTarget
import net.minecraft.world.level.pathfinder.PathType
import net.minecraft.world.level.pathfinder.WalkNodeEvaluator
object MoveToOwnerTask {
fun create(condition: Expression, completionRange: Expression, maxDistance: Expression, teleportDistance: Expression, speedMultiplier: Expression): OneShot<PokemonEntity> = BehaviorBuilder.create {
fun create(
condition: Expression,
completionRange: Expression,
maxDistance: Expression,
teleportDistance: Expression,
speedMultiplier: Expression,
sprintDistance: Expression
): OneShot<PokemonEntity> = BehaviorBuilder.create {
it.group(
it.registered(MemoryModuleType.WALK_TARGET)
).apply(it) { walkTarget ->
@@ -53,17 +60,30 @@ object MoveToOwnerTask {
val completionRange = mainThreadRuntime.resolveInt(completionRange)
val horizontalHitboxRadius = maxOf(entity.boundingBox.xsize, entity.boundingBox.zsize) / 2
val distanceToOwner = entity.distanceTo(owner) - horizontalHitboxRadius
if (entity.distanceTo(owner) - horizontalHitboxRadius > teleportDistance) {
if (distanceToOwner > teleportDistance) {
if (tryTeleport(entity, owner)) {
entity.brain.eraseMemory(MemoryModuleType.LOOK_TARGET)
entity.brain.eraseMemory(MemoryModuleType.WALK_TARGET)
}
return@Trigger true
} else if (entity.distanceTo(owner) - horizontalHitboxRadius > maxDistance && it.tryGet(walkTarget).isEmpty && !entity.brain.hasMemoryValue(CobblemonMemories.PATH_COOLDOWN)) {
} else if (distanceToOwner > maxDistance && it.tryGet(walkTarget).isEmpty && !entity.brain.hasMemoryValue(CobblemonMemories.PATH_COOLDOWN)) {
val sprintDistance = mainThreadRuntime.resolveFloat(sprintDistance)
entity.brain.setMemoryWithExpiry(CobblemonMemories.PATH_COOLDOWN, true, 60L)
entity.brain.setMemory(MemoryModuleType.LOOK_TARGET, EntityTracker(owner, true))
entity.brain.setMemory(MemoryModuleType.WALK_TARGET, WalkTarget(owner, speedMultiplier, maxOf(completionRange, ceil(horizontalHitboxRadius).toInt()) - 1))
entity.brain.setMemory(
MemoryModuleType.WALK_TARGET,
CobblemonWalkTarget(
target = EntityTracker(owner, false),
speedModifier = speedMultiplier,
// Path has to end within the stop gap or it finishes before the mon gets close enough
completionRange = floor(completionRange + horizontalHitboxRadius + owner.bbWidth / 2.0).toInt(),
sprintDistance = sprintDistance,
stopDistance = completionRange.toFloat(),
sprintWithTarget = true
)
)
return@Trigger true
}
return@Trigger false
@@ -0,0 +1,39 @@
/*
* 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.item
import com.cobblemon.mod.common.CobblemonSounds
import net.minecraft.sounds.SoundSource
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemNameBlockItem
import net.minecraft.world.item.SignApplicator
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.entity.SignBlockEntity
import net.minecraft.world.level.block.entity.SignText
class GlowItem(block: Block, settings: Properties) : ItemNameBlockItem(block, settings), SignApplicator {
override fun tryApplyToSign(level: Level, sign: SignBlockEntity, isFront: Boolean, player: Player?): Boolean {
if (sign.updateText({ signText: SignText? -> signText!!.setHasGlowingText(true) }, isFront)) {
level.playSound(
null as Player?,
sign.blockPos,
CobblemonSounds.BRIGHT_POWDER_USE,
SoundSource.BLOCKS,
1.0f,
1.0f
)
return true
} else {
return false
}
}
}
@@ -13,10 +13,12 @@ import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.item.battle.BagItem
import com.cobblemon.mod.common.item.battle.SimpleBagItemLike
import net.minecraft.world.item.ItemNameBlockItem
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import net.minecraft.world.level.block.Block
class MoomooMilk(properties: Properties) : CobblemonItem(properties), SimpleBagItemLike {
class MoomooMilk(properties: Properties, block: Block) : ItemNameBlockItem(block, properties), SimpleBagItemLike {
override val bagItem = object : BagItem {
override val itemName = "item.cobblemon.moomoo_milk"
override val returnItem = Items.GLASS_BOTTLE
@@ -12,9 +12,11 @@ import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonItemComponents
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.api.riding.stats.RidingStat
import com.cobblemon.mod.common.api.tms.TechnicalMachines
import com.cobblemon.mod.common.block.TypeGemClusterBlock
import com.cobblemon.mod.common.item.AprijuiceItem
import com.cobblemon.mod.common.item.components.RideBoostsComponent
import com.cobblemon.mod.common.item.components.TMMoveComponent
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.core.component.DataComponents
import net.minecraft.core.registries.BuiltInRegistries
@@ -40,44 +42,111 @@ object CobblemonItemGroups {
private val ALL = arrayListOf<ItemGroupHolder>()
private val INJECTORS = hashMapOf<ResourceKey<CreativeModeTab>, (injector: Injector) -> Unit>()
@JvmStatic val BLOCKS_KEY = this.create("blocks", this::blockEntries) {
@JvmStatic
val BLOCKS_KEY = this.create("blocks", this::blockEntries) {
ItemStack(CobblemonItems.PC)
}
@JvmStatic val UTILITY_ITEMS_KEY = this.create("utility_item", this::utilityItemEntries) {
@JvmStatic
val UTILITY_ITEMS_KEY = this.create("utility_item", this::utilityItemEntries) {
ItemStack(CobblemonItems.POKE_BALL)
}
@JvmStatic val AGRICULTURE_KEY = this.create("agriculture", this::agricultureEntries) {
@JvmStatic
val AGRICULTURE_KEY = this.create("agriculture", this::agricultureEntries) {
ItemStack(CobblemonItems.MEDICINAL_LEEK)
}
@JvmStatic val ARCHAEOLOGY_KEY = this.create("archaeology", this::archaeologyEntries) {
@JvmStatic
val ARCHAEOLOGY_KEY = this.create("archaeology", this::archaeologyEntries) {
ItemStack(CobblemonItems.HELIX_FOSSIL)
}
@JvmStatic val CONSUMABLES_KEY = this.create("consumables", this::consumableEntries) {
@JvmStatic
val CONSUMABLES_KEY = this.create("consumables", this::consumableEntries) {
ItemStack(CobblemonItems.ROASTED_LEEK)
}
@JvmStatic val HELD_ITEMS_KEY = this.create("held_item", this::heldItemEntries) {
@JvmStatic
val HELD_ITEMS_KEY = this.create("held_item", this::heldItemEntries) {
ItemStack(CobblemonItems.EXP_SHARE)
}
@JvmStatic val EVOLUTION_ITEMS_KEY = this.create("evolution_item", this::evolutionItemEntries) {
@JvmStatic
val EVOLUTION_ITEMS_KEY = this.create("evolution_item", this::evolutionItemEntries) {
ItemStack(CobblemonItems.BLACK_AUGURITE)
}
@JvmStatic val BLOCKS get() = BuiltInRegistries.CREATIVE_MODE_TAB.get(BLOCKS_KEY)
@JvmStatic val UTILITY_ITEMS get() = BuiltInRegistries.CREATIVE_MODE_TAB.get(UTILITY_ITEMS_KEY)
@JvmStatic val AGRICULTURE get() = BuiltInRegistries.CREATIVE_MODE_TAB.get(AGRICULTURE_KEY)
@JvmStatic val ARCHAEOLOGY get() = BuiltInRegistries.CREATIVE_MODE_TAB.get(ARCHAEOLOGY_KEY)
@JvmStatic val CONSUMABLES get() = BuiltInRegistries.CREATIVE_MODE_TAB.get(CONSUMABLES_KEY)
@JvmStatic val HELD_ITEMS get() = BuiltInRegistries.CREATIVE_MODE_TAB.get(HELD_ITEMS_KEY)
@JvmStatic val EVOLUTION_ITEMS get() = BuiltInRegistries.CREATIVE_MODE_TAB.get(EVOLUTION_ITEMS_KEY)
@JvmStatic
val TM_ITEMS_KEY = this.create("tm_item", this::tmItemEntries) {
ItemStack(CobblemonItems.BLANK_TM)
}
@JvmStatic val BUILDING_BLOCKS_INJECTIONS = this.inject(ResourceKey.create(BuiltInRegistries.CREATIVE_MODE_TAB.key(), ResourceLocation.parse("building_blocks")), this::blocksInjections)
@JvmStatic val COLORED_BLOCKS_INJECTIONS = this.inject(ResourceKey.create(BuiltInRegistries.CREATIVE_MODE_TAB.key(), ResourceLocation.parse("colored_blocks")), this::coloredBlocksInjections)
@JvmStatic val FUNCTIONAL_BLOCKS = this.inject(ResourceKey.create(BuiltInRegistries.CREATIVE_MODE_TAB.key(), ResourceLocation.parse("functional_blocks")), this::functionalBlocksInjections)
@JvmStatic val REDSTONE_BLOCKS = this.inject(ResourceKey.create(BuiltInRegistries.CREATIVE_MODE_TAB.key(), ResourceLocation.parse("redstone_blocks")), this::redstoneBlocksInjections)
@JvmStatic val FOOD_INJECTIONS = this.inject(ResourceKey.create(BuiltInRegistries.CREATIVE_MODE_TAB.key(), ResourceLocation.parse("food_and_drinks")), this::foodInjections)
@JvmStatic val TOOLS_AND_UTILITIES_INJECTIONS = this.inject(ResourceKey.create(BuiltInRegistries.CREATIVE_MODE_TAB.key(), ResourceLocation.parse("tools_and_utilities")), this::toolsAndUtilitiesInjections)
@JvmStatic val INGREDIENTS_INJECTIONS = this.inject(ResourceKey.create(BuiltInRegistries.CREATIVE_MODE_TAB.key(), ResourceLocation.parse("ingredients")), this::ingredientsInjections)
@JvmStatic val OP_BLOCKS_INJECTIONS = this.inject(ResourceKey.create(BuiltInRegistries.CREATIVE_MODE_TAB.key(), ResourceLocation.parse("op_blocks")), this::opBlocksInjections)
@JvmStatic
val BLOCKS get() = BuiltInRegistries.CREATIVE_MODE_TAB.get(BLOCKS_KEY)
@JvmStatic
val UTILITY_ITEMS get() = BuiltInRegistries.CREATIVE_MODE_TAB.get(UTILITY_ITEMS_KEY)
@JvmStatic
val AGRICULTURE get() = BuiltInRegistries.CREATIVE_MODE_TAB.get(AGRICULTURE_KEY)
@JvmStatic
val ARCHAEOLOGY get() = BuiltInRegistries.CREATIVE_MODE_TAB.get(ARCHAEOLOGY_KEY)
@JvmStatic
val CONSUMABLES get() = BuiltInRegistries.CREATIVE_MODE_TAB.get(CONSUMABLES_KEY)
@JvmStatic
val HELD_ITEMS get() = BuiltInRegistries.CREATIVE_MODE_TAB.get(HELD_ITEMS_KEY)
@JvmStatic
val EVOLUTION_ITEMS get() = BuiltInRegistries.CREATIVE_MODE_TAB.get(EVOLUTION_ITEMS_KEY)
@JvmStatic
val TM_ITEMS get() = BuiltInRegistries.CREATIVE_MODE_TAB.get(TM_ITEMS_KEY)
@JvmStatic
val BUILDING_BLOCKS_INJECTIONS = this.inject(
ResourceKey.create(
BuiltInRegistries.CREATIVE_MODE_TAB.key(),
ResourceLocation.parse("building_blocks")
), this::blocksInjections
)
@JvmStatic
val COLORED_BLOCKS_INJECTIONS = this.inject(
ResourceKey.create(
BuiltInRegistries.CREATIVE_MODE_TAB.key(),
ResourceLocation.parse("colored_blocks")
), this::coloredBlocksInjections
)
@JvmStatic
val FUNCTIONAL_BLOCKS = this.inject(
ResourceKey.create(
BuiltInRegistries.CREATIVE_MODE_TAB.key(),
ResourceLocation.parse("functional_blocks")
), this::functionalBlocksInjections
)
@JvmStatic
val REDSTONE_BLOCKS = this.inject(
ResourceKey.create(
BuiltInRegistries.CREATIVE_MODE_TAB.key(),
ResourceLocation.parse("redstone_blocks")
), this::redstoneBlocksInjections
)
@JvmStatic
val FOOD_INJECTIONS = this.inject(
ResourceKey.create(
BuiltInRegistries.CREATIVE_MODE_TAB.key(),
ResourceLocation.parse("food_and_drinks")
), this::foodInjections
)
@JvmStatic
val TOOLS_AND_UTILITIES_INJECTIONS = this.inject(
ResourceKey.create(
BuiltInRegistries.CREATIVE_MODE_TAB.key(),
ResourceLocation.parse("tools_and_utilities")
), this::toolsAndUtilitiesInjections
)
@JvmStatic
val INGREDIENTS_INJECTIONS = this.inject(
ResourceKey.create(
BuiltInRegistries.CREATIVE_MODE_TAB.key(),
ResourceLocation.parse("ingredients")
), this::ingredientsInjections
)
@JvmStatic
val OP_BLOCKS_INJECTIONS = this.inject(
ResourceKey.create(BuiltInRegistries.CREATIVE_MODE_TAB.key(), ResourceLocation.parse("op_blocks")),
this::opBlocksInjections
)
fun register(consumer: (holder: ItemGroupHolder) -> CreativeModeTab) {
ALL.forEach(consumer::invoke)
@@ -96,13 +165,20 @@ object CobblemonItemGroups {
val displayName: Component = Component.translatable("itemGroup.${key.location().namespace}.${key.location().path}")
)
private fun create(name: String, entryCollector: DisplayItemsGenerator, displayIconProvider: () -> ItemStack): ResourceKey<CreativeModeTab> {
private fun create(
name: String,
entryCollector: DisplayItemsGenerator,
displayIconProvider: () -> ItemStack
): ResourceKey<CreativeModeTab> {
val key = ResourceKey.create(BuiltInRegistries.CREATIVE_MODE_TAB.key(), cobblemonResource(name))
this.ALL += ItemGroupHolder(key, displayIconProvider, entryCollector)
return key
}
private fun inject(key: ResourceKey<CreativeModeTab>, consumer: (injector: Injector) -> Unit): (injector: Injector) -> Unit {
private fun inject(
key: ResourceKey<CreativeModeTab>,
consumer: (injector: Injector) -> Unit
): (injector: Injector) -> Unit {
this.INJECTORS[key] = consumer
return consumer
}
@@ -122,6 +198,14 @@ object CobblemonItemGroups {
entries.accept(CobblemonItems.GALARICA_NUTS)
entries.accept(CobblemonItems.HEARTY_GRAINS)
entries.accept(CobblemonItems.TASTY_TAIL)
entries.accept(CobblemonItems.POMOHANA)
entries.accept(CobblemonItems.PAUVILLEA)
entries.accept(CobblemonItems.LOBAILA)
entries.accept(CobblemonItems.WISTENSU)
entries.accept(CobblemonItems.VOLTACONIA)
entries.accept(CobblemonItems.VERDROOT)
entries.accept(CobblemonItems.MISTLEAF)
entries.accept(CobblemonItems.LUCIBELL)
// entries.accept(CobblemonItems.BUGWORT) // TODO after 1.7
entries.accept(CobblemonItems.RED_APRICORN)
@@ -631,8 +715,6 @@ object CobblemonItemGroups {
entries.accept(CobblemonItems.ABILITY_CAPSULE)
entries.accept(CobblemonItems.ABILITY_PATCH)
entries.accept(CobblemonItems.BLANK_TM)
entries.accept(CobblemonItems.POKE_BAIT)
entries.accept(CobblemonItems.POKE_SNACK)
entries.accept(CobblemonItems.POKE_CAKE)
@@ -649,15 +731,17 @@ object CobblemonItemGroups {
private fun defaultAprijuice(item: AprijuiceItem): ItemStack {
return ItemStack(item).apply {
this.set(CobblemonItemComponents.RIDE_BOOST, RideBoostsComponent(
mapOf(
RidingStat.ACCELERATION to 1,
RidingStat.JUMP to 1,
RidingStat.SPEED to 1,
RidingStat.STAMINA to 1,
RidingStat.SKILL to 1
this.set(
CobblemonItemComponents.RIDE_BOOST, RideBoostsComponent(
mapOf(
RidingStat.ACCELERATION to 1,
RidingStat.JUMP to 1,
RidingStat.SPEED to 1,
RidingStat.STAMINA to 1,
RidingStat.SKILL to 1
)
)
))
)
}
}
@@ -848,6 +932,16 @@ object CobblemonItemGroups {
entries.accept(CobblemonItems.PSYCHIC_SEED)
}
private fun tmItemEntries(displayContext: ItemDisplayParameters, entries: Output) {
entries.accept(CobblemonItems.BLANK_TM)
TechnicalMachines
.tmMap
.entries
.sortedBy { it.value.moveName.name }
.sortedBy { it.value.moveName.elementalType.order }
.forEach { entries.accept(TMMoveComponent.createStack(it.value.moveName)) }
}
private fun utilityItemEntries(displayContext: ItemDisplayParameters, entries: Output) {
CobblemonItems.pokeBalls.forEach(entries::accept)
CobblemonItems.pokeRods.forEach(entries::accept)
@@ -855,7 +949,7 @@ object CobblemonItemGroups {
}
private fun blocksInjections(injector: Injector) {
if(Cobblemon.implementation.isModInstalled("adorn")) { //adorn modifies vanilla block tab and if we don't follow suit we crash the game
if (Cobblemon.implementation.isModInstalled("adorn")) { //adorn modifies vanilla block tab and if we don't follow suit we crash the game
injector.putLast(CobblemonItems.APRICORN_LOG)
injector.putLast(CobblemonItems.APRICORN_WOOD)
injector.putLast(CobblemonItems.STRIPPED_APRICORN_LOG)
@@ -974,7 +1068,10 @@ object CobblemonItemGroups {
injector.putAfter(CobblemonItems.NOSTALGIC_SHERD, CobblemonItems.HELIX_SHERD)
injector.putAfter(CobblemonItems.SUSPICIOUS_SHERD, CobblemonItems.NOSTALGIC_SHERD)
injector.putAfter(CobblemonItems.AUTOMATON_ARMOR_TRIM_SMITHING_TEMPLATE, Items.SPIRE_ARMOR_TRIM_SMITHING_TEMPLATE)
injector.putAfter(
CobblemonItems.AUTOMATON_ARMOR_TRIM_SMITHING_TEMPLATE,
Items.SPIRE_ARMOR_TRIM_SMITHING_TEMPLATE
)
injector.putAfter(CobblemonItems.POKEROD_SMITHING_TEMPLATE, Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE)
}
@@ -0,0 +1,61 @@
/*
* 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.debug
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
/**
* Opens the speed tuner with every species' current speeds. Species behaviour lives in server data so the client
* can't read it itself.
*/
class OpenSpeedTunerPacket(val entries: List<SpeedTunerEntry>) : NetworkPacket<OpenSpeedTunerPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeCollection(entries) { buf, entry -> entry.encode(buf) }
}
companion object {
val ID = cobblemonResource("open_speed_tuner")
fun decode(buffer: RegistryFriendlyByteBuf): OpenSpeedTunerPacket {
return OpenSpeedTunerPacket(buffer.readList { buf -> SpeedTunerEntry.decode(buf) })
}
}
}
/** A species' raw walk speed and its sprint speed in blocks per second, null if it can't sprint. */
data class SpeedTunerEntry(
val speciesId: ResourceLocation,
val dex: Int,
val walkSpeed: Float,
val sprintSpeed: Float?
) {
fun encode(buffer: FriendlyByteBuf) {
buffer.writeResourceLocation(speciesId)
buffer.writeVarInt(dex)
buffer.writeFloat(walkSpeed)
buffer.writeBoolean(sprintSpeed != null)
sprintSpeed?.let { buffer.writeFloat(it) }
}
companion object {
fun decode(buffer: FriendlyByteBuf): SpeedTunerEntry {
val speciesId = buffer.readResourceLocation()
val dex = buffer.readVarInt()
val walkSpeed = buffer.readFloat()
val sprintSpeed = if (buffer.readBoolean()) buffer.readFloat() else null
return SpeedTunerEntry(speciesId, dex, walkSpeed, sprintSpeed)
}
}
}
@@ -0,0 +1,28 @@
/*
* 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.server.debug
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
/** Asks the server to open the speed tuner, which checks debug keys and permission first. */
class RequestOpenSpeedTunerPacket : NetworkPacket<RequestOpenSpeedTunerPacket> {
override val id = ID
companion object {
val ID = cobblemonResource("request_open_speed_tuner")
fun decode(buffer: RegistryFriendlyByteBuf): RequestOpenSpeedTunerPacket {
return RequestOpenSpeedTunerPacket()
}
}
override fun encode(buffer: RegistryFriendlyByteBuf) {}
}
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.server.debug
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
/** Sets a species' sprint speed in blocks per second on the running server. Null takes sprinting away. */
class SetSpeedTunerSprintSpeedPacket(val speciesId: ResourceLocation, val sprintSpeed: Float?) : NetworkPacket<SetSpeedTunerSprintSpeedPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeResourceLocation(speciesId)
buffer.writeBoolean(sprintSpeed != null)
sprintSpeed?.let { buffer.writeFloat(it) }
}
companion object {
val ID = cobblemonResource("set_speed_tuner_sprint_speed")
fun decode(buffer: RegistryFriendlyByteBuf): SetSpeedTunerSprintSpeedPacket {
val speciesId = buffer.readResourceLocation()
val sprintSpeed = if (buffer.readBoolean()) buffer.readFloat() else null
return SetSpeedTunerSprintSpeedPacket(speciesId, sprintSpeed)
}
}
}
@@ -0,0 +1,32 @@
/*
* 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.server.debug
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
/** Sets a species' walkSpeed, in the species data's own units, on the running server. */
class SetSpeedTunerWalkSpeedPacket(val speciesId: ResourceLocation, val walkSpeed: Float) : NetworkPacket<SetSpeedTunerWalkSpeedPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeResourceLocation(speciesId)
buffer.writeFloat(walkSpeed)
}
companion object {
val ID = cobblemonResource("set_speed_tuner_walk_speed")
fun decode(buffer: RegistryFriendlyByteBuf): SetSpeedTunerWalkSpeedPacket {
return SetSpeedTunerWalkSpeedPacket(buffer.readResourceLocation(), buffer.readFloat())
}
}
}
@@ -0,0 +1,53 @@
/*
* 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.serverhandling.debug
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.api.permission.CobblemonPermissions.USE_SPEED_TUNER
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.net.messages.client.debug.OpenSpeedTunerPacket
import com.cobblemon.mod.common.net.messages.client.debug.SpeedTunerEntry
import com.cobblemon.mod.common.net.messages.server.debug.RequestOpenSpeedTunerPacket
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.mainThreadRuntime
import com.cobblemon.mod.common.util.resolveFloat
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
object RequestOpenSpeedTunerHandler : ServerNetworkPacketHandler<RequestOpenSpeedTunerPacket> {
override fun handle(
packet: RequestOpenSpeedTunerPacket,
server: MinecraftServer,
player: ServerPlayer
) {
if (!Cobblemon.config.enableDebugKeys) {
player.sendSystemMessage(lang("requires_debug_keys").red())
return
}
if (!Cobblemon.permissionValidator.hasPermission(player, USE_SPEED_TUNER)) {
player.sendSystemMessage(lang("requires_permission").red())
return
}
val entries = PokemonSpecies.implemented.map { species ->
val walk = species.standardForm.behaviour.moving.walk
SpeedTunerEntry(
speciesId = species.resourceIdentifier,
dex = species.nationalPokedexNumber,
// Speeds that are expressions needing a pokemon context can't be resolved here, show them as 0
walkSpeed = runCatching { mainThreadRuntime.resolveFloat(walk.walkSpeed) }.getOrDefault(0F),
sprintSpeed = walk.sprintSpeed?.let { runCatching { mainThreadRuntime.resolveFloat(it) }.getOrDefault(0F) }
)
}
OpenSpeedTunerPacket(entries).sendToPlayer(player)
}
}
@@ -0,0 +1,31 @@
/*
* 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.serverhandling.debug
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.api.permission.CobblemonPermissions.USE_SPEED_TUNER
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.net.messages.server.debug.SetSpeedTunerSprintSpeedPacket
import com.cobblemon.mod.common.util.asExpression
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
/**
* Changes the loaded species data so every pokemon of that species sprints at the new speed straight away.
* Only lasts until the species data is reloaded, the tuner export is what makes it permanent.
*/
object SetSpeedTunerSprintSpeedHandler : ServerNetworkPacketHandler<SetSpeedTunerSprintSpeedPacket> {
override fun handle(packet: SetSpeedTunerSprintSpeedPacket, server: MinecraftServer, player: ServerPlayer) {
if (!Cobblemon.config.enableDebugKeys) return
if (!Cobblemon.permissionValidator.hasPermission(player, USE_SPEED_TUNER)) return
val species = PokemonSpecies.getByIdentifier(packet.speciesId) ?: return
species.standardForm.behaviour.moving.walk.sprintSpeed = packet.sprintSpeed?.toString()?.asExpression()
}
}
@@ -0,0 +1,31 @@
/*
* 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.serverhandling.debug
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.api.permission.CobblemonPermissions.USE_SPEED_TUNER
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.net.messages.server.debug.SetSpeedTunerWalkSpeedPacket
import com.cobblemon.mod.common.util.asExpression
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
/**
* Changes the loaded species data so every pokemon of that species walks at the new speed straight away.
* Only lasts until the species data is reloaded, the tuner export is what makes it permanent.
*/
object SetSpeedTunerWalkSpeedHandler : ServerNetworkPacketHandler<SetSpeedTunerWalkSpeedPacket> {
override fun handle(packet: SetSpeedTunerWalkSpeedPacket, server: MinecraftServer, player: ServerPlayer) {
if (!Cobblemon.config.enableDebugKeys) return
if (!Cobblemon.permissionValidator.hasPermission(player, USE_SPEED_TUNER)) return
val species = PokemonSpecies.getByIdentifier(packet.speciesId) ?: return
species.standardForm.behaviour.moving.walk.walkSpeed = packet.walkSpeed.toString().asExpression()
}
}
@@ -19,11 +19,15 @@ class WalkBehaviour {
val canWalk = true
val avoidsLand = false
var walkSpeed: Expression = "0.35".asExpression()
// Top speed in blocks per second while sprinting, player sprint is about 5.6. Only species with one can sprint
var sprintSpeed: Expression? = null
@Transient
val struct = ObjectValue(this).also {
it.addFunction("can_walk") { DoubleValue(canWalk) }
it.addFunction("avoids_land") { DoubleValue(avoidsLand) }
it.addFunction("walk_speed") { it.environment.createDuplicateRuntime().resolveFloat(walkSpeed) }
it.addFunction("can_sprint") { DoubleValue(sprintSpeed != null) }
it.addFunction("sprint_speed") { params -> sprintSpeed?.let { params.environment.createDuplicateRuntime().resolveFloat(it) } ?: 0F }
}
}
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.util.math
import net.minecraft.util.Mth
/**
* Eases one value towards a target with inertial easing, see SpringUtils and https://theorangeduck.com/page/inertial-easing
* Keeps its velocity when the target changes and gets there within 1.5x [blendTime]
*
* With [isAngle] the value is in degrees and always eases the short way round.
*/
class InertialEase(
private var blendTime: Float,
private val isAngle: Boolean = false
) {
companion object {
const val SECONDS_PER_TICK = 0.05F
}
var value = 0F
private set
private val vals = InertialVals()
// How far along the curve one tick is
private var step = SECONDS_PER_TICK / blendTime
private var target = 0F
/** Moves one tick towards [newTarget] */
fun update(newTarget: Float) {
if (newTarget != target) {
val difference = if (isAngle) Mth.wrapDegrees(newTarget - value) else newTarget - value
// Velocity along the current curve, carried into the new one so there's no sudden change
smoothstepSolve(vals, difference, smoothstepDt(vals.t, vals.s))
vals.offset = value - smoothstep(vals.t, vals.s)
target = newTarget
}
vals.t += step
value = vals.offset + smoothstep(vals.t, vals.s)
if (isAngle) {
// Keep turning angles in range so they don't grow forever
val turns = value - Mth.wrapDegrees(value)
if (turns != 0F) {
vals.offset -= turns
value -= turns
}
}
}
/** Changes how long easing takes. A curve in progress is re-fit so it keeps the same speed and target */
fun setBlendTime(newBlendTime: Float) {
if (newBlendTime == blendTime) return
val currentVelocity = velocity()
blendTime = newBlendTime
step = SECONDS_PER_TICK / blendTime
if (!isSettled()) {
val difference = if (isAngle) Mth.wrapDegrees(target - value) else target - value
// Velocity along the curve is per unit of curve time, which is blendTime seconds long
smoothstepSolve(vals, difference, currentVelocity * blendTime)
vals.offset = value - smoothstep(vals.t, vals.s)
}
}
/** True once the curve has reached its target */
fun isSettled() = vals.t >= 1F
/** How fast the value is changing in units per second */
fun velocity() = smoothstepDt(vals.t, vals.s) / blendTime
/** Value between last tick and this one read straight off the curve */
fun valueAt(partialTick: Float): Float {
val eased = vals.offset + smoothstep(vals.t - step * (1 - partialTick), vals.s)
return if (isAngle) Mth.wrapDegrees(eased) else eased
}
/** Jumps straight to [to] with no easing */
fun snap(to: Float) {
vals.t = 1F
vals.s = 0F
vals.offset = to
target = to
value = to
}
}
@@ -0,0 +1,25 @@
/*
* 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.math
/**
* Fastest speed that the speed ease can still stop from within [distance]. The ease always takes [blendTicks]
* to stop and averages half the speed on the way, so it covers speed × blendTicks / 2.
* Capped at [distance] so the last tick lands on the target instead of stepping past it.
*/
fun arriveSpeed(distance: Double, maxSpeed: Double, blendTicks: Double): Double {
return minOf(maxSpeed, 2 * distance / blendTicks, distance)
}
/**
* Fastest speed at [distance] from somewhere that has to be passed at [slowSpeed].
*/
fun slowDownSpeed(distance: Double, slowSpeed: Double, blendTicks: Double): Double {
return maxOf(slowSpeed, 2 * distance / blendTicks - slowSpeed)
}
@@ -33,10 +33,7 @@
"condition": "q.is_holding_item",
"animation": "q.bedrock('venusaur', 'hold_item')"
},
{
"condition": "q.is_ridden",
"animation": "q.bedrock('venusaur', 'ride_ground_run')"
},
"q.bedrock('venusaur', 'ride_ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('venusaur', 'rider_overlay')"
@@ -96,10 +93,7 @@
"condition": "q.is_holding_item",
"animation": "q.bedrock('venusaur', 'hold_item')"
},
{
"condition": "q.is_ridden",
"animation": "q.bedrock('venusaur', 'ride_ground_walk')"
},
"q.bedrock('venusaur', 'ride_ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('venusaur', 'rider_overlay')"
@@ -120,10 +114,7 @@
"condition": "q.is_holding_item",
"animation": "q.bedrock('venusaur', 'hold_item')"
},
{
"condition": "q.is_ridden",
"animation": "q.bedrock('venusaur', 'ride_ground_run')"
},
"q.bedrock('venusaur', 'ride_ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('venusaur', 'rider_overlay')"
@@ -121,10 +121,7 @@
"q.look('neck4', 0.35, 0.35, 5, -5, 15, -15)",
"q.look('head', 0.4, 0.25, 5, -5, 10, -10)",
"q.bedrock('charizard', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('charizard', 'ride_ground_run')"
},
"q.bedrock('charizard', 'ride_ground_run')",
{
"condition": "q.is_holding_item",
"animation": "q.bedrock('charizard', 'hold_item')"
@@ -167,10 +164,7 @@
"q.look('neck4', 0.35, 0.35, 5, -5, 15, -15)",
"q.look('head', 0.4, 0.25, 5, -5, 10, -10)",
"q.bedrock('charizard', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('charizard', 'ride_ground_run')"
},
"q.bedrock('charizard', 'ride_ground_run')",
{
"condition": "q.is_holding_item",
"animation": "q.bedrock('charizard', 'hold_item')"
@@ -208,10 +202,7 @@
"q.look('neck4', 0.35, 0.35, 5, -5, 15, -15)",
"q.look('head', 0.4, 0.25, 5, -5, 10, -10)",
"q.bedrock('charizard', 'air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('charizard', 'ride_air_fly')"
},
"q.bedrock('charizard', 'ride_air_fly')",
{
"condition": "q.is_holding_item",
"animation": "q.bedrock('charizard', 'hold_item')"
@@ -233,10 +224,7 @@
"q.look('head', 0.4, 0.25, 5, -5, 10, -10)",
"q.bedrock('charizard', 'air_glide')",
"q.bedrock('charizard', 'ride_air_dive')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('charizard', 'ride_air_fly')"
},
"q.bedrock('charizard', 'ride_air_fly')",
{
"condition": "q.is_holding_item",
"animation": "q.bedrock('charizard', 'hold_item')"
@@ -88,10 +88,7 @@
"animations": [
"q.look('head_ai')",
"q.bedrock('blastoise', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('blastoise', 'ride_ground_walk')"
}
"q.bedrock('blastoise', 'ride_ground_walk')"
],
"transformTicks": 5,
"transformToTicks": 5
@@ -104,10 +101,7 @@
"animations": [
"q.look('head_ai')",
"q.bedrock('blastoise', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('blastoise', 'ride_ground_run')"
}
"q.bedrock('blastoise', 'ride_ground_run')"
],
"transformTicks": 5,
"transformToTicks": 5
@@ -209,10 +203,7 @@
"animations": [
"q.look('head_ai')",
"q.bedrock('blastoise', 'surfacewater_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('blastoise', 'ride_water_swim')"
}
"q.bedrock('blastoise', 'ride_water_swim')"
],
"quirks": [
"q.bedrock_quirk('blastoise', 'blink')"
@@ -383,10 +383,7 @@
"condition": "q.is_ridden",
"animation": "q.bedrock('pidgeot', 'ride_air_glide')"
},
{
"condition": "q.is_ridden",
"animation": "q.bedrock('pidgeot', 'ride_dive')"
}
"q.bedrock('pidgeot', 'ride_dive')"
],
"quirks": [
"q.bedrock_quirk('pidgeot', 'blink')"
@@ -45,7 +45,7 @@
"animations": [
"q.bedrock('parasect', 'ground_walk')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('parasect', 'ride_ground_run')"
}
]
@@ -57,7 +57,7 @@
"animations": [
"q.bedrock('parasect', 'ground_run')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('parasect', 'ride_ground_run')"
}
],
@@ -51,10 +51,7 @@
"animations": [
"q.look('head')",
"q.bedrock('arcanine', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('arcanine', 'ride_ground_walk')"
}
"q.bedrock('arcanine', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('arcanine', 'blink')"]
},
@@ -65,10 +62,7 @@
"animations": [
"q.look('head')",
"q.bedrock('arcanine', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('arcanine', 'ride_ground_run')"
}
"q.bedrock('arcanine', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('arcanine', 'blink')"]
},
@@ -78,10 +72,7 @@
"animations": [
"q.look('head')",
"q.bedrock('arcanine', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('arcanine', 'ride_ground_walk')"
}
"q.bedrock('arcanine', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('arcanine', 'blink')"]
},
@@ -63,7 +63,7 @@
"q.look('head_ai')",
"q.bedrock('dewgong', 'water_swim')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('dewgong', 'ride_water_swim')"
}
],
@@ -77,7 +77,7 @@
"q.look('head_ai')",
"q.bedrock('dewgong', 'water_swim')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('dewgong', 'ride_water_swim')"
}
],
@@ -91,7 +91,7 @@
"q.look('head_ai')",
"q.bedrock('dewgong', 'water_idle')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('dewgong', 'ride_water_swim')"
}
],
@@ -106,7 +106,7 @@
"q.look('head_ai')",
"q.bedrock('dewgong', 'water_idle')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('dewgong', 'ride_water_swim')"
}
],
@@ -43,10 +43,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhyhorn', 'ride_jump')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhyhorn', 'ride_ground_run')"
}
"q.bedrock('rhyhorn', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('rhyhorn', 'blink')"
@@ -61,10 +58,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhyhorn', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhyhorn', 'ride_ground_walk')"
}
"q.bedrock('rhyhorn', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('rhyhorn', 'blink')",
@@ -77,10 +71,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhyhorn', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhyhorn', 'ride_ground_walk')"
}
"q.bedrock('rhyhorn', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('rhyhorn', 'blink')"]
},
@@ -90,10 +81,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhyhorn', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhyhorn', 'ride_ground_run')"
}
"q.bedrock('rhyhorn', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('rhyhorn', 'blink')"]
},
@@ -44,10 +44,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhydon', 'ride_jump')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhydon', 'ride_ground_run')"
}
"q.bedrock('rhydon', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('rhydon', 'blink')"
@@ -61,10 +58,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhydon', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhydon', 'ride_ground_run')"
}
"q.bedrock('rhydon', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('rhydon', 'blink')",
@@ -78,10 +72,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhydon', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhydon', 'ride_ground_walk')"
}
"q.bedrock('rhydon', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('rhydon', 'blink')"
@@ -93,10 +84,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhydon', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhydon', 'ride_ground_walk')"
}
"q.bedrock('rhydon', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('rhydon', 'blink')"]
},
@@ -26,10 +26,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros', 'ride_jump')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros', 'ride_ground_run')"
}
"q.bedrock('tauros', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('tauros', 'blink')"
@@ -43,10 +40,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros', 'ride_ground_run')"
}
"q.bedrock('tauros', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('tauros', 'blink')"]
},
@@ -56,10 +50,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros', 'ride_ground_walk')"
}
"q.bedrock('tauros', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('tauros', 'blink')"]
},
@@ -69,10 +60,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros', 'ride_ground_walk')"
}
"q.bedrock('tauros', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('tauros', 'blink')"]
},
@@ -26,10 +26,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_aqua', 'ride_jump_liquid')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_aqua', 'ride_surfacewater_swim')"
}
"q.bedrock('tauros_paldean_aqua', 'ride_surfacewater_swim')"
],
"quirks": [
"q.bedrock_quirk('tauros_paldean_aqua', 'blink')"
@@ -43,10 +40,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_aqua', 'ride_jump_land')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_aqua', 'ride_ground_run')"
}
"q.bedrock('tauros_paldean_aqua', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('tauros_paldean_aqua', 'blink')"
@@ -61,10 +55,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_aqua', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_aqua', 'ride_ground_run')"
}
"q.bedrock('tauros_paldean_aqua', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('tauros_paldean_aqua', 'blink')"]
},
@@ -75,10 +66,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_aqua', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_aqua', 'ride_ground_walk')"
}
"q.bedrock('tauros_paldean_aqua', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('tauros_paldean_aqua', 'blink')"]
},
@@ -89,10 +77,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_aqua', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_aqua', 'ride_ground_walk')"
}
"q.bedrock('tauros_paldean_aqua', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('tauros_paldean_aqua', 'blink')"]
},
@@ -101,10 +86,7 @@
"poseTypes": ["WALK", "SWIM"],
"animations": [
"q.bedrock('tauros_paldean_aqua', 'surfacewater_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_aqua', 'ride_surfacewater_swim')"
}
"q.bedrock('tauros_paldean_aqua', 'ride_surfacewater_swim')"
],
"quirks": ["q.bedrock_quirk('tauros_paldean_aqua', 'blink')"]
},
@@ -114,10 +96,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_aqua', 'surfacewater_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_aqua', 'ride_surfacewater_swim')"
}
"q.bedrock('tauros_paldean_aqua', 'ride_surfacewater_swim')"
],
"quirks": ["q.bedrock_quirk('tauros_paldean_aqua', 'blink')"]
},
@@ -25,10 +25,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_blaze', 'ride_jump')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_blaze', 'ride_ground_run')"
}
"q.bedrock('tauros_paldean_blaze', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('tauros_paldean_blaze', 'blink')"
@@ -42,10 +39,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_blaze', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_blaze', 'ride_ground_run')"
}
"q.bedrock('tauros_paldean_blaze', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('tauros_paldean_blaze', 'blink')"]
},
@@ -55,10 +49,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_blaze', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_blaze', 'ride_ground_walk')"
}
"q.bedrock('tauros_paldean_blaze', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('tauros_paldean_blaze', 'blink')"]
},
@@ -68,10 +59,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_blaze', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_blaze', 'ride_ground_walk')"
}
"q.bedrock('tauros_paldean_blaze', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('tauros_paldean_blaze', 'blink')"]
},
@@ -134,10 +134,7 @@
"animations": [
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('gyarados', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gyarados', 'ride_ground_walk')"
}
"q.bedrock('gyarados', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('gyarados', 'blink')",
@@ -153,10 +150,7 @@
"animations": [
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('gyarados', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gyarados', 'ride_ground_walk')"
}
"q.bedrock('gyarados', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('gyarados', 'blink')",
@@ -170,10 +164,7 @@
"animations": [
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('gyarados', 'water_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gyarados', 'ride_water_swim')"
}
"q.bedrock('gyarados', 'ride_water_swim')"
],
"quirks": [
"q.bedrock_quirk('gyarados', 'blink')",
@@ -191,10 +182,7 @@
"animations": [
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('gyarados', 'water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gyarados', 'ride_water_swim')"
}
"q.bedrock('gyarados', 'ride_water_swim')"
],
"quirks": [
"q.bedrock_quirk('gyarados', 'blink')",
@@ -258,10 +246,7 @@
"animations": [
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('gyarados', 'surface_water_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gyarados', 'ride_surface_water')"
}
"q.bedrock('gyarados', 'ride_surface_water')"
],
"quirks": [
"q.bedrock_quirk('gyarados', 'blink')",
@@ -284,10 +269,7 @@
"animations": [
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('gyarados', 'surface_water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gyarados', 'ride_surface_water')"
}
"q.bedrock('gyarados', 'ride_surface_water')"
],
"quirks": [
"q.bedrock_quirk('gyarados', 'blink')",
@@ -306,10 +288,7 @@
"animations": [
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('gyarados', 'air_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gyarados', 'ride_air_fly')"
}
"q.bedrock('gyarados', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('gyarados', 'blink')",
@@ -322,10 +301,7 @@
"animations": [
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('gyarados', 'air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gyarados', 'ride_air_fly')"
}
"q.bedrock('gyarados', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('gyarados', 'blink')",
@@ -73,10 +73,7 @@
"animations": [
"q.look('head')",
"q.bedrock('lapras', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('lapras', 'ride_ground_walk')"
}
"q.bedrock('lapras', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('lapras', 'blink')",
@@ -89,10 +86,7 @@
"animations": [
"q.look('head')",
"q.bedrock('lapras', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('lapras', 'ride_ground_walk')"
}
"q.bedrock('lapras', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('lapras', 'blink')"]
},
@@ -124,10 +118,7 @@
"animations": [
"q.look('head')",
"q.bedrock('lapras', 'surfacewater_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('lapras', 'ride_surfacewater_swim')"
}
"q.bedrock('lapras', 'ride_surfacewater_swim')"
],
"quirks": [
"q.bedrock_quirk('lapras', 'blink')",
@@ -160,10 +151,7 @@
"animations": [
"q.look('head')",
"q.bedrock('lapras', 'surfacewater_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('lapras', 'ride_surfacewater_swim')"
}
"q.bedrock('lapras', 'ride_surfacewater_swim')"
],
"quirks": [
"q.bedrock_quirk('lapras', 'blink')",
@@ -58,10 +58,7 @@
"q.look('neck', 1, 1, 25, -20, 20, -20)",
"q.look('head', 1, 1, 22.5, -10, 15, -15)",
"q.bedrock('aerodactyl', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('aerodactyl', 'ride_ground_walk')"
}
"q.bedrock('aerodactyl', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('aerodactyl', 'blink')"
@@ -83,10 +80,7 @@
"q.look('neck', 1, 1, 25, -20, 20, -20)",
"q.look('head', 1, 1, 22.5, -10, 15, -15)",
"q.bedrock('aerodactyl', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('aerodactyl', 'ride_ground_walk')"
}
"q.bedrock('aerodactyl', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('aerodactyl', 'blink')"]
},
@@ -97,10 +91,7 @@
"q.look('neck', 1, 1, 25, -20, 20, -20)",
"q.look('head', 1, 1, 22.5, -10, 15, -15)",
"q.bedrock('aerodactyl', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('aerodactyl', 'ride_ground_run')"
}
"q.bedrock('aerodactyl', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('aerodactyl', 'blink')"
@@ -170,10 +161,7 @@
"q.look('head', 1, 1, 22.5, -10, 15, -15)",
"q.bedrock('aerodactyl', 'air_glide')",
"q.bedrock('aerodactyl', 'air_dive')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('aerodactyl', 'ride_air_glide')"
}
"q.bedrock('aerodactyl', 'ride_air_glide')"
],
"quirks": [
"q.bedrock_quirk('aerodactyl', 'blink')"
@@ -61,7 +61,7 @@
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('dragonite', 'ground_idle')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('dragonite', 'ride_ground_walk')"
}
],
@@ -76,7 +76,7 @@
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('dragonite', 'ground_walk')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('dragonite', 'ride_ground_walk')"
}
],
@@ -116,7 +116,7 @@
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('dragonite', 'air_glide')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('dragonite', 'ride_air_fly')"
}
],
@@ -133,7 +133,7 @@
"q.bedrock('dragonite', 'air_glide')",
"q.bedrock('dragonite', 'ride_air_dive')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('dragonite', 'ride_air_glide')"
}
],
@@ -62,10 +62,7 @@
"animations": [
"q.look('head')",
"q.bedrock('ariados', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('ariados', 'ride_ground_walk')"
}
"q.bedrock('ariados', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('ariados', 'blink')"],
"transformTicks": 5
@@ -77,10 +74,7 @@
"animations": [
"q.look('head')",
"q.bedrock('ariados', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('ariados', 'ride_ground_run')"
}
"q.bedrock('ariados', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('ariados', 'blink')"
@@ -85,7 +85,7 @@
"q.look('head', 1, 1, 35, -15, 15, -15)",
"q.bedrock('girafarig', 'ground_walk')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('girafarig', 'ride_ground_walk')"
}
],
@@ -104,7 +104,7 @@
"q.look('head', 1, 1, 35, -15, 15, -15)",
"q.bedrock('girafarig', 'ground_run')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('girafarig', 'ride_ground_run')"
}
],
@@ -44,10 +44,7 @@
"condition": "!q.in_air && !q.is_sprinting",
"animations": [
"q.bedrock('heracross', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('heracross', 'ride_ground_run')"
}
"q.bedrock('heracross', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('heracross', 'blink')"]
},
@@ -56,10 +53,7 @@
"condition": "!q.in_air && q.is_sprinting",
"animations": [
"q.bedrock('heracross', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('heracross', 'ride_ground_run')"
}
"q.bedrock('heracross', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('heracross', 'blink')"]
},
@@ -78,10 +72,7 @@
"condition": "!q.is_gliding",
"animations": [
"q.bedrock('heracross', 'air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('heracross', 'ride_air_fly')"
}
"q.bedrock('heracross', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('heracross', 'blink')"
@@ -93,10 +84,7 @@
"animations": [
"q.bedrock('heracross', 'air_fly')",
"q.bedrock('heracross', 'ride_dive')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('heracross', 'ride_air_fly')"
}
"q.bedrock('heracross', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('heracross', 'blink')"
@@ -52,10 +52,7 @@
"animations": [
"q.look('head')",
"q.bedrock('ursaring', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('ursaring', 'ride_ground_walk')"
}
"q.bedrock('ursaring', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('ursaring', 'blink')"
@@ -69,10 +66,7 @@
"animations": [
"q.look('head')",
"q.bedrock('ursaring', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('ursaring', 'ride_ground_run')"
}
"q.bedrock('ursaring', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('ursaring', 'blink')"],
"transformTicks": 5
@@ -63,10 +63,7 @@
"animations": [
"q.look('head')",
"q.bedrock('piloswine', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('piloswine', 'ride_ground_walk')"
}
"q.bedrock('piloswine', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('piloswine', 'blink')",
@@ -79,10 +76,7 @@
"animations": [
"q.look('head')",
"q.bedrock('piloswine', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('piloswine', 'ride_ground_run')"
}
"q.bedrock('piloswine', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('piloswine', 'blink')"
@@ -59,10 +59,7 @@
"animations": [
"q.look('head')",
"q.bedrock('mantine', 'water_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('mantine', 'ride_water_swim')"
}
"q.bedrock('mantine', 'ride_water_swim')"
],
"quirks": [
"q.bedrock_quirk('mantine', 'blink')"
@@ -75,10 +72,7 @@
"animations": [
"q.look('head')",
"q.bedrock('mantine', 'water_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('mantine', 'ride_water_swim')"
}
"q.bedrock('mantine', 'ride_water_swim')"
],
"quirks": [
"q.bedrock_quirk('mantine', 'blink')"
@@ -91,10 +85,7 @@
"animations": [
"q.look('head')",
"q.bedrock('mantine', 'water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('mantine', 'ride_water_swim')"
}
"q.bedrock('mantine', 'ride_water_swim')"
],
"quirks": [
"q.bedrock_quirk('mantine', 'blink')"
@@ -107,10 +98,7 @@
"animations": [
"q.look('head')",
"q.bedrock('mantine', 'water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('mantine', 'ride_water_swim')"
}
"q.bedrock('mantine', 'ride_water_swim')"
],
"quirks": [
"q.bedrock_quirk('mantine', 'blink')"
@@ -121,10 +109,7 @@
"animations": [
"q.look('head')",
"q.bedrock('mantine', 'water_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('mantine', 'ride_water_swim')"
}
"q.bedrock('mantine', 'ride_water_swim')"
],
"quirks": [
"q.bedrock_quirk('mantine', 'blink')"
@@ -54,10 +54,7 @@
"animations": [
"q.look('head', 0.4, 0.25, 5, -5, 10, -10)",
"q.bedrock('skarmory', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('skarmory', 'ride_ground_run')"
}
"q.bedrock('skarmory', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('skarmory', 'blink')"
@@ -79,10 +76,7 @@
"animations": [
"q.look('head')",
"q.bedrock('skarmory', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('skarmory', 'ride_ground_run')"
}
"q.bedrock('skarmory', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('skarmory', 'blink')"]
},
@@ -68,10 +68,7 @@
"animations": [
"q.look('head')",
"q.bedrock('slaking', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('slaking', 'ride_ground')"
}
"q.bedrock('slaking', 'ride_ground')"
],
"quirks": ["q.bedrock_quirk('slaking', 'blink')"]
},
@@ -82,10 +79,7 @@
"animations": [
"q.look('head')",
"q.bedrock('slaking', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('slaking', 'ride_ground')"
}
"q.bedrock('slaking', 'ride_ground')"
],
"quirks": ["q.bedrock_quirk('slaking', 'blink')"]
},
@@ -95,10 +89,7 @@
"animations": [
"q.look('head')",
"q.bedrock('slaking', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('slaking', 'ride_ground')"
}
"q.bedrock('slaking', 'ride_ground')"
],
"quirks": ["q.bedrock_quirk('slaking', 'blink')"]
},
@@ -27,10 +27,7 @@
"isTouchingWater": false,
"animations": [
"q.bedrock('sharpedo', 'water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('sharpedo', 'ride_water_swim')"
}
"q.bedrock('sharpedo', 'ride_water_swim')"
],
"transformTicks": 3,
"transformToTicks": 3
@@ -66,10 +63,7 @@
"isUnderWater": true,
"animations": [
"q.bedrock('sharpedo', 'water_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('sharpedo', 'ride_water_swim')"
}
"q.bedrock('sharpedo', 'ride_water_swim')"
]
},
"swimming": {
@@ -78,10 +72,7 @@
"isUnderWater": true,
"animations": [
"q.bedrock('sharpedo', 'water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('sharpedo', 'ride_water_swim')"
}
"q.bedrock('sharpedo', 'ride_water_swim')"
]
}
}
@@ -63,10 +63,7 @@
"animations": [
"q.look('head')",
"q.bedrock('camerupt', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('camerupt', 'ride_ground_run')"
}
"q.bedrock('camerupt', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('camerupt', 'blink')"]
},
@@ -76,10 +73,7 @@
"animations": [
"q.look('head')",
"q.bedrock('camerupt', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('camerupt', 'ride_ground_run')"
}
"q.bedrock('camerupt', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('camerupt', 'blink')"]
},
@@ -90,10 +84,7 @@
"animations": [
"q.look('head')",
"q.bedrock('camerupt', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('camerupt', 'ride_ground_run')"
}
"q.bedrock('camerupt', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('camerupt', 'blink')"]
}
@@ -61,10 +61,7 @@
"animations": [
"q.look('head_ai')",
"q.bedrock('flygon', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('flygon', 'ride_ground_walk')"
}
"q.bedrock('flygon', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('flygon', 'blink')"]
},
@@ -74,10 +71,7 @@
"animations": [
"q.look('head_ai')",
"q.bedrock('flygon', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('flygon', 'ride_ground_run')"
}
"q.bedrock('flygon', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('flygon', 'blink')"]
},
@@ -112,10 +106,7 @@
"animations": [
"q.look('head_ai')",
"q.bedrock('flygon', 'air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('flygon', 'ride_air_fly')"
}
"q.bedrock('flygon', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('flygon', 'blink')"
@@ -128,10 +119,7 @@
"q.look('head_ai')",
"q.bedrock('flygon', 'air_glide')",
"q.bedrock('flygon', 'ride_dive')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('flygon', 'ride_air_fly')"
}
"q.bedrock('flygon', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('flygon', 'blink')"
@@ -81,10 +81,7 @@
"condition": "q.is_holding_item",
"animation": "q.bedrock('altaria', 'hold_item')"
},
{
"condition": "q.is_ridden",
"animation": "q.bedrock('altaria', 'ride_air_fly')"
}
"q.bedrock('altaria', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('altaria', 'blink')"
@@ -104,10 +101,7 @@
"condition": "q.is_holding_item",
"animation": "q.bedrock('altaria', 'hold_item')"
},
{
"condition": "q.is_ridden",
"animation": "q.bedrock('altaria', 'ride_air_fly')"
}
"q.bedrock('altaria', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('altaria', 'blink')"
@@ -14,10 +14,7 @@
"isTouchingWater": false,
"animations": [
"q.bedrock('milotic', 'water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('milotic', 'ride_water_swim')"
}
"q.bedrock('milotic', 'ride_water_swim')"
],
"transformTicks": 3,
"transformToTicks": 3
@@ -62,10 +59,7 @@
"animations": [
"q.look('head')",
"q.bedrock('milotic', 'water_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('milotic', 'ride_water_swim')"
}
"q.bedrock('milotic', 'ride_water_swim')"
],
"quirks": ["q.bedrock_quirk('milotic', 'blink')"]
},
@@ -75,10 +69,7 @@
"animations": [
"q.look('head')",
"q.bedrock('milotic', 'water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('milotic', 'ride_water_swim')"
}
"q.bedrock('milotic', 'ride_water_swim')"
],
"quirks": ["q.bedrock_quirk('milotic', 'blink')"]
},
@@ -14,10 +14,7 @@
"isTouchingWater": false,
"animations": [
"q.bedrock('milotic', 'water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('milotic', 'ride_water_swim')"
}
"q.bedrock('milotic', 'ride_water_swim')"
],
"transformTicks": 3,
"transformToTicks": 3
@@ -62,10 +59,7 @@
"animations": [
"q.look('head')",
"q.bedrock('milotic', 'water_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('milotic', 'ride_water_swim')"
}
"q.bedrock('milotic', 'ride_water_swim')"
],
"quirks": ["q.bedrock_quirk('milotic', 'blink')"]
},
@@ -75,10 +69,7 @@
"animations": [
"q.look('head')",
"q.bedrock('milotic', 'water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('milotic', 'ride_water_swim')"
}
"q.bedrock('milotic', 'ride_water_swim')"
],
"quirks": ["q.bedrock_quirk('milotic', 'blink')"]
},
@@ -82,10 +82,7 @@
"q.look('neck4', 0.5, 0.5, 5, -5, 10, -10)",
"q.look('neck5', 0.5, 0.5, 5, -5, 5, -5)",
"q.bedrock('tropius', 'air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tropius', 'ride_air_fly')"
}
"q.bedrock('tropius', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('tropius', 'blink')"
@@ -102,10 +99,7 @@
"q.look('neck5', 0.5, 0.5, 5, -5, 5, -5)",
"q.bedrock('tropius', 'air_glide')",
"q.bedrock('tropius', 'ride_dive')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tropius', 'ride_air_glide')"
}
"q.bedrock('tropius', 'ride_air_glide')"
],
"quirks": [
"q.bedrock_quirk('tropius', 'blink')"
@@ -57,7 +57,7 @@
"q.look('head')",
"q.bedrock('salamence', 'air_idle')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('salamence', 'ride_air_fly')"
}
],
@@ -74,7 +74,7 @@
"q.look('head')",
"q.bedrock('salamence', 'air_fly')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('salamence', 'ride_air_fly')"
}
],
@@ -92,7 +92,7 @@
"q.bedrock('salamence', 'air_glide')",
"q.bedrock('salamence', 'ride_dive')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('salamence', 'ride_air_glide')"
}
],
@@ -109,7 +109,7 @@
"q.look('head')",
"q.bedrock('salamence', 'ride_jump')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('salamence', 'ride_ground_run')"
}
],
@@ -125,7 +125,7 @@
"q.look('head')",
"q.bedrock('salamence', 'ground_walk')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('salamence', 'ride_ground_run')"
}
],
@@ -141,7 +141,7 @@
"q.look('head')",
"q.bedrock('salamence', 'ground_run')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('salamence', 'ride_ground_run')"
}
],
@@ -167,10 +167,7 @@
"condition": "!q.in_air && q.is_sprinting",
"animations": [
"q.bedrock('metagross', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('metagross', 'ride_ground_run')"
}
"q.bedrock('metagross', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('metagross', 'blink')"
@@ -72,10 +72,7 @@
"animations": [
"q.look('head')",
"q.bedrock('staraptor', 'air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('staraptor', 'ride_air_fly')"
}
"q.bedrock('staraptor', 'ride_air_fly')"
],
"quirks": ["q.bedrock_quirk('staraptor', 'blink')"]
},
@@ -86,10 +83,7 @@
"q.look('head')",
"q.bedrock('staraptor', 'air_glide')",
"q.bedrock('staraptor', 'ride_dive')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('staraptor', 'ride_air_glide')"
}
"q.bedrock('staraptor', 'ride_air_glide')"
],
"quirks": ["q.bedrock_quirk('staraptor', 'blink')"
]
@@ -49,7 +49,7 @@
"q.look('head')",
"q.bedrock('bastiodon', 'ground_idle')",
{
"condition": "q.has_entity && q.is_ridden",
"condition": "q.has_entity",
"animation": "q.bedrock('bastiodon', 'ride_ground_walk')"
},
{
@@ -87,7 +87,7 @@
"q.look('head')",
"q.bedrock('bastiodon', 'ground_walk')",
{
"condition": "q.has_entity && q.is_ridden",
"condition": "q.has_entity",
"animation": "q.bedrock('bastiodon', 'ride_ground_walk')"
},
{
@@ -107,7 +107,7 @@
"q.look('head')",
"q.bedrock('bastiodon', 'ground_run')",
{
"condition": "q.has_entity && q.is_ridden",
"condition": "q.has_entity",
"animation": "q.bedrock('bastiodon', 'ride_ground_walk')"
},
{
@@ -52,10 +52,7 @@
"animations": [
"q.look('head')",
"q.bedrock('gastrodon_east', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gastrodon_east', 'ride_ground_run')"
}
"q.bedrock('gastrodon_east', 'ride_ground_run')"
],
"transformToTicks": 2
},
@@ -77,10 +74,7 @@
"animations": [
"q.look('head')",
"q.bedrock('gastrodon_east', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gastrodon_east', 'ride_ground_walk')"
}
"q.bedrock('gastrodon_east', 'ride_ground_walk')"
]
},
"sleep": {
@@ -52,10 +52,7 @@
"animations": [
"q.look('head')",
"q.bedrock('gastrodon_west', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gastrodon_west', 'ride_ground_run')"
}
"q.bedrock('gastrodon_west', 'ride_ground_run')"
],
"transformToTicks": 2
},
@@ -77,10 +74,7 @@
"animations": [
"q.look('head')",
"q.bedrock('gastrodon_west', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gastrodon_west', 'ride_ground_walk')"
}
"q.bedrock('gastrodon_west', 'ride_ground_walk')"
]
},
"sleep": {

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