mirror of
https://gitlab.com/cable-mc/cobblemon.git
synced 2026-10-27 12:36:24 -04:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d54e51fb9 | ||
|
|
e3c1fff02a | ||
|
|
c672a4d690 | ||
|
|
e64df48441 | ||
|
|
acd375f533 | ||
|
|
9076c659de | ||
|
|
9292504f8c | ||
|
|
ad484abf56 | ||
|
|
fc1c7cf6ca | ||
|
|
33abca9b43 | ||
|
|
ea3c051772 | ||
|
|
4cde730259 | ||
|
|
2bcce06318 | ||
|
|
930c3568bb | ||
|
|
ffe480b12f |
+125
-16
@@ -1,47 +1,156 @@
|
||||
# Changelog
|
||||
## [1.8.3 (MONTH xth, 2026)](#1-8-3)
|
||||
## [1.8.2 (MONTH xth, 2026)](#1-8-2)
|
||||
|
||||
### Additions
|
||||
- You can now drag to select multiple Pokémon inside the PC menu.
|
||||
- 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, 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:
|
||||
- Ring Target, Metronome, and Eject Button.
|
||||
- The following items can now be worn by Players and Pokémon:
|
||||
- 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.
|
||||
|
||||
### Pokémon Added
|
||||
#### Generation 4
|
||||
- Snover
|
||||
- Abomasnow
|
||||
- Burmy
|
||||
- Wormadam
|
||||
- Mothim
|
||||
|
||||
#### Generation 5
|
||||
- Tympole
|
||||
- Palpitoad
|
||||
- Seismitoad
|
||||
|
||||
#### Generation 6
|
||||
- Helioptile
|
||||
- Heliolisk
|
||||
|
||||
#### Generation 7
|
||||
- Grubbin
|
||||
- Charjabug
|
||||
- Vikavolt
|
||||
- Minior
|
||||
|
||||
#### Generation 8
|
||||
- Eternatus
|
||||
|
||||
#### Generation 9
|
||||
- Greavard
|
||||
- Houndstone
|
||||
|
||||
### Model updates for the following Pokémon
|
||||
- Sudowoodo
|
||||
- Spheal
|
||||
- Cacnea
|
||||
- Cacturne
|
||||
- Binacle
|
||||
- Barbaracle
|
||||
- Glimmet
|
||||
- Glimmora
|
||||
- Deerling
|
||||
- Sawsbuck (All forms)
|
||||
- Absol
|
||||
- Poliwag
|
||||
- Politoed
|
||||
- Ekans
|
||||
- Oddish
|
||||
- Gloom
|
||||
- Vileplume
|
||||
- Bellossom
|
||||
- Shroomish
|
||||
|
||||
### Animation updates for the following Pokémon
|
||||
- Treecko (New Shoulder Mount!)
|
||||
- Bonsly (New Shoulder Mount!)
|
||||
- Sudowoodo
|
||||
- Cacnea
|
||||
- Cacturne
|
||||
- Ekans
|
||||
- Arbok
|
||||
- Oddish
|
||||
- Gloom
|
||||
- Vileplume
|
||||
- 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 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.
|
||||
- Exceptions being "forced" evolutions which will continue to stop after the first one
|
||||
|
||||
### Fixes
|
||||
- Fixed datapacks with item interaction evolutions that don't specify an item crashing the world on load.
|
||||
- Fixed the Pokédex evolution tree crashing when a datapack's dex entry is missing an evolution's form.
|
||||
- Fixed the Pokédex evolution tree crashing with a stack overflow on evolutions that loop back on themselves.
|
||||
- Fixed the Pokédex evolution tree crashing when looking at a dex that is not part of the "National Dex" aggregate.
|
||||
- Fixed evolution trees not building properly when a pokemon has several pokedex entries for different regions.
|
||||
- Fixed form-changing evolutions sometimes resolving to the source form in the Pokédex evolution tree.
|
||||
- Broken datapack evolutions are now logged and skipped in the Pokédex evolution tree instead of crashing.
|
||||
- Fixed friendship booster items not being recognised as such
|
||||
- Fixed conflicts with other mods that would make use of recipe books in certain ways (Farmer's Delight Cooking Pot)
|
||||
- Fixed Pokémon battles stalling when a sidemod cancels the `THROWN_POKEBALL_HIT` hit event
|
||||
- Fixed choice feature tab completion in `spawnpokemon/pokegive/pokeedit` commands
|
||||
- Correctly grants "They're Minerals!" advancement when planting a type gem
|
||||
- Shoulder mounted Pokémon now follow player body animations, improving their positioning with custom player animations and default actions.
|
||||
- Shoulder mounted Pokémon now sit slightly higher with chest armor equipped for better alignment, with a configurable blacklist.
|
||||
- Type Gem Blocks now have advancements to give you the recipe in the recipe book (the data for the advancements were inside the recipes somehow??)
|
||||
- Some other recipes without advancements have also been corrected
|
||||
- Corrected the TM Recipes for Charge Beam and Payday
|
||||
- Type Gem blocks now properly generate clusters off of the block.
|
||||
- Starter Screen Categories will no longer stop rendering if they are bigger than the viewport, and you are not viewing top or bottom edges
|
||||
- Random Starter option now properly handled in height calculation, so now it won't run off into other categories or be un-accessible due to scroll limits if it causes a new row
|
||||
- Mochi and Feathers no longer drop glass bottles on use
|
||||
- Fixed a rare set of concurrency bugs related to reloading packs on NeoForge; fixes a ModernFix issue
|
||||
- Removed unnecessary conditions for Mimikyu's standing pose. Hopefully fixes the rare instance of it using its shoulder animations during battle in multiplayer scenarios.
|
||||
- Fixed Rockruff line not blinking
|
||||
- Fixed Flygon sometimes using its running animation instead of walking
|
||||
- Fixed Bramblin always sleeping outside of battle
|
||||
- Fixed Dragonair using its battle idle as a normal idle
|
||||
- Fixed Lycanroc's head movement
|
||||
- Added head movement to Archaludon
|
||||
- Fixed Duraludon line not having any sleep animations
|
||||
- Fixed Diglett line not having their faint animations
|
||||
- Fixed Kingambit moving its legs while walking
|
||||
- Fixed Alolan Diglett's cry not playing
|
||||
- Fixed shiny alpha Snorlax using the non-shiny texture
|
||||
- Various Structure fixes
|
||||
- Fixed shiny Pokémon under certain circumstances forgetting taught TMs
|
||||
- Fixed certain Pokémon sounds sometimes being doubled when played at specific times in their animations
|
||||
- Fixed ball capture beam origin coming from behind the ball.
|
||||
- Fixed Safari Ball catch rate outside of battle
|
||||
|
||||
### Developer
|
||||
- Bumped NeoForge to `21.1.195` to get access to MixinExtras `0.5` and the expressions annotation
|
||||
- Evolution requirements can now set `pokedexIcon` and `pokedexTooltip` directly in their JSON to control how they're shown on the Pokédex's evolution tree, for example:
|
||||
```json
|
||||
{
|
||||
"variant": "attack_defence_ratio",
|
||||
"pokedexIcon": "minecraft:diamond_sword",
|
||||
"pokedexTooltip": "cobblemon.ui.pokedex.requirement.attack_defence_ratio.tooltip"
|
||||
}
|
||||
```
|
||||
- Fixed wrong order of operations in the code flow prior to the `FOSSIL_REVIVED` event causing pokemon to appear without entity link
|
||||
- Fixed TestPartySlot command not correctly comparing the provided spec with the target Pokémon
|
||||
|
||||
### Molang & Datapacks
|
||||
- Fixed `q.pokemon.nature` returning class instance instead of proper string value (`cobblemon:adamant`)
|
||||
- Fixed `q.entity.is_looking_at` result being always `1` for whatever entity passed as target
|
||||
- A new `negate` variant for evolution requirements has been added, which will negate (invert) the provided inner condition.
|
||||
- Added Molang functions for individual Pokédex ids, wherein a subdex is passed in as a string to `dex_caught_count` etc.
|
||||
|
||||
### Particles
|
||||
|
||||
#### New Move Animations
|
||||
|
||||
### Localization
|
||||
- Updated translations for:
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
- Added `cobblemon:catch_alpha_pokemon` advancement trigger and `totalAlphaCaptureCount` stat for captured Alpha Pokémon.
|
||||
|
||||
### New rideable Pokémon
|
||||
- Pidgeot
|
||||
- Crobat
|
||||
- Skarmory
|
||||
- Milotic
|
||||
@@ -107,9 +106,7 @@
|
||||
- Avalugg (Hisuian Form)
|
||||
|
||||
### Model updates for the following Pokémon
|
||||
- Pidgey
|
||||
- Pidgeotto
|
||||
- Pidgeot
|
||||
|
||||
- Sandshrew
|
||||
- Sandslash
|
||||
- Nidoking
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
# Changelog
|
||||
## [1.8.2 (October 11th, 2026)](#1-8-2)
|
||||
|
||||
### 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, 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.
|
||||
- I wasn't finished. 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:
|
||||
- Ring Target, Metronome, and Eject Button.
|
||||
- The following items can now be worn by Players and Pokémon:
|
||||
- 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 whatever you've rebound use/interact to).
|
||||
- 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.
|
||||
- The recall animation now renders the ball in hand with the player's arm raised.
|
||||
- Added Oricorio Flowers.
|
||||
- Added Terrain Plants, four plants grown from Electric, Grassy, Misty, and Psychic Seeds, which glow under specific environmental conditions and can be farmed for more of their seeds.
|
||||
- Berries can now be placed in Flower Pots.
|
||||
- The evolution tree for a Pokémon can now be viewed in the Pokédex, showing the 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.
|
||||
- Added paper as a new cosmetic item for Pikachu.
|
||||
- Burmy changes its cloak after a battle it took part in, based on the biome.
|
||||
- Trash Cloak in the Deep Dark, Nether and End.
|
||||
- Plant Cloak in lush biomes such as forests, jungles, plains and swamps.
|
||||
- Sandy Cloak in sandy biomes, beaches and caves.
|
||||
|
||||
### Pokémon Added
|
||||
#### Generation 4
|
||||
- Snover
|
||||
- Abomasnow
|
||||
- Burmy
|
||||
- Wormadam
|
||||
- Mothim
|
||||
|
||||
#### Generation 5
|
||||
- Tympole
|
||||
- Palpitoad
|
||||
- Seismitoad
|
||||
|
||||
#### Generation 6
|
||||
- Helioptile
|
||||
- Heliolisk
|
||||
|
||||
#### Generation 7
|
||||
- Grubbin
|
||||
- Charjabug
|
||||
- Vikavolt
|
||||
- Minior
|
||||
|
||||
#### Generation 8
|
||||
- Eternatus
|
||||
|
||||
#### Generation 9
|
||||
- Greavard
|
||||
- Houndstone
|
||||
|
||||
### Model updates for the following Pokémon
|
||||
- Sudowoodo
|
||||
- Spheal
|
||||
- Cacnea
|
||||
- Cacturne
|
||||
- Binacle
|
||||
- Barbaracle
|
||||
- Glimmet
|
||||
- Glimmora
|
||||
- Deerling
|
||||
- Sawsbuck (All forms)
|
||||
- Absol
|
||||
- Poliwag
|
||||
- Politoed
|
||||
- Ekans
|
||||
- Oddish
|
||||
- Gloom
|
||||
- Vileplume
|
||||
- Bellossom
|
||||
- Shroomish
|
||||
- Chespin
|
||||
- Quilladin
|
||||
- Chesnaught
|
||||
- Fennekin
|
||||
- Braixen
|
||||
- Delphox
|
||||
- Froakie
|
||||
- Frogadier
|
||||
- Greninja
|
||||
- Shinx
|
||||
- Luxio
|
||||
- Luxray
|
||||
- Slakoth
|
||||
- Riolu
|
||||
- Lucario
|
||||
- Corsola
|
||||
- Mime Jr.
|
||||
|
||||
### Animation updates for the following Pokémon
|
||||
- Treecko (New Shoulder Mount!)
|
||||
- Bonsly (New Shoulder Mount!)
|
||||
- Sudowoodo
|
||||
- Cacnea
|
||||
- Cacturne
|
||||
- Ekans
|
||||
- Arbok
|
||||
- Oddish
|
||||
- Gloom
|
||||
- Vileplume
|
||||
- Bellossom
|
||||
- Pichu
|
||||
- Pikachu
|
||||
- Raichu (+ Alolan)
|
||||
- Greninja
|
||||
- Slakoth
|
||||
|
||||
### Changes
|
||||
- Slowed the speed of Type Gem growth, and revamped their generation to grow in a more crystal-like shape.
|
||||
- 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 and consistently within the Recipe Book's categories.
|
||||
- Updated the following recipes
|
||||
- Never-Melt Ice, Focus Sash, Blunder Policy, Weakness Policy, Blank TM.
|
||||
- Made ride rotations less jittery in multiplayer.
|
||||
- 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 a TM creative tab.
|
||||
- Made regional and Vivillon split-evo locations mutually exclusive.
|
||||
- Vivillon's patterns are now viewable as forms with form names and unique Pokédex entries displayed.
|
||||
- Item evolutions now unlock **all** locked evolutions whose requirements are met instead of stopping after the first.
|
||||
- Exceptions being "forced" evolutions which stop after the first one.
|
||||
- Removed Mega Drain as a TM.
|
||||
- PokeSnacks (and other area spawns) will now start the vertical offset checking from the center rather than base coordinate
|
||||
- This will help PokeSnacks to work inside caves or tighter spaces better.
|
||||
|
||||
### Fixes
|
||||
- Type Gem blocks now properly generate clusters off of the block.
|
||||
- Correctly grant "They're Minerals!" advancement when planting a Type Gem.
|
||||
- Shoulder mounted Pokémon now follow player body animations, improving their positioning with custom player animations and default actions.
|
||||
- Shoulder mounted Pokémon now sit slightly higher with chest armor equipped for better alignment, with a configurable blacklist.
|
||||
- Type Gem Blocks now have advancements to give you the recipe in the recipe book (the data for the advancements were inside the recipes somehow??).
|
||||
- Some other recipes without advancements have also been corrected.
|
||||
- Corrected the TM Recipes for Charge Beam and Payday.
|
||||
- Starter Screen Categories will no longer stop rendering if they are bigger than the viewport and you are not viewing the top or bottom edges.
|
||||
- The Random Starter option no longer runs off into other categories or becomes unaccessible due to scroll limits if it causes a new row.
|
||||
- Mochi and Feathers no longer drop glass bottles on use.
|
||||
- Fixed a rare set of concurrency bugs related to reloading packs on NeoForge; fixes a ModernFix compatibility issue.
|
||||
- Removed unnecessary conditions for Mimikyu's standing pose. Hopefully fixes the rare instance of it using its shoulder animations during battle in multiplayer scenarios.
|
||||
- Fixed Rockruff line not blinking.
|
||||
- Fixed Flygon sometimes using its running animation instead of walking.
|
||||
- Fixed Bramblin always sleeping outside of battle.
|
||||
- Fixed Dragonair using its battle idle as a normal idle.
|
||||
- Fixed Lycanroc's head movement.
|
||||
- Added head movement to Archaludon.
|
||||
- Fixed Duraludon line not having any sleep animations.
|
||||
- Fixed Diglett line not having their faint animations.
|
||||
- Fixed Kingambit moving its legs while walking.
|
||||
- Fixed Alolan Diglett's cry not playing.
|
||||
- Fixed shiny alpha Snorlax using the non-shiny texture.
|
||||
- Fixed Crabominable's head breaking when looking around.
|
||||
- Fixed Shiny Dipplin and Hydrapple's textures.
|
||||
- Various structure fixes.
|
||||
- Fixed shiny Pokémon under certain circumstances forgetting taught TMs.
|
||||
- Fixed certain Pokémon sounds sometimes being doubled when played at specific times in their animations.
|
||||
- Fixed ball capture beam origin coming from behind the ball.
|
||||
- Fixed Safari Ball catch rate outside of battle.
|
||||
- Fixed alpha eye (and other) particles not respecting the client particle density setting in video options.
|
||||
- Fixed the TM Machine UI not closing when player is moving away from it.
|
||||
- Moved the config category for alpha/shiny chance config fields for fossil machine and honey log slathering to Spawning.
|
||||
- Fixed crashes for addon species with non-Cobblemon namespaces in evolution, fishing bait, and Pokédex lookups.
|
||||
- Bees will now grow Apricorns and the Galarica Nut Bush, which were in the bee growables tag but never handled, and correctly spend their nectar when they do.
|
||||
- Saccharine Leaves can now be bone mealed.
|
||||
- Fixed equipping a stack of wearable items instead of one item of a stack.
|
||||
- Fixed TM stack consumption in TM Machine screen when a player teaches a move to a Pokémon.
|
||||
- Fixed every Miltank giving mushroom stew, not only mushroom variants.
|
||||
- Medicinal Leeks can now be eaten when they aren't being planted, matching their food properties.
|
||||
- Sweeping attacks no longer set owned Pokémon on fire (e.g. with Fire Aspect) or knock them back. Friendly Fire is now off.
|
||||
- Fixed Pokémon not losing their previous form's signature moves when changing forms (e.g. Rotom keeping Overheat after leaving its Heat form).
|
||||
- Form-specific moves gained from a form change are now only granted if the Pokémon is high enough level to learn them, and no longer create duplicates in benched moves.
|
||||
- Spinda now learns Teeter Dance at level 32 instead of via TM.
|
||||
- Fixed Oricorio Pa'u form description in the Pokédex.
|
||||
- Fixed PC playing sound on close when opened via command.
|
||||
|
||||
### Developer
|
||||
- Evolution requirements can now set `pokedexIcon` and `pokedexTooltip` directly in their JSON to control how they're shown on the Pokédex's evolution tree, for example:
|
||||
```json
|
||||
{
|
||||
"variant": "attack_defence_ratio",
|
||||
"pokedexIcon": "minecraft:diamond_sword",
|
||||
"pokedexTooltip": "cobblemon.ui.pokedex.requirement.attack_defence_ratio.tooltip"
|
||||
}
|
||||
```
|
||||
- Fixed the wrong order of operations in the code flow prior to the `FOSSIL_REVIVED` event causing Pokémon to appear without the entity link.
|
||||
- Fixed TestPartySlot command not correctly comparing the provided spec with the target Pokémon.
|
||||
|
||||
### Molang & Datapacks
|
||||
- A new `negate` variant for evolution requirements has been added, which will negate (invert) the provided inner condition.
|
||||
- Added Molang functions for individual Pokédex ids, wherein a subdex is passed in as a string to `dex_caught_count` etc.
|
||||
- Added a `battle_ended` callback, backed by the new `BATTLE_ENDED` event, which fires whenever a battle ends for any reason, alongside a multitude of flags not previously covered by `BATTLE_VICTORY`.
|
||||
- Added `battle_pokemon` to battle actor structs and `has_faced_opponents` to battle Pokémon structs.
|
||||
- Added the `cobblemon:form_change/bagworm_cloak/plant_cloak`, `sandy_cloak` and `trash_cloak` biome tags, which control Burmy's cloak after battle.
|
||||
|
||||
### Localization
|
||||
- Updated translations for:
|
||||
- Brazilian Portuguese
|
||||
- Canadian French
|
||||
- Japanese
|
||||
- Korean
|
||||
- Russian
|
||||
- Ukrainian
|
||||
@@ -8,42 +8,54 @@
|
||||
|
||||
package com.cobblemon.mod.common.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import com.cobblemon.mod.common.block.SaccharineLeafBlock;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.animal.Bee;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.BonemealableBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
|
||||
|
||||
|
||||
@Mixin(targets = "net.minecraft.world.entity.animal.Bee$BeeGrowCropGoal")
|
||||
public abstract class BeeEntityMixin {
|
||||
|
||||
@Shadow(aliases = {"this$0", "field_20373"}) @Final private Bee bee;
|
||||
@Unique
|
||||
private BlockState cobblemon$result = null;
|
||||
|
||||
@Inject(method = "tick()V", at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lnet/minecraft/world/level/block/BonemealableBlock;performBonemeal(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/util/RandomSource;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)V",
|
||||
shift = At.Shift.BY,
|
||||
by = 2
|
||||
),
|
||||
locals = LocalCapture.CAPTURE_FAILHARD
|
||||
)
|
||||
private void injectCustomGrowth(CallbackInfo ci, int i, BlockPos blockPos, BlockState blockState, Block block) {
|
||||
if (block instanceof SaccharineLeafBlock) {
|
||||
|
||||
int age = blockState.getValue(SaccharineLeafBlock.Companion.getAGE());
|
||||
boolean waterlogged = blockState.getValue(BlockStateProperties.WATERLOGGED);
|
||||
|
||||
if (age < 2 && !waterlogged) {
|
||||
this.cobblemon$result = blockState.setValue(SaccharineLeafBlock.Companion.getAGE(), age + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Grows bonemealable blocks vanilla's hardcoded checks miss, returning the state so it handles the rest.
|
||||
@ModifyVariable(method = "tick", at = @At(value = "LOAD", ordinal = 0), index = 5)
|
||||
private BlockState cobblemon$growBonemealableBlocks(BlockState grownState, @Local(ordinal = 0) BlockState blockState, @Local BlockPos pos) {
|
||||
if (grownState != null || blockState.is(Blocks.CAVE_VINES) || blockState.is(Blocks.CAVE_VINES_PLANT)) {
|
||||
return grownState;
|
||||
private BlockState applyCustomBlockState(BlockState before) {
|
||||
if(this.cobblemon$result != null && before == null) {
|
||||
BlockState result = this.cobblemon$result;
|
||||
this.cobblemon$result = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!(blockState.getBlock() instanceof BonemealableBlock bonemealable)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ServerLevel level = (ServerLevel) this.bee.level();
|
||||
if (!bonemealable.isValidBonemealTarget(level, pos, blockState)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
bonemealable.performBonemeal(level, this.bee.getRandom(), pos, blockState);
|
||||
return level.getBlockState(pos);
|
||||
return before;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.IntTag;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
@@ -46,24 +45,4 @@ public class ChunkStorageMixin {
|
||||
int curVersion = vanillaFixed.contains(VERSION_KEY) ? vanillaFixed.getInt(VERSION_KEY) : 0;
|
||||
return DataFixTypes.CHUNK.update(CobblemonSchemas.getDATA_FIXER(), vanillaFixed, curVersion, CobblemonSchemas.DATA_VERSION);
|
||||
}
|
||||
|
||||
// Our update on the same MC version still needs its own fixes so run them here in that case.
|
||||
@Inject(
|
||||
method = "upgradeChunkTag(Lnet/minecraft/resources/ResourceKey;Ljava/util/function/Supplier;Lnet/minecraft/nbt/CompoundTag;Ljava/util/Optional;)Lnet/minecraft/nbt/CompoundTag;",
|
||||
at = @At("HEAD"),
|
||||
cancellable = true
|
||||
)
|
||||
private void cobblemon$doChunkFixOnCurrentVersion(
|
||||
ResourceKey<Level> levelKey,
|
||||
Supplier<DimensionDataStorage> storage,
|
||||
CompoundTag chunkData,
|
||||
Optional<ResourceKey<MapCodec<? extends ChunkGenerator>>> chunkGeneratorKey,
|
||||
CallbackInfoReturnable<CompoundTag> cir
|
||||
) {
|
||||
if (ChunkStorage.getVersion(chunkData) != SharedConstants.getCurrentVersion().getDataVersion().getVersion()) {
|
||||
return;
|
||||
}
|
||||
int curVersion = chunkData.contains(VERSION_KEY) ? chunkData.getInt(VERSION_KEY) : 0;
|
||||
cir.setReturnValue(DataFixTypes.CHUNK.update(CobblemonSchemas.getDATA_FIXER(), chunkData, curVersion, CobblemonSchemas.DATA_VERSION));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,16 +35,13 @@ import com.cobblemon.mod.common.util.DataKeys;
|
||||
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.EntityDimensions;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
@@ -53,7 +50,6 @@ import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.food.FoodProperties;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.enchantment.EnchantmentHelper;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -375,33 +371,4 @@ public abstract class PlayerMixin extends LivingEntity implements ScannableEntit
|
||||
this.cobblemon$rideEyePos = rideEyePos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeping attacks apply post-attack enchantment effects (e.g. Fire Aspect) to every entity caught
|
||||
* in the sweep, even if the damage itself was blocked. Skip them for Pokémon immune to the attack.
|
||||
*/
|
||||
@WrapOperation(
|
||||
method = "attack",
|
||||
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/enchantment/EnchantmentHelper;doPostAttackEffects(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/damagesource/DamageSource;)V")
|
||||
)
|
||||
private void cobblemon$skipPostAttackEffectsOnImmunePokemon(ServerLevel level, Entity target, DamageSource source, Operation<Void> original) {
|
||||
if (target instanceof PokemonEntity pokemonEntity && pokemonEntity.isInvulnerableTo(source)) {
|
||||
return;
|
||||
}
|
||||
original.call(level, target, source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeping attacks also knock back every entity caught in the sweep regardless of whether it took damage.
|
||||
*/
|
||||
@WrapOperation(
|
||||
method = "attack",
|
||||
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;knockback(DDD)V")
|
||||
)
|
||||
private void cobblemon$skipKnockbackOnImmunePokemon(LivingEntity target, double strength, double x, double z, Operation<Void> original, @Local DamageSource source) {
|
||||
if (target instanceof PokemonEntity pokemonEntity && pokemonEntity.isInvulnerableTo(source)) {
|
||||
return;
|
||||
}
|
||||
original.call(target, strength, x, z);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+17
-7
@@ -16,7 +16,6 @@ import com.cobblemon.mod.common.item.crafting.CookingPotShapelessRecipe;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import net.minecraft.client.ClientRecipeBook;
|
||||
import net.minecraft.client.RecipeBookCategories;
|
||||
import net.minecraft.client.gui.screens.recipebook.RecipeCollection;
|
||||
@@ -64,12 +63,23 @@ public abstract class ClientRecipeBookMixin {
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "setupCollections",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lcom/google/common/collect/ImmutableMap;copyOf(Ljava/util/Map;)Lcom/google/common/collect/ImmutableMap;"
|
||||
))
|
||||
private void addCustomAggregateCategories(Iterable<RecipeHolder<?>> recipes, RegistryAccess registryAccess, CallbackInfo ci, @Local(name = "map2") Map<RecipeBookCategories, List<RecipeCollection>> map2) {
|
||||
@Inject(method = "setupCollections", at = @At("RETURN"))
|
||||
private void addCustomAggregateCategories(Iterable<RecipeHolder<?>> recipes, RegistryAccess registryAccess, CallbackInfo ci) {
|
||||
Map<RecipeBookCategories, List<List<RecipeHolder<?>>>> map = categorizeAndGroupRecipes(recipes);
|
||||
Map<RecipeBookCategories, List<RecipeCollection>> map2 = Maps.newHashMap();
|
||||
ImmutableList.Builder<RecipeCollection> builder = ImmutableList.builder();
|
||||
|
||||
map.forEach((recipeBookCategories, list) -> {
|
||||
List<RecipeCollection> collections = list.stream()
|
||||
.map(listx -> new RecipeCollection(registryAccess, listx))
|
||||
.peek(builder::add)
|
||||
.collect(ImmutableList.toImmutableList());
|
||||
map2.put(recipeBookCategories, collections);
|
||||
});
|
||||
RecipeBookCategories.AGGREGATE_CATEGORIES.forEach((recipeBookCategories, list) -> map2.put(recipeBookCategories, (List) list.stream().flatMap((recipeBookCategoriesx) -> ((List) map2.getOrDefault(recipeBookCategoriesx, ImmutableList.of())).stream()).collect(ImmutableList.toImmutableList())));
|
||||
CobblemonRecipeCategories.Companion.getCustomAggregateCategories().forEach((recipeBookCategories, list) -> map2.put(recipeBookCategories, (List) list.stream().flatMap((recipeBookCategoriesx) -> ((List) map2.getOrDefault(recipeBookCategoriesx, ImmutableList.of())).stream()).collect(ImmutableList.toImmutableList())));
|
||||
|
||||
this.collectionsByTab = ImmutableMap.copyOf(map2);
|
||||
this.allCollections = builder.build();
|
||||
}
|
||||
}
|
||||
+43
-13
@@ -9,25 +9,55 @@
|
||||
package com.cobblemon.mod.common.mixin.client;
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonRecipeCategories;
|
||||
import com.llamalad7.mixinextras.expression.Definition;
|
||||
import com.llamalad7.mixinextras.expression.Expression;
|
||||
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import net.minecraft.client.ClientRecipeBook;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.RecipeBookCategories;
|
||||
import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent;
|
||||
import net.minecraft.client.gui.screens.recipebook.RecipeBookPage;
|
||||
import net.minecraft.client.gui.screens.recipebook.RecipeBookTabButton;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(RecipeBookComponent.class)
|
||||
public class RecipeBookComponentMixin {
|
||||
@Definition(id = "recipeBookCategories", local = @Local(name = "recipeBookCategories"))
|
||||
@Definition(id = "FURNACE_SEARCH", field = "Lnet/minecraft/client/RecipeBookCategories;FURNACE_SEARCH:Lnet/minecraft/client/RecipeBookCategories;")
|
||||
@Expression("recipeBookCategories != FURNACE_SEARCH")
|
||||
@ModifyExpressionValue(
|
||||
method = "updateTabs",
|
||||
at = @At(value = "MIXINEXTRAS:EXPRESSION")
|
||||
)
|
||||
private boolean notCookingPotSearch(boolean original, @Local(name = "recipeBookCategories") RecipeBookCategories recipeBookCategories) {
|
||||
return original && recipeBookCategories != CobblemonRecipeCategories.COOKING_POT_SEARCH.toVanillaCategory();
|
||||
|
||||
@Shadow private ClientRecipeBook book;
|
||||
|
||||
@Shadow protected Minecraft minecraft;
|
||||
|
||||
@Shadow private int width;
|
||||
|
||||
@Shadow private int xOffset;
|
||||
|
||||
@Shadow private int height;
|
||||
|
||||
@Shadow @Final private RecipeBookPage recipeBookPage;
|
||||
|
||||
@Inject(method = "updateTabs", at = @At("HEAD"), cancellable = true)
|
||||
private void customUpdateTabs(CallbackInfo ci) {
|
||||
RecipeBookComponent instance = (RecipeBookComponent) (Object) this;
|
||||
int i = (width - 147) / 2 - xOffset - 30;
|
||||
int j = (height - 166) / 2 + 3;
|
||||
int k = 27;
|
||||
int l = 0;
|
||||
|
||||
for (RecipeBookTabButton recipeBookTabButton : instance.tabButtons) {
|
||||
RecipeBookCategories recipeBookCategories = recipeBookTabButton.getCategory();
|
||||
if (recipeBookCategories != RecipeBookCategories.CRAFTING_SEARCH && recipeBookCategories != RecipeBookCategories.FURNACE_SEARCH && recipeBookCategories != CobblemonRecipeCategories.COOKING_POT_SEARCH.toVanillaCategory()) {
|
||||
if (recipeBookTabButton.updateVisibility(book)) {
|
||||
recipeBookTabButton.setPosition(i, j + 27 * l++);
|
||||
recipeBookTabButton.startAnimation(minecraft);
|
||||
}
|
||||
} else {
|
||||
recipeBookTabButton.visible = true;
|
||||
recipeBookTabButton.setPosition(i, j + k * l++);
|
||||
}
|
||||
}
|
||||
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
@@ -710,11 +710,11 @@ 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(CobblemonSounds.ITEM_BLOCK_METAL_CAN_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.EXTENDER))
|
||||
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_JAR_GLASS_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.GLASS_BOTTLE))
|
||||
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_JAR_GLASS_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.GLASS_BOTTLE))
|
||||
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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -425,7 +433,7 @@ object CobblemonNetwork {
|
||||
list.add(PacketRegisterInfo(MovesRegistrySyncPacket.ID, MovesRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
|
||||
list.add(PacketRegisterInfo(BerryRegistrySyncPacket.ID, BerryRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
|
||||
list.add(PacketRegisterInfo(SpeciesRegistrySyncPacket.ID, SpeciesRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
|
||||
list.add(PacketRegisterInfo(DexEvolutionDisplaySyncPacket.ID, DexEvolutionDisplaySyncPacket::decode, DataRegistrySyncPacketHandler()))
|
||||
list.add(PacketRegisterInfo(SpeciesEvolutionSyncPacket.ID, SpeciesEvolutionSyncPacket::decode, DataRegistrySyncPacketHandler()))
|
||||
list.add(PacketRegisterInfo(PropertiesCompletionRegistrySyncPacket.ID, PropertiesCompletionRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
|
||||
list.add(PacketRegisterInfo(StandardSpeciesFeatureSyncPacket.ID, StandardSpeciesFeatureSyncPacket::decode, DataRegistrySyncPacketHandler()))
|
||||
list.add(PacketRegisterInfo(GlobalSpeciesFeatureSyncPacket.ID, GlobalSpeciesFeatureSyncPacket::decode, DataRegistrySyncPacketHandler()))
|
||||
@@ -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
|
||||
@@ -566,7 +575,6 @@ object CobblemonNetwork {
|
||||
list.add(PacketRegisterInfo(SwapPartyPokemonPacket.ID, SwapPartyPokemonPacket::decode, SwapPartyPokemonHandler))
|
||||
|
||||
list.add(PacketRegisterInfo(MovePCPokemonPacket.ID, MovePCPokemonPacket::decode, MovePCPokemonHandler))
|
||||
list.add(PacketRegisterInfo(MovePCPokemonBatchPacket.ID, MovePCPokemonBatchPacket::decode, MovePCPokemonBatchHandler))
|
||||
list.add(PacketRegisterInfo(MovePartyPokemonPacket.ID, MovePartyPokemonPacket::decode, MovePartyPokemonHandler))
|
||||
|
||||
list.add(PacketRegisterInfo(SwapPCPartyPokemonPacket.ID, SwapPCPartyPokemonPacket::decode, SwapPCPartyPokemonHandler))
|
||||
@@ -650,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
|
||||
|
||||
@@ -720,35 +720,5 @@ object CobblemonSounds : PlatformRegistry<Registry<SoundEvent>, ResourceKey<Regi
|
||||
ITEM_BLOCK_DICE_STEP
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val ITEM_BLOCK_METAL_CAN_PLACE = this.create("block.item_block.metal_can.place")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_METAL_CAN_BREAK = this.create("block.item_block.metal_can.break")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_METAL_CAN_STEP = this.create("block.item_block.metal_can.step")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_METAL_CAN_SOUNDS = SoundType(1f, 1.1f,
|
||||
ITEM_BLOCK_METAL_CAN_BREAK,
|
||||
ITEM_BLOCK_METAL_CAN_STEP,
|
||||
ITEM_BLOCK_METAL_CAN_PLACE,
|
||||
ITEM_BLOCK_METAL_CAN_STEP,
|
||||
ITEM_BLOCK_METAL_CAN_STEP
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val ITEM_BLOCK_JAR_GLASS_LIQUID_PLACE = this.create("block.item_block.jar_glass_liquid.place")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_JAR_GLASS_LIQUID_BREAK = this.create("block.item_block.jar_glass_liquid.break")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_JAR_GLASS_LIQUID_STEP = this.create("block.item_block.jar_glass_liquid.step")
|
||||
@JvmField
|
||||
val ITEM_BLOCK_JAR_GLASS_LIQUID_SOUNDS = SoundType(1f, 1.1f,
|
||||
ITEM_BLOCK_JAR_GLASS_LIQUID_BREAK,
|
||||
ITEM_BLOCK_JAR_GLASS_LIQUID_STEP,
|
||||
ITEM_BLOCK_JAR_GLASS_LIQUID_PLACE,
|
||||
ITEM_BLOCK_JAR_GLASS_LIQUID_STEP,
|
||||
ITEM_BLOCK_JAR_GLASS_LIQUID_STEP
|
||||
)
|
||||
|
||||
private fun create(name: String): SoundEvent = this.create(name, SoundEvent.createVariableRangeEvent(cobblemonResource(name)))
|
||||
}
|
||||
|
||||
+9
-3
@@ -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
|
||||
)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.battles.model
|
||||
|
||||
/**
|
||||
* How a [PokemonBattle] came to an end.
|
||||
*/
|
||||
enum class BattleEndResult {
|
||||
/** One side defeated the other. */
|
||||
VICTORY,
|
||||
/** A wild Pokémon was captured, ending the battle. */
|
||||
CAPTURE,
|
||||
/** An actor forfeited, giving the victory to the other side. */
|
||||
FORFEIT,
|
||||
/** A player fled from a wild Pokémon. */
|
||||
FLED,
|
||||
/** The battle was stopped without a winner, such as by a command, a player disconnecting or an error. */
|
||||
STOPPED
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
|
||||
import com.cobblemon.mod.common.api.battles.model.actor.EntityBackedBattleActor
|
||||
import com.cobblemon.mod.common.api.battles.model.actor.FleeableBattleActor
|
||||
import com.cobblemon.mod.common.api.events.CobblemonEvents
|
||||
import com.cobblemon.mod.common.api.events.battles.BattleEndedEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.BattleFledEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.BattleFleeAttemptEvent
|
||||
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
|
||||
@@ -125,12 +124,6 @@ open class PokemonBattle(
|
||||
var winners = listOf<BattleActor>()
|
||||
var losers = listOf<BattleActor>()
|
||||
var ended = false
|
||||
/** How the battle ended, or null if it hasn't ended yet. */
|
||||
var endResult: BattleEndResult? = null
|
||||
/** The actor that forfeited the battle, if any. */
|
||||
var forfeitedBy: BattleActor? = null
|
||||
/** Whether a player fled from the battle. */
|
||||
var fled = false
|
||||
// TEMP battle showcase stuff
|
||||
var announcingRules = false
|
||||
var turn: Int = 0
|
||||
@@ -258,14 +251,6 @@ open class PokemonBattle(
|
||||
|
||||
fun end() {
|
||||
ended = true
|
||||
val result = when {
|
||||
winners.isNotEmpty() && showdownMessages.any { "capture" in it } -> BattleEndResult.CAPTURE
|
||||
winners.isNotEmpty() && forfeitedBy != null -> BattleEndResult.FORFEIT
|
||||
winners.isNotEmpty() -> BattleEndResult.VICTORY
|
||||
fled -> BattleEndResult.FLED
|
||||
else -> BattleEndResult.STOPPED
|
||||
}
|
||||
endResult = result
|
||||
val awardToFainted = Cobblemon.config.awardExperienceToFaintedPokemon
|
||||
val awardOnLoss = Cobblemon.config.awardExperienceOnBattleLoss
|
||||
this.actors.forEach { actor ->
|
||||
@@ -347,7 +332,6 @@ open class PokemonBattle(
|
||||
}
|
||||
}
|
||||
}
|
||||
CobblemonEvents.BATTLE_ENDED.post(BattleEndedEvent(this, result, forfeitedBy.takeIf { result == BattleEndResult.FORFEIT }))
|
||||
sendUpdate(BattleEndPacket())
|
||||
BattleRegistry.closeBattle(this)
|
||||
}
|
||||
@@ -518,7 +502,6 @@ open class PokemonBattle(
|
||||
.forEach{it.pokemon.heal()}
|
||||
CobblemonEvents.BATTLE_FLED.post(BattleFledEvent(this, actors.asSequence().filterIsInstance<PlayerBattleActor>().iterator().next()))
|
||||
actors.filterIsInstance<EntityBackedBattleActor<*>>().mapNotNull { it.entity }.forEach { it.sendSystemMessage(battleLang("flee").yellow()) }
|
||||
fled = true
|
||||
stop()
|
||||
}
|
||||
}
|
||||
@@ -556,7 +539,6 @@ open class PokemonBattle(
|
||||
private fun checkForfeit(): Boolean {
|
||||
val forfeit = actors.find { it.responses.any { it is ForfeitActionResponse } }
|
||||
return forfeit?.let {
|
||||
forfeitedBy = it
|
||||
this.dispatchWaiting { this.broadcastChatMessage(battleLang("forfeit", it.getName()).red()) }
|
||||
writeShowdownAction(">forcelose ${it.showdownId}")
|
||||
true
|
||||
|
||||
+12
-1
@@ -11,6 +11,9 @@ package com.cobblemon.mod.common.api.conditional
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonPrimitive
|
||||
import com.google.gson.JsonSerializationContext
|
||||
import com.google.gson.JsonSerializer
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
@@ -21,11 +24,19 @@ import java.lang.reflect.Type
|
||||
* @author Hiroku
|
||||
* @since July 17th, 2022
|
||||
*/
|
||||
interface RegistryLikeAdapter<B> : JsonDeserializer<RegistryLikeCondition<B>> {
|
||||
interface RegistryLikeAdapter<B> : JsonDeserializer<RegistryLikeCondition<B>>, JsonSerializer<RegistryLikeCondition<B>> {
|
||||
val registryLikeConditions: MutableList<(JsonElement) -> RegistryLikeCondition<B>?>
|
||||
|
||||
override fun deserialize(json: JsonElement, type: Type, ctx: JsonDeserializationContext): RegistryLikeCondition<B> {
|
||||
return registryLikeConditions.firstNotNullOfOrNull { it(json) }
|
||||
?: throw IllegalArgumentException("Unable to deserialize $json")
|
||||
}
|
||||
|
||||
override fun serialize(src: RegistryLikeCondition<B>, typeOfSrc: Type, ctx: JsonSerializationContext): JsonElement {
|
||||
return when (src) {
|
||||
is RegistryLikeTagCondition<B> -> JsonPrimitive(RegistryLikeTagCondition.PREFIX + src.tag.location())
|
||||
is RegistryLikeIdentifierCondition<B> -> JsonPrimitive(src.identifier.toString())
|
||||
else -> throw IllegalArgumentException("Unable to serialize $src")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import com.cobblemon.mod.common.api.events.battles.BattleFaintedEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.BattleFledEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.BattleFleeAttemptEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.BattleStartedEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.BattleEndedEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.BattleVictoryEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.instruction.FormeChangeEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.instruction.MegaEvolutionEvent
|
||||
@@ -140,9 +139,6 @@ object CobblemonEvents {
|
||||
@JvmField
|
||||
val BATTLE_FAINTED = EventObservable<BattleFaintedEvent>()
|
||||
|
||||
@JvmField
|
||||
val BATTLE_ENDED = EventObservable<BattleEndedEvent>()
|
||||
|
||||
// instructions
|
||||
@JvmField
|
||||
val MEGA_EVOLUTION = EventObservable<MegaEvolutionEvent>()
|
||||
|
||||
-69
@@ -1,69 +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.events.battles
|
||||
|
||||
import com.bedrockk.molang.runtime.value.DoubleValue
|
||||
import com.bedrockk.molang.runtime.value.MoValue
|
||||
import com.bedrockk.molang.runtime.value.StringValue
|
||||
import com.cobblemon.mod.common.api.battles.model.BattleEndResult
|
||||
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
|
||||
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
|
||||
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
|
||||
import com.cobblemon.mod.common.util.asArrayValue
|
||||
|
||||
/**
|
||||
* Event fired whenever a [PokemonBattle] ends, regardless of how it ended (victory, fleeing, being stopped, etc).
|
||||
*
|
||||
* [PokemonBattle.winners] and [PokemonBattle.losers] are only populated if the [result] has a winner,
|
||||
* that is [BattleEndResult.VICTORY], [BattleEndResult.CAPTURE] or [BattleEndResult.FORFEIT].
|
||||
*
|
||||
* @param result How the battle ended.
|
||||
* @param forfeitedBy The actor that forfeited, if the [result] is [BattleEndResult.FORFEIT].
|
||||
*/
|
||||
data class BattleEndedEvent(
|
||||
override val battle: PokemonBattle,
|
||||
val result: BattleEndResult,
|
||||
val forfeitedBy: BattleActor?
|
||||
) : BattleEvent {
|
||||
/** Whether one side won the battle, which includes captures and forfeits. */
|
||||
val hasWinner = battle.winners.isNotEmpty()
|
||||
val wasVictory = result == BattleEndResult.VICTORY
|
||||
val wasCapture = result == BattleEndResult.CAPTURE
|
||||
val wasForfeit = result == BattleEndResult.FORFEIT
|
||||
val wasFled = result == BattleEndResult.FLED
|
||||
val wasStopped = result == BattleEndResult.STOPPED
|
||||
|
||||
/** Whether the battle was between players and wild Pokémon. */
|
||||
val isPvW = battle.isPvW
|
||||
/** Whether the battle was between players and NPCs. */
|
||||
val isPvN = battle.isPvN
|
||||
/** Whether the battle was between players. */
|
||||
val isPvP = battle.isPvP
|
||||
|
||||
val context = mutableMapOf<String, MoValue>(
|
||||
"battle" to battle.struct,
|
||||
"result" to StringValue(result.name.lowercase()),
|
||||
"has_winner" to DoubleValue(hasWinner),
|
||||
"was_victory" to DoubleValue(wasVictory),
|
||||
"was_capture" to DoubleValue(wasCapture),
|
||||
"was_forfeit" to DoubleValue(wasForfeit),
|
||||
"was_fled" to DoubleValue(wasFled),
|
||||
"was_stopped" to DoubleValue(wasStopped),
|
||||
"is_pvw" to DoubleValue(isPvW),
|
||||
"is_pvn" to DoubleValue(isPvN),
|
||||
"is_pvp" to DoubleValue(isPvP),
|
||||
"winners" to battle.winners.asArrayValue { it.struct },
|
||||
"losers" to battle.losers.asArrayValue { it.struct },
|
||||
"players" to battle.actors.filter { it.type == ActorType.PLAYER }.asArrayValue { it.struct },
|
||||
"npcs" to battle.actors.filter { it.type == ActorType.NPC }.asArrayValue { it.struct },
|
||||
"wild_pokemon" to battle.actors.filter { it.type == ActorType.WILD }.asArrayValue { it.struct }
|
||||
).apply {
|
||||
forfeitedBy?.let { put("forfeited_by", it.struct) }
|
||||
}
|
||||
}
|
||||
-2
@@ -16,7 +16,6 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
|
||||
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
|
||||
import com.cobblemon.mod.common.battles.actor.PokemonBattleActor
|
||||
import com.cobblemon.mod.common.entity.npc.NPCBattleActor
|
||||
import com.cobblemon.mod.common.util.asArrayValue
|
||||
|
||||
object BattleActorMoLangFunctions : AbstractMoLangFunctionHolder<BattleActor>() {
|
||||
override fun BattleActor.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
|
||||
@@ -26,7 +25,6 @@ object BattleActorMoLangFunctions : AbstractMoLangFunctionHolder<BattleActor>()
|
||||
functions["is_npc"] = { DoubleValue(actor.type == ActorType.NPC) }
|
||||
functions["is_player"] = { DoubleValue(actor.type == ActorType.PLAYER) }
|
||||
functions["is_wild"] = { DoubleValue(actor.type == ActorType.WILD) }
|
||||
functions["battle_pokemon"] = { actor.pokemonList.asArrayValue { it.struct } }
|
||||
|
||||
when (actor) {
|
||||
is NPCBattleActor -> {
|
||||
|
||||
+2
-2
@@ -98,14 +98,14 @@ object LivingEntityMoLangFunctions : AbstractMoLangFunctionHolder<LivingEntity>(
|
||||
val targetEntity = params.get<MoValue>(0)
|
||||
val maxDistance = params.getDoubleOrNull(1)?.toFloat() ?: 2.0F
|
||||
|
||||
val target =
|
||||
val entity =
|
||||
if (targetEntity is ObjectValue<*> && targetEntity.obj is Entity) {
|
||||
targetEntity.obj as Entity
|
||||
} else {
|
||||
return@isLookingAt DoubleValue.ZERO
|
||||
}
|
||||
|
||||
DoubleValue(entity.isLookingAt(target, maxDistance))
|
||||
DoubleValue(entity.isLookingAt(entity, maxDistance))
|
||||
}
|
||||
functions["is_living_entity"] = { DoubleValue.ONE }
|
||||
functions["is_flying"] = { DoubleValue(entity.isFallFlying) }
|
||||
|
||||
+9
-4
@@ -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"] = {
|
||||
|
||||
+1
-1
@@ -212,7 +212,7 @@ object PokemonMoLangFunctions : AbstractMoLangFunctionHolder<Pokemon>() {
|
||||
// Lol. Lmao, even.
|
||||
// TO-DO: Subscribe to [PokemonSpecies.observable].
|
||||
functions["pre_evolution"] = { pokemon.species.preEvolution ?: DoubleValue.ZERO }
|
||||
functions["nature"] = { StringValue(pokemon.nature.name.toString()) }
|
||||
functions["nature"] = { StringValue(pokemon.nature.toString()) }
|
||||
functions["is_wild"] = { DoubleValue(pokemon.entity?.let { it.ownerUUID == null } == true) }
|
||||
functions["is_shiny"] = { DoubleValue(pokemon.shiny) }
|
||||
functions["is_in_party"] = { DoubleValue(pokemon.storeCoordinates.get()?.store is PartyStore) }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,6 +10,9 @@ package com.cobblemon.mod.common.api.pokedex
|
||||
|
||||
import com.cobblemon.mod.common.api.data.JsonDataRegistry
|
||||
import com.cobblemon.mod.common.api.pokedex.def.PokedexDef
|
||||
import com.cobblemon.mod.common.api.pokedex.entry.PokedexEntry
|
||||
import com.cobblemon.mod.common.api.pokedex.entry.PokedexForm
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.reactive.SimpleObservable
|
||||
import com.cobblemon.mod.common.net.messages.client.data.PokedexDexSyncPacket
|
||||
import com.cobblemon.mod.common.util.adapters.CodecBackedAdapter
|
||||
@@ -61,6 +64,38 @@ object Dexes : JsonDataRegistry<PokedexDef> {
|
||||
PokedexDexSyncPacket(dexEntryMap.values).sendToPlayer(player)
|
||||
}
|
||||
|
||||
fun findDexEntryBySpeciesId(regionId: ResourceLocation, speciesId: ResourceLocation): PokedexEntry? {
|
||||
return dexEntryMap[regionId]?.getEntries()?.firstOrNull { it -> it.speciesId == speciesId }
|
||||
}
|
||||
|
||||
fun findDexEntryBySpeciesId(speciesId: ResourceLocation): PokedexEntry? {
|
||||
return findDexEntryBySpeciesId(cobblemonResource("national"), speciesId)
|
||||
}
|
||||
|
||||
fun findRootDexEntryBySpeciesId(speciesId: ResourceLocation, formName: String): Pair<PokedexEntry, PokedexForm> {
|
||||
var rootSpecies = PokemonSpecies.getByIdentifier(speciesId)!!
|
||||
val initialFormEquivalents = getFormEquivalents(formName)
|
||||
var rootForm = rootSpecies.forms.firstOrNull { initialFormEquivalents.contains(it.name.lowercase()) }
|
||||
?: rootSpecies.standardForm
|
||||
|
||||
var preEvolution = rootForm.preEvolution
|
||||
while (preEvolution != null) {
|
||||
rootSpecies = preEvolution.species
|
||||
rootForm = preEvolution.resolveForm(rootForm.aspects.toSet())
|
||||
preEvolution = rootForm.preEvolution
|
||||
}
|
||||
|
||||
val entry = findDexEntryBySpeciesId(rootSpecies.resourceIdentifier)
|
||||
?: throw RuntimeException("Root dex entry not found")
|
||||
|
||||
val formEquivalents = getFormEquivalents(rootForm.name)
|
||||
val form =
|
||||
entry.forms.firstOrNull { formEquivalents.contains(it.displayForm.lowercase()) } ?:
|
||||
entry.forms.first { it.displayForm.equals("Normal", ignoreCase = true) }
|
||||
|
||||
return entry to form
|
||||
}
|
||||
|
||||
fun getFormEquivalents(formName: String): List<String> {
|
||||
val key = equivalentFormsLookup[formName.lowercase()]
|
||||
return equivalentForms[key] ?: listOf(formName.lowercase())
|
||||
|
||||
@@ -8,20 +8,13 @@
|
||||
|
||||
package com.cobblemon.mod.common.api.pokedex.entry
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.CobblemonItems
|
||||
import com.cobblemon.mod.common.api.data.JsonDataRegistry
|
||||
import com.cobblemon.mod.common.api.molang.ExpressionLike
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.reactive.SimpleObservable
|
||||
import com.cobblemon.mod.common.net.messages.client.data.DexEntrySyncPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.data.DexEvolutionDisplaySyncPacket
|
||||
import com.cobblemon.mod.common.pokemon.FormData
|
||||
import com.cobblemon.mod.common.util.adapters.ExpressionLikeAdapter
|
||||
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
@@ -45,8 +38,6 @@ object DexEntries : JsonDataRegistry<PokedexEntry> {
|
||||
|
||||
val entries = mutableMapOf<ResourceLocation, PokedexEntry>()
|
||||
|
||||
val evolutionDisplays = mutableListOf<DexEvolutionDisplay>()
|
||||
|
||||
override fun reload(data: Map<ResourceLocation, PokedexEntry>) {
|
||||
data.forEach { _, entry ->
|
||||
entries[entry.id] = entry
|
||||
@@ -59,7 +50,6 @@ object DexEntries : JsonDataRegistry<PokedexEntry> {
|
||||
}
|
||||
}
|
||||
}
|
||||
computeEvolutionDisplays()
|
||||
observable.emit(this)
|
||||
}
|
||||
|
||||
@@ -67,58 +57,5 @@ object DexEntries : JsonDataRegistry<PokedexEntry> {
|
||||
|
||||
override fun sync(player: ServerPlayer) {
|
||||
DexEntrySyncPacket(entries.values).sendToPlayer(player)
|
||||
DexEvolutionDisplaySyncPacket(evolutionDisplays).sendToPlayer(player)
|
||||
}
|
||||
|
||||
fun computeEvolutionDisplays() {
|
||||
evolutionDisplays.clear()
|
||||
PokemonSpecies.species.forEach {
|
||||
try {
|
||||
evolutionDisplays.addAll(buildDisplays(it.standardForm) + it.forms.flatMap { form -> buildDisplays(form) })
|
||||
} catch (e: Exception) {
|
||||
Cobblemon.LOGGER.error("Failed to build dex evolution displays for ${it.resourceIdentifier}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun buildDisplays(form: FormData): List<DexEvolutionDisplay> {
|
||||
val result = mutableListOf<DexEvolutionDisplay>()
|
||||
(form.ownEvolutions ?: form.evolutions).forEach { evolution ->
|
||||
val requirements = mutableListOf<DexEvolutionRequirementDisplay>()
|
||||
getToolTipHeaderIcon(evolution)?.let { header ->
|
||||
requirements.add(header)
|
||||
}
|
||||
evolution.requirements.mapNotNull { resolveRequirementData(it) }.forEach { requirement ->
|
||||
requirements.add(requirement)
|
||||
}
|
||||
|
||||
listOfNotNull(evolution.result, evolution.shedder).forEach resultLoop@{ resultProperties ->
|
||||
val resultSpecies = resultProperties.species
|
||||
?.let { PokemonSpecies.getByIdentifier(it.asIdentifierDefaultingNamespace()) }
|
||||
?: return@resultLoop
|
||||
|
||||
// The result's own aspects win, otherwise carry the source form's aspects over
|
||||
// like a real evolution does (Alola-Bias Pichu -> Alola-Bias Pikachu)
|
||||
val resultForm = resultSpecies.getForm(resultProperties.aspects).takeIf { it != resultSpecies.standardForm }
|
||||
?: resultSpecies.getForm(form.aspects.toSet() + resultProperties.aspects)
|
||||
val toAspects = resultProperties.aspects + resultForm.aspects
|
||||
val resultRequirements = if (resultProperties === evolution.shedder) {
|
||||
requirements + CobblemonDexEvolutionRequirementDisplay(CobblemonItems.POKE_BALL, lang("ui.pokedex.evolution.shedder.tooltip"))
|
||||
} else {
|
||||
requirements
|
||||
}
|
||||
|
||||
result.add(DexEvolutionDisplay(
|
||||
fromSpecies = form.species.resourceIdentifier,
|
||||
fromForm = form.name,
|
||||
fromAspects = form.aspects.toSet(),
|
||||
toSpecies = resultSpecies.resourceIdentifier,
|
||||
toForm = resultForm.name,
|
||||
toAspects = toAspects,
|
||||
requirements = resultRequirements,
|
||||
))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
-466
@@ -1,466 +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.pokedex.entry
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonItems
|
||||
import com.cobblemon.mod.common.api.conditional.RegistryLikeCondition
|
||||
import com.cobblemon.mod.common.api.conditional.RegistryLikeIdentifierCondition
|
||||
import com.cobblemon.mod.common.api.conditional.RegistryLikeTagCondition
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.IntSpeciesFeatureProvider
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeatures
|
||||
import com.cobblemon.mod.common.api.pokemon.requirement.Requirement
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.api.text.add
|
||||
import com.cobblemon.mod.common.api.text.green
|
||||
import com.cobblemon.mod.common.api.types.ElementalTypes
|
||||
import com.cobblemon.mod.common.pokemon.evolution.variants.ItemInteractionEvolution
|
||||
import com.cobblemon.mod.common.pokemon.evolution.variants.TradeEvolution
|
||||
import com.cobblemon.mod.common.pokemon.requirements.*
|
||||
import com.cobblemon.mod.common.util.adapters.RegistryAccessResolver
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import com.cobblemon.mod.common.util.asTranslated
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.core.registries.Registries
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.network.chat.MutableComponent
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.util.ExtraCodecs
|
||||
import net.minecraft.world.item.Item
|
||||
import net.minecraft.world.item.Items
|
||||
import net.minecraft.world.level.biome.Biome
|
||||
import net.minecraft.world.level.levelgen.structure.Structure
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
data class DexEvolutionDisplay(
|
||||
val fromSpecies: ResourceLocation,
|
||||
val fromForm: String,
|
||||
val fromAspects: Set<String>,
|
||||
val toSpecies: ResourceLocation,
|
||||
val toForm: String,
|
||||
val toAspects: Set<String>,
|
||||
val requirements: List<DexEvolutionRequirementDisplay>,
|
||||
)
|
||||
|
||||
interface DexEvolutionRequirementDisplay {
|
||||
val icon: Item
|
||||
val tooltip: MutableComponent
|
||||
}
|
||||
|
||||
data class CobblemonDexEvolutionRequirementDisplay(
|
||||
override val icon: Item,
|
||||
override val tooltip: MutableComponent,
|
||||
) : DexEvolutionRequirementDisplay
|
||||
|
||||
data class CobblemonBiomeDexEvolutionRequirementDisplay(
|
||||
override val icon: Item,
|
||||
override val tooltip: MutableComponent,
|
||||
val condition: ExtraCodecs.TagOrElementLocation? = null,
|
||||
val antiCondition: ExtraCodecs.TagOrElementLocation? = null,
|
||||
) : DexEvolutionRequirementDisplay
|
||||
|
||||
fun getToolTipHeaderIcon(evolution: Evolution): DexEvolutionRequirementDisplay? {
|
||||
when (evolution) {
|
||||
is ItemInteractionEvolution -> return getToolTipHeaderIcon(evolution)
|
||||
is TradeEvolution -> return getToolTipHeaderIcon(evolution)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
fun getToolTipHeaderIcon(itemInteractionEvolution: ItemInteractionEvolution): CobblemonDexEvolutionRequirementDisplay? {
|
||||
val item = itemInteractionEvolution.requiredContext.items.orElse(null)?.firstOrNull()?.value() ?: return null
|
||||
return CobblemonDexEvolutionRequirementDisplay(item, lang("ui.pokedex.evolution.item_interact.tooltip", item.description.plainCopy().green()))
|
||||
}
|
||||
|
||||
fun getToolTipHeaderIcon(tradeEvolution: TradeEvolution): CobblemonDexEvolutionRequirementDisplay {
|
||||
val species = tradeEvolution.requiredContext.species
|
||||
val tooltip = if (species == null) {
|
||||
lang("ui.pokedex.evolution.trade.tooltip.with_any_species")
|
||||
} else {
|
||||
PokemonSpecies.getByIdentifier(species.asIdentifierDefaultingNamespace())
|
||||
?.let { lang("ui.pokedex.evolution.trade.tooltip.with_species", it.translatedName.green()) }
|
||||
?: lang("ui.pokedex.evolution.trade.tooltip.with_any_species")
|
||||
}
|
||||
return CobblemonDexEvolutionRequirementDisplay(CobblemonItems.LINK_CABLE, tooltip)
|
||||
}
|
||||
|
||||
fun resolveRequirementData(requirement: Requirement): DexEvolutionRequirementDisplay? {
|
||||
val icon = requirement.pokedexIcon?.let {
|
||||
RegistryAccessResolver.getAccess().registryOrThrow(Registries.ITEM).get(it)
|
||||
}
|
||||
|
||||
val tooltip = requirement.pokedexTooltip?.let {
|
||||
Component.translatable(it)
|
||||
}
|
||||
|
||||
if (icon != null && tooltip != null) {
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
return buildRequirementData(requirement)
|
||||
}
|
||||
|
||||
fun buildRequirementData(requirement: Requirement): DexEvolutionRequirementDisplay? {
|
||||
when (requirement) {
|
||||
is AdvancementRequirement -> return buildAdvancementRequirement(requirement)
|
||||
is BattleCriticalHitsRequirement -> return buildBattleCriticalHitsRequirement(requirement)
|
||||
is BiomeRequirement -> return buildBiomeRequirement(requirement)
|
||||
is BlocksTraveledRequirement -> return buildBlocksTraveledRequirement(requirement)
|
||||
is DamageTakenRequirement -> return buildDamageTakenRequirement(requirement)
|
||||
is DefeatRequirement -> return buildDefeatRequirement(requirement)
|
||||
is FriendshipRequirement -> return buildFriendshipRequirement(requirement)
|
||||
is HeldItemRequirement -> return buildHeldItemRequirement(requirement)
|
||||
is LevelRequirement -> return buildLevelRequirement(requirement)
|
||||
is MoonPhaseRequirement -> return buildMoonPhaseRequirement(requirement)
|
||||
is MoveSetRequirement -> return buildMoveSetRequirement(requirement)
|
||||
is MoveTypeRequirement -> return buildMoveTypeRequirement(requirement)
|
||||
is NegateRequirement -> return buildNegateRequirement(requirement)
|
||||
is OwnerHoldsItemRequirement -> return buildOwnerHoldsItemRequirement(requirement)
|
||||
is PartyMemberRequirement -> return buildPartyMemberRequirement(requirement)
|
||||
is PokemonPropertiesRequirement -> return buildPokemonPropertiesRequirement(requirement)
|
||||
is PropertyRangeRequirement -> return buildPropertyRangeRequirement(requirement)
|
||||
is RecoilRequirement -> return buildRecoilRequirement(requirement)
|
||||
is StatCompareRequirement -> return buildStatCompareRequirement(requirement)
|
||||
is StatEqualRequirement -> return buildStatEqualRequirement(requirement)
|
||||
is StructureRequirement -> return buildStructureRequirement(requirement)
|
||||
is TimeRangeRequirement -> return buildTimeRangeRequirement(requirement)
|
||||
is UseMoveRequirement -> return buildUseMoveRequirement(requirement)
|
||||
is WeatherRequirement -> return buildWeatherRequirement(requirement)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun buildAdvancementRequirement(advancementRequirement: AdvancementRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon = Items.BOOK
|
||||
val identifier = advancementRequirement.requiredAdvancement
|
||||
val advancementLanguageKey = "${identifier.namespace}.${identifier.path.substringAfterLast('/')}"
|
||||
val advancementComponent = Component.translatable("advancements.$advancementLanguageKey").green()
|
||||
val tooltip = lang("ui.pokedex.requirement.advancement.tooltip", advancementComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildBattleCriticalHitsRequirement(battleCriticalHitsRequirement: BattleCriticalHitsRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon = CobblemonItems.MEDICINAL_LEEK
|
||||
val criticalsComponent = Component.literal(battleCriticalHitsRequirement.amount.toString()).green()
|
||||
val tooltip = lang("ui.pokedex.requirement.battle_critical_hits.tooltip", criticalsComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
private fun tagDisplayName(registry: String, id: ResourceLocation): MutableComponent {
|
||||
return Component.translatableWithFallback("tag.$registry.${id.namespace}.${id.path.replace('/', '.')}", prettifyPath(id.path))
|
||||
}
|
||||
|
||||
private fun prettifyPath(path: String): String {
|
||||
return path.substringAfterLast('/').removePrefix("is_").split('_').joinToString(" ") { part -> part.replaceFirstChar(Char::titlecase) }
|
||||
}
|
||||
|
||||
fun buildNegateRequirement(negateRequirement: NegateRequirement): DexEvolutionRequirementDisplay? {
|
||||
val inner = negateRequirement.inner
|
||||
when (inner) {
|
||||
is BiomeRequirement -> buildBiomeRequirement(inner, negated = true)?.let { return it }
|
||||
is StructureRequirement -> buildStructureRequirement(inner, negated = true)?.let { return it }
|
||||
}
|
||||
val innerDisplay = resolveRequirementData(inner) ?: return null
|
||||
return CobblemonDexEvolutionRequirementDisplay(innerDisplay.icon, lang("ui.pokedex.requirement.negate.tooltip", innerDisplay.tooltip))
|
||||
}
|
||||
|
||||
fun buildBiomeRequirement(biomeRequirement: BiomeRequirement, negated: Boolean = false): DexEvolutionRequirementDisplay? {
|
||||
// Negating "in X" is "not in X"; negating both a condition and an anticondition isn't expressible as one line
|
||||
if (negated && biomeRequirement.biomeCondition != null && biomeRequirement.biomeAnticondition != null) return null
|
||||
val condition = if (negated) biomeRequirement.biomeAnticondition else biomeRequirement.biomeCondition
|
||||
val antiCondition = if (negated) biomeRequirement.biomeCondition else biomeRequirement.biomeAnticondition
|
||||
if (condition == null && antiCondition == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val icon = Items.COMPASS
|
||||
val displayName: (RegistryLikeCondition<Biome>) -> MutableComponent = {
|
||||
when (it) {
|
||||
is RegistryLikeTagCondition -> tagDisplayName("worldgen.biome", it.tag.location())
|
||||
is RegistryLikeIdentifierCondition -> Component.translatable("biome.${it.identifier.namespace}.${it.identifier.path}")
|
||||
else -> Component.literal(it.toString())
|
||||
}
|
||||
}
|
||||
|
||||
val tooltip = if (condition != null && antiCondition != null) {
|
||||
lang("ui.pokedex.requirement.biome.both.tooltip", displayName(condition).green(), displayName(antiCondition).green())
|
||||
} else if (condition != null) {
|
||||
lang("ui.pokedex.requirement.biome.tooltip", displayName(condition).green())
|
||||
} else {
|
||||
lang("ui.pokedex.requirement.biome.anticondition.tooltip", displayName(antiCondition!!).green())
|
||||
}
|
||||
|
||||
var conditionConverted: ExtraCodecs.TagOrElementLocation? = null
|
||||
if (condition != null) {
|
||||
val conditionIdentifier = if (condition is RegistryLikeIdentifierCondition) condition.identifier else (condition as RegistryLikeTagCondition).tag.location
|
||||
conditionConverted = ExtraCodecs.TagOrElementLocation(conditionIdentifier, condition is RegistryLikeTagCondition)
|
||||
}
|
||||
|
||||
var antiConditionConverted: ExtraCodecs.TagOrElementLocation? = null
|
||||
if (antiCondition != null) {
|
||||
val conditionIdentifier = if (antiCondition is RegistryLikeIdentifierCondition) antiCondition.identifier else (antiCondition as RegistryLikeTagCondition).tag.location
|
||||
antiConditionConverted = ExtraCodecs.TagOrElementLocation(conditionIdentifier, antiCondition is RegistryLikeTagCondition)
|
||||
}
|
||||
|
||||
return CobblemonBiomeDexEvolutionRequirementDisplay(icon, tooltip, conditionConverted, antiConditionConverted)
|
||||
}
|
||||
|
||||
fun buildBlocksTraveledRequirement(blocksTraveledRequirement: BlocksTraveledRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon = Items.LEATHER_BOOTS
|
||||
val blocksComponent = Component.literal(blocksTraveledRequirement.amount.toString()).green()
|
||||
val tooltip = lang("ui.pokedex.requirement.blocks_traveled.tooltip", blocksComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildDamageTakenRequirement(damageTakenRequirement: DamageTakenRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon = CobblemonItems.PUNCHING_GLOVE
|
||||
val blocksComponent = Component.literal(damageTakenRequirement.amount.toString()).green()
|
||||
val tooltip = lang("ui.pokedex.requirement.damage_taken.tooltip", blocksComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildDefeatRequirement(defeatRequirement: DefeatRequirement): DexEvolutionRequirementDisplay? {
|
||||
val description = describePokemonProperties(defeatRequirement.target) ?: return null
|
||||
val icon = CobblemonItems.PUNCHING_GLOVE
|
||||
val defeatedAmount = defeatRequirement.amount
|
||||
|
||||
val descriptionComponent = Component.literal(defeatedAmount.toString()).green().add(" ").add(description)
|
||||
val tooltip = lang("ui.pokedex.requirement.defeat.tooltip", descriptionComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildFriendshipRequirement(friendshipRequirement: FriendshipRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon = CobblemonItems.SOOTHE_BELL
|
||||
val friendshipAmountComponent = Component.literal(friendshipRequirement.amount.toString()).green()
|
||||
val tooltip = lang("ui.pokedex.requirement.friendship.tooltip", friendshipAmountComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildHeldItemRequirement(heldItemRequirement: HeldItemRequirement): DexEvolutionRequirementDisplay? {
|
||||
val items = heldItemRequirement.itemCondition.items.getOrNull() ?: return null
|
||||
val item = items.firstOrNull()?.value() ?: return null
|
||||
val itemComponent = items.unwrapKey().map { tag ->
|
||||
val id = tag.location()
|
||||
val fallback = id.path.substringAfterLast('/').split('_').joinToString(" ") { part -> part.replaceFirstChar(Char::titlecase) }
|
||||
Component.translatableWithFallback("tag.item.${id.namespace}.${id.path.replace('/', '.')}", fallback)
|
||||
}.orElseGet { item.description.plainCopy() }
|
||||
val tooltip = lang("ui.pokedex.requirement.held_item.tooltip", itemComponent.green())
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(item, tooltip)
|
||||
}
|
||||
|
||||
fun buildLevelRequirement(levelRequirement: LevelRequirement): DexEvolutionRequirementDisplay {
|
||||
val minLevel = levelRequirement.minLevel
|
||||
val maxLevel = levelRequirement.maxLevel
|
||||
|
||||
val icon = CobblemonItems.RARE_CANDY
|
||||
val tooltip =
|
||||
if (minLevel == maxLevel || maxLevel == Int.MAX_VALUE)
|
||||
lang("ui.pokedex.requirement.level.tooltip.min", Component.literal(minLevel.toString()).green())
|
||||
else
|
||||
lang("ui.pokedex.requirement.level.tooltip.between", Component.literal(minLevel.toString()).green(), Component.literal(maxLevel.toString()).green())
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildMoonPhaseRequirement(moonPhaseRequirement: MoonPhaseRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon = Items.CLOCK
|
||||
val moonPhaseComponent = moonPhaseRequirement.moonPhase.displayName().green()
|
||||
val tooltip = lang("ui.pokedex.requirement.moon_phase.tooltip", moonPhaseComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildMoveSetRequirement(moveSetRequirement: MoveSetRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon = CobblemonItems.TECHNICAL_MACHINE
|
||||
val tooltip = lang("ui.pokedex.requirement.has_move.tooltip", moveSetRequirement.move.displayName.green())
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildMoveTypeRequirement(moveTypeRequirement: MoveTypeRequirement): DexEvolutionRequirementDisplay {
|
||||
val type = ElementalTypes.get(moveTypeRequirement.type.name)
|
||||
val icon = when (type) {
|
||||
ElementalTypes.NORMAL -> CobblemonItems.NORMAL_GEM
|
||||
ElementalTypes.FIRE -> CobblemonItems.FIRE_GEM
|
||||
ElementalTypes.WATER -> CobblemonItems.WATER_GEM
|
||||
ElementalTypes.GRASS -> CobblemonItems.GRASS_GEM
|
||||
ElementalTypes.ELECTRIC -> CobblemonItems.ELECTRIC_GEM
|
||||
ElementalTypes.ICE -> CobblemonItems.ICE_GEM
|
||||
ElementalTypes.FIGHTING -> CobblemonItems.FIGHTING_GEM
|
||||
ElementalTypes.POISON -> CobblemonItems.POISON_GEM
|
||||
ElementalTypes.GROUND -> CobblemonItems.GROUND_GEM
|
||||
ElementalTypes.FLYING -> CobblemonItems.FLYING_GEM
|
||||
ElementalTypes.PSYCHIC -> CobblemonItems.PSYCHIC_GEM
|
||||
ElementalTypes.BUG -> CobblemonItems.BUG_GEM
|
||||
ElementalTypes.ROCK -> CobblemonItems.ROCK_GEM
|
||||
ElementalTypes.GHOST -> CobblemonItems.GHOST_GEM
|
||||
ElementalTypes.DRAGON -> CobblemonItems.DRAGON_GEM
|
||||
ElementalTypes.DARK -> CobblemonItems.DARK_GEM
|
||||
ElementalTypes.STEEL -> CobblemonItems.STEEL_GEM
|
||||
ElementalTypes.FAIRY -> CobblemonItems.FAIRY_GEM
|
||||
else -> CobblemonItems.NORMAL_GEM
|
||||
}
|
||||
|
||||
val tooltip = lang("ui.pokedex.requirement.has_move_type.tooltip", moveTypeRequirement.type.displayName.green())
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildOwnerHoldsItemRequirement(ownerHoldsItemRequirement: OwnerHoldsItemRequirement): DexEvolutionRequirementDisplay? {
|
||||
val item = ownerHoldsItemRequirement.itemCondition.items.orElse(null)?.firstOrNull()?.value() ?: return null
|
||||
val tooltip = lang("ui.pokedex.requirement.owner_held_item.tooltip", item.description.plainCopy().green())
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(item, tooltip)
|
||||
}
|
||||
|
||||
fun buildPartyMemberRequirement(partyMemberRequirement: PartyMemberRequirement): DexEvolutionRequirementDisplay? {
|
||||
val description = describePokemonProperties(partyMemberRequirement.target) ?: return null
|
||||
val icon = CobblemonItems.POKE_BALL
|
||||
val tooltip = if (partyMemberRequirement.contains) {
|
||||
lang("ui.pokedex.requirement.party_member.tooltip", description)
|
||||
} else {
|
||||
lang("ui.pokedex.requirement.party_member.anticondition.tooltip", description)
|
||||
}
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildPokemonPropertiesRequirement(pokemonPropertiesRequirement: PokemonPropertiesRequirement): DexEvolutionRequirementDisplay? {
|
||||
val description = describePokemonProperties(pokemonPropertiesRequirement.target) ?: return null
|
||||
val icon = Items.NAME_TAG
|
||||
val tooltip = lang("ui.pokedex.requirement.pokemon_properties.tooltip", description)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildPropertyRangeRequirement(propertyRangeRequirement: PropertyRangeRequirement): DexEvolutionRequirementDisplay? {
|
||||
val provider = SpeciesFeatures.getFeature(propertyRangeRequirement.feature) as? IntSpeciesFeatureProvider ?: return null
|
||||
val displayName = provider.display?.name?.takeIf { it.isNotBlank() }?.asTranslated()?.green() ?: return null
|
||||
val icon = provider.itemPoints.keys.firstOrNull()?.let { BuiltInRegistries.ITEM.get(it) } ?: Items.NAME_TAG
|
||||
|
||||
val range = propertyRangeRequirement.range
|
||||
val tooltip = lang(
|
||||
"ui.pokedex.requirement.property_range.tooltip",
|
||||
displayName,
|
||||
Component.literal(range.first.toString()).green(),
|
||||
Component.literal(range.last.toString()).green()
|
||||
)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildRecoilRequirement(recoilRequirement: RecoilRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon = CobblemonItems.LIFE_ORB
|
||||
val recoilComponent = Component.literal(recoilRequirement.amount.toString()).green()
|
||||
val tooltip = lang("ui.pokedex.requirement.recoil.tooltip", recoilComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildStatCompareRequirement(statCompareRequirement: StatCompareRequirement): DexEvolutionRequirementDisplay {
|
||||
val highStat = Stats.getStat(statCompareRequirement.highStat)
|
||||
val lowStat = Stats.getStat(statCompareRequirement.lowStat)
|
||||
|
||||
val icon = when (highStat) {
|
||||
Stats.HP, Stats.EVASION, Stats.ACCURACY -> CobblemonItems.HEALTH_MOCHI
|
||||
Stats.ATTACK -> CobblemonItems.MUSCLE_MOCHI
|
||||
Stats.DEFENCE -> CobblemonItems.RESIST_MOCHI
|
||||
Stats.SPECIAL_ATTACK -> CobblemonItems.GENIUS_MOCHI
|
||||
Stats.SPECIAL_DEFENCE -> CobblemonItems.CLEVER_MOCHI
|
||||
Stats.SPEED -> CobblemonItems.SWIFT_MOCHI
|
||||
}
|
||||
|
||||
val highStatComponent = highStat.displayName.plainCopy().green()
|
||||
val lowStatComponent = lowStat.displayName.plainCopy().green()
|
||||
val tooltip = lang("ui.pokedex.requirement.stat_compare.tooltip", highStatComponent, lowStatComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildStatEqualRequirement(statEqualRequirement: StatEqualRequirement): DexEvolutionRequirementDisplay {
|
||||
val statOne = Stats.getStat(statEqualRequirement.statOne)
|
||||
val statTwo = Stats.getStat(statEqualRequirement.statTwo)
|
||||
|
||||
val statOneComponent = statOne.displayName.plainCopy().green()
|
||||
val statTwoComponent = statTwo.displayName.plainCopy().green()
|
||||
|
||||
val icon = CobblemonItems.FRESH_START_MOCHI
|
||||
val tooltip = lang("ui.pokedex.requirement.stat_equal.tooltip", statOneComponent, statTwoComponent)
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildStructureRequirement(structureRequirement: StructureRequirement, negated: Boolean = false): DexEvolutionRequirementDisplay? {
|
||||
if (negated && structureRequirement.structureCondition != null && structureRequirement.structureAnticondition != null) return null
|
||||
val condition = if (negated) structureRequirement.structureAnticondition else structureRequirement.structureCondition
|
||||
val anticondition = if (negated) structureRequirement.structureCondition else structureRequirement.structureAnticondition
|
||||
if (condition == null && anticondition == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val icon = Items.FILLED_MAP
|
||||
val displayName: (RegistryLikeCondition<Structure>) -> MutableComponent = {
|
||||
when (it) {
|
||||
is RegistryLikeTagCondition -> tagDisplayName("worldgen.structure", it.tag.location())
|
||||
is RegistryLikeIdentifierCondition -> Component.literal(prettifyPath(it.identifier.path))
|
||||
else -> Component.literal(it.toString())
|
||||
}
|
||||
}
|
||||
val tooltip = if (condition != null && anticondition != null) {
|
||||
lang("ui.pokedex.requirement.structure.both.tooltip", displayName(condition).green(), displayName(anticondition).green())
|
||||
} else if (condition != null) {
|
||||
lang("ui.pokedex.requirement.structure.tooltip", displayName(condition).green())
|
||||
} else {
|
||||
lang("ui.pokedex.requirement.structure.anticondition.tooltip", displayName(anticondition!!).green())
|
||||
}
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildTimeRangeRequirement(timeRangeRequirement: TimeRangeRequirement): DexEvolutionRequirementDisplay? {
|
||||
val icon = Items.CLOCK
|
||||
val timeRangeComponent = timeRangeRequirement.range.displayName() ?: return null
|
||||
val tooltip = lang("ui.pokedex.requirement.time_range.tooltip", timeRangeComponent.green())
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildUseMoveRequirement(useMoveRequirement: UseMoveRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon = CobblemonItems.CHOICE_BAND
|
||||
val tooltip = lang("ui.pokedex.requirement.use_move.tooltip", useMoveRequirement.move.displayName.green())
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
|
||||
fun buildWeatherRequirement(weatherRequirement: WeatherRequirement): DexEvolutionRequirementDisplay {
|
||||
val icon: Item
|
||||
val tooltip: MutableComponent
|
||||
|
||||
if (weatherRequirement.isThundering == true) {
|
||||
icon = Items.LIGHTNING_ROD
|
||||
tooltip = lang("ui.pokedex.requirement.weather.tooltip.thundering")
|
||||
} else {
|
||||
icon = Items.WATER_BUCKET
|
||||
tooltip = lang("ui.pokedex.requirement.weather.tooltip.raining")
|
||||
}
|
||||
|
||||
return CobblemonDexEvolutionRequirementDisplay(icon, tooltip)
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import com.cobblemon.mod.common.api.moves.MoveTemplate
|
||||
import com.cobblemon.mod.common.api.moves.MovesetBuilder
|
||||
import com.cobblemon.mod.common.api.moves.adapters.MoveTemplateAdapter
|
||||
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies.getByIdentifier
|
||||
import com.cobblemon.mod.common.api.pokemon.effect.ShoulderEffect
|
||||
import com.cobblemon.mod.common.api.pokemon.effect.adapter.ShoulderEffectAdapter
|
||||
import com.cobblemon.mod.common.api.pokemon.egg.EggGroup
|
||||
@@ -43,6 +44,9 @@ import com.cobblemon.mod.common.api.riding.sound.RideSoundSettingsList
|
||||
import com.cobblemon.mod.common.api.spawning.TimeRange
|
||||
import com.cobblemon.mod.common.api.types.ElementalType
|
||||
import com.cobblemon.mod.common.api.types.adapters.ElementalTypeAdapter
|
||||
import com.cobblemon.mod.common.net.messages.client.data.FormEvolutionOverride
|
||||
import com.cobblemon.mod.common.net.messages.client.data.SpeciesEvolutionEntry
|
||||
import com.cobblemon.mod.common.net.messages.client.data.SpeciesEvolutionSyncPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.data.SpeciesRegistrySyncPacket
|
||||
import com.cobblemon.mod.common.pokemon.FormData
|
||||
import com.cobblemon.mod.common.pokemon.Species
|
||||
@@ -242,6 +246,18 @@ object PokemonSpecies : JsonDataRegistry<Species> {
|
||||
|
||||
override fun sync(player: ServerPlayer) {
|
||||
SpeciesRegistrySyncPacket(species.toList()).sendToPlayer(player)
|
||||
// Sent after the species themselves so every species is already registered by the time this is decoded.
|
||||
val evolutionEntries = species.map { singleSpecies ->
|
||||
val formOverrides = singleSpecies.forms.mapNotNull { form ->
|
||||
if (form.ownEvolutions == null && form.ownPreEvolution == null) {
|
||||
null
|
||||
} else {
|
||||
FormEvolutionOverride(form.name, form.ownEvolutions, form.ownPreEvolution)
|
||||
}
|
||||
}
|
||||
SpeciesEvolutionEntry(singleSpecies.resourceIdentifier, singleSpecies.evolutions, singleSpecies.preEvolution, formOverrides)
|
||||
}
|
||||
SpeciesEvolutionSyncPacket(evolutionEntries).sendToPlayer(player)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-16
@@ -15,8 +15,6 @@ import com.cobblemon.mod.common.block.entity.PokeSnackBlockEntity
|
||||
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
|
||||
import com.cobblemon.mod.common.client.render.MatrixWrapper
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.ParticleStatus
|
||||
import net.minecraft.core.particles.BlockParticleOption
|
||||
import net.minecraft.core.particles.ParticleOptions
|
||||
import net.minecraft.core.particles.ParticleTypes
|
||||
@@ -42,25 +40,13 @@ sealed class ParticleData {
|
||||
val chance: Double,
|
||||
val amount: Int,
|
||||
val locatorResolver: (Map<String, MatrixWrapper>) -> List<String>
|
||||
): ParticleData() {
|
||||
fun amount(): Int = when(Minecraft.getInstance().options.particles().get()) {
|
||||
ParticleStatus.ALL -> amount
|
||||
ParticleStatus.DECREASED -> amount / 2
|
||||
ParticleStatus.MINIMAL -> amount / 4
|
||||
}
|
||||
}
|
||||
): ParticleData()
|
||||
|
||||
data class MinecraftParticle(
|
||||
val particle: ParticleOptions,
|
||||
val chance: Double,
|
||||
val amount: Int
|
||||
): ParticleData() {
|
||||
fun amount(): Int = when(Minecraft.getInstance().options.particles().get()) {
|
||||
ParticleStatus.ALL -> amount
|
||||
ParticleStatus.DECREASED -> amount / 2
|
||||
ParticleStatus.MINIMAL -> amount / 4
|
||||
}
|
||||
}
|
||||
): ParticleData()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,8 @@ interface PreEvolution {
|
||||
|
||||
val form: FormData
|
||||
|
||||
fun resolveForm(extraAspects: Set<String> = emptySet()): FormData = form
|
||||
|
||||
companion object {
|
||||
|
||||
fun of(species: Species, form: FormData = species.standardForm): PreEvolution = StandardPreEvolution(species, form)
|
||||
|
||||
+47
-31
@@ -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))
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -22,7 +22,6 @@ import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
|
||||
import com.cobblemon.mod.common.api.types.ElementalTypes
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.entity.pokemon.ai.sensors.AlphaLevelMatchingSensor
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.entity.Entity
|
||||
|
||||
@@ -77,7 +76,7 @@ open class SpawnBaitInfluence(val effects: List<SpawnBait.Effect>, val onUsed: (
|
||||
?: return super.affectWeight(detail, spawnablePosition, weight)
|
||||
|
||||
val detailSpecies = pokemonDetail.pokemon.species
|
||||
?.let { PokemonSpecies.getByIdentifier(it.asIdentifierDefaultingNamespace()) }
|
||||
?.let { PokemonSpecies.getByName(it) }
|
||||
?: return super.affectWeight(detail, spawnablePosition, weight)
|
||||
|
||||
val formData = detailSpecies.getForm(pokemonDetail.pokemon.aspects)
|
||||
|
||||
@@ -27,7 +27,6 @@ import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.util.isBoxLoaded
|
||||
import com.cobblemon.mod.common.util.removeIf
|
||||
import com.cobblemon.mod.common.util.squeezeWithinBounds
|
||||
import com.cobblemon.mod.common.util.toBlockPos
|
||||
import com.cobblemon.mod.common.util.toVec3f
|
||||
import com.cobblemon.mod.common.util.weightedSelection
|
||||
import kotlin.math.max
|
||||
@@ -191,60 +190,73 @@ interface Spawner {
|
||||
}
|
||||
|
||||
fun isValidStartPoint(world: Level, chunk: ChunkAccess, startPos: BlockPos.MutableBlockPos): Boolean {
|
||||
val abovePos = startPos.above()
|
||||
if (!world.isLoaded(startPos) || !world.isLoaded(abovePos)) {
|
||||
val y = startPos.y
|
||||
if (!world.isLoaded(startPos) || !world.isLoaded(startPos.setY(y + 1))) {
|
||||
return false
|
||||
}
|
||||
val mid = chunk.getBlockState(startPos)
|
||||
val above = chunk.getBlockState(abovePos)
|
||||
|
||||
return above.isPathfindable(PathComputationType.AIR) && mid.isAir
|
||||
val mid = chunk.getBlockState(startPos.setY(y))
|
||||
val above = chunk.getBlockState(startPos.setY(y + 1))
|
||||
|
||||
// Above must be non-solid
|
||||
if (!above.isPathfindable(PathComputationType.AIR)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Position must be non-air
|
||||
if (mid.isAir) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun constrainArea(area: SpawningZoneInput): SpawningZoneInput? {
|
||||
val world = area.world
|
||||
val centerPos = area.getCenter().toBlockPos()
|
||||
val basePos = BlockPos.MutableBlockPos(area.baseX, area.baseY, area.baseZ)
|
||||
val originalY = area.baseY
|
||||
|
||||
val (chunkX, chunkZ) = Pair(SectionPos.blockToSectionCoord(area.baseX), SectionPos.blockToSectionCoord(area.baseZ))
|
||||
|
||||
val (centerChunkX, centerChunkZ) = Pair(SectionPos.blockToSectionCoord(centerPos.x), SectionPos.blockToSectionCoord(centerPos.z))
|
||||
// if the chunk isn't loaded, we don't want to go further & we don't want the getChunk function below to load/create the chunk.
|
||||
if (!world.areEntitiesLoaded(ChunkPos.asLong(centerChunkX, centerChunkZ))) return null
|
||||
if (!area.world.areEntitiesLoaded(ChunkPos.asLong(chunkX, chunkZ))) return null
|
||||
|
||||
val chunk = world.getChunk(centerChunkX, centerChunkZ, ChunkStatus.FULL) ?: return null
|
||||
val chunk = area.world.getChunk(chunkX, chunkZ, ChunkStatus.FULL) ?: return null
|
||||
|
||||
val probePos = centerPos.mutable()
|
||||
var validY = if (isValidStartPoint(world, chunk, probePos)) centerPos.y else null
|
||||
var valid = isValidStartPoint(area.world, chunk, basePos)
|
||||
|
||||
if (validY == null) {
|
||||
for (offset in 1..Cobblemon.config.maxVerticalCorrectionBlocks) {
|
||||
if (isValidStartPoint(world, chunk, probePos.set(centerPos.x, centerPos.y + offset, centerPos.z))) {
|
||||
validY = centerPos.y + offset
|
||||
if (!valid) {
|
||||
var offset = 1
|
||||
do {
|
||||
if (isValidStartPoint(area.world, chunk, basePos.setY(originalY + offset))) {
|
||||
valid = true
|
||||
basePos.y = originalY + offset
|
||||
break
|
||||
} else if (isValidStartPoint(world, chunk, probePos.set(centerPos.x, centerPos.y - offset, centerPos.z))) {
|
||||
validY = centerPos.y - offset
|
||||
} else if (isValidStartPoint(area.world, chunk, basePos.setY(originalY - offset))) {
|
||||
valid = true
|
||||
basePos.y = originalY - offset
|
||||
break
|
||||
}
|
||||
}
|
||||
offset++
|
||||
} while (offset <= Cobblemon.config.maxVerticalCorrectionBlocks)
|
||||
}
|
||||
val targetY = validY ?: return null
|
||||
val newBaseY = (targetY - (area.height / 2)).coerceIn(world.minBuildHeight, world.maxBuildHeight)
|
||||
val minPos = world.squeezeWithinBounds(BlockPos(area.baseX, newBaseY, area.baseZ))
|
||||
val maxPos = world.squeezeWithinBounds(BlockPos(area.baseX + area.length, newBaseY + area.height, area.baseZ + area.width))
|
||||
|
||||
val finalBox = AABB(
|
||||
minPos.x.toDouble(), minPos.y.toDouble(), minPos.z.toDouble(),
|
||||
maxPos.x.toDouble(), maxPos.y.toDouble(), maxPos.z.toDouble()
|
||||
)
|
||||
if (minPos.x < maxPos.x && minPos.y < maxPos.y && minPos.z < maxPos.z && world.isBoxLoaded(finalBox)) {
|
||||
return SpawningZoneInput(
|
||||
cause = area.cause,
|
||||
world = world,
|
||||
baseX = minPos.x,
|
||||
baseY = minPos.y,
|
||||
baseZ = minPos.z,
|
||||
length = maxPos.x - minPos.x,
|
||||
height = maxPos.y - minPos.y,
|
||||
width = maxPos.z - minPos.z
|
||||
)
|
||||
if (valid) {
|
||||
val min = area.world.squeezeWithinBounds(basePos)
|
||||
val max = area.world.squeezeWithinBounds(basePos.move(area.length, area.height, area.width))
|
||||
if (area.world.isLoaded(min) && area.world.isLoaded(max) &&
|
||||
min.x < max.x && min.y < max.y && min.z < max.z
|
||||
) {
|
||||
return SpawningZoneInput(
|
||||
cause = area.cause,
|
||||
world = area.world,
|
||||
baseX = min.x,
|
||||
baseY = min.y,
|
||||
baseZ = min.z,
|
||||
length = max.x - min.x,
|
||||
height = max.y - min.y,
|
||||
width = max.z - min.z
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
|
||||
@@ -29,7 +29,6 @@ import com.cobblemon.mod.common.net.messages.client.storage.pc.InitializePCPacke
|
||||
import com.cobblemon.mod.common.net.messages.client.storage.pc.MoveClientPCPokemonPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.storage.pc.SetPCPokemonPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.UnlockPCBoxWallpaperPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.storage.pc.MovePCPokemonBatchPacket
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import com.cobblemon.mod.common.util.DataKeys
|
||||
import com.cobblemon.mod.common.util.asTranslated
|
||||
@@ -271,34 +270,6 @@ open class PCStore(
|
||||
}
|
||||
}
|
||||
|
||||
fun moveBatch(moves: List<Triple<UUID, PCPosition, PCPosition>>): Boolean {
|
||||
if (moves.isEmpty() || moves.size > MovePCPokemonBatchPacket.MAX_MOVES) return false
|
||||
if (moves.map { it.second }.toSet().size != moves.size ||
|
||||
moves.map { it.third }.toSet().size != moves.size ||
|
||||
moves.map { it.first }.toSet().size != moves.size
|
||||
) return false
|
||||
if (moves.any { (_, source, target) -> !isValidPosition(source) || !isValidPosition(target) }) return false
|
||||
|
||||
val sourcePositions = moves.mapTo(mutableSetOf()) { it.second }
|
||||
val pokemonMoves = moves.map { (pokemonID, source, target) ->
|
||||
val pokemon = get(source)?.takeIf { it.uuid == pokemonID } ?: return false
|
||||
Triple(source, target, pokemon)
|
||||
}
|
||||
if (pokemonMoves.any { (_, target, _) -> get(target) != null && target !in sourcePositions }) return false
|
||||
|
||||
pokemonMoves.forEach { (source, _, _) -> setAtPosition(source, null) }
|
||||
pokemonMoves.forEach { (_, target, pokemon) -> setAtPosition(target, pokemon) }
|
||||
|
||||
pokemonMoves.forEach { (_, _, pokemon) ->
|
||||
sendPacketToObservers(RemoveClientPokemonPacket(this, pokemon.uuid))
|
||||
}
|
||||
pokemonMoves.forEach { (_, target, pokemon) ->
|
||||
sendPacketToObservers(SetPCPokemonPacket(uuid, target) { pokemon })
|
||||
}
|
||||
pcChangeObservable.emit(Unit)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun loadPositionFromNBT(nbt: CompoundTag): StoreCoordinates<PCPosition> {
|
||||
return StoreCoordinates(this, PCPosition(nbt.getShort(DataKeys.STORE_BOX).toInt(), nbt.getByte(DataKeys.STORE_SLOT).toInt()))
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ object CobblemonItemTags {
|
||||
@JvmField val DESTINY_KNOT = create("held/destiny_knot")
|
||||
@JvmField val EVERSTONE = create("held/everstone")
|
||||
@JvmField val EXPERIENCE_SHARE = create("held/experience_share")
|
||||
@JvmField val IS_FRIENDSHIP_BOOSTER = create("held/is_friendship_booster")
|
||||
@JvmField val IS_FRIENDSHIP_BOOSTER = create("is_friendship_booster")
|
||||
@JvmField val ANY_HELD_ITEM = create("held/is_held_item")
|
||||
@JvmField val BLACKLISTED_ITEMS_TO_HOLD = create("held/blacklisted_items_to_hold")
|
||||
@JvmField val WHITELISTED_ITEMS_TO_HOLD = create("held/whitelisted_items_to_hold")
|
||||
|
||||
@@ -83,8 +83,7 @@ open class BattlePokemon(
|
||||
"max_health" to Function { DoubleValue(maxHealth.toDouble()) },
|
||||
"ivs" to Function { effectedPokemon.ivs.struct },
|
||||
"nature" to Function { StringValue(effectedPokemon.nature.name.toString()) },
|
||||
"moveset" to Function { effectedPokemon.moveSet.toStruct() },
|
||||
"has_faced_opponents" to Function { DoubleValue(facedOpponents.isNotEmpty()) }
|
||||
"moveset" to Function { effectedPokemon.moveSet.toStruct() }
|
||||
))
|
||||
|
||||
val uuid: UUID
|
||||
|
||||
@@ -34,9 +34,7 @@ import net.minecraft.world.item.alchemy.Potions
|
||||
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.BonemealableBlock
|
||||
import net.minecraft.world.level.block.DispenserBlock
|
||||
import net.minecraft.world.level.block.LeavesBlock
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
@@ -54,7 +52,7 @@ import net.minecraft.world.phys.shapes.EntityCollisionContext
|
||||
import net.minecraft.world.phys.shapes.Shapes
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
|
||||
class SaccharineLeafBlock(settings: Properties) : LeavesBlock(settings), BonemealableBlock {
|
||||
class SaccharineLeafBlock(settings: Properties) : LeavesBlock(settings) {
|
||||
companion object {
|
||||
val AGE: IntegerProperty = BlockStateProperties.AGE_2
|
||||
val DISTANCE: IntegerProperty = BlockStateProperties.DISTANCE
|
||||
@@ -183,15 +181,6 @@ class SaccharineLeafBlock(settings: Properties) : LeavesBlock(settings), Bonemea
|
||||
super.randomTick(state, world, pos, random)
|
||||
}
|
||||
|
||||
override fun isValidBonemealTarget(level: LevelReader, pos: BlockPos, state: BlockState): Boolean =
|
||||
state.getValue(AGE) < MAX_AGE && !state.getValue(BlockStateProperties.WATERLOGGED)
|
||||
|
||||
override fun isBonemealSuccess(level: Level, random: RandomSource, pos: BlockPos, state: BlockState) = true
|
||||
|
||||
override fun performBonemeal(level: ServerLevel, random: RandomSource, pos: BlockPos, state: BlockState) {
|
||||
level.setBlock(pos, changeAge(state, 1), UPDATE_CLIENTS)
|
||||
}
|
||||
|
||||
override fun placeLiquid(level: LevelAccessor, pos: BlockPos, state: BlockState, fluidState: FluidState): Boolean {
|
||||
if (!state.getValue(BlockStateProperties.WATERLOGGED) && fluidState.type === Fluids.WATER) {
|
||||
val hasHoney = state.getValue(AGE) > 0
|
||||
|
||||
@@ -19,7 +19,6 @@ import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.util.RandomSource
|
||||
import net.minecraft.util.StringRepresentable
|
||||
import net.minecraft.world.item.BlockItem
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.component.BlockItemStateProperties
|
||||
@@ -34,8 +33,8 @@ import net.minecraft.world.level.block.Mirror
|
||||
import net.minecraft.world.level.block.Rotation
|
||||
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.DirectionProperty
|
||||
import net.minecraft.world.level.block.state.properties.EnumProperty
|
||||
import net.minecraft.world.level.block.state.properties.IntegerProperty
|
||||
import net.minecraft.world.level.material.PushReaction
|
||||
import net.minecraft.world.level.storage.loot.LootParams
|
||||
@@ -62,27 +61,22 @@ class TypeGemClusterBlock(
|
||||
}
|
||||
val gemToClusterMap: MutableMap<Block, TypeGemClusterBlock> = mutableMapOf()
|
||||
|
||||
val SHOULD_GROW: BooleanProperty = BooleanProperty.create("should_grow")
|
||||
val FACING: DirectionProperty = DirectionalBlock.FACING
|
||||
val STAGE: IntegerProperty = IntegerProperty.create("stage", 0, 3)
|
||||
val GROWTH_TYPE: EnumProperty<GrowthType> = EnumProperty.create("growth_type", GrowthType::class.java)
|
||||
val STUNTED: BooleanProperty = BooleanProperty.create("stunted")
|
||||
|
||||
fun clusterFromGemBlock(gemBlock: Block): TypeGemClusterBlock? {
|
||||
return gemToClusterMap[gemBlock]
|
||||
}
|
||||
}
|
||||
|
||||
enum class GrowthType(private val label: String, val chance: Float): StringRepresentable {
|
||||
DECORATION("none", 0.25f),
|
||||
FULL("full", 0.5f),
|
||||
STUNTED("stunted", 0.25f);
|
||||
override fun getSerializedName(): String = label
|
||||
}
|
||||
|
||||
init {
|
||||
registerDefaultState(stateDefinition.any()
|
||||
.setValue(FACING, Direction.UP)
|
||||
.setValue(SHOULD_GROW, true)
|
||||
.setValue(STAGE, 0)
|
||||
.setValue(GROWTH_TYPE, GrowthType.FULL)
|
||||
.setValue(STUNTED, false)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -95,19 +89,15 @@ class TypeGemClusterBlock(
|
||||
|
||||
return defaultBlockState()
|
||||
.setValue(FACING, facing)
|
||||
.setValue(SHOULD_GROW, shouldGrow)
|
||||
.setValue(STAGE, 0)
|
||||
.setValue(GROWTH_TYPE, if(shouldGrow) GrowthType.FULL else GrowthType.DECORATION)
|
||||
.setValue(STUNTED, false)
|
||||
}
|
||||
|
||||
public override fun isRandomlyTicking(state: BlockState): Boolean {
|
||||
// if growth type is either stunted or full, we need to tick
|
||||
return state.getValue(GROWTH_TYPE) != GrowthType.DECORATION
|
||||
}
|
||||
public override fun isRandomlyTicking(state: BlockState): Boolean = state.getValue(SHOULD_GROW)
|
||||
|
||||
override fun randomTick(state: BlockState, level: ServerLevel, pos: BlockPos, random: RandomSource) {
|
||||
/*
|
||||
advanceGrowth(state, level, pos, random)
|
||||
*/
|
||||
}
|
||||
|
||||
fun advanceGrowth(state: BlockState, level: LevelAccessor, pos: BlockPos, random: RandomSource) {
|
||||
@@ -121,8 +111,15 @@ class TypeGemClusterBlock(
|
||||
return
|
||||
}
|
||||
|
||||
// At STAGE_3
|
||||
val isStunted = state.getValue(STUNTED)
|
||||
val facing = state.getValue(FACING)
|
||||
|
||||
if (isStunted) {
|
||||
level.setBlock(pos, state.setValue(SHOULD_GROW, false), UPDATE_ALL)
|
||||
return
|
||||
}
|
||||
|
||||
// Check neighboring gem blocks
|
||||
val hasConflict = Direction.entries
|
||||
.filter { it != facing.opposite }
|
||||
@@ -135,36 +132,61 @@ class TypeGemClusterBlock(
|
||||
if (hasConflict) {
|
||||
level.setBlock(
|
||||
pos,
|
||||
state.setValue(GROWTH_TYPE, GrowthType.STUNTED),
|
||||
state.setValue(STUNTED, true).setValue(SHOULD_GROW, false),
|
||||
UPDATE_ALL
|
||||
)
|
||||
}
|
||||
} else {
|
||||
var nextState = nextStage.defaultBlockState()
|
||||
if (nextState.hasProperty(FACING)) {
|
||||
nextState = nextState.setValue(FACING, facing)
|
||||
}
|
||||
level.setBlock(pos, nextState, UPDATE_ALL)
|
||||
if (isGem(nextState)) {
|
||||
TypeGemCoreBlock.addClustersToBlock(state, pos, level, random)
|
||||
}
|
||||
attachDecorativeClusters(level, pos, random)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isGem(state: BlockState): Boolean {
|
||||
return state.`is`(CobblemonBlockTags.TYPE_GEM_BLOCKS)
|
||||
}
|
||||
|
||||
private fun attachDecorativeClusters(level: LevelAccessor, gemPos: BlockPos, random: RandomSource) {
|
||||
val openDirections = Direction.entries.filter { direction ->
|
||||
level.getBlockState(gemPos.relative(direction)).isAir
|
||||
}.toMutableList()
|
||||
|
||||
if (openDirections.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val minClusters = maxOf(1, (openDirections.size * 0.75f).toInt())
|
||||
val clusterCount = random.nextInt(minClusters, openDirections.size + 1)
|
||||
|
||||
repeat(clusterCount) {
|
||||
if (openDirections.isEmpty()) return@repeat
|
||||
|
||||
val index = random.nextInt(openDirections.size)
|
||||
val direction = openDirections.removeAt(index)
|
||||
val clusterPos = gemPos.relative(direction)
|
||||
val stage = random.nextInt(0, 4)
|
||||
|
||||
val clusterState = defaultBlockState()
|
||||
.setValue(FACING, direction)
|
||||
.setValue(STAGE, stage)
|
||||
.setValue(SHOULD_GROW, false) // so we can reduce lag... hopefully
|
||||
.setValue(STUNTED, false)
|
||||
|
||||
level.setBlock(clusterPos, clusterState, UPDATE_ALL)
|
||||
}
|
||||
}
|
||||
|
||||
fun gemClusterCanGrow(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
|
||||
if (state.getValue(GROWTH_TYPE) == GrowthType.DECORATION) return false
|
||||
if (state.getValue(GROWTH_TYPE) == GrowthType.STUNTED
|
||||
&& state.getValue(STAGE) == 3) return false
|
||||
if (!state.getValue(SHOULD_GROW)) return false
|
||||
|
||||
val supportPos = pos.relative(state.getValue(FACING).opposite)
|
||||
val supportBlock = level.getBlockState(supportPos).block
|
||||
return CobblemonBlocks.typeGemBlocks().containsValue(supportBlock) || (supportBlock == CobblemonBlocks.TYPE_GEM_CORE) // Cannot grow if not on any gem blocks
|
||||
}
|
||||
|
||||
|
||||
override fun getDrops(state: BlockState, params: LootParams.Builder): List<ItemStack> {
|
||||
val drops = super.getDrops(state, params)
|
||||
|
||||
@@ -178,7 +200,8 @@ class TypeGemClusterBlock(
|
||||
DataComponents.BLOCK_STATE,
|
||||
BlockItemStateProperties.EMPTY
|
||||
.with(STAGE, state.getValue(STAGE))
|
||||
.with(GROWTH_TYPE, GrowthType.DECORATION)
|
||||
.with(SHOULD_GROW, false)
|
||||
.with(STUNTED, false)
|
||||
)
|
||||
stack.set(
|
||||
DataComponents.CUSTOM_MODEL_DATA,
|
||||
@@ -249,7 +272,7 @@ class TypeGemClusterBlock(
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
super.createBlockStateDefinition(builder)
|
||||
builder.add(FACING, STAGE, GROWTH_TYPE)
|
||||
builder.add(FACING, SHOULD_GROW, STAGE, STUNTED)
|
||||
}
|
||||
|
||||
override fun getCloneItemStack(level: LevelReader, pos: BlockPos, state: BlockState): ItemStack {
|
||||
@@ -260,7 +283,8 @@ class TypeGemClusterBlock(
|
||||
DataComponents.BLOCK_STATE,
|
||||
BlockItemStateProperties.EMPTY
|
||||
.with(STAGE, state.getValue(STAGE))
|
||||
.with(GROWTH_TYPE, GrowthType.DECORATION)
|
||||
.with(SHOULD_GROW, false)
|
||||
.with(STUNTED, false)
|
||||
)
|
||||
stack.set(
|
||||
DataComponents.CUSTOM_MODEL_DATA,
|
||||
|
||||
@@ -11,10 +11,8 @@ package com.cobblemon.mod.common.block
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.CobblemonBlocks
|
||||
import com.cobblemon.mod.common.api.tags.CobblemonBlockTags
|
||||
import com.cobblemon.mod.common.block.TypeGemClusterBlock.Companion.FACING
|
||||
import com.cobblemon.mod.common.block.TypeGemClusterBlock.Companion.GROWTH_TYPE
|
||||
import com.cobblemon.mod.common.block.TypeGemClusterBlock.Companion.STAGE
|
||||
import com.cobblemon.mod.common.block.TypeGemClusterBlock.GrowthType
|
||||
import com.cobblemon.mod.common.block.TypeGemClusterBlock.Companion.SHOULD_GROW
|
||||
import com.cobblemon.mod.common.block.TypeGemClusterBlock.Companion.STUNTED
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
@@ -23,8 +21,8 @@ import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.util.RandomSource
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.level.LevelAccessor
|
||||
import net.minecraft.world.level.WorldGenLevel
|
||||
import net.minecraft.world.level.block.Block
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import net.minecraft.world.level.block.DirectionalBlock
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import java.util.*
|
||||
@@ -32,6 +30,13 @@ import java.util.*
|
||||
class TypeGemCoreBlock(properties: Properties) : Block(properties) {
|
||||
|
||||
companion object {
|
||||
const val MAX_CONNECTED_GEMS = 7
|
||||
const val MIN_DISTANCE_BETWEEN_GEMS = 1
|
||||
const val CONTINUATION_CHANCE = 0.8f
|
||||
|
||||
// Upper safety check to prevent infinite loops
|
||||
const val MAX_FORCE_GROW_ATTEMPTS = 64
|
||||
|
||||
val BLOCK_TO_CLUSTER: Map<ResourceLocation, Block> by lazy {
|
||||
mapOf(
|
||||
ResourceLocation.parse("cobblemon:normal_gem_block") to CobblemonBlocks.TYPE_GEM_CLUSTER_NORMAL,
|
||||
@@ -55,6 +60,28 @@ class TypeGemCoreBlock(properties: Properties) : Block(properties) {
|
||||
)
|
||||
}
|
||||
|
||||
fun setStuntState(level: LevelAccessor, gemBlocks: List<Pair<BlockState, BlockPos>>, stunted: Boolean) {
|
||||
for ((_, gemPos) in gemBlocks) {
|
||||
for (dir in Direction.entries) {
|
||||
val neighborPos = gemPos.relative(dir)
|
||||
val neighborState = level.getBlockState(neighborPos)
|
||||
|
||||
if (neighborState.block is TypeGemClusterBlock) {
|
||||
if (neighborState.hasProperty(STUNTED) && neighborState.getValue(STUNTED) != stunted) {
|
||||
|
||||
var updatedState = neighborState.setValue(STUNTED, stunted)
|
||||
|
||||
if (updatedState.hasProperty(SHOULD_GROW)) {
|
||||
updatedState = updatedState.setValue(SHOULD_GROW, !stunted)
|
||||
}
|
||||
|
||||
level.setBlock(neighborPos, updatedState, 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getConnectedGemBlocks(level: BlockGetter, pos: BlockPos): List<Pair<BlockState, BlockPos>> {
|
||||
val connectedGems = mutableListOf<Pair<BlockState, BlockPos>>()
|
||||
val visited = mutableSetOf<BlockPos>()
|
||||
@@ -80,159 +107,119 @@ class TypeGemCoreBlock(properties: Properties) : Block(properties) {
|
||||
}
|
||||
return connectedGems
|
||||
}
|
||||
|
||||
fun addClustersToBlock(state: BlockState, pos: BlockPos, level: LevelAccessor, random: RandomSource) {
|
||||
Direction.entries.forEach {
|
||||
if (random.nextFloat() < Cobblemon.config.typeGemGrowthChance) {
|
||||
val faceState = level.getBlockState(pos.relative(it))
|
||||
val randomGrowthType = decideClusterGrowthType(level, pos)
|
||||
val newState = BLOCK_TO_CLUSTER[BuiltInRegistries.BLOCK.getKey(state.block)]?.defaultBlockState()
|
||||
?.setValue(DirectionalBlock.FACING, it)
|
||||
?.setValue(GROWTH_TYPE, randomGrowthType)
|
||||
?: return
|
||||
|
||||
if (faceState.`is`(Blocks.AIR)) {
|
||||
level.setBlock(pos.relative(it), newState, 3)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun decideClusterGrowthType(level: LevelAccessor, pos: BlockPos, random: RandomSource = RandomSource.create()): GrowthType {
|
||||
val growthTypes = if (getConnectedGemBlocks(level, pos).size > Cobblemon.config.typeGemMaxConnected) {
|
||||
listOf(GrowthType.DECORATION, GrowthType.STUNTED)
|
||||
} else {
|
||||
GrowthType.entries
|
||||
}
|
||||
val totalChance = growthTypes.sumOf { it.chance.toDouble() }
|
||||
var roll = random.nextDouble() * totalChance
|
||||
|
||||
for (type in growthTypes) {
|
||||
roll -= type.chance.toDouble()
|
||||
if (roll <= 0.0) {
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
return GrowthType.DECORATION
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConnectedClusters(level: LevelAccessor, pos: BlockPos): List<Pair<BlockState, BlockPos>> {
|
||||
val gemBlocks = getConnectedGemBlocks(level, pos)
|
||||
val clusters = mutableListOf<Pair<BlockState, BlockPos>>()
|
||||
val visited = mutableSetOf<BlockPos>()
|
||||
|
||||
for ((gemState, gemPos) in gemBlocks) {
|
||||
val expectedCluster = BLOCK_TO_CLUSTER[BuiltInRegistries.BLOCK.getKey(gemState.block)] ?: continue
|
||||
|
||||
for (direction in Direction.entries) {
|
||||
val clusterPos = gemPos.relative(direction)
|
||||
if (!visited.add(clusterPos)) continue
|
||||
|
||||
val clusterState = level.getBlockState(clusterPos)
|
||||
if (clusterState.block == expectedCluster) {
|
||||
clusters += clusterState to clusterPos
|
||||
}
|
||||
}
|
||||
}
|
||||
return clusters
|
||||
}
|
||||
|
||||
override fun randomTick(state: BlockState, level: ServerLevel, pos: BlockPos, random: RandomSource) {
|
||||
// should we tick at all?
|
||||
if (random.nextFloat() >= Cobblemon.config.typeGemTickChance) {
|
||||
return
|
||||
}
|
||||
val gemCount = getConnectedGemBlocks(level, pos).size
|
||||
grow(level, pos, random)
|
||||
}
|
||||
|
||||
// if growth has stopped fully but min growth hasn't been reached, randomly set a stopped bud to GROWTH_TYPE full
|
||||
val clusters = getConnectedClusters(level, pos)
|
||||
val fullClusterCount = clusters.count { (clusterState, _) ->
|
||||
clusterState.getValue(GROWTH_TYPE) == GrowthType.FULL
|
||||
fun forceGrow(level: WorldGenLevel, pos: BlockPos, random: RandomSource, percentage: Float) {
|
||||
val desiredLength = MAX_CONNECTED_GEMS * percentage
|
||||
|
||||
repeat(MAX_FORCE_GROW_ATTEMPTS) {
|
||||
val (grown, clusterSize) = grow(level, pos, random, true)
|
||||
if (!grown || clusterSize >= desiredLength) return
|
||||
}
|
||||
|
||||
if (fullClusterCount == 0) {
|
||||
if (gemCount < Cobblemon.config.typeGemMinGrowth && clusters.isNotEmpty()) {
|
||||
println("Force set a cluster to FULL at size $gemCount")
|
||||
val randomCluster = clusters.random()
|
||||
level.setBlockAndUpdate(randomCluster.second, randomCluster.first.setValue(GROWTH_TYPE, GrowthType.FULL))
|
||||
Cobblemon.LOGGER.warn(
|
||||
"Forced type gem growth at {} stopped after {} attempts without reaching its target size; keeping the partial formation.",
|
||||
pos,
|
||||
MAX_FORCE_GROW_ATTEMPTS
|
||||
)
|
||||
}
|
||||
|
||||
private fun grow(
|
||||
level: WorldGenLevel,
|
||||
pos: BlockPos,
|
||||
random: RandomSource,
|
||||
forced: Boolean = false
|
||||
): Pair<Boolean, Int> {
|
||||
val connectedGems = getConnectedGemBlocks(level, pos) // maybe we can store this and access it for better performance? check for changes in list rather than checking each time grow is called? idk
|
||||
val gemCount = connectedGems.size
|
||||
val overLimit = gemCount >= MAX_CONNECTED_GEMS
|
||||
|
||||
if (overLimit) {
|
||||
// if there is no open air pockets to grow any new clusters then exit early
|
||||
if (!hasBreathingRoom(level, connectedGems)) {
|
||||
return Pair(false, gemCount)
|
||||
}
|
||||
// max type gem block count met, tell clusters to grow but not turn into blocks
|
||||
setStuntState(level, connectedGems, true)
|
||||
//return Pair(false, gemCount)
|
||||
} else {
|
||||
// make sure connected clusters know they can grow and turn into blocks
|
||||
setStuntState(level, connectedGems, false)
|
||||
}
|
||||
|
||||
val javaRandom = java.util.Random(random.nextLong())
|
||||
|
||||
for ((gemState, gemPos) in connectedGems.shuffled(javaRandom)) {
|
||||
val registryKey = BuiltInRegistries.BLOCK.getKey(gemState.block)
|
||||
val clusterBlock = BLOCK_TO_CLUSTER[registryKey] ?: continue // If there is no block to grow, skip the block
|
||||
|
||||
for (dir in Direction.entries.shuffled(javaRandom)) {
|
||||
val targetPos = gemPos.relative(dir)
|
||||
val targetState = level.getBlockState(targetPos)
|
||||
|
||||
if (!targetState.isAir) continue
|
||||
if (!isPositionValidForGrowth(level, targetPos)) continue
|
||||
|
||||
// Place gem cluster
|
||||
val placeState = clusterBlock.defaultBlockState()
|
||||
.setValue(DirectionalBlock.FACING, dir)
|
||||
.setValue(TypeGemClusterBlock.STAGE, 0)
|
||||
.setValue(SHOULD_GROW, true)
|
||||
|
||||
level.setBlock(targetPos, placeState, UPDATE_ALL)
|
||||
|
||||
// During world gen, the region silently discards writes into neighboring chunks that are already fully generated.
|
||||
// Through ImposterProtoChunk with writes set to disabled. Checks if the block is still air after the write and skips if so as it can't be filled.
|
||||
// Preventing infinite loops.
|
||||
if (!level.getBlockState(targetPos).`is`(clusterBlock)) continue
|
||||
|
||||
if (forced) {
|
||||
forceAdvanceClusterGrowth(level, targetPos, random)
|
||||
}
|
||||
|
||||
val updatedGemCount = if (forced) getConnectedGemBlocks(level, pos).size else connectedGems.size
|
||||
return Pair(true, updatedGemCount)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
expand(level, pos, random)
|
||||
// println("[TypeGemCoreBlock] No valid spot found for gem placement.")
|
||||
return Pair(false, connectedGems.size)
|
||||
}
|
||||
|
||||
fun expand(level: LevelAccessor, pos: BlockPos, random: RandomSource, forceGrow: Boolean = false) {
|
||||
getConnectedGemBlocks(level, pos).forEach {
|
||||
val state = it.first
|
||||
val pos = it.second
|
||||
addClustersToBlock(state, pos, level, random)
|
||||
growClusters(pos, level, random, forceGrow)
|
||||
private fun forceAdvanceClusterGrowth(level: WorldGenLevel, clusterPos: BlockPos, random: RandomSource) {
|
||||
repeat(5) {
|
||||
val clusterState = level.getBlockState(clusterPos)
|
||||
val clusterBlock = clusterState.block as? TypeGemClusterBlock ?: return
|
||||
clusterBlock.advanceGrowth(clusterState, level, clusterPos, random)
|
||||
}
|
||||
}
|
||||
|
||||
private fun growClusters(pos: BlockPos, level: LevelAccessor, random: RandomSource, forceGrow: Boolean) {
|
||||
Direction.entries.forEach { direction ->
|
||||
val relativePos = pos.relative(direction)
|
||||
val relativeState = level.getBlockState(relativePos)
|
||||
val clusterBlock = relativeState.block as? TypeGemClusterBlock ?: return@forEach
|
||||
|
||||
if ((relativeState.getValue(DirectionalBlock.FACING) == direction &&
|
||||
random.nextFloat() < Cobblemon.config.typeGemGrowthChance) || forceGrow
|
||||
) {
|
||||
clusterBlock.advanceGrowth(relativeState, level, relativePos, random)
|
||||
private fun hasBreathingRoom(level: WorldGenLevel, gems: List<Pair<BlockState, BlockPos>>): Boolean {
|
||||
for ((_, gemPos) in gems) {
|
||||
for (dir in Direction.entries) {
|
||||
if (level.getBlockState(gemPos.relative(dir)).isAir) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun addInitialClusters(pos: BlockPos, level: ServerLevel, clusterType: TypeGemClusterBlock) {
|
||||
Direction.entries.forEach {
|
||||
val faceState = level.getBlockState(pos.relative(it))
|
||||
|
||||
if (faceState.`is`(Blocks.AIR)) {
|
||||
level.setBlockAndUpdate(pos.relative(it), clusterType.defaultBlockState().setValue(
|
||||
DirectionalBlock.FACING, it)
|
||||
.setValue(GROWTH_TYPE, GrowthType.FULL)
|
||||
)
|
||||
}
|
||||
private fun isPositionValidForGrowth(level: WorldGenLevel, pos: BlockPos): Boolean {
|
||||
return Direction.entries.none { dir ->
|
||||
val nearby = pos.relative(dir)
|
||||
isGem(level.getBlockState(nearby)) &&
|
||||
pos.distManhattan(nearby) < MIN_DISTANCE_BETWEEN_GEMS
|
||||
}
|
||||
}
|
||||
|
||||
private fun isGem(state: BlockState): Boolean {
|
||||
return state.`is`(CobblemonBlockTags.TYPE_GEM_BLOCKS)
|
||||
}
|
||||
|
||||
override fun isRandomlyTicking(state: BlockState): Boolean = true
|
||||
|
||||
private fun attachDecorativeClusters(level: LevelAccessor, gemPos: BlockPos, random: RandomSource) {
|
||||
val openDirections = Direction.entries.filter { direction ->
|
||||
level.getBlockState(gemPos.relative(direction)).isAir
|
||||
}.toMutableList()
|
||||
|
||||
if (openDirections.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val gemState = level.getBlockState(gemPos)
|
||||
val clusterBlock = BLOCK_TO_CLUSTER[BuiltInRegistries.BLOCK.getKey(gemState.block)] ?: return
|
||||
|
||||
val minClusters = maxOf(1, (openDirections.size * 0.75f).toInt())
|
||||
val clusterCount = random.nextInt(minClusters, openDirections.size + 1)
|
||||
|
||||
repeat(clusterCount) {
|
||||
if (openDirections.isEmpty()) return@repeat
|
||||
|
||||
val index = random.nextInt(openDirections.size)
|
||||
val direction = openDirections.removeAt(index)
|
||||
val clusterPos = gemPos.relative(direction)
|
||||
val stage = random.nextInt(0, 4)
|
||||
|
||||
val clusterState = clusterBlock.defaultBlockState()
|
||||
.setValue(FACING, direction)
|
||||
.setValue(STAGE, stage)
|
||||
.setValue(GROWTH_TYPE, GrowthType.DECORATION)
|
||||
|
||||
level.setBlock(clusterPos, clusterState, UPDATE_ALL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+5
-1
@@ -171,7 +171,11 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
|
||||
fun attemptSpawn(player: Player) {
|
||||
val cause = SpawnCause(spawner = spawner, entity = player)
|
||||
val zoneInput = spawner.getZoneInput(cause)
|
||||
val spawnAction = spawner.calculateSpawnActionsForArea(zoneInput, 1).firstOrNull() ?: return
|
||||
val spawnAction = spawner.calculateSpawnActionsForArea(zoneInput, 1).firstOrNull()
|
||||
|
||||
if (spawnAction == null) {
|
||||
return
|
||||
}
|
||||
|
||||
CobblemonEvents.POKE_SNACK_SPAWN_POKEMON_PRE.postThen(
|
||||
PokeSnackSpawnPokemonEvent.Pre(this, spawnAction),
|
||||
|
||||
@@ -334,6 +334,8 @@ class TMMachineBlockEntity(pos: BlockPos, state: BlockState) :
|
||||
level?.updateNeighborsAt(blockPos, currentState.block)
|
||||
}
|
||||
|
||||
override fun stillValid(player: Player): Boolean = tmMachineInventory.stillValid(player)
|
||||
|
||||
override fun canPlaceItem(slot: Int, stack: ItemStack): Boolean = tmMachineInventory.canPlaceItem(slot, stack)
|
||||
|
||||
override fun canTakeItem(target: Container, slot: Int, stack: ItemStack): Boolean =
|
||||
|
||||
@@ -111,7 +111,6 @@ object CobblemonClient {
|
||||
val battleOverlay: BattleOverlay by lazy { BattleOverlay() }
|
||||
val pokedexUsageContext: PokedexUsageContext by lazy { PokedexUsageContext() }
|
||||
val rideControlsOverlay: RideControlsOverlay by lazy { RideControlsOverlay() }
|
||||
var pcHealthToggle: Boolean = false
|
||||
|
||||
fun onLogin() {
|
||||
clientPlayerData = ClientGeneralPlayerData()
|
||||
|
||||
+2
-2
@@ -475,7 +475,7 @@ class PokemonClientDelegate : PosableState(), PokemonSideDelegate {
|
||||
val locators = particleData.locatorResolver(locatorStates).filter(::isLocatorVisible)
|
||||
if (particleData.chance > random.nextDouble()) {
|
||||
locators.forEach { locator ->
|
||||
repeat(particleData.amount()) {
|
||||
repeat(particleData.amount) {
|
||||
runtime.resolve("q.particle('${particleData.particle}', '$locator')".asExpressionLike())
|
||||
}
|
||||
}
|
||||
@@ -485,7 +485,7 @@ class PokemonClientDelegate : PosableState(), PokemonSideDelegate {
|
||||
is ParticleData.MinecraftParticle -> {
|
||||
if (particleData.chance > random.nextDouble()) {
|
||||
val box = currentEntity.boundingBox
|
||||
repeat(particleData.amount()) {
|
||||
repeat(particleData.amount) {
|
||||
val x = box.minX + (box.maxX - box.minX) * random.nextDouble()
|
||||
val y = box.minY + (box.maxY - box.minY) * random.nextDouble()
|
||||
val z = box.minZ + (box.maxZ - box.minZ) * random.nextDouble()
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
+240
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -17,7 +17,6 @@ import com.cobblemon.mod.common.api.text.text
|
||||
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
|
||||
import com.cobblemon.mod.common.pokemon.Species
|
||||
import com.cobblemon.mod.common.util.adapters.IntRangesAdapter
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import java.awt.Color
|
||||
import net.minecraft.client.Minecraft
|
||||
@@ -329,7 +328,7 @@ class HabitatSpawnEditorGUI(
|
||||
private fun parseSpecies(value: String): Species? {
|
||||
val lowered = value.lowercase()
|
||||
pokemonSpeciesByTranslatedName[lowered]?.let { return it }
|
||||
return runCatching { PokemonSpecies.getByIdentifier(lowered.asIdentifierDefaultingNamespace()) }.getOrNull()
|
||||
return runCatching { PokemonSpecies.getByName(lowered) }.getOrNull()
|
||||
}
|
||||
|
||||
private fun parsePhases(raw: String): IntRanges? {
|
||||
|
||||
@@ -29,8 +29,7 @@ class BoxStorageSlot(
|
||||
|
||||
val grabbedSlot = parent.grabbedSlot
|
||||
return if (grabbedSlot == null) {
|
||||
val pokemon = getPokemon()
|
||||
pokemon == null || !parent.isInMultiSelection(pokemon, position)
|
||||
true
|
||||
} else {
|
||||
grabbedSlot.getPokemon() != getPokemon()
|
||||
}
|
||||
|
||||
@@ -50,15 +50,13 @@ import net.minecraft.network.chat.Component
|
||||
import net.minecraft.sounds.SoundEvent
|
||||
import kotlin.isInitialized
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.util.FastColor
|
||||
|
||||
class PCGUI(
|
||||
val pc: ClientPC,
|
||||
val party: ClientParty,
|
||||
val configuration: PCGUIConfiguration,
|
||||
val openOnBox: Int = CobblemonClient.lastPcBoxViewed,
|
||||
val unseenWallpapers: MutableSet<ResourceLocation> = mutableSetOf(),
|
||||
val silent: Boolean = false
|
||||
val unseenWallpapers: MutableSet<ResourceLocation> = mutableSetOf()
|
||||
) : Screen(Component.translatable("cobblemon.ui.pc.title")), CobblemonRenderable {
|
||||
|
||||
companion object {
|
||||
@@ -99,8 +97,6 @@ class PCGUI(
|
||||
private val buttonWallpaperResource = cobblemonResource("textures/gui/pc/pc_button_set_wallpaper.png")
|
||||
private val buttonInfoArrow = cobblemonResource("textures/gui/pc/info_arrow.png")
|
||||
|
||||
private val buttonHealthToggleResource = cobblemonResource("textures/gui/pc/pc_button_toggle_health.png")
|
||||
|
||||
private val topSpacerResource = cobblemonResource("textures/gui/pc/pc_spacer_top.png")
|
||||
private val bottomSpacerResource = cobblemonResource("textures/gui/pc/pc_spacer_bottom.png")
|
||||
private val rightSpacerResource = cobblemonResource("textures/gui/pc/pc_spacer_right.png")
|
||||
@@ -132,55 +128,6 @@ class PCGUI(
|
||||
var selectPointerOffsetIncrement = false
|
||||
var displayOptions = false
|
||||
|
||||
private var dragStartX: Int? = null
|
||||
private var dragStartY: Int? = null
|
||||
private var dragEndX: Int? = null
|
||||
private var dragEndY: Int? = null
|
||||
private var selectionDragActive = false
|
||||
|
||||
data class SelectionArea(val left: Int, val top: Int, val right: Int, val bottom: Int)
|
||||
|
||||
private data class BoxGridBounds(val left: Int, val top: Int, val right: Int, val bottom: Int) {
|
||||
fun contains(x: Int, y: Int) = x in left..< right && y >= top && y < bottom
|
||||
}
|
||||
|
||||
private fun getBoxGridBounds(): BoxGridBounds {
|
||||
val left = storageWidget.x
|
||||
val top = storageWidget.y
|
||||
return BoxGridBounds(
|
||||
left,
|
||||
top,
|
||||
left + StorageWidget.SCREEN_WIDTH,
|
||||
top + StorageWidget.SCREEN_HEIGHT
|
||||
)
|
||||
}
|
||||
|
||||
fun getSelectionArea(): SelectionArea? {
|
||||
if (!selectionDragActive) return null
|
||||
val startX = dragStartX ?: return null
|
||||
val startY = dragStartY ?: return null
|
||||
val endX = dragEndX ?: return null
|
||||
val endY = dragEndY ?: return null
|
||||
val bounds = getBoxGridBounds()
|
||||
if (!bounds.contains(startX, startY)) return null
|
||||
|
||||
val columns = 6
|
||||
val rows = 5
|
||||
val slotPitch = StorageSlot.SIZE + StorageWidget.BOX_SLOT_PADDING
|
||||
val gridLeft = bounds.left + StorageWidget.BOX_SLOT_START_OFFSET_X
|
||||
val gridTop = bounds.top + StorageWidget.BOX_SLOT_START_OFFSET_Y + if (displayOptions) 5 else 0
|
||||
val startColumn = ((startX - gridLeft) / slotPitch).coerceIn(0, columns - 1)
|
||||
val endColumn = (((endX.coerceIn(bounds.left, bounds.right - 1)) - gridLeft) / slotPitch).coerceIn(0, columns - 1)
|
||||
val startRow = ((startY - gridTop) / slotPitch).coerceIn(0, rows - 1)
|
||||
val endRow = (((endY.coerceIn(bounds.top, bounds.bottom - 1)) - gridTop) / slotPitch).coerceIn(0, rows - 1)
|
||||
return SelectionArea(
|
||||
gridLeft + minOf(startColumn, endColumn) * slotPitch, // left
|
||||
gridTop + minOf(startRow, endRow) * slotPitch, // top
|
||||
gridLeft + (maxOf(startColumn, endColumn) * slotPitch) + StorageSlot.SIZE, // right
|
||||
gridTop + (maxOf(startRow, endRow) * slotPitch) + StorageSlot.SIZE // bottom
|
||||
)
|
||||
}
|
||||
|
||||
override fun renderBlurredBackground(delta: Float) {}
|
||||
|
||||
override fun renderMenuBackground(context: GuiGraphics) {}
|
||||
@@ -271,6 +218,7 @@ class PCGUI(
|
||||
storageWidget.setupStorageSlots()
|
||||
for (button in optionButtons) {
|
||||
button.visible = displayOptions
|
||||
button.highlighted = false
|
||||
}
|
||||
}.also { it.highlighted = displayOptions }
|
||||
)
|
||||
@@ -301,28 +249,12 @@ class PCGUI(
|
||||
configuration.showParty = isVisible
|
||||
wallpaperWidget.visible = !isVisible
|
||||
(it as IconButton).highlighted = !isVisible
|
||||
if (unseenWallpapers.isNotEmpty()) MarkPCBoxWallpapersSeenPacket(unseenWallpapers).sendToServer()
|
||||
if (!unseenWallpapers.isEmpty()) MarkPCBoxWallpapersSeenPacket(unseenWallpapers).sendToServer()
|
||||
}.also {
|
||||
it.visible = displayOptions
|
||||
optionButtons.add(it)
|
||||
})
|
||||
|
||||
val healthButton = this.addRenderableWidget(IconButton(
|
||||
pX = x + 230,
|
||||
pY = y + 31,
|
||||
buttonWidth = 20,
|
||||
buttonHeight = 20,
|
||||
resource = buttonHealthToggleResource,
|
||||
label = "toggle_health_bars",
|
||||
) {
|
||||
CobblemonClient.pcHealthToggle = !CobblemonClient.pcHealthToggle
|
||||
(it as IconButton).highlighted = CobblemonClient.pcHealthToggle
|
||||
}.also {
|
||||
it.visible = displayOptions
|
||||
optionButtons.add(it)
|
||||
})
|
||||
healthButton.highlighted = CobblemonClient.pcHealthToggle
|
||||
|
||||
PokemonSortMode.entries.forEachIndexed { index, sortType ->
|
||||
val typeName = sortType.name.lowercase()
|
||||
this.addRenderableWidget(IconButton(
|
||||
@@ -382,20 +314,6 @@ class PCGUI(
|
||||
super.init()
|
||||
}
|
||||
|
||||
private fun renderSelectionBox(context: GuiGraphics, area: SelectionArea) {
|
||||
context.pose().pushPose()
|
||||
context.pose().translate(0.0, 0.0, 1500.0)
|
||||
val borderColor = FastColor.ARGB32.color(255, 205, 212, 255)
|
||||
val bgColor = FastColor.ARGB32.color(75, 133, 183, 230)
|
||||
val thickness = 1
|
||||
context.fill(area.left, area.top, area.right, area.bottom, bgColor)
|
||||
context.fill(area.left, area.top, area.right, area.top + thickness, borderColor)
|
||||
context.fill(area.left, area.bottom - thickness, area.right, area.bottom, borderColor)
|
||||
context.fill(area.left, area.top, area.left + thickness, area.bottom, borderColor)
|
||||
context.fill(area.right - thickness, area.top, area.right, area.bottom, borderColor)
|
||||
context.pose().popPose()
|
||||
}
|
||||
|
||||
override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
val matrices = context.pose()
|
||||
renderBackground(context, mouseX, mouseY, delta)
|
||||
@@ -536,20 +454,6 @@ class PCGUI(
|
||||
shadow = true
|
||||
)
|
||||
|
||||
// var dexNo = pokemon.species.nationalPokedexNumber.toString()
|
||||
// while (dexNo.length < 4) {
|
||||
// dexNo = "0$dexNo"
|
||||
// }
|
||||
// drawScaledText(
|
||||
// context = context,
|
||||
// font = CobblemonResources.DEFAULT_LARGE,
|
||||
// text = Component.literal(dexNo).bold(),
|
||||
// x = x + 38,
|
||||
// y = y + 1.5,
|
||||
// shadow = true,
|
||||
// scale = 0.85f
|
||||
// )
|
||||
|
||||
drawScaledText(
|
||||
context = context,
|
||||
font = CobblemonResources.DEFAULT_LARGE,
|
||||
@@ -780,14 +684,12 @@ class PCGUI(
|
||||
scale = SCALE
|
||||
)
|
||||
|
||||
if (optionButtons.isNotEmpty() && displayOptions) {
|
||||
if (!optionButtons.isEmpty() && displayOptions) {
|
||||
for (button in optionButtons) button.showAlt = hasShiftDown()
|
||||
}
|
||||
|
||||
super.render(context, mouseX, mouseY, delta)
|
||||
|
||||
getSelectionArea()?.let { renderSelectionBox(context, it) }
|
||||
|
||||
// Item Tooltip
|
||||
if (pokemon != null) {
|
||||
val displayedItem = if (showCosmeticItem) pokemon.cosmeticItem else pokemon.heldItemNoCopy()
|
||||
@@ -801,7 +703,7 @@ class PCGUI(
|
||||
}
|
||||
|
||||
fun closeNormally(unlink: Boolean = true) {
|
||||
if (!silent) playSound(CobblemonSounds.PC_OFF)
|
||||
playSound(CobblemonSounds.PC_OFF)
|
||||
Minecraft.getInstance().setScreen(null)
|
||||
if (unlink) {
|
||||
UnlinkPlayerFromPCPacket().sendToServer()
|
||||
@@ -809,20 +711,6 @@ class PCGUI(
|
||||
}
|
||||
|
||||
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
storageWidget.clearPendingStorageSlotClick()
|
||||
selectionDragActive = false
|
||||
val clickX = mouseX.toInt()
|
||||
val clickY = mouseY.toInt()
|
||||
if (storageWidget.multiSelection == null && button == 0 &&
|
||||
getBoxGridBounds().contains(clickX, clickY) &&
|
||||
storageWidget.pastureWidget == null) {
|
||||
dragStartX = clickX
|
||||
dragStartY = clickY
|
||||
} else {
|
||||
dragStartX = null
|
||||
dragStartY = null
|
||||
}
|
||||
|
||||
// Trigger child component function as they will need to check the entire screen click area
|
||||
if (::boxNameWidget.isInitialized) boxNameWidget.mouseClicked(mouseX, mouseY, button)
|
||||
return super.mouseClicked(mouseX, mouseY, button)
|
||||
@@ -851,50 +739,16 @@ class PCGUI(
|
||||
storageWidget.pastureWidget?.let { pasture ->
|
||||
if (pasture.pastureScrollList.isHovered) pasture.pastureScrollList.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
|
||||
}
|
||||
if (button == 0 && storageWidget.pastureWidget == null) {
|
||||
val startX = dragStartX
|
||||
val startY = dragStartY
|
||||
if (startX != null && startY != null) {
|
||||
val offsetX = mouseX - startX
|
||||
val offsetY = mouseY - startY
|
||||
if (offsetX * offsetX + offsetY * offsetY >= 25.0) {
|
||||
selectionDragActive = true
|
||||
storageWidget.clearPendingStorageSlotClick()
|
||||
dragEndX = mouseX.toInt()
|
||||
dragEndY = mouseY.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
|
||||
}
|
||||
|
||||
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
val handled = super.mouseReleased(mouseX, mouseY, button)
|
||||
if (button == 0) {
|
||||
if (selectionDragActive) {
|
||||
dragEndX = mouseX.toInt()
|
||||
dragEndY = mouseY.toInt()
|
||||
storageWidget.updateSelection()
|
||||
storageWidget.clearPendingStorageSlotClick()
|
||||
} else {
|
||||
storageWidget.completePendingStorageSlotClick(mouseX, mouseY)
|
||||
}
|
||||
}
|
||||
dragStartX = null
|
||||
dragStartY = null
|
||||
dragEndX = null
|
||||
dragEndY = null
|
||||
selectionDragActive = false
|
||||
return handled
|
||||
}
|
||||
|
||||
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
|
||||
val boxNameFocused = this::boxNameWidget.isInitialized && boxNameWidget.isFocused
|
||||
val filterFocused = this::filterWidget.isInitialized && filterWidget.isFocused
|
||||
|
||||
if (isInventoryKeyPressed(minecraft, keyCode, scanCode) && !boxNameFocused && !filterFocused) {
|
||||
saveMarkings(isPreviewInParty ?: false)
|
||||
if (!silent) playSound(CobblemonSounds.PC_OFF)
|
||||
playSound(CobblemonSounds.PC_OFF)
|
||||
UnlinkPlayerFromPCPacket().sendToServer()
|
||||
Minecraft.getInstance().setScreen(null)
|
||||
return true
|
||||
@@ -905,7 +759,7 @@ class PCGUI(
|
||||
if (!filterFocused && !boxNameFocused) {
|
||||
when (keyCode) {
|
||||
InputConstants.KEY_ESCAPE -> {
|
||||
if (!silent) playSound(CobblemonSounds.PC_OFF)
|
||||
playSound(CobblemonSounds.PC_OFF)
|
||||
UnlinkPlayerFromPCPacket().sendToServer()
|
||||
onClose()
|
||||
return true
|
||||
@@ -977,4 +831,4 @@ class PCGUI(
|
||||
markingsWidget.setActivePokemon(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.cobblemon.mod.common.client.gui.pc
|
||||
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
|
||||
data class PCMultiSelection(
|
||||
private val grid: MutableList<MutableList<Pokemon?>> = mutableListOf(),
|
||||
val sourceBox: Int = 0,
|
||||
val sourceColumn: Int = 0,
|
||||
val sourceRow: Int = 0
|
||||
) {
|
||||
fun getHorizontalSize(): Int = grid.firstOrNull()?.size ?: 0
|
||||
fun getVerticalSize(): Int = grid.size
|
||||
|
||||
constructor(horizontalSize: Int, verticalSize: Int, sourceBox: Int, sourceColumn: Int, sourceRow: Int) : this(
|
||||
MutableList(verticalSize) { MutableList<Pokemon?>(horizontalSize) { null } },
|
||||
sourceBox,
|
||||
sourceColumn,
|
||||
sourceRow
|
||||
)
|
||||
|
||||
fun getPokemon(x: Int, y: Int): Pokemon? {
|
||||
return grid[y][x]
|
||||
}
|
||||
|
||||
fun setPokemon(x: Int, y: Int, pokemon: Pokemon?) {
|
||||
grid[y][x] = pokemon
|
||||
}
|
||||
|
||||
fun pokemonEntries(): List<Triple<Int, Int, Pokemon>> {
|
||||
return grid.flatMapIndexed { y, row ->
|
||||
row.mapIndexedNotNull { x, pokemon -> pokemon?.let { Triple(x, y, it) } }
|
||||
}
|
||||
}
|
||||
|
||||
fun getPokemonCount(): Int = pokemonEntries().size
|
||||
|
||||
}
|
||||
@@ -45,13 +45,11 @@ class ReleaseButton(
|
||||
vOffset = if (isHovered(mouseX.toDouble(), mouseY.toDouble())) HEIGHT else 0,
|
||||
textureHeight = HEIGHT * 2
|
||||
)
|
||||
val pokemonCount = parent.multiSelection?.getPokemonCount() ?: 0
|
||||
|
||||
drawScaledText(
|
||||
context = context,
|
||||
font = CobblemonResources.DEFAULT_LARGE,
|
||||
text = if (pokemonCount > 1)
|
||||
{ lang("ui.pc.release_all").bold() } else { lang("ui.pc.release").bold() },
|
||||
text = lang("ui.pc.release").bold(),
|
||||
x = x + (WIDTH / 2),
|
||||
y = y + 3.5,
|
||||
centered = true,
|
||||
|
||||
@@ -10,11 +10,10 @@ package com.cobblemon.mod.common.client.gui.pc
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.gui.blitk
|
||||
import com.cobblemon.mod.common.client.CobblemonClient
|
||||
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
|
||||
import com.cobblemon.mod.common.client.gui.PokemonGUIAnimationStyle
|
||||
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
|
||||
import com.cobblemon.mod.common.client.gui.pasture.PasturePCGUIConfiguration
|
||||
import com.cobblemon.mod.common.client.gui.PokemonGUIAnimationStyle
|
||||
import com.cobblemon.mod.common.client.render.drawScaledText
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
|
||||
import com.cobblemon.mod.common.client.render.renderScaledGuiItemIcon
|
||||
@@ -23,19 +22,13 @@ import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
|
||||
import juuxel.adorn.util.Colors
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.gui.GuiGraphics
|
||||
import net.minecraft.client.gui.components.Button
|
||||
import net.minecraft.client.renderer.RenderType
|
||||
import net.minecraft.client.sounds.SoundManager
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.util.Mth
|
||||
import org.joml.Quaternionf
|
||||
import org.joml.Vector3f
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
open class StorageSlot(
|
||||
x: Int, y: Int,
|
||||
@@ -61,17 +54,9 @@ open class StorageSlot(
|
||||
}
|
||||
|
||||
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
val selectionArea = parent.pcGui.getSelectionArea()
|
||||
if (selectionArea != null && parent.getSelectedPokemon(selectionArea).contains(this)) {
|
||||
val highlightColor = 0x40FFFFFF
|
||||
context.fill(x + 1, y, x + (SIZE - 1), y + SIZE, highlightColor)
|
||||
context.fill(x, y + 1, x + 1, y + (SIZE - 1), highlightColor)
|
||||
context.fill(x + SIZE - 1, y + 1, x + SIZE, y + (SIZE - 1), highlightColor)
|
||||
}
|
||||
if (shouldRender()) {
|
||||
renderSlot(context, x, y, delta)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun renderSlot(context: GuiGraphics, posX: Int, posY: Int, partialTicks: Float) {
|
||||
@@ -145,23 +130,6 @@ open class StorageSlot(
|
||||
matrixStack = matrices
|
||||
)
|
||||
}
|
||||
|
||||
// Pokemon health
|
||||
// Taken from vanilla durability rendering
|
||||
val currentHealth = pokemon.currentHealth
|
||||
val maxHealth = pokemon.maxHealth.toFloat()
|
||||
if (currentHealth < maxHealth && CobblemonClient.pcHealthToggle) {
|
||||
val damageValue = maxHealth - currentHealth
|
||||
val f = max(0.0f, (maxHealth - (maxHealth - currentHealth)) / maxHealth)
|
||||
val color = Mth.hsvToRgb(f / 3.0f, 1.0f, 1.0f)
|
||||
val totalWidth = 12
|
||||
val width = (totalWidth - damageValue * totalWidth / pokemon.maxHealth.toFloat()).roundToInt();
|
||||
val k = posX + 1
|
||||
val l = posY + 6
|
||||
val bgColor = if (this is PartyStorageSlot) Colors.BLACK else 0xFF3E3E3E.toInt()
|
||||
context.fill(RenderType.guiOverlay(), k, l, k + totalWidth, l + 1, bgColor)
|
||||
context.fill(RenderType.guiOverlay(), k, l, k + width, l + 1, color or bgColor)
|
||||
}
|
||||
matrices.popPose()
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ package com.cobblemon.mod.common.client.gui.pc
|
||||
import com.cobblemon.mod.common.CobblemonNetwork
|
||||
import com.cobblemon.mod.common.CobblemonSounds
|
||||
import com.cobblemon.mod.common.api.gui.blitk
|
||||
import com.cobblemon.mod.common.api.storage.StorePosition
|
||||
import com.cobblemon.mod.common.api.storage.party.PartyPosition
|
||||
import com.cobblemon.mod.common.api.storage.pc.PCPosition
|
||||
import com.cobblemon.mod.common.api.text.bold
|
||||
@@ -38,9 +39,8 @@ import net.minecraft.client.gui.components.Button
|
||||
import net.minecraft.client.gui.screens.Screen
|
||||
import net.minecraft.client.resources.sounds.SimpleSoundInstance
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.sounds.SoundEvent
|
||||
import java.util.UUID
|
||||
import com.cobblemon.mod.common.net.messages.server.storage.pc.MovePCPokemonBatchPacket.PCPokemonMove
|
||||
|
||||
class StorageWidget(
|
||||
pX: Int, pY: Int,
|
||||
@@ -77,14 +77,9 @@ class StorageWidget(
|
||||
|
||||
var displayConfirmRelease = false
|
||||
var screenLoaded = false
|
||||
var selectedPosition: PartyPosition? = null
|
||||
var selectedPosition: StorePosition? = null
|
||||
var grabbedSlot: GrabbedStorageSlot? = null
|
||||
|
||||
var multiSelection: PCMultiSelection? = null
|
||||
private val multiGrabbedSlots = mutableMapOf<UUID, GrabbedStorageSlot>()
|
||||
private var pendingStorageSlotClick: Button? = null
|
||||
private var movePending = false
|
||||
|
||||
var box = 0
|
||||
set(value) {
|
||||
field =
|
||||
@@ -118,22 +113,16 @@ class StorageWidget(
|
||||
subKey = "ui.generic.yes",
|
||||
onPress = {
|
||||
if (canDeleteSelected() && displayConfirmRelease) {
|
||||
val selection = multiSelection
|
||||
if (selection != null) {
|
||||
selection.pokemonEntries().forEach { (column, row, pokemon) ->
|
||||
val position = PCPosition(
|
||||
selection.sourceBox,
|
||||
(selection.sourceRow + row) * 6 + selection.sourceColumn + column
|
||||
)
|
||||
CobblemonNetwork.sendToServer(ReleasePCPokemonPacket(pokemon.uuid, position))
|
||||
}
|
||||
} else {
|
||||
val position = selectedPosition ?: return@ReleaseConfirmButton
|
||||
val pokemon = getSelectedPokemon() ?: return@ReleaseConfirmButton
|
||||
val position = selectedPosition ?: return@ReleaseConfirmButton
|
||||
val pokemon = getSelectedPokemon() ?: return@ReleaseConfirmButton
|
||||
|
||||
CobblemonNetwork.sendToServer(ReleasePartyPokemonPacket(pokemon.uuid, position))
|
||||
val packet = when (position) {
|
||||
is PartyPosition -> ReleasePartyPokemonPacket(pokemon.uuid, position)
|
||||
is PCPosition -> ReleasePCPokemonPacket(pokemon.uuid, position)
|
||||
else -> return@ReleaseConfirmButton
|
||||
}
|
||||
|
||||
CobblemonNetwork.sendToServer(packet)
|
||||
playSound(CobblemonSounds.PC_RELEASE)
|
||||
resetSelected()
|
||||
displayConfirmRelease = false
|
||||
@@ -160,8 +149,9 @@ class StorageWidget(
|
||||
}
|
||||
|
||||
fun canDeleteSelected(): Boolean {
|
||||
multiSelection?.let { return it.pokemonEntries().isNotEmpty() }
|
||||
return party.filterNotNull().size > 1 && selectedPosition != null && grabbedSlot != null
|
||||
return !(selectedPosition is PartyPosition && party.filterNotNull().size <= 1)
|
||||
&& selectedPosition != null
|
||||
&& grabbedSlot != null
|
||||
}
|
||||
|
||||
fun setupStorageSlots() {
|
||||
@@ -180,7 +170,7 @@ class StorageWidget(
|
||||
parent = this,
|
||||
pc = pc,
|
||||
position = PCPosition(box, index),
|
||||
onPress = { pendingStorageSlotClick = it }
|
||||
onPress = { this.onStorageSlotClicked(it) }
|
||||
).also { widget ->
|
||||
this.addWidget(widget)
|
||||
this.boxSlots.add(widget)
|
||||
@@ -210,7 +200,7 @@ class StorageWidget(
|
||||
parent = this,
|
||||
party = party,
|
||||
position = PartyPosition(partyIndex),
|
||||
onPress = { pendingStorageSlotClick = it }
|
||||
onPress = { this.onStorageSlotClicked(it) }
|
||||
).also { widget ->
|
||||
this.addWidget(widget)
|
||||
this.partySlots.add(widget)
|
||||
@@ -220,117 +210,14 @@ class StorageWidget(
|
||||
|
||||
private fun getSelectedPokemon(): Pokemon? {
|
||||
val selectedPosition = this.selectedPosition ?: return null
|
||||
return party.get(selectedPosition)
|
||||
}
|
||||
|
||||
fun getSelectedPokemon(area: PCGUI.SelectionArea): List<BoxStorageSlot> {
|
||||
return boxSlots.filter { slot ->
|
||||
slot.x < area.right && slot.x + StorageSlot.SIZE > area.left &&
|
||||
slot.y < area.bottom && slot.y + StorageSlot.SIZE > area.top &&
|
||||
slot.getPokemon()?.let(pcGui.search::passes) == true
|
||||
return when (selectedPosition) {
|
||||
is PCPosition -> pc.get(selectedPosition)
|
||||
is PartyPosition -> party.get(selectedPosition)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun clearPendingStorageSlotClick() {
|
||||
pendingStorageSlotClick = null
|
||||
}
|
||||
|
||||
fun completePendingStorageSlotClick(mouseX: Double, mouseY: Double) {
|
||||
val button = pendingStorageSlotClick ?: return
|
||||
pendingStorageSlotClick = null
|
||||
if (button.isMouseOver(mouseX, mouseY)) {
|
||||
onStorageSlotClicked(button)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSelection() {
|
||||
val area = pcGui.getSelectionArea()
|
||||
if (area == null) {
|
||||
multiSelection = null
|
||||
return
|
||||
}
|
||||
|
||||
selectedPosition = null
|
||||
grabbedSlot = null
|
||||
pcGui.setPreviewPokemon(null)
|
||||
|
||||
val selectedSlots = getSelectedPokemon(area)
|
||||
if (selectedSlots.isEmpty()) {
|
||||
multiSelection = null
|
||||
return
|
||||
}
|
||||
|
||||
val minColumn = selectedSlots.minOf { it.position.slot % 6 }
|
||||
val maxColumn = selectedSlots.maxOf { it.position.slot % 6 }
|
||||
val minRow = selectedSlots.minOf { it.position.slot / 6 }
|
||||
val maxRow = selectedSlots.maxOf { it.position.slot / 6 }
|
||||
val selection = PCMultiSelection(maxColumn - minColumn + 1, maxRow - minRow + 1, box, minColumn, minRow)
|
||||
|
||||
selectedSlots.forEach { slot ->
|
||||
val pokemon = slot.getPokemon() ?: return@forEach
|
||||
val slotIndex = slot.position.slot
|
||||
val column = slotIndex % 6
|
||||
val row = slotIndex / 6
|
||||
if (column in minColumn..maxColumn && row in minRow..maxRow) {
|
||||
selection.setPokemon(column - minColumn, row - minRow, pokemon)
|
||||
}
|
||||
}
|
||||
playSound(CobblemonSounds.PC_GRAB)
|
||||
|
||||
multiSelection = selection
|
||||
}
|
||||
|
||||
private fun moveMultiSelectionTo(destination: PCPosition): Boolean {
|
||||
val selection = multiSelection ?: return false
|
||||
|
||||
val width = selection.getHorizontalSize()
|
||||
val height = selection.getVerticalSize()
|
||||
val destinationColumn = destination.slot % 6
|
||||
val destinationRow = destination.slot / 6
|
||||
if (destinationColumn + width > 6 || destinationRow + height > 5) return false
|
||||
|
||||
val moves = mutableListOf<PCPokemonMove>()
|
||||
val selectedSources = mutableSetOf<PCPosition>()
|
||||
for (row in 0 until height) {
|
||||
for (column in 0 until width) {
|
||||
val pokemon = selection.getPokemon(column, row) ?: continue
|
||||
val source = PCPosition(selection.sourceBox, (selection.sourceRow + row) * 6 + selection.sourceColumn + column)
|
||||
val target = PCPosition(destination.box, (destinationRow + row) * 6 + destinationColumn + column)
|
||||
selectedSources.add(source)
|
||||
moves.add(PCPokemonMove(pokemon.uuid, source, target))
|
||||
}
|
||||
}
|
||||
|
||||
if (moves.isEmpty()) return false
|
||||
|
||||
for (row in 0 until height) {
|
||||
for (column in 0 until width) {
|
||||
if (selection.getPokemon(column, row) == null) continue
|
||||
val target = PCPosition(destination.box, (destinationRow + row) * 6 + destinationColumn + column)
|
||||
val displacedPokemon = pc.get(target) ?: continue
|
||||
if (target in selectedSources) continue
|
||||
|
||||
val displacedSource = PCPosition(
|
||||
selection.sourceBox,
|
||||
(selection.sourceRow + row) * 6 + selection.sourceColumn + column
|
||||
)
|
||||
if (pc.get(displacedSource) != null && displacedSource !in selectedSources) return false
|
||||
moves.add(PCPokemonMove(displacedPokemon.uuid, target, displacedSource))
|
||||
}
|
||||
}
|
||||
|
||||
if (moves.map { it.newPosition }.toSet().size != moves.size) return false
|
||||
MovePCPokemonBatchPacket(moves).sendToServer()
|
||||
return true
|
||||
}
|
||||
|
||||
fun isInMultiSelection(pokemon: Pokemon, position: PCPosition): Boolean {
|
||||
val selection = multiSelection ?: return false
|
||||
return selection.sourceBox == position.box && selection.pokemonEntries().any { it.third == pokemon }
|
||||
}
|
||||
|
||||
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
updatePendingMove()
|
||||
val matrices = context.pose()
|
||||
// Party Label
|
||||
if (pcGui.configuration.showParty) {
|
||||
@@ -353,14 +240,11 @@ class StorageWidget(
|
||||
shadow = true
|
||||
)
|
||||
|
||||
val pokemonCount = multiSelection?.getPokemonCount() ?: 0
|
||||
|
||||
if (canDeleteSelected() && displayConfirmRelease) {
|
||||
drawScaledText(
|
||||
context = context,
|
||||
font = CobblemonResources.DEFAULT_LARGE,
|
||||
text = if (pokemonCount > 1)
|
||||
{ lang("ui.pc.release_all").bold() } else { lang("ui.pc.release").bold() },
|
||||
text = lang("ui.pc.release").bold(),
|
||||
x = x + 223,
|
||||
y = y + 119,
|
||||
centered = true
|
||||
@@ -424,7 +308,6 @@ class StorageWidget(
|
||||
slot.render(context, mouseX, mouseY, delta)
|
||||
val pokemon = slot.getPokemon()
|
||||
if (grabbedSlot == null && slot.isHovered(mouseX, mouseY)
|
||||
&& multiSelection == null
|
||||
&& pokemon != null && pokemon != pcGui.previewPokemon
|
||||
) {
|
||||
pcGui.setPreviewPokemon(pokemon, pcGui.isPreviewInParty ?: false)
|
||||
@@ -441,7 +324,6 @@ class StorageWidget(
|
||||
slot.render(context, mouseX, mouseY, delta)
|
||||
val pokemon = slot.getPokemon()
|
||||
if (grabbedSlot == null && slot.isHovered(mouseX, mouseY)
|
||||
&& multiSelection == null
|
||||
&& pokemon != null && pokemon != pcGui.previewPokemon
|
||||
) {
|
||||
pcGui.setPreviewPokemon(pokemon, pcGui.isPreviewInParty ?: true)
|
||||
@@ -453,32 +335,10 @@ class StorageWidget(
|
||||
pastureWidget?.render(context, mouseX, mouseY, delta)
|
||||
matrices.pushPose()
|
||||
matrices.translate(0.0, 0.0, 1000.0)
|
||||
renderSelectedPokemon(context, mouseX, mouseY, delta)
|
||||
grabbedSlot?.render(context, mouseX, mouseY, delta)
|
||||
matrices.popPose()
|
||||
}
|
||||
|
||||
private fun renderSelectedPokemon(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
if (movePending) return
|
||||
val selection = multiSelection
|
||||
if (selection == null) {
|
||||
grabbedSlot?.render(context, mouseX, mouseY, delta)
|
||||
return
|
||||
}
|
||||
|
||||
val slotOffset = StorageSlot.SIZE + BOX_SLOT_PADDING
|
||||
selection.pokemonEntries().forEach { (column, row, pokemon) ->
|
||||
val grabbed = multiGrabbedSlots.getOrPut(pokemon.uuid) {
|
||||
GrabbedStorageSlot(0, 0, this, pokemon)
|
||||
}
|
||||
grabbed.render(
|
||||
context,
|
||||
mouseX + column * slotOffset,
|
||||
mouseY + row * slotOffset,
|
||||
delta
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseClicked(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
|
||||
if (displayConfirmRelease) {
|
||||
if (releaseYesButton.isHovered(pMouseX, pMouseY)) releaseYesButton.mouseClicked(pMouseX, pMouseY, pButton)
|
||||
@@ -511,55 +371,23 @@ class StorageWidget(
|
||||
else -> return
|
||||
}
|
||||
|
||||
val clickedPokemon = when (clickedPosition) {
|
||||
is PCPosition -> pc.get(clickedPosition)
|
||||
is PartyPosition -> party.get(clickedPosition)
|
||||
else -> null
|
||||
}
|
||||
|
||||
multiSelection?.let { selection ->
|
||||
when (clickedPosition) {
|
||||
is PCPosition -> {
|
||||
if (clickedPosition.box == selection.sourceBox &&
|
||||
clickedPosition.slot == selection.sourceRow * 6 + selection.sourceColumn
|
||||
) {
|
||||
resetSelected()
|
||||
} else if (moveMultiSelectionTo(clickedPosition)) {
|
||||
playSound(CobblemonSounds.PC_DROP)
|
||||
movePending = true
|
||||
}
|
||||
}
|
||||
is PartyPosition -> {
|
||||
val selectedPokemon = selection.pokemonEntries().singleOrNull() ?: return
|
||||
val (column, row, pokemon) = selectedPokemon
|
||||
val source = PCPosition(
|
||||
selection.sourceBox,
|
||||
(selection.sourceRow + row) * 6 + selection.sourceColumn + column
|
||||
)
|
||||
if (pc.get(source) !== pokemon) return
|
||||
val packet = clickedPokemon?.let {
|
||||
SwapPCPartyPokemonPacket(it.uuid, clickedPosition, pokemon.uuid, source)
|
||||
} ?: MovePCPokemonToPartyPacket(pokemon.uuid, source, clickedPosition)
|
||||
packet.sendToServer()
|
||||
playSound(CobblemonSounds.PC_DROP)
|
||||
movePending = true
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Reset release confirmation
|
||||
displayConfirmRelease = false
|
||||
|
||||
// Clicking on itself, so unselect position
|
||||
if (selectedPosition == clickedPosition) {
|
||||
if (this.selectedPosition != null && this.selectedPosition == clickedPosition) {
|
||||
if (grabbedSlot != null) playSound(CobblemonSounds.PC_DROP)
|
||||
resetSelected()
|
||||
return
|
||||
}
|
||||
|
||||
// Check for selecting a Pokémon
|
||||
val clickedPokemon = when(button) {
|
||||
is BoxStorageSlot -> pc.get(clickedPosition as PCPosition)
|
||||
is PartyStorageSlot -> party.get(clickedPosition as PartyPosition)
|
||||
else -> null
|
||||
}
|
||||
|
||||
val selectOverride = pcGui.configuration.selectOverride
|
||||
if (selectOverride != null) {
|
||||
selectOverride(pcGui, clickedPosition, clickedPokemon)
|
||||
@@ -592,47 +420,59 @@ class StorageWidget(
|
||||
}
|
||||
}
|
||||
|
||||
if (clickedPosition is PCPosition) {
|
||||
val column = clickedPosition.slot % 6
|
||||
val row = clickedPosition.slot / 6
|
||||
multiSelection = PCMultiSelection(1, 1, clickedPosition.box, column, row).also {
|
||||
it.setPokemon(0, 0, clickedPokemon)
|
||||
}
|
||||
playSound(CobblemonSounds.PC_GRAB)
|
||||
return
|
||||
} else if (clickedPosition is PartyPosition) {
|
||||
this.selectedPosition = clickedPosition
|
||||
this.pcGui.setPreviewPokemon(clickedPokemon, pcGui.isPreviewInParty ?: false)
|
||||
grabbedSlot = GrabbedStorageSlot(
|
||||
x = button.x,
|
||||
y = button.y,
|
||||
parent = this,
|
||||
pokemon = clickedPokemon
|
||||
)
|
||||
playSound(CobblemonSounds.PC_GRAB)
|
||||
}
|
||||
this.selectedPosition = clickedPosition
|
||||
this.pcGui.setPreviewPokemon(clickedPokemon, pcGui.isPreviewInParty ?: false)
|
||||
grabbedSlot = GrabbedStorageSlot(
|
||||
x = button.x,
|
||||
y = button.y,
|
||||
parent = this,
|
||||
pokemon = clickedPokemon
|
||||
)
|
||||
playSound(CobblemonSounds.PC_GRAB)
|
||||
|
||||
}
|
||||
} else {
|
||||
val sourcePosition = selectedPosition ?: return
|
||||
val selectedPokemon = party.get(sourcePosition) ?: return
|
||||
// Handle movement within the Box
|
||||
val selectedPokemon = when(this.selectedPosition) {
|
||||
is PCPosition -> pc.get(this.selectedPosition as PCPosition)
|
||||
is PartyPosition -> party.get(this.selectedPosition as PartyPosition)
|
||||
else -> null
|
||||
} ?: return
|
||||
|
||||
// Box to Box
|
||||
if (this.selectedPosition is PCPosition && clickedPosition is PCPosition) {
|
||||
val packet = clickedPokemon?.let { SwapPCPokemonPacket(it.uuid, clickedPosition, selectedPokemon.uuid, this.selectedPosition as PCPosition) }
|
||||
?: MovePCPokemonPacket(selectedPokemon.uuid, selectedPosition as PCPosition, clickedPosition)
|
||||
packet.sendToServer()
|
||||
playSound(CobblemonSounds.PC_DROP)
|
||||
resetSelected()
|
||||
}
|
||||
// Box to Party
|
||||
else if (this.selectedPosition is PCPosition && clickedPosition is PartyPosition) {
|
||||
val packet = clickedPokemon?.let { SwapPCPartyPokemonPacket(clickedPokemon.uuid, clickedPosition, selectedPokemon.uuid, this.selectedPosition as PCPosition) }
|
||||
?: MovePCPokemonToPartyPacket(selectedPokemon.uuid, this.selectedPosition as PCPosition, clickedPosition)
|
||||
packet.sendToServer()
|
||||
playSound(CobblemonSounds.PC_DROP)
|
||||
resetSelected()
|
||||
}
|
||||
// Party to Box
|
||||
if (clickedPosition is PCPosition) {
|
||||
else if (this.selectedPosition is PartyPosition && clickedPosition is PCPosition) {
|
||||
if (ServerSettings.preventCompletePartyDeposit && this.party.filterNotNull().size == 1 && clickedPokemon == null) {
|
||||
return
|
||||
}
|
||||
val packet = clickedPokemon?.let { SwapPCPartyPokemonPacket(selectedPokemon.uuid, sourcePosition, clickedPokemon.uuid, clickedPosition) }
|
||||
?: MovePartyPokemonToPCPacket(selectedPokemon.uuid, sourcePosition, clickedPosition)
|
||||
val packet = clickedPokemon?.let { SwapPCPartyPokemonPacket(selectedPokemon.uuid, this.selectedPosition as PartyPosition, clickedPokemon.uuid, clickedPosition) }
|
||||
?: MovePartyPokemonToPCPacket(selectedPokemon.uuid, this.selectedPosition as PartyPosition, clickedPosition)
|
||||
packet.sendToServer()
|
||||
playSound(CobblemonSounds.PC_DROP)
|
||||
movePending = true
|
||||
resetSelected()
|
||||
}
|
||||
// Party to Party
|
||||
else if (clickedPosition is PartyPosition) {
|
||||
val packet = clickedPokemon?.let { SwapPartyPokemonPacket(it.uuid, clickedPosition, selectedPokemon.uuid, sourcePosition) }
|
||||
?: MovePartyPokemonPacket(selectedPokemon.uuid, sourcePosition, clickedPosition)
|
||||
else if (this.selectedPosition is PartyPosition && clickedPosition is PartyPosition) {
|
||||
val packet = clickedPokemon?.let { SwapPartyPokemonPacket(it.uuid, clickedPosition, selectedPokemon.uuid, this.selectedPosition as PartyPosition) }
|
||||
?: MovePartyPokemonPacket(selectedPokemon.uuid, selectedPosition as PartyPosition, clickedPosition)
|
||||
packet.sendToServer()
|
||||
playSound(CobblemonSounds.PC_DROP)
|
||||
movePending = true
|
||||
resetSelected()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -640,36 +480,6 @@ class StorageWidget(
|
||||
fun resetSelected() {
|
||||
selectedPosition = null
|
||||
grabbedSlot = null
|
||||
multiSelection = null
|
||||
multiGrabbedSlots.clear()
|
||||
pcGui.setPreviewPokemon(null)
|
||||
}
|
||||
|
||||
// annoyingly complex solution, but server/client desync causes flashing when moving pokemon. this should fix it
|
||||
// if you can think of a simpler solution, feel free to implement it
|
||||
private fun updatePendingMove() {
|
||||
if (!movePending) return
|
||||
val selection = multiSelection
|
||||
|
||||
if (selection != null) {
|
||||
val updated = selection.pokemonEntries().any { (column, row, pokemon) ->
|
||||
val source = PCPosition(selection.sourceBox, (selection.sourceRow + row) * 6 + selection.sourceColumn + column)
|
||||
pc.get(source)?.uuid != pokemon.uuid
|
||||
}
|
||||
if (updated) {
|
||||
movePending = false
|
||||
resetSelected()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val source = selectedPosition
|
||||
val pokemon = grabbedSlot?.getPokemon()
|
||||
if (source != null && pokemon != null) {
|
||||
if (party.get(source)?.uuid != pokemon.uuid) {
|
||||
movePending = false
|
||||
resetSelected()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.BASE_HEIG
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.BASE_WIDTH
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_HEIGHT
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HEADER_BAR_HEIGHT
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.ICONS_ARROW_PADDING_AROUND
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.ICONS_PADDING_AROUND
|
||||
@@ -51,7 +52,8 @@ import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_MOVES
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_SIZE
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_STATS
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.widgets.*
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.widgets.EvolutionTreeWidget
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.widgets.evolutiontree.EvolutionTreeWidget
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.widgets.evolutiontree.debugEvolutionRequirementRendering
|
||||
import com.cobblemon.mod.common.client.pokedex.PokedexType
|
||||
import com.cobblemon.mod.common.client.render.drawScaledText
|
||||
import com.cobblemon.mod.common.net.messages.server.block.AdjustBlockEntityViewerCountPacket
|
||||
@@ -192,7 +194,6 @@ class PokedexGUI private constructor(
|
||||
public override fun init() {
|
||||
super.init()
|
||||
clearWidgets()
|
||||
evolutionTreeExpanded = false
|
||||
|
||||
availableRegions = Dexes.dexEntryMap.keys.toList()
|
||||
selectedRegionIndex = 0
|
||||
@@ -827,6 +828,10 @@ class PokedexGUI private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
if (tabInfoIndex == TAB_EVOLUTION_TREE && keyCode == InputConstants.KEY_SEMICOLON) {
|
||||
debugEvolutionRequirementRendering()
|
||||
}
|
||||
|
||||
return super.keyPressed(keyCode, scanCode, modifiers)
|
||||
}
|
||||
|
||||
|
||||
+2
-23
@@ -16,17 +16,13 @@ import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.mojang.blaze3d.vertex.PoseStack
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.gui.GuiGraphics
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.network.chat.MutableComponent
|
||||
import net.minecraft.network.chat.Style
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import java.util.Optional
|
||||
|
||||
private val tooltipEdge = cobblemonResource("textures/gui/pokedex/tooltip_edge.png")
|
||||
private val tooltipBackground = cobblemonResource("textures/gui/pokedex/tooltip_background.png")
|
||||
|
||||
private const val TOOLTIP_SLICE_SIZE = 11
|
||||
private const val TOOLTIP_MAX_TEXT_WIDTH = 160
|
||||
|
||||
fun renderTooltip(
|
||||
context: GuiGraphics,
|
||||
@@ -53,12 +49,7 @@ fun renderTooltip(
|
||||
posY: Int,
|
||||
delta: Float,
|
||||
offsetY: Int = 0,
|
||||
maxTextWidth: Int = TOOLTIP_MAX_TEXT_WIDTH,
|
||||
) {
|
||||
val text = text.flatMap { wrapTooltipLine(it, maxTextWidth) }
|
||||
// Only a tooltip that fits on a single line is centered; wrapped text is left-aligned
|
||||
val centered = text.size == 1
|
||||
|
||||
val poseStack = context.pose()
|
||||
val textWidth = text.maxOf { Minecraft.getInstance().font.width(it.font(CobblemonResources.DEFAULT_LARGE)) }
|
||||
val tooltipPadding = 6
|
||||
@@ -87,6 +78,8 @@ fun renderTooltip(
|
||||
blitSlicedVertical(poseStack, tooltipBackground, posX - (tooltipWidth / 2), tooltipTop, tooltipWidth, totalHeight)
|
||||
blitSlicedVertical(poseStack, tooltipEdge, posX + (tooltipWidth / 2), tooltipTop, 1, totalHeight)
|
||||
|
||||
val centered = text.size == 1
|
||||
|
||||
text.forEachIndexed { index, it ->
|
||||
drawScaledText(
|
||||
context = context,
|
||||
@@ -102,20 +95,6 @@ fun renderTooltip(
|
||||
poseStack.popPose()
|
||||
}
|
||||
|
||||
private fun wrapTooltipLine(line: MutableComponent, maxWidth: Int): List<MutableComponent> {
|
||||
val styled = line.copy().font(CobblemonResources.DEFAULT_LARGE)
|
||||
if (Minecraft.getInstance().font.width(styled) <= maxWidth) return listOf(line)
|
||||
|
||||
return Minecraft.getInstance().font.splitter.splitLines(styled, maxWidth, Style.EMPTY).map { wrapped ->
|
||||
val component = Component.empty()
|
||||
wrapped.visit({ style, string ->
|
||||
component.append(Component.literal(string).withStyle(style))
|
||||
Optional.empty<Unit>()
|
||||
}, Style.EMPTY)
|
||||
component
|
||||
}
|
||||
}
|
||||
|
||||
// Stretching the whole 11px image over an arbitrary height thickens the caps as more lines are added, so
|
||||
// instead the caps are drawn 1:1 and only the flat fill row is stretched to cover the remaining height
|
||||
private fun blitSlicedVertical(
|
||||
|
||||
+5
-4
@@ -6,7 +6,7 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.pokedex.entry
|
||||
package com.cobblemon.mod.common.client.gui.pokedex.widgets.evolutiontree
|
||||
|
||||
import com.cobblemon.mod.common.api.pokemon.Natures
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
@@ -21,6 +21,7 @@ import com.cobblemon.mod.common.api.text.green
|
||||
import com.cobblemon.mod.common.api.types.ElementalTypes
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import com.cobblemon.mod.common.util.asTranslated
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.locale.Language
|
||||
@@ -28,14 +29,14 @@ import net.minecraft.network.chat.Component
|
||||
import net.minecraft.network.chat.MutableComponent
|
||||
import net.minecraft.world.item.Items
|
||||
|
||||
fun describePokemonProperties(properties: PokemonProperties): MutableComponent? {
|
||||
internal fun describePokemonProperties(properties: PokemonProperties): MutableComponent? {
|
||||
if (hasUndescribedProperty(properties)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val descriptors = mutableListOf<MutableComponent>()
|
||||
|
||||
properties.species?.let { PokemonSpecies.getByIdentifier(it.asIdentifierDefaultingNamespace()) }?.let {
|
||||
properties.species?.let { PokemonSpecies.getByIdentifier(cobblemonResource(it)) }?.let {
|
||||
descriptors.add(it.translatedName.plainCopy().green())
|
||||
}
|
||||
properties.gender?.let {
|
||||
@@ -135,7 +136,7 @@ private fun describeStringSpeciesFeature(property: StringSpeciesFeature): Mutabl
|
||||
return null
|
||||
}
|
||||
|
||||
val value = PokemonSpecies.getByIdentifier(property.value.asIdentifierDefaultingNamespace())?.translatedName?.plainCopy()
|
||||
val value = PokemonSpecies.getByIdentifier(cobblemonResource(property.value))?.translatedName?.plainCopy()
|
||||
?: Component.literal(property.value)
|
||||
|
||||
return lang(key, value.green())
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.pokedex.widgets.evolutiontree
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.network.chat.Component
|
||||
|
||||
fun debugEvolutionRequirementRendering() {
|
||||
if (!Cobblemon.config.enableDebugKeys) {
|
||||
return
|
||||
}
|
||||
|
||||
val gaps = mutableListOf<Pair<String, String>>()
|
||||
|
||||
for (species in PokemonSpecies.implemented) {
|
||||
val evolutions = species.evolutions + species.forms.flatMap { it.evolutions }
|
||||
for (evolution in evolutions) {
|
||||
if (renderEvolutionData(evolution) == null) {
|
||||
gaps.add("Evolution: ${evolution::class.simpleName}" to species.showdownId())
|
||||
}
|
||||
for (requirement in evolution.requirements) {
|
||||
if (resolveRequirementData(requirement) == null) {
|
||||
gaps.add("Requirement: ${requirement::class.simpleName}" to species.showdownId())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val player = Minecraft.getInstance().player
|
||||
if (gaps.isEmpty()) {
|
||||
player?.sendSystemMessage(Component.literal("Evolution render debug: no gaps found."))
|
||||
return
|
||||
}
|
||||
|
||||
val byType = gaps.groupBy({ it.first }, { it.second })
|
||||
println("Evolution render debug found ${gaps.size} gap(s) across ${byType.size} type(s):")
|
||||
byType.forEach { (type, species) -> println(" $type: ${species.distinct().joinToString(", ")}") }
|
||||
|
||||
player?.sendSystemMessage(Component.literal("Evolution render debug: ${gaps.size} gap(s) across ${byType.size} type(s), see log for details."))
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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.pokedex.widgets.evolutiontree
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonItems
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
|
||||
import com.cobblemon.mod.common.api.pokemon.requirement.Requirement
|
||||
import com.cobblemon.mod.common.api.text.green
|
||||
import com.cobblemon.mod.common.pokemon.evolution.variants.BlockClickEvolution
|
||||
import com.cobblemon.mod.common.pokemon.evolution.variants.DummyEvolution
|
||||
import com.cobblemon.mod.common.pokemon.evolution.variants.ItemInteractionEvolution
|
||||
import com.cobblemon.mod.common.pokemon.evolution.variants.LevelUpEvolution
|
||||
import com.cobblemon.mod.common.pokemon.evolution.variants.TradeEvolution
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.itemRegistry
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.network.chat.MutableComponent
|
||||
import net.minecraft.world.item.Item
|
||||
|
||||
data class RenderEvolutionData(
|
||||
val icons: List<Item>,
|
||||
val tooltips: List<MutableComponent>,
|
||||
)
|
||||
|
||||
internal fun resolveRequirementData(requirement: Requirement): RenderRequirementData? {
|
||||
val icon = requirement.pokedexIcon?.let {
|
||||
Minecraft.getInstance()?.player?.level()?.itemRegistry?.get(it)
|
||||
}
|
||||
|
||||
val tooltip = requirement.pokedexTooltip?.let {
|
||||
Component.translatable(it)
|
||||
}
|
||||
|
||||
if (icon != null && tooltip != null) {
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
return renderRequirementData(requirement)
|
||||
}
|
||||
|
||||
fun renderEvolutionData(evolution: Evolution): RenderEvolutionData? {
|
||||
when (evolution) {
|
||||
is BlockClickEvolution -> return renderBlockClickEvolution(evolution)
|
||||
is DummyEvolution -> return renderDummyEvolution(evolution)
|
||||
is ItemInteractionEvolution -> return renderItemInteractionEvolution(evolution)
|
||||
is LevelUpEvolution -> return renderLevelUpEvolution(evolution)
|
||||
is TradeEvolution -> return renderTradeEvolution(evolution)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun renderBlockClickEvolution(blockClickEvolution: BlockClickEvolution): RenderEvolutionData? {
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO: Use dummyEvolution?
|
||||
fun renderDummyEvolution(evolution: Evolution): RenderEvolutionData? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun renderItemInteractionEvolution(itemInteractionEvolution: ItemInteractionEvolution): RenderEvolutionData? {
|
||||
val item = itemInteractionEvolution.requiredContext.items.get().first().value()
|
||||
val icons = mutableListOf(item)
|
||||
val tooltips =
|
||||
mutableListOf(lang("ui.pokedex.evolution.item_interact.tooltip", item.description.plainCopy().green()))
|
||||
|
||||
val requirementsDataList = itemInteractionEvolution.requirements.mapNotNull { requirement ->
|
||||
resolveRequirementData(requirement)
|
||||
}
|
||||
|
||||
requirementsDataList.forEach { requirement ->
|
||||
icons.add(requirement.icon)
|
||||
tooltips.add(requirement.tooltip)
|
||||
}
|
||||
|
||||
return RenderEvolutionData(icons, tooltips)
|
||||
}
|
||||
|
||||
fun renderLevelUpEvolution(levelUpEvolution: LevelUpEvolution): RenderEvolutionData? {
|
||||
val requirementsDataList = levelUpEvolution.requirements.map { requirement ->
|
||||
resolveRequirementData(requirement)
|
||||
}
|
||||
|
||||
val icons = requirementsDataList.mapNotNull { requirementData -> requirementData?.icon }
|
||||
val tooltips = requirementsDataList.mapNotNull { requirementData -> requirementData?.tooltip }
|
||||
|
||||
return RenderEvolutionData(icons, tooltips)
|
||||
}
|
||||
|
||||
fun renderTradeEvolution(tradeEvolution: TradeEvolution): RenderEvolutionData? {
|
||||
val icons = mutableListOf<Item>(CobblemonItems.LINK_CABLE)
|
||||
val tooltips = mutableListOf<MutableComponent>()
|
||||
|
||||
val species = tradeEvolution.requiredContext.species
|
||||
if (species == null) {
|
||||
tooltips.add(lang("ui.pokedex.evolution.trade.tooltip.with_any_species"))
|
||||
} else {
|
||||
PokemonSpecies.getByIdentifier(cobblemonResource(species))?.let {
|
||||
tooltips.add(lang("ui.pokedex.evolution.trade.tooltip.with_species", it.translatedName.green()))
|
||||
}
|
||||
}
|
||||
|
||||
val requirementsDataList = tradeEvolution.requirements.mapNotNull { requirement ->
|
||||
resolveRequirementData(requirement)
|
||||
}
|
||||
|
||||
requirementsDataList.forEach { requirement ->
|
||||
icons.add(requirement.icon)
|
||||
tooltips.add(requirement.tooltip)
|
||||
}
|
||||
|
||||
return RenderEvolutionData(icons, tooltips)
|
||||
}
|
||||
+379
@@ -0,0 +1,379 @@
|
||||
/*
|
||||
* 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.pokedex.widgets.evolutiontree
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonItems
|
||||
import com.cobblemon.mod.common.api.conditional.RegistryLikeCondition
|
||||
import com.cobblemon.mod.common.api.conditional.RegistryLikeIdentifierCondition
|
||||
import com.cobblemon.mod.common.api.conditional.RegistryLikeTagCondition
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.IntSpeciesFeatureProvider
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeatures
|
||||
import com.cobblemon.mod.common.api.pokemon.requirement.Requirement
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.api.text.add
|
||||
import com.cobblemon.mod.common.api.text.green
|
||||
import com.cobblemon.mod.common.api.text.red
|
||||
import com.cobblemon.mod.common.api.types.ElementalTypes
|
||||
import com.cobblemon.mod.common.pokemon.requirements.*
|
||||
import com.cobblemon.mod.common.util.asTranslated
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.network.chat.MutableComponent
|
||||
import net.minecraft.world.item.Item
|
||||
import net.minecraft.world.item.Items
|
||||
import net.minecraft.world.level.biome.Biome
|
||||
import net.minecraft.world.level.levelgen.structure.Structure
|
||||
|
||||
data class RenderRequirementData(
|
||||
val icon: Item,
|
||||
val tooltip: MutableComponent,
|
||||
)
|
||||
|
||||
fun renderRequirementData(requirement: Requirement): RenderRequirementData? {
|
||||
when (requirement) {
|
||||
is AdvancementRequirement -> return renderAdvancementRequirement(requirement)
|
||||
is AnyRequirement -> return renderAnyRequirement(requirement)
|
||||
is AreaRequirement -> return renderAreaRequirement(requirement)
|
||||
is AttackDefenceRatioRequirement -> return renderAttackDefenceRatioRequirement(requirement)
|
||||
is BattleCriticalHitsRequirement -> return renderBattleCriticalHitsRequirement(requirement)
|
||||
is BiomeRequirement -> return renderBiomeRequirement(requirement)
|
||||
is BlocksTraveledRequirement -> return renderBlocksTraveledRequirement(requirement)
|
||||
is DamageTakenRequirement -> return renderDamageTakenRequirement(requirement)
|
||||
is DefeatRequirement -> return renderDefeatRequirement(requirement)
|
||||
is FriendshipRequirement -> return renderFriendshipRequirement(requirement)
|
||||
is HeldItemRequirement -> return renderHeldItemRequirement(requirement)
|
||||
is LevelRequirement -> return renderLevelRequirement(requirement)
|
||||
is MoonPhaseRequirement -> return renderMoonPhaseRequirement(requirement)
|
||||
is MoveSetRequirement -> return renderMoveSetRequirement(requirement)
|
||||
is MoveTypeRequirement -> return renderMoveTypeRequirement(requirement)
|
||||
is OwnerHoldsItemRequirement -> return renderOwnerHoldsItemRequirement(requirement)
|
||||
is PartyMemberRequirement -> return renderPartyMemberRequirement(requirement)
|
||||
is PokemonPropertiesRequirement -> return renderPokemonPropertiesRequirement(requirement)
|
||||
is PropertyRangeRequirement -> return renderPropertyRangeRequirement(requirement)
|
||||
is RecoilRequirement -> return renderRecoilRequirement(requirement)
|
||||
is StatCompareRequirement -> return renderStatCompareRequirement(requirement)
|
||||
is StatEqualRequirement -> return renderStatEqualRequirement(requirement)
|
||||
is StructureRequirement -> return renderStructureRequirement(requirement)
|
||||
is TimeRangeRequirement -> return renderTimeRangeRequirement(requirement)
|
||||
is UseMoveRequirement -> return renderUseMoveRequirement(requirement)
|
||||
is WeatherRequirement -> return renderWeatherRequirement(requirement)
|
||||
is WorldRequirement -> return renderWorldRequirement(requirement)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun renderAdvancementRequirement(advancementRequirement: AdvancementRequirement): RenderRequirementData? {
|
||||
val icon = Items.BOOK
|
||||
val identifier = advancementRequirement.requiredAdvancement
|
||||
val advancementLanguageKey = "${identifier.namespace}.${identifier.path.substringAfterLast('/')}"
|
||||
val advancementComponent = Component.translatable("advancements.$advancementLanguageKey").green()
|
||||
val tooltip = lang("ui.pokedex.requirement.advancement.tooltip", advancementComponent)
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderAnyRequirement(anyRequirement: AnyRequirement): RenderRequirementData? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun renderAreaRequirement(areaRequirement: AreaRequirement): RenderRequirementData? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun renderAttackDefenceRatioRequirement(attackDefenceRatioRequirement: AttackDefenceRatioRequirement): RenderRequirementData? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun renderBattleCriticalHitsRequirement(battleCriticalHitsRequirement: BattleCriticalHitsRequirement): RenderRequirementData? {
|
||||
val icon = CobblemonItems.MEDICINAL_LEEK
|
||||
val criticalsComponent = Component.literal(battleCriticalHitsRequirement.amount.toString()).green()
|
||||
val tooltip = lang("ui.pokedex.requirement.battle_critical_hits.tooltip", criticalsComponent)
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderBiomeRequirement(biomeRequirement: BiomeRequirement): RenderRequirementData? {
|
||||
val condition = biomeRequirement.biomeCondition
|
||||
val anticondition = biomeRequirement.biomeAnticondition
|
||||
if (condition == null && anticondition == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val icon = Items.COMPASS
|
||||
val displayName: (RegistryLikeCondition<Biome>) -> MutableComponent = {
|
||||
when (it) {
|
||||
is RegistryLikeTagCondition -> Component.literal(it.tag.location().path.substringAfterLast('/').split('_').joinToString(" ") { part -> part.replaceFirstChar(Char::titlecase) })
|
||||
is RegistryLikeIdentifierCondition -> Component.translatable("biome.${it.identifier.namespace}.${it.identifier.path}")
|
||||
else -> Component.literal(it.toString())
|
||||
}
|
||||
}
|
||||
val tooltip = if (condition != null && anticondition != null) {
|
||||
lang("ui.pokedex.requirement.biome.both.tooltip", displayName(condition).green(), displayName(anticondition).green())
|
||||
} else if (condition != null) {
|
||||
lang("ui.pokedex.requirement.biome.tooltip", displayName(condition).green())
|
||||
} else {
|
||||
lang("ui.pokedex.requirement.biome.anticondition.tooltip", displayName(anticondition!!).green())
|
||||
}
|
||||
|
||||
val playerBiome = Minecraft.getInstance()?.player?.let { it.level().getBiome(it.blockPosition()) }
|
||||
if (playerBiome != null) {
|
||||
val isMet = (condition == null || condition.fits(playerBiome)) && (anticondition == null || !anticondition.fits(playerBiome))
|
||||
tooltip.add(if (isMet) Component.literal(" ✔").green() else Component.literal(" ✘").red())
|
||||
}
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderBlocksTraveledRequirement(blocksTraveledRequirement: BlocksTraveledRequirement): RenderRequirementData? {
|
||||
val icon = Items.LEATHER_BOOTS
|
||||
val blocksComponent = Component.literal(blocksTraveledRequirement.amount.toString()).green()
|
||||
val tooltip = lang("ui.pokedex.requirement.blocks_traveled.tooltip", blocksComponent)
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderDamageTakenRequirement(damageTakenRequirement: DamageTakenRequirement): RenderRequirementData? {
|
||||
val icon = CobblemonItems.PUNCHING_GLOVE
|
||||
val blocksComponent = Component.literal(damageTakenRequirement.amount.toString()).green()
|
||||
val tooltip = lang("ui.pokedex.requirement.damage_taken.tooltip", blocksComponent)
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderDefeatRequirement(defeatRequirement: DefeatRequirement): RenderRequirementData? {
|
||||
val description = describePokemonProperties(defeatRequirement.target) ?: return null
|
||||
val icon = CobblemonItems.PUNCHING_GLOVE
|
||||
val defeatedAmount = defeatRequirement.amount
|
||||
|
||||
val descriptionComponent = Component.literal(defeatedAmount.toString()).green().add(" ").add(description)
|
||||
val tooltip = lang("ui.pokedex.requirement.defeat.tooltip", descriptionComponent)
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderFriendshipRequirement(friendshipRequirement: FriendshipRequirement): RenderRequirementData? {
|
||||
val icon = CobblemonItems.SOOTHE_BELL
|
||||
val friendshipAmountComponent = Component.literal(friendshipRequirement.amount.toString()).green()
|
||||
val tooltip = lang("ui.pokedex.requirement.friendship.tooltip", friendshipAmountComponent)
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderHeldItemRequirement(heldItemRequirement: HeldItemRequirement): RenderRequirementData? {
|
||||
val item = heldItemRequirement.itemCondition.items.get().first().value()
|
||||
val tooltip = lang("ui.pokedex.requirement.held_item.tooltip", item.description.plainCopy().green())
|
||||
|
||||
return RenderRequirementData(item, tooltip)
|
||||
}
|
||||
|
||||
fun renderLevelRequirement(levelRequirement: LevelRequirement): RenderRequirementData? {
|
||||
val minLevel = levelRequirement.minLevel
|
||||
val maxLevel = levelRequirement.maxLevel
|
||||
|
||||
val icon = CobblemonItems.RARE_CANDY
|
||||
val tooltip =
|
||||
if (minLevel == maxLevel || maxLevel == Int.MAX_VALUE)
|
||||
lang("ui.pokedex.requirement.level.tooltip.min", Component.literal(minLevel.toString()).green())
|
||||
else
|
||||
lang("ui.pokedex.requirement.level.tooltip.between", Component.literal(minLevel.toString()).green(), Component.literal(maxLevel.toString()).green())
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderMoonPhaseRequirement(moonPhaseRequirement: MoonPhaseRequirement): RenderRequirementData? {
|
||||
val icon = Items.CLOCK
|
||||
val moonPhaseComponent = moonPhaseRequirement.moonPhase.displayName().green()
|
||||
val tooltip = lang("ui.pokedex.requirement.moon_phase.tooltip", moonPhaseComponent)
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderMoveSetRequirement(moveSetRequirement: MoveSetRequirement): RenderRequirementData? {
|
||||
val icon = CobblemonItems.TECHNICAL_MACHINE
|
||||
val tooltip = lang("ui.pokedex.requirement.has_move.tooltip", moveSetRequirement.move.displayName.green())
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderMoveTypeRequirement(moveTypeRequirement: MoveTypeRequirement): RenderRequirementData? {
|
||||
val type = ElementalTypes.get(moveTypeRequirement.type.name)
|
||||
val icon = when (type) {
|
||||
ElementalTypes.NORMAL -> CobblemonItems.NORMAL_GEM
|
||||
ElementalTypes.FIRE -> CobblemonItems.FIRE_GEM
|
||||
ElementalTypes.WATER -> CobblemonItems.WATER_GEM
|
||||
ElementalTypes.GRASS -> CobblemonItems.GRASS_GEM
|
||||
ElementalTypes.ELECTRIC -> CobblemonItems.ELECTRIC_GEM
|
||||
ElementalTypes.ICE -> CobblemonItems.ICE_GEM
|
||||
ElementalTypes.FIGHTING -> CobblemonItems.FIGHTING_GEM
|
||||
ElementalTypes.POISON -> CobblemonItems.POISON_GEM
|
||||
ElementalTypes.GROUND -> CobblemonItems.GROUND_GEM
|
||||
ElementalTypes.FLYING -> CobblemonItems.FLYING_GEM
|
||||
ElementalTypes.PSYCHIC -> CobblemonItems.PSYCHIC_GEM
|
||||
ElementalTypes.BUG -> CobblemonItems.BUG_GEM
|
||||
ElementalTypes.ROCK -> CobblemonItems.ROCK_GEM
|
||||
ElementalTypes.GHOST -> CobblemonItems.GHOST_GEM
|
||||
ElementalTypes.DRAGON -> CobblemonItems.DRAGON_GEM
|
||||
ElementalTypes.DARK -> CobblemonItems.DARK_GEM
|
||||
ElementalTypes.STEEL -> CobblemonItems.STEEL_GEM
|
||||
ElementalTypes.FAIRY -> CobblemonItems.FAIRY_GEM
|
||||
else -> CobblemonItems.NORMAL_GEM
|
||||
}
|
||||
|
||||
val tooltip = lang("ui.pokedex.requirement.has_move_type.tooltip", moveTypeRequirement.type.displayName.green())
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderOwnerHoldsItemRequirement(ownerHoldsItemRequirement: OwnerHoldsItemRequirement): RenderRequirementData? {
|
||||
val item = ownerHoldsItemRequirement.itemCondition.items.get().first().value()
|
||||
val tooltip = lang("ui.pokedex.requirement.owner_held_item.tooltip", item.description.plainCopy().green())
|
||||
|
||||
return RenderRequirementData(item, tooltip)
|
||||
}
|
||||
|
||||
fun renderPartyMemberRequirement(partyMemberRequirement: PartyMemberRequirement): RenderRequirementData? {
|
||||
val description = describePokemonProperties(partyMemberRequirement.target) ?: return null
|
||||
val icon = CobblemonItems.POKE_BALL
|
||||
val tooltip = if (partyMemberRequirement.contains) {
|
||||
lang("ui.pokedex.requirement.party_member.tooltip", description)
|
||||
} else {
|
||||
lang("ui.pokedex.requirement.party_member.anticondition.tooltip", description)
|
||||
}
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderPokemonPropertiesRequirement(pokemonPropertiesRequirement: PokemonPropertiesRequirement): RenderRequirementData? {
|
||||
val description = describePokemonProperties(pokemonPropertiesRequirement.target) ?: return null
|
||||
val icon = Items.NAME_TAG
|
||||
val tooltip = lang("ui.pokedex.requirement.pokemon_properties.tooltip", description)
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderPropertyRangeRequirement(propertyRangeRequirement: PropertyRangeRequirement): RenderRequirementData? {
|
||||
val provider = SpeciesFeatures.getFeature(propertyRangeRequirement.feature) as? IntSpeciesFeatureProvider ?: return null
|
||||
val displayName = provider.display?.name?.takeIf { it.isNotBlank() }?.asTranslated()?.green() ?: return null
|
||||
val icon = provider.itemPoints.keys.firstOrNull()?.let { BuiltInRegistries.ITEM.get(it) } ?: Items.NAME_TAG
|
||||
|
||||
val range = propertyRangeRequirement.range
|
||||
val tooltip = lang(
|
||||
"ui.pokedex.requirement.property_range.tooltip",
|
||||
displayName,
|
||||
Component.literal(range.first.toString()).green(),
|
||||
Component.literal(range.last.toString()).green()
|
||||
)
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderRecoilRequirement(recoilRequirement: RecoilRequirement): RenderRequirementData? {
|
||||
val icon = CobblemonItems.LIFE_ORB
|
||||
val recoilComponent = Component.literal(recoilRequirement.amount.toString()).green()
|
||||
val tooltip = lang("ui.pokedex.requirement.recoil.tooltip", recoilComponent)
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderStatCompareRequirement(statCompareRequirement: StatCompareRequirement): RenderRequirementData? {
|
||||
val highStat = Stats.getStat(statCompareRequirement.highStat)
|
||||
val lowStat = Stats.getStat(statCompareRequirement.lowStat)
|
||||
|
||||
val icon = when (highStat) {
|
||||
Stats.HP, Stats.EVASION, Stats.ACCURACY -> CobblemonItems.HEALTH_MOCHI
|
||||
Stats.ATTACK -> CobblemonItems.MUSCLE_MOCHI
|
||||
Stats.DEFENCE -> CobblemonItems.RESIST_MOCHI
|
||||
Stats.SPECIAL_ATTACK -> CobblemonItems.GENIUS_MOCHI
|
||||
Stats.SPECIAL_DEFENCE -> CobblemonItems.CLEVER_MOCHI
|
||||
Stats.SPEED -> CobblemonItems.SWIFT_MOCHI
|
||||
}
|
||||
|
||||
val highStatComponent = highStat.displayName.plainCopy().green()
|
||||
val lowStatComponent = lowStat.displayName.plainCopy().green()
|
||||
val tooltip = lang("ui.pokedex.requirement.stat_compare.tooltip", highStatComponent, lowStatComponent)
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderStatEqualRequirement(statEqualRequirement: StatEqualRequirement): RenderRequirementData? {
|
||||
val statOne = Stats.getStat(statEqualRequirement.statOne)
|
||||
val statTwo = Stats.getStat(statEqualRequirement.statTwo)
|
||||
|
||||
val statOneComponent = statOne.displayName.plainCopy().green()
|
||||
val statTwoComponent = statTwo.displayName.plainCopy().green()
|
||||
|
||||
val icon = CobblemonItems.FRESH_START_MOCHI
|
||||
val tooltip = lang("ui.pokedex.requirement.stat_equal.tooltip", statOneComponent, statTwoComponent)
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderStructureRequirement(structureRequirement: StructureRequirement): RenderRequirementData? {
|
||||
val condition = structureRequirement.structureCondition
|
||||
val anticondition = structureRequirement.structureAnticondition
|
||||
if (condition == null && anticondition == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val icon = Items.STRUCTURE_BLOCK
|
||||
val displayName: (RegistryLikeCondition<Structure>) -> MutableComponent = {
|
||||
val path = when (it) {
|
||||
is RegistryLikeTagCondition -> it.tag.location().path
|
||||
is RegistryLikeIdentifierCondition -> it.identifier.path
|
||||
else -> it.toString()
|
||||
}
|
||||
Component.literal(path.substringAfterLast('/').split('_').joinToString(" ") { part -> part.replaceFirstChar(Char::titlecase) })
|
||||
}
|
||||
val tooltip = if (condition != null && anticondition != null) {
|
||||
lang("ui.pokedex.requirement.structure.both.tooltip", displayName(condition).green(), displayName(anticondition).green())
|
||||
} else if (condition != null) {
|
||||
lang("ui.pokedex.requirement.structure.tooltip", displayName(condition).green())
|
||||
} else {
|
||||
lang("ui.pokedex.requirement.structure.anticondition.tooltip", displayName(anticondition!!).green())
|
||||
}
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderTimeRangeRequirement(timeRangeRequirement: TimeRangeRequirement): RenderRequirementData? {
|
||||
val icon = Items.CLOCK
|
||||
val timeRangeComponent = timeRangeRequirement.range.displayName() ?: return null
|
||||
val tooltip = lang("ui.pokedex.requirement.time_range.tooltip", timeRangeComponent.green())
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderUseMoveRequirement(useMoveRequirement: UseMoveRequirement): RenderRequirementData? {
|
||||
val icon = CobblemonItems.CHOICE_BAND
|
||||
val tooltip = lang("ui.pokedex.requirement.use_move.tooltip", useMoveRequirement.move.displayName.green())
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderWeatherRequirement(weatherRequirement: WeatherRequirement): RenderRequirementData? {
|
||||
val icon: Item
|
||||
val tooltip: MutableComponent
|
||||
|
||||
if (weatherRequirement.isThundering == true) {
|
||||
icon = Items.LIGHTNING_ROD
|
||||
tooltip = lang("ui.pokedex.requirement.weather.tooltip.thundering")
|
||||
} else {
|
||||
icon = Items.WATER_BUCKET
|
||||
tooltip = lang("ui.pokedex.requirement.weather.tooltip.raining")
|
||||
}
|
||||
|
||||
return RenderRequirementData(icon, tooltip)
|
||||
}
|
||||
|
||||
fun renderWorldRequirement(worldRequirement: WorldRequirement): RenderRequirementData? {
|
||||
return null
|
||||
}
|
||||
+81
-178
@@ -6,18 +6,20 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.pokedex.widgets
|
||||
package com.cobblemon.mod.common.client.gui.pokedex.widgets.evolutiontree
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.CobblemonItems
|
||||
import com.cobblemon.mod.common.api.gui.*
|
||||
import com.cobblemon.mod.common.api.pokedex.Dexes
|
||||
import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
|
||||
import com.cobblemon.mod.common.api.pokedex.entry.*
|
||||
import com.cobblemon.mod.common.api.pokedex.entry.PokedexEntry
|
||||
import com.cobblemon.mod.common.api.pokedex.entry.PokedexForm
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
|
||||
import com.cobblemon.mod.common.api.text.add
|
||||
import com.cobblemon.mod.common.api.text.bold
|
||||
import com.cobblemon.mod.common.api.text.green
|
||||
import com.cobblemon.mod.common.api.text.red
|
||||
import com.cobblemon.mod.common.client.CobblemonClient
|
||||
import com.cobblemon.mod.common.client.CobblemonResources
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUI
|
||||
@@ -27,22 +29,18 @@ import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.POKEMON_D
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_SLOT_SIZE
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.ScaledButton
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.renderTooltip
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.widgets.PokemonSlotWidget
|
||||
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
|
||||
import com.cobblemon.mod.common.client.render.drawScaledText
|
||||
import com.cobblemon.mod.common.client.render.renderScaledGuiItemIcon
|
||||
import com.cobblemon.mod.common.pokemon.FormData
|
||||
import com.cobblemon.mod.common.pokemon.Gender
|
||||
import com.cobblemon.mod.common.pokemon.RenderablePokemon
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.gui.GuiGraphics
|
||||
import net.minecraft.core.registries.Registries
|
||||
import net.minecraft.locale.Language
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.network.chat.MutableComponent
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.tags.TagKey
|
||||
|
||||
class EvolutionTreeWidget(
|
||||
val parent: PokedexGUI,
|
||||
@@ -94,8 +92,6 @@ class EvolutionTreeWidget(
|
||||
|
||||
private var hoveredTooltips: List<MutableComponent>? = null
|
||||
|
||||
private var viewedForm: FormData? = null
|
||||
|
||||
private val expandButton: ScaledButton = ScaledButton(
|
||||
0f,
|
||||
0f,
|
||||
@@ -104,14 +100,11 @@ class EvolutionTreeWidget(
|
||||
resource = expandResource,
|
||||
clickAction = {
|
||||
parent.evolutionTreeExpanded = !parent.evolutionTreeExpanded
|
||||
expandButton.resource = if (parent.evolutionTreeExpanded) minimizeResource else expandResource
|
||||
updatePositioning()
|
||||
}
|
||||
).apply { addWidget(this) }
|
||||
|
||||
init {
|
||||
updatePositioning()
|
||||
}
|
||||
|
||||
override fun renderWidget(
|
||||
guiGraphics: GuiGraphics,
|
||||
mouseX: Int,
|
||||
@@ -153,31 +146,9 @@ class EvolutionTreeWidget(
|
||||
if (pokedexEntry == null || pokedexForm == null) return
|
||||
|
||||
updatePositioning()
|
||||
var rootSpecies = pokedexEntry
|
||||
var rootForm = pokedexForm
|
||||
var preEvolution = findPreEvolution(rootSpecies, rootForm)
|
||||
// Stop at whatever the dex can't resolve, and at cycles (datapacks)
|
||||
val visited = mutableSetOf(rootSpecies.speciesId to rootForm.displayForm)
|
||||
while (preEvolution != null) {
|
||||
val preEvolutionSpecies = DexEntries.entries.values.firstOrNull { it.speciesId == preEvolution.fromSpecies &&
|
||||
it.forms.any {form -> form.displayForm.equals(preEvolution.fromForm, ignoreCase = true)}} ?: break
|
||||
val preEvolutionForm = preEvolutionSpecies.forms.firstOrNull { it.displayForm.equals(preEvolution.fromForm, ignoreCase = true) } ?: break
|
||||
if (!visited.add(preEvolutionSpecies.speciesId to preEvolutionForm.displayForm)) break
|
||||
rootSpecies = preEvolutionSpecies
|
||||
rootForm = preEvolutionForm
|
||||
preEvolution = findPreEvolution(rootSpecies, rootForm)
|
||||
}
|
||||
|
||||
val viewedFormEquivalents = Dexes.getFormEquivalents(pokedexForm.displayForm)
|
||||
viewedForm = PokemonSpecies.getByIdentifier(pokedexEntry.speciesId)
|
||||
?.forms?.firstOrNull { viewedFormEquivalents.contains(it.name.lowercase()) }
|
||||
|
||||
val formEquivalents = Dexes.getFormEquivalents(rootForm.displayForm)
|
||||
val form =
|
||||
rootSpecies.forms.firstOrNull { formEquivalents.contains(it.displayForm.lowercase()) } ?:
|
||||
rootSpecies.forms.first { it.displayForm.equals("Normal", ignoreCase = true) }
|
||||
|
||||
val rootEvolutionNode = buildEvolutionTree(rootSpecies, form)
|
||||
val (rootPokedexEntry, rootPokedexForm) = Dexes.findRootDexEntryBySpeciesId(pokedexEntry.speciesId, pokedexForm.displayForm)
|
||||
val rootEvolutionNode = buildEvolutionTree(rootPokedexEntry, rootPokedexForm, null)
|
||||
|
||||
// val isSameEntry = previousRootPokedexEntry != null && pokedexEntry?.id == previousRootPokedexEntry?.id
|
||||
// val isSameForm =
|
||||
@@ -201,13 +172,6 @@ class EvolutionTreeWidget(
|
||||
panningY = 0.0
|
||||
}
|
||||
|
||||
/** The evolution leading to [form]; the standard form falls back to any form of the species (no evolution leads to regular Vivillon). */
|
||||
private fun findPreEvolution(entry: PokedexEntry, form: PokedexForm): DexEvolutionDisplay? {
|
||||
return DexEntries.evolutionDisplays.firstOrNull { it.toSpecies == entry.speciesId && form.displayForm.equals(it.toForm, ignoreCase = true) }
|
||||
?: DexEntries.evolutionDisplays.takeIf { form.displayForm.equals("Normal", ignoreCase = true) }
|
||||
?.firstOrNull { it.toSpecies == entry.speciesId }
|
||||
}
|
||||
|
||||
fun updatePositioning() {
|
||||
x = pX + X_MARGIN
|
||||
y = pY + if (parent.evolutionTreeExpanded) Y_MARGIN_EXPANDED else Y_MARGIN_MINIMIZED
|
||||
@@ -216,7 +180,6 @@ class EvolutionTreeWidget(
|
||||
panningX = 0.0
|
||||
panningY = 0.0
|
||||
|
||||
expandButton.resource = if (parent.evolutionTreeExpanded) minimizeResource else expandResource
|
||||
expandButton.buttonX = x + EXPAND_BUTTON_X_MARGIN
|
||||
expandButton.buttonY = y + EXPAND_BUTTON_Y_MARGIN
|
||||
}
|
||||
@@ -224,62 +187,61 @@ class EvolutionTreeWidget(
|
||||
private fun buildEvolutionTree(
|
||||
pokedexEntry: PokedexEntry,
|
||||
pokedexForm: PokedexForm,
|
||||
aspects: Set<String> = emptySet(),
|
||||
ancestors: Set<Pair<ResourceLocation, String>> = emptySet(),
|
||||
evolution: Evolution? = null,
|
||||
resultProperties: PokemonProperties? = evolution?.result,
|
||||
renderData: RenderEvolutionData? = evolution?.let { renderEvolutionData(it) }
|
||||
): EvolutionNode {
|
||||
// Datapack evolutions can loop back (A -> B -> A), so stop at anything already on this branch
|
||||
val path = ancestors + (pokedexEntry.speciesId to pokedexForm.displayForm.lowercase())
|
||||
val discoveryLevel = CobblemonClient.clientPokedexData.getHighestKnowledgeFor(pokedexEntry, pokedexForm, aspects.toMutableSet())
|
||||
val species = PokemonSpecies.getByIdentifier(pokedexEntry.speciesId)!!
|
||||
|
||||
val formEquivalents = Dexes.getFormEquivalents(pokedexForm.displayForm)
|
||||
val evolutions = DexEntries.evolutionDisplays.filter { it.fromSpecies == pokedexEntry.speciesId && formEquivalents.contains(it.fromForm.lowercase()) }
|
||||
val form = species.forms.firstOrNull { formEquivalents.contains(it.name.lowercase()) }
|
||||
val parentSpecies = Dexes.findDexEntryBySpeciesId(species.resourceIdentifier)!!
|
||||
val parentPokedexForm = parentSpecies.forms.first { formEquivalents.contains(it.displayForm.lowercase()) }
|
||||
|
||||
val children = evolutions.mapNotNull {
|
||||
val childPokedexEntry = DexEntries.entries.values.firstOrNull { entry -> entry.speciesId == it.toSpecies &&
|
||||
entry.forms.any {form -> form.displayForm.equals(it.toForm, ignoreCase = true)}}
|
||||
if (childPokedexEntry == null) {
|
||||
Cobblemon.LOGGER.warn("Dex entry not found for species: ${it.toSpecies}")
|
||||
return@mapNotNull null
|
||||
val aspects = resultProperties?.aspects?.toMutableSet() ?: mutableSetOf()
|
||||
form?.aspects?.let { aspects.addAll(it) }
|
||||
|
||||
val discoveryLevel = CobblemonClient.clientPokedexData.getHighestKnowledgeFor(pokedexEntry, pokedexForm, aspects)
|
||||
|
||||
val evolutions = form?.evolutions ?: species.evolutions
|
||||
|
||||
// Avoid evolution.result.create() here: it builds a full battle-ready Pokemon (initializeMovesetFromDefault
|
||||
// etc.) which relies on server-only data like CobblemonMovesetBuilders and crashes on the client
|
||||
val children = evolutions.flatMap { evolution ->
|
||||
val baseRenderData = renderEvolutionData(evolution)
|
||||
|
||||
listOfNotNull(evolution.result, evolution.shedder).mapNotNull { resultProperties ->
|
||||
val resultSpecies = resultProperties.species
|
||||
?.let { PokemonSpecies.getByIdentifier(it.asIdentifierDefaultingNamespace()) }
|
||||
?: return@mapNotNull null
|
||||
|
||||
val resultForm = resultSpecies.getForm(aspects + resultProperties.aspects)
|
||||
|
||||
val speciesId = resultSpecies.resourceIdentifier
|
||||
val childPokedexEntry = Dexes.findDexEntryBySpeciesId(speciesId)
|
||||
|
||||
if (childPokedexEntry == null) {
|
||||
Cobblemon.LOGGER.warn("Dex entry not found for species: $speciesId")
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
val formEquivalents = Dexes.getFormEquivalents(resultForm.name.lowercase())
|
||||
val childPokedexForm = childPokedexEntry.forms.first { formEquivalents.contains(it.displayForm.lowercase()) }
|
||||
|
||||
val childRenderData = if (resultProperties === evolution.shedder) {
|
||||
RenderEvolutionData(
|
||||
icons = (baseRenderData?.icons ?: emptyList()) + CobblemonItems.POKE_BALL,
|
||||
tooltips = (baseRenderData?.tooltips ?: emptyList()) + lang("ui.pokedex.evolution.shedder.tooltip"),
|
||||
)
|
||||
} else {
|
||||
baseRenderData
|
||||
}
|
||||
|
||||
return@mapNotNull buildEvolutionTree(childPokedexEntry, childPokedexForm, evolution, resultProperties, childRenderData)
|
||||
}
|
||||
val formEquivalents = Dexes.getFormEquivalents(it.toForm.lowercase())
|
||||
val childPokedexForm = childPokedexEntry.forms.firstOrNull { formEquivalents.contains(it.displayForm.lowercase()) }
|
||||
if (childPokedexForm == null) {
|
||||
Cobblemon.LOGGER.warn("Dex form ${it.toForm} not found for species: ${it.toSpecies}")
|
||||
return@mapNotNull null
|
||||
}
|
||||
if ((childPokedexEntry.speciesId to childPokedexForm.displayForm.lowercase()) in path) {
|
||||
Cobblemon.LOGGER.warn("Evolution loop found at species: ${it.toSpecies} form: ${it.toForm}")
|
||||
return@mapNotNull null
|
||||
}
|
||||
buildEvolutionTree(childPokedexEntry, childPokedexForm, it.toAspects, path)
|
||||
}
|
||||
|
||||
return EvolutionNode(
|
||||
pokedexEntry = pokedexEntry,
|
||||
pokedexForm = pokedexForm,
|
||||
aspects = aspects,
|
||||
pokedexEntryProgress = discoveryLevel,
|
||||
children = mergeEquivalentChildren(children),
|
||||
renderDataAlternatives = evolutions,
|
||||
)
|
||||
}
|
||||
|
||||
private fun mergeEquivalentChildren(children: List<EvolutionNode>): List<EvolutionNode> {
|
||||
return children
|
||||
.groupBy { Triple(it.pokedexEntry.speciesId, it.pokedexForm.displayForm.lowercase(), it.aspects) }
|
||||
.values
|
||||
.map { group ->
|
||||
if (group.size == 1) return@map group.first()
|
||||
val first = group.first()
|
||||
EvolutionNode(
|
||||
first.pokedexEntry,
|
||||
first.pokedexForm,
|
||||
first.aspects,
|
||||
first.pokedexEntryProgress,
|
||||
first.children,
|
||||
group.flatMap { it.renderDataAlternatives }
|
||||
)
|
||||
}
|
||||
return EvolutionNode(pokedexEntry, parentPokedexForm, aspects, discoveryLevel, children, renderData)
|
||||
}
|
||||
|
||||
private fun renderTreeDrawingNode(
|
||||
@@ -296,19 +258,15 @@ class EvolutionTreeWidget(
|
||||
val (childX, _) = getDrawingCoordinates(child)
|
||||
|
||||
val lineStartX = treeX + SCROLL_SLOT_SIZE
|
||||
val lineEndX = childX
|
||||
val lineY = treeY + SCROLL_SLOT_SIZE / 2
|
||||
|
||||
val formEquivalents = Dexes.getFormEquivalents(child.value.pokedexForm.displayForm)
|
||||
val alternatives = treeDrawingNode.value.renderDataAlternatives.filter {
|
||||
it.toSpecies == child.value.pokedexEntry.speciesId && formEquivalents.contains(it.toForm.lowercase()) && it.toAspects == child.value.aspects
|
||||
}
|
||||
|
||||
alternatives.takeIf { it.isNotEmpty() }?.let {
|
||||
child.value.renderData?.let { renderData ->
|
||||
drawEvolutionInfo(
|
||||
guiGraphics,
|
||||
it,
|
||||
renderData,
|
||||
lineStartX,
|
||||
childX,
|
||||
lineEndX,
|
||||
lineY,
|
||||
mouseX,
|
||||
mouseY,
|
||||
@@ -340,19 +298,16 @@ class EvolutionTreeWidget(
|
||||
treeDrawingNode.children.forEach { child ->
|
||||
val (childX, childY) = getDrawingCoordinates(child)
|
||||
|
||||
val lineStartX = treeForkEnd
|
||||
val lineEndX = childX
|
||||
val lineY = childY + SCROLL_SLOT_SIZE / 2
|
||||
|
||||
val formEquivalents = Dexes.getFormEquivalents(child.value.pokedexForm.displayForm)
|
||||
val alternatives = treeDrawingNode.value.renderDataAlternatives.filter {
|
||||
it.toSpecies == child.value.pokedexEntry.speciesId && formEquivalents.contains(it.toForm.lowercase()) && it.toAspects == child.value.aspects
|
||||
}
|
||||
|
||||
alternatives.takeIf { it.isNotEmpty() }?.let {
|
||||
child.value.renderData?.let { renderData ->
|
||||
drawEvolutionInfo(
|
||||
guiGraphics,
|
||||
it,
|
||||
treeForkEnd,
|
||||
childX,
|
||||
renderData,
|
||||
lineStartX,
|
||||
lineEndX,
|
||||
lineY,
|
||||
mouseX,
|
||||
mouseY,
|
||||
@@ -391,7 +346,7 @@ class EvolutionTreeWidget(
|
||||
if (formName == "normal") null
|
||||
else lang("ui.pokedex.info.form.${species}-${formName.replace("-", "")}")
|
||||
|
||||
formComponent?.let { speciesComponent.copy().add(" (").add(it).add(")") } ?: speciesComponent
|
||||
formComponent?.add(" ")?.add(speciesComponent) ?: speciesComponent
|
||||
}
|
||||
|
||||
guiGraphics.disableScissor()
|
||||
@@ -453,7 +408,7 @@ class EvolutionTreeWidget(
|
||||
|
||||
private fun drawEvolutionInfo(
|
||||
guiGraphics: GuiGraphics,
|
||||
renderDataAlternatives: List<DexEvolutionDisplay>,
|
||||
renderData: RenderEvolutionData,
|
||||
lineStartX: Int,
|
||||
lineEndX: Int,
|
||||
lineY: Int,
|
||||
@@ -461,26 +416,6 @@ class EvolutionTreeWidget(
|
||||
mouseY: Int,
|
||||
delta: Float,
|
||||
) {
|
||||
val icons = renderDataAlternatives.flatMap { it.requirements.filter(::isDisplayable).map { req -> req.icon } }.distinct()
|
||||
val playerBiome = Minecraft.getInstance().player?.let { it.level().getBiome(it.blockPosition()) }
|
||||
val alternativeTooltips = renderDataAlternatives.map { alternative ->
|
||||
alternative.requirements.filter(::isDisplayable).map { req ->
|
||||
if (req is CobblemonBiomeDexEvolutionRequirementDisplay) {
|
||||
val tooltip = req.tooltip.copy()
|
||||
if (playerBiome != null) {
|
||||
val conditionFits = req.condition?.let { if (it.tag) playerBiome.`is`(TagKey.create(Registries.BIOME, it.id)) else playerBiome.`is`(it.id) } ?: true
|
||||
val antiConditionFits = req.antiCondition?.let { if (it.tag) playerBiome.`is`(TagKey.create(Registries.BIOME, it.id)) else playerBiome.`is`(it.id) } ?: false
|
||||
val isMet = conditionFits && !antiConditionFits
|
||||
tooltip.add(if (isMet) Component.literal(" ✔").green() else Component.literal(" ✘").red())
|
||||
}
|
||||
tooltip
|
||||
} else {
|
||||
req.tooltip
|
||||
}
|
||||
}
|
||||
}
|
||||
val tooltips = combineAlternativeTooltips(alternativeTooltips)
|
||||
|
||||
val lineXMiddle = (lineStartX + lineEndX) / 2
|
||||
val iconX = (lineXMiddle + ICON_X_PADDING - ICON_SLOT_HALF_PADDING).toInt()
|
||||
val iconY = (lineY + ICON_Y_PADDING - ICON_SLOT_HALF_PADDING).toInt()
|
||||
@@ -496,7 +431,7 @@ class EvolutionTreeWidget(
|
||||
return
|
||||
}
|
||||
|
||||
if (icons.isEmpty()) {
|
||||
if (renderData.icons.isEmpty()) {
|
||||
guiGraphics.hLine(
|
||||
lineStartX,
|
||||
lineEndX,
|
||||
@@ -527,8 +462,8 @@ class EvolutionTreeWidget(
|
||||
height = iconSize,
|
||||
)
|
||||
|
||||
val index = (System.currentTimeMillis() / MILLIS_BETWEEN_ICONS_CYCLE) % icons.size
|
||||
val item = icons[index.toInt()]
|
||||
val index = (System.currentTimeMillis() / MILLIS_BETWEEN_ICONS_CYCLE) % renderData.icons.size
|
||||
val item = renderData.icons[index.toInt()]
|
||||
|
||||
renderScaledGuiItemIcon(
|
||||
itemStack = item.defaultInstance,
|
||||
@@ -543,49 +478,17 @@ class EvolutionTreeWidget(
|
||||
val isIconHovered = isElementHovered(
|
||||
mouseX,
|
||||
mouseY,
|
||||
iconX,
|
||||
iconX + iconSize,
|
||||
iconY,
|
||||
iconY + iconSize,
|
||||
iconX.toInt(),
|
||||
iconX.toInt() + iconSize,
|
||||
iconY.toInt(),
|
||||
iconY.toInt() + iconSize,
|
||||
)
|
||||
|
||||
if (tooltips.isNotEmpty() &&
|
||||
if (renderData.tooltips.isNotEmpty() &&
|
||||
(isLineHovered || isIconHovered) &&
|
||||
this.isHovered(mouseX.toDouble(), mouseY.toDouble())
|
||||
) {
|
||||
hoveredTooltips = tooltips.map { it.copy() }
|
||||
}
|
||||
}
|
||||
|
||||
/** Hides biome requirements naming untranslated biomes, e.g. from mods that aren't installed (aether:skyroot_forest) */
|
||||
private fun isDisplayable(requirement: DexEvolutionRequirementDisplay): Boolean {
|
||||
if (requirement !is CobblemonBiomeDexEvolutionRequirementDisplay) return true
|
||||
return listOfNotNull(requirement.condition, requirement.antiCondition).all {
|
||||
it.tag || Language.getInstance().has("biome.${it.id.namespace}.${it.id.path}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun combineAlternativeTooltips(alternatives: List<List<Component>>): List<Component> {
|
||||
val groups = alternatives.filter { it.isNotEmpty() }
|
||||
if (groups.size <= 1) return groups.firstOrNull() ?: emptyList()
|
||||
|
||||
val sharedStrings = groups.map { group -> group.map { it.string }.toSet() }.reduce { acc, strings -> acc intersect strings }
|
||||
val shared = groups.first().filter { it.string in sharedStrings }.distinctBy { it.string }
|
||||
val remainders = groups
|
||||
.map { group -> group.filter { it.string !in sharedStrings } }
|
||||
.distinctBy { group -> group.map { it.string } }
|
||||
|
||||
// An alternative needing nothing beyond the shared lines makes the others irrelevant
|
||||
if (remainders.any { it.isEmpty() }) return shared
|
||||
if (remainders.size == 1) return shared + remainders.first()
|
||||
|
||||
return if (remainders.size >= 3 && remainders.all { it.size == 1 }) {
|
||||
shared + lang("ui.pokedex.evolution.one_of").bold() + remainders.map { it.first() }
|
||||
} else {
|
||||
// Show each alternative in full, otherwise it's unclear where the first one starts
|
||||
groups.distinctBy { group -> group.map { it.string } }.flatMapIndexed { index, group ->
|
||||
if (index == 0) group else listOf(lang("ui.pokedex.evolution.or").bold()) + group
|
||||
}
|
||||
hoveredTooltips = renderData.tooltips
|
||||
}
|
||||
}
|
||||
|
||||
@@ -663,6 +566,6 @@ class EvolutionTreeWidget(
|
||||
val aspects: Set<String>,
|
||||
val pokedexEntryProgress: PokedexEntryProgress,
|
||||
override val children: List<EvolutionNode>,
|
||||
val renderDataAlternatives: List<DexEvolutionDisplay>,
|
||||
val renderData: RenderEvolutionData?,
|
||||
) : TreeNode<EvolutionNode>
|
||||
}
|
||||
+1
-4
@@ -1041,10 +1041,7 @@ class TMMachineScreen(containerMenu: TMMachineMenu, val inventory: Inventory, ti
|
||||
// Scroll to move if moves list contains held TM move
|
||||
if (tmList.get().contains(heldStackTm)) {
|
||||
val moveListIndex = tmList.get().indexOf(heldStackTm)
|
||||
movesScrollingList.scrollAmount = movesScrollingList.maxScroll * when (moveListIndex) {
|
||||
tmList.get().lastIndex -> 1.0
|
||||
else -> (moveListIndex / tmList.get().size.toDouble())
|
||||
}
|
||||
movesScrollingList.scrollAmount = (if (moveListIndex == tmList.get().lastIndex) 1.0 else (moveListIndex / tmList.get().size.toDouble()))* movesScrollingList.maxScroll
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -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)
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -24,9 +24,8 @@ object OpenPCHandler : ClientNetworkPacketHandler<OpenPCPacket> {
|
||||
party = CobblemonClient.storage.party,
|
||||
configuration = PCGUIConfiguration(),
|
||||
openOnBox = packet.box ?: CobblemonClient.lastPcBoxViewed,
|
||||
unseenWallpapers = packet.unseenWallpapers.toMutableSet(),
|
||||
silent = packet.silent
|
||||
unseenWallpapers = packet.unseenWallpapers.toMutableSet()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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())
|
||||
|
||||
+6
-36
@@ -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
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen1
|
||||
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.createTransformation
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pose.CobblemonPose
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pose.ModelPartTransformation
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import com.cobblemon.mod.common.entity.PoseType.Companion.MOVING_POSES
|
||||
import com.cobblemon.mod.common.entity.PoseType.Companion.STATIONARY_POSES
|
||||
import com.cobblemon.mod.common.entity.PoseType.Companion.UI_POSES
|
||||
import net.minecraft.client.model.geom.ModelPart
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
class CaterpieModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
|
||||
override val rootPart = root.registerChildWithAllChildren("caterpie")
|
||||
override val head = getPart("head")
|
||||
|
||||
override var portraitScale = 2.1F
|
||||
override var portraitTranslation = Vec3(-0.55, -1.55, 0.0)
|
||||
override var profileScale = 0.8F
|
||||
override var profileTranslation = Vec3(0.0, 0.55, 0.0)
|
||||
|
||||
lateinit var sleep: Pose
|
||||
lateinit var standing: Pose
|
||||
lateinit var walk: Pose
|
||||
lateinit var shoulderLeft: CobblemonPose
|
||||
lateinit var shoulderRight: CobblemonPose
|
||||
|
||||
val shoulderOffset = 3
|
||||
|
||||
|
||||
override val cryAnimation = CryProvider { bedrockStateful("caterpie", "cry") }
|
||||
|
||||
override fun registerPoses() {
|
||||
val blink = quirk { bedrockStateful("caterpie", "blink")}
|
||||
sleep = registerPose(
|
||||
poseType = PoseType.SLEEP,
|
||||
transformTicks = 10,
|
||||
animations = arrayOf(bedrock("caterpie", "sleep"))
|
||||
)
|
||||
|
||||
standing = registerPose(
|
||||
poseName = "standing",
|
||||
poseTypes = STATIONARY_POSES + UI_POSES,
|
||||
quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("caterpie", "ground_idle")
|
||||
)
|
||||
)
|
||||
|
||||
walk = registerPose(
|
||||
poseName = "walking",
|
||||
poseTypes = MOVING_POSES,
|
||||
quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("caterpie", "ground_walk")
|
||||
)
|
||||
)
|
||||
|
||||
shoulderLeft = registerPose(
|
||||
poseType = PoseType.SHOULDER_LEFT,
|
||||
quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
bedrock("caterpie", "ground_idle")
|
||||
),
|
||||
transformedParts = arrayOf(
|
||||
rootPart.createTransformation().addPosition(ModelPartTransformation.X_AXIS, shoulderOffset)
|
||||
)
|
||||
)
|
||||
|
||||
shoulderRight = registerPose(
|
||||
poseType = PoseType.SHOULDER_RIGHT,
|
||||
quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
bedrock("caterpie", "ground_idle")
|
||||
),
|
||||
transformedParts = arrayOf(
|
||||
rootPart.createTransformation().addPosition(ModelPartTransformation.X_AXIS, -shoulderOffset)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getFaintAnimation(state: PosableState) = bedrockStateful("caterpie", "faint")
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen1
|
||||
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.createTransformation
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pose.ModelPartTransformation
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import com.cobblemon.mod.common.entity.PoseType.Companion.MOVING_POSES
|
||||
import com.cobblemon.mod.common.entity.PoseType.Companion.STATIONARY_POSES
|
||||
import com.cobblemon.mod.common.entity.PoseType.Companion.UI_POSES
|
||||
import net.minecraft.client.model.geom.ModelPart
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
class WeedleModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
|
||||
override val rootPart = root.registerChildWithAllChildren("weedle")
|
||||
override val head = getPart("head")
|
||||
|
||||
override var portraitScale = 2.2F
|
||||
override var portraitTranslation = Vec3(-0.31, -1.83, 0.0)
|
||||
override var profileScale = 0.8F
|
||||
override var profileTranslation = Vec3(0.1, 0.6, 0.0)
|
||||
|
||||
lateinit var sleep: Pose
|
||||
lateinit var standing: Pose
|
||||
lateinit var walk: Pose
|
||||
lateinit var shoulderLeft: Pose
|
||||
lateinit var shoulderRight: Pose
|
||||
|
||||
val shoulderOffset = 3.8
|
||||
val shoulderZOffset = -3
|
||||
|
||||
override val cryAnimation = CryProvider { bedrockStateful("weedle", "cry") }
|
||||
|
||||
override fun registerPoses() {
|
||||
val blink = quirk { bedrockStateful("weedle", "blink")}
|
||||
sleep = registerPose(
|
||||
poseType = PoseType.SLEEP,
|
||||
animations = arrayOf(bedrock("weedle", "sleep"))
|
||||
)
|
||||
|
||||
standing = registerPose(
|
||||
poseName = "standing",
|
||||
poseTypes = STATIONARY_POSES + UI_POSES,
|
||||
quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("weedle", "ground_idle2")
|
||||
)
|
||||
)
|
||||
|
||||
walk = registerPose(
|
||||
poseName = "walk",
|
||||
poseTypes = MOVING_POSES,
|
||||
quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("weedle", "ground_walk")
|
||||
)
|
||||
)
|
||||
|
||||
shoulderLeft = registerPose(
|
||||
poseType = PoseType.SHOULDER_LEFT,
|
||||
quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
bedrock("weedle", "ground_idle")
|
||||
),
|
||||
transformedParts = arrayOf(
|
||||
rootPart.createTransformation().addPosition(ModelPartTransformation.X_AXIS, shoulderOffset),
|
||||
rootPart.createTransformation().addPosition(ModelPartTransformation.Z_AXIS, shoulderZOffset)
|
||||
)
|
||||
)
|
||||
|
||||
shoulderRight = registerPose(
|
||||
poseType = PoseType.SHOULDER_RIGHT,
|
||||
quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
bedrock("weedle", "ground_idle")
|
||||
),
|
||||
transformedParts = arrayOf(
|
||||
rootPart.createTransformation().addPosition(ModelPartTransformation.X_AXIS, -shoulderOffset),
|
||||
rootPart.createTransformation().addPosition(ModelPartTransformation.Z_AXIS, shoulderZOffset)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getFaintAnimation(state: PosableState) = bedrockStateful("weedle", "faint")
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen4
|
||||
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.animation.QuadrupedWalkAnimation
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.QuadrupedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import net.minecraft.client.model.geom.ModelPart
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
class LuxioModel (root: ModelPart) : PokemonPosableModel(root), HeadedFrame, QuadrupedFrame {
|
||||
override val rootPart = root.registerChildWithAllChildren("luxio")
|
||||
override val head = getPart("head")
|
||||
|
||||
override val foreLeftLeg = getPart("front_leg_left")
|
||||
override val foreRightLeg = getPart("front_leg_right")
|
||||
override val hindLeftLeg = getPart("back_leg_left")
|
||||
override val hindRightLeg = getPart("back_leg_right")
|
||||
|
||||
override var portraitScale = 1.8F
|
||||
override var portraitTranslation = Vec3(-0.6, -0.25, 0.0)
|
||||
|
||||
override var profileScale = 0.7F
|
||||
override var profileTranslation = Vec3(0.0, 0.7, 0.0)
|
||||
|
||||
lateinit var standing: Pose
|
||||
lateinit var walk: Pose
|
||||
lateinit var sleep: Pose
|
||||
|
||||
override fun registerPoses() {
|
||||
// val blink = quirk { bedrockStateful("luxio", "blink") }
|
||||
sleep = registerPose(
|
||||
poseType = PoseType.SLEEP,
|
||||
animations = arrayOf(bedrock("luxio", "sleep"))
|
||||
)
|
||||
|
||||
standing = registerPose(
|
||||
poseName = "standing",
|
||||
poseTypes = PoseType.UI_POSES + PoseType.STATIONARY_POSES,
|
||||
// quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("luxio", "idle")
|
||||
)
|
||||
)
|
||||
|
||||
walk = registerPose(
|
||||
poseName = "walk",
|
||||
poseTypes = PoseType.MOVING_POSES,
|
||||
// quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
QuadrupedWalkAnimation(this, periodMultiplier = 1.1F),
|
||||
singleBoneLook(),
|
||||
bedrock("luxio", "idle")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// override fun getFaintAnimation(
|
||||
// pokemonEntity: PokemonEntity,
|
||||
// state: PosableState<PokemonEntity>
|
||||
// ) = if (state.isPosedIn(standing, walk)) bedrockStateful("luxio", "faint") else null
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen4
|
||||
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.animation.QuadrupedWalkAnimation
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.QuadrupedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import net.minecraft.client.model.geom.ModelPart
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
class LuxrayModel (root: ModelPart) : PokemonPosableModel(root), HeadedFrame, QuadrupedFrame {
|
||||
override val rootPart = root.registerChildWithAllChildren("luxray")
|
||||
override val head = getPart("head")
|
||||
|
||||
override val foreLeftLeg = getPart("front_leg_left")
|
||||
override val foreRightLeg = getPart("front_leg_right")
|
||||
override val hindLeftLeg = getPart("back_leg_left")
|
||||
override val hindRightLeg = getPart("back_leg_right")
|
||||
|
||||
override var portraitScale = 1.8F
|
||||
override var portraitTranslation = Vec3(-0.9, 0.6, 0.0)
|
||||
|
||||
override var profileScale = 0.66F
|
||||
override var profileTranslation = Vec3(0.0, 0.7, 0.0)
|
||||
|
||||
lateinit var standing: Pose
|
||||
lateinit var walk: Pose
|
||||
|
||||
override fun registerPoses() {
|
||||
// val blink = quirk { bedrockStateful("luxray", "blink") }
|
||||
standing = registerPose(
|
||||
poseName = "standing",
|
||||
poseTypes = PoseType.UI_POSES + PoseType.STATIONARY_POSES,
|
||||
// quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("luxray", "ground_idle")
|
||||
)
|
||||
)
|
||||
|
||||
walk = registerPose(
|
||||
poseName = "walk",
|
||||
poseTypes = PoseType.MOVING_POSES,
|
||||
// quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
QuadrupedWalkAnimation(this, periodMultiplier = 1.1F),
|
||||
bedrock("luxray", "ground_idle"),
|
||||
singleBoneLook()
|
||||
// bedrock("luxray", "idle")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// override fun getFaintAnimation(
|
||||
// pokemonEntity: PokemonEntity,
|
||||
// state: PosableState<PokemonEntity>
|
||||
// ) = if (state.isPosedIn(standing, walk)) bedrockStateful("luxray", "faint") else null
|
||||
}
|
||||
+66
@@ -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.client.render.models.blockbench.pokemon.gen4
|
||||
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.animation.QuadrupedWalkAnimation
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.QuadrupedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import net.minecraft.client.model.geom.ModelPart
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
class ShinxModel (root: ModelPart) : PokemonPosableModel(root), HeadedFrame, QuadrupedFrame {
|
||||
override val rootPart = root.registerChildWithAllChildren("shinx")
|
||||
override val head = getPart("head")
|
||||
|
||||
override val foreLeftLeg = getPart("leg_left")
|
||||
override val foreRightLeg = getPart("leg_right")
|
||||
override val hindLeftLeg = getPart("back_leg_left")
|
||||
override val hindRightLeg = getPart("back_leg_right")
|
||||
|
||||
override var portraitScale = 1.8F
|
||||
override var portraitTranslation = Vec3(-0.6, -0.25, 0.0)
|
||||
|
||||
override var profileScale = 0.66F
|
||||
override var profileTranslation = Vec3(0.0, 0.7, 0.0)
|
||||
|
||||
lateinit var standing: Pose
|
||||
lateinit var walk: Pose
|
||||
|
||||
override fun registerPoses() {
|
||||
// val blink = quirk { bedrockStateful("shinx", "blink") }
|
||||
standing = registerPose(
|
||||
poseName = "standing",
|
||||
poseTypes = PoseType.UI_POSES + PoseType.STATIONARY_POSES,
|
||||
// quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("shinx", "ground_idle")
|
||||
)
|
||||
)
|
||||
|
||||
walk = registerPose(
|
||||
poseName = "walk",
|
||||
poseTypes = PoseType.MOVING_POSES,
|
||||
// quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
QuadrupedWalkAnimation(this, periodMultiplier = 1.1F),
|
||||
singleBoneLook(),
|
||||
bedrock("shinx", "ground_idle")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// override fun getFaintAnimation(
|
||||
// pokemonEntity: PokemonEntity,
|
||||
// state: PosableState<PokemonEntity>
|
||||
// ) = if (state.isPosedIn(standing, walk)) bedrockStateful("shinx", "faint") else null
|
||||
}
|
||||
+59
@@ -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.client.render.models.blockbench.pokemon.gen5
|
||||
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pose.CobblemonPose
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import net.minecraft.client.model.geom.ModelPart
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
class JellicentModel (root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
|
||||
override val rootPart = root.registerChildWithAllChildren("jellicent")
|
||||
override val head = getPart("head")
|
||||
|
||||
override var portraitScale = 1.13F
|
||||
override var portraitTranslation = Vec3(-0.56, 1.64, 0.0)
|
||||
|
||||
override var profileScale = 0.51F
|
||||
override var profileTranslation = Vec3(0.0, 1.05, 0.0)
|
||||
|
||||
lateinit var standing: CobblemonPose
|
||||
lateinit var walk: CobblemonPose
|
||||
|
||||
override fun registerPoses() {
|
||||
val blink = quirk { bedrockStateful("jellicent", "blink") }
|
||||
|
||||
standing = registerPose(
|
||||
poseName = "standing",
|
||||
poseTypes = PoseType.STATIONARY_POSES + PoseType.UI_POSES,
|
||||
quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("jellicent", "render")
|
||||
)
|
||||
)
|
||||
|
||||
walk = registerPose(
|
||||
poseName = "walk",
|
||||
poseTypes = PoseType.MOVING_POSES,
|
||||
quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("jellicent", "render")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// override fun getFaintAnimation(
|
||||
// pokemonEntity: PokemonEntity,
|
||||
// state: PosableState<PokemonEntity>
|
||||
// ) = if (state.isPosedIn(standing, walk)) bedrockStateful("jellicent", "faint") else null
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen6
|
||||
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.animation.BimanualSwingAnimation
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.animation.BipedWalkAnimation
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BimanualFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BipedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
|
||||
import com.cobblemon.mod.common.entity.PoseType.Companion.MOVING_POSES
|
||||
import com.cobblemon.mod.common.entity.PoseType.Companion.STATIONARY_POSES
|
||||
import com.cobblemon.mod.common.entity.PoseType.Companion.UI_POSES
|
||||
import net.minecraft.client.model.geom.ModelPart
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
class ChesnaughtModel (root: ModelPart) : PokemonPosableModel(root), HeadedFrame, BipedFrame, BimanualFrame {
|
||||
override val rootPart = root.registerChildWithAllChildren("chesnaught")
|
||||
override val head = getPart("head")
|
||||
override val rightArm = getPart("arm_right")
|
||||
override val leftArm = getPart("arm_left")
|
||||
override val leftLeg = getPart("leg_left")
|
||||
override val rightLeg = getPart("leg_right")
|
||||
|
||||
override var portraitScale = 2.2F
|
||||
override var portraitTranslation = Vec3(-1.0, 1.65, 0.0)
|
||||
|
||||
override var profileScale = 0.6F
|
||||
override var profileTranslation = Vec3(0.0, 0.85, 0.0)
|
||||
|
||||
|
||||
lateinit var standing: Pose
|
||||
lateinit var walk: Pose
|
||||
|
||||
override val cryAnimation = CryProvider { bedrockStateful("chesnaught", "cry") }
|
||||
|
||||
override fun registerPoses() {
|
||||
val blink = quirk { bedrockStateful("chesnaught", "blink")}
|
||||
|
||||
standing = registerPose(
|
||||
poseName = "standing",
|
||||
poseTypes = STATIONARY_POSES + UI_POSES,
|
||||
quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("chesnaught", "ground_idle")
|
||||
)
|
||||
)
|
||||
|
||||
walk = registerPose(
|
||||
poseName = "walk",
|
||||
poseTypes = MOVING_POSES,
|
||||
quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
BimanualSwingAnimation(this),
|
||||
BipedWalkAnimation(this),
|
||||
bedrock("chesnaught", "ground_idle")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen6
|
||||
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BimanualFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BipedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import net.minecraft.client.model.geom.ModelPart
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
class ChespinModel (root: ModelPart) : PokemonPosableModel(root), HeadedFrame, BipedFrame, BimanualFrame {
|
||||
override val rootPart = root.registerChildWithAllChildren("chespin")
|
||||
override val head = getPart("head")
|
||||
override val rightArm = getPart("arm_right")
|
||||
override val leftArm = getPart("arm_left")
|
||||
override val leftLeg = getPart("leg_left")
|
||||
override val rightLeg = getPart("leg_right")
|
||||
|
||||
override var portraitScale = 2.03F
|
||||
override var portraitTranslation = Vec3(-0.12, -0.44, 0.0)
|
||||
|
||||
override var profileScale = 0.77F
|
||||
override var profileTranslation = Vec3(0.0, 0.6, 0.0)
|
||||
|
||||
lateinit var standing: Pose
|
||||
lateinit var walk: Pose
|
||||
lateinit var sleep: Pose
|
||||
|
||||
override val cryAnimation = CryProvider { bedrockStateful("chespin", "cry") }
|
||||
|
||||
override fun registerPoses() {
|
||||
val blink = quirk { bedrockStateful("chespin", "blink")}
|
||||
|
||||
standing = registerPose(
|
||||
poseName = "standing",
|
||||
poseTypes = PoseType.STATIONARY_POSES + PoseType.UI_POSES,
|
||||
quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("chespin", "ground_idle")
|
||||
)
|
||||
)
|
||||
|
||||
walk = registerPose(
|
||||
poseName = "walk",
|
||||
poseTypes = PoseType.MOVING_POSES,
|
||||
quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("chespin", "ground_walk")
|
||||
)
|
||||
)
|
||||
|
||||
sleep = registerPose(
|
||||
poseType = PoseType.SLEEP,
|
||||
transformTicks = 10,
|
||||
animations = arrayOf(bedrock("chespin", "sleep"))
|
||||
)
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen6
|
||||
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BimanualFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BipedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import com.cobblemon.mod.common.util.isBattling
|
||||
import net.minecraft.client.model.geom.ModelPart
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
class FroakieModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame, BipedFrame, BimanualFrame {
|
||||
override val rootPart = root.registerChildWithAllChildren("froakie")
|
||||
override val head = getPart("head")
|
||||
override val rightArm = getPart("arm_right")
|
||||
override val leftArm = getPart("arm_left")
|
||||
override val rightLeg = getPart("leg_right")
|
||||
override val leftLeg = getPart("leg_left")
|
||||
|
||||
override var portraitScale = 1.86F
|
||||
override var portraitTranslation = Vec3(-0.25, -0.51, 0.0)
|
||||
|
||||
override var profileScale = 0.75F
|
||||
override var profileTranslation = Vec3(0.0, 0.56, 0.0)
|
||||
|
||||
lateinit var sleep: Pose
|
||||
lateinit var standing: Pose
|
||||
lateinit var float: Pose
|
||||
lateinit var swim: Pose
|
||||
lateinit var walk: Pose
|
||||
lateinit var battleidle: Pose
|
||||
|
||||
override val cryAnimation = CryProvider { bedrockStateful("froakie", "cry") }
|
||||
|
||||
override fun registerPoses() {
|
||||
val blink = quirk { bedrockStateful("froakie", "blink")}
|
||||
sleep = registerPose(
|
||||
poseType = PoseType.SLEEP,
|
||||
transformTicks = 10,
|
||||
quirks = arrayOf(blink),
|
||||
animations = arrayOf(bedrock("froakie", "sleep"))
|
||||
)
|
||||
|
||||
standing = registerPose(
|
||||
poseName = "standing",
|
||||
poseTypes = PoseType.UI_POSES + PoseType.STATIONARY_POSES - PoseType.FLOAT,
|
||||
transformTicks = 10,
|
||||
quirks = arrayOf(blink),
|
||||
condition = { !it.isBattling },
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("froakie", "ground_idle")
|
||||
)
|
||||
)
|
||||
|
||||
walk = registerPose(
|
||||
poseName = "walk",
|
||||
poseType = PoseType.WALK,
|
||||
transformTicks = 10,
|
||||
quirks = arrayOf(blink),
|
||||
condition = { !it.isBattling },
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("froakie", "ground_walk")
|
||||
)
|
||||
)
|
||||
|
||||
float = registerPose(
|
||||
poseName = "swim_idle",
|
||||
poseTypes = setOf(PoseType.FLOAT, PoseType.HOVER),
|
||||
transformTicks = 10,
|
||||
quirks = arrayOf(blink),
|
||||
condition = { !it.isBattling },
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("froakie", "water_idle")
|
||||
)
|
||||
)
|
||||
|
||||
swim = registerPose(
|
||||
poseName = "swim",
|
||||
poseTypes = setOf(PoseType.SWIM, PoseType.FLY),
|
||||
transformTicks = 10,
|
||||
quirks = arrayOf(blink),
|
||||
condition = { !it.isBattling },
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("froakie", "water_swim")
|
||||
)
|
||||
)
|
||||
|
||||
battleidle = registerPose(
|
||||
poseName = "battle_idle",
|
||||
poseTypes = PoseType.STATIONARY_POSES,
|
||||
transformTicks = 10,
|
||||
quirks = arrayOf(blink),
|
||||
condition = { it.isBattling },
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("froakie", "battle_idle")
|
||||
)
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
override fun getFaintAnimation(state: PosableState) = if (state.isPosedIn(standing, walk, battleidle, swim, float, sleep)) bedrockStateful("froakie", "faint") else null
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen6
|
||||
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BimanualFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BipedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import com.cobblemon.mod.common.util.isBattling
|
||||
import net.minecraft.client.model.geom.ModelPart
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
class FrogadierModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame, BipedFrame, BimanualFrame {
|
||||
override val rootPart = root.registerChildWithAllChildren("frogadier")
|
||||
override val head = getPart("head")
|
||||
override val rightArm = getPart("arm_right")
|
||||
override val leftArm = getPart("arm_left")
|
||||
override val rightLeg = getPart("leg_right")
|
||||
override val leftLeg = getPart("leg_left")
|
||||
|
||||
override var portraitScale = 1.9F
|
||||
override var portraitTranslation = Vec3(-0.1, 1.4, 0.0)
|
||||
|
||||
override var profileScale = 0.7F
|
||||
override var profileTranslation = Vec3(0.0, 0.7, 0.0)
|
||||
|
||||
lateinit var sleep: Pose
|
||||
lateinit var standing: Pose
|
||||
lateinit var float: Pose
|
||||
lateinit var swim: Pose
|
||||
lateinit var walk: Pose
|
||||
lateinit var battleidle: Pose
|
||||
|
||||
override val cryAnimation = CryProvider { bedrockStateful("frogadier", "cry") }
|
||||
|
||||
override fun registerPoses() {
|
||||
val blink = quirk { bedrockStateful("frogadier", "blink")}
|
||||
sleep = registerPose(
|
||||
poseType = PoseType.SLEEP,
|
||||
transformTicks = 10,
|
||||
quirks = arrayOf(blink),
|
||||
animations = arrayOf(bedrock("frogadier", "sleep"))
|
||||
)
|
||||
|
||||
standing = registerPose(
|
||||
poseName = "standing",
|
||||
poseTypes = PoseType.UI_POSES + PoseType.STATIONARY_POSES - PoseType.FLOAT,
|
||||
transformTicks = 10,
|
||||
quirks = arrayOf(blink),
|
||||
condition = { !it.isBattling },
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("frogadier", "ground_idle")
|
||||
)
|
||||
)
|
||||
|
||||
walk = registerPose(
|
||||
poseName = "walk",
|
||||
poseType = PoseType.WALK,
|
||||
transformTicks = 10,
|
||||
quirks = arrayOf(blink),
|
||||
condition = { !it.isBattling },
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("frogadier", "ground_walk")
|
||||
)
|
||||
)
|
||||
|
||||
float = registerPose(
|
||||
poseName = "swim_idle",
|
||||
poseTypes = setOf(PoseType.FLOAT, PoseType.HOVER),
|
||||
transformTicks = 10,
|
||||
quirks = arrayOf(blink),
|
||||
condition = { !it.isBattling },
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("frogadier", "water_idle")
|
||||
)
|
||||
)
|
||||
|
||||
swim = registerPose(
|
||||
poseName = "swim",
|
||||
poseTypes = setOf(PoseType.SWIM, PoseType.FLY),
|
||||
transformTicks = 10,
|
||||
quirks = arrayOf(blink),
|
||||
condition = { !it.isBattling },
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("frogadier", "water_swim")
|
||||
)
|
||||
)
|
||||
|
||||
battleidle = registerPose(
|
||||
poseName = "battle_idle",
|
||||
poseTypes = PoseType.STATIONARY_POSES,
|
||||
transformTicks = 10,
|
||||
quirks = arrayOf(blink),
|
||||
condition = { it.isBattling },
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("frogadier", "battle_idle")
|
||||
)
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
override fun getFaintAnimation(state: PosableState) = if (state.isPosedIn(standing, walk, battleidle, swim, float, sleep)) bedrockStateful("frogadier", "faint") else null
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen6
|
||||
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.createTransformation
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BimanualFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BipedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import com.cobblemon.mod.common.util.isBattling
|
||||
import net.minecraft.client.model.geom.ModelPart
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
class GreninjaModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame, BipedFrame, BimanualFrame {
|
||||
override val rootPart = root.registerChildWithAllChildren("greninja")
|
||||
override val head = getPart("head")
|
||||
override val rightArm = getPart("arm_right")
|
||||
override val leftArm = getPart("arm_left")
|
||||
override val rightLeg = getPart("leg_right")
|
||||
override val leftLeg = getPart("leg_left")
|
||||
val shurikenright = getPart("hand_shuriken_right")
|
||||
val shurikenleft = getPart("hand_shuriken_left")
|
||||
|
||||
override var portraitScale = 2.2F
|
||||
override var portraitTranslation = Vec3(-0.1, 1.34, 0.0)
|
||||
|
||||
override var profileScale = 0.7F
|
||||
override var profileTranslation = Vec3(0.0, 0.7, 0.0)
|
||||
|
||||
lateinit var sleep: Pose
|
||||
lateinit var standing: Pose
|
||||
lateinit var float: Pose
|
||||
lateinit var swim: Pose
|
||||
lateinit var walk: Pose
|
||||
lateinit var battleidle: Pose
|
||||
|
||||
override val cryAnimation = CryProvider { bedrockStateful("greninja", "cry") }
|
||||
|
||||
override fun registerPoses() {
|
||||
val blink = quirk { bedrockStateful("greninja", "blink")}
|
||||
sleep = registerPose(
|
||||
poseType = PoseType.SLEEP,
|
||||
transformTicks = 10,
|
||||
transformedParts = arrayOf(
|
||||
shurikenleft.createTransformation().withVisibility(visibility = false),
|
||||
shurikenright.createTransformation().withVisibility(visibility = false)
|
||||
),
|
||||
animations = arrayOf(bedrock("greninja", "sleep"))
|
||||
)
|
||||
|
||||
standing = registerPose(
|
||||
poseName = "standing",
|
||||
poseTypes = PoseType.UI_POSES + PoseType.STATIONARY_POSES - PoseType.FLOAT,
|
||||
transformTicks = 10,
|
||||
quirks = arrayOf(blink),
|
||||
condition = { !it.isBattling },
|
||||
transformedParts = arrayOf(
|
||||
shurikenleft.createTransformation().withVisibility(visibility = false),
|
||||
shurikenright.createTransformation().withVisibility(visibility = false)
|
||||
),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("greninja", "ground_idle")
|
||||
)
|
||||
)
|
||||
|
||||
walk = registerPose(
|
||||
poseName = "walk",
|
||||
poseType = PoseType.WALK,
|
||||
transformTicks = 10,
|
||||
quirks = arrayOf(blink),
|
||||
transformedParts = arrayOf(
|
||||
shurikenleft.createTransformation().withVisibility(visibility = false),
|
||||
shurikenright.createTransformation().withVisibility(visibility = false)
|
||||
),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("greninja", "ground_walk")
|
||||
)
|
||||
)
|
||||
|
||||
float = registerPose(
|
||||
poseName = "swim_idle",
|
||||
poseTypes = setOf(PoseType.FLOAT, PoseType.HOVER),
|
||||
transformTicks = 10,
|
||||
quirks = arrayOf(blink),
|
||||
condition = { !it.isBattling },
|
||||
transformedParts = arrayOf(
|
||||
shurikenleft.createTransformation().withVisibility(visibility = false),
|
||||
shurikenright.createTransformation().withVisibility(visibility = false)
|
||||
),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("greninja", "water_idle")
|
||||
)
|
||||
)
|
||||
|
||||
swim = registerPose(
|
||||
poseName = "swim",
|
||||
poseTypes = setOf(PoseType.SWIM, PoseType.FLY),
|
||||
transformTicks = 10,
|
||||
quirks = arrayOf(blink),
|
||||
condition = { !it.isBattling },
|
||||
transformedParts = arrayOf(
|
||||
shurikenleft.createTransformation().withVisibility(visibility = false),
|
||||
shurikenright.createTransformation().withVisibility(visibility = false)
|
||||
),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("greninja", "water_swim")
|
||||
)
|
||||
)
|
||||
|
||||
battleidle = registerPose(
|
||||
poseName = "battle_idle",
|
||||
poseTypes = PoseType.STATIONARY_POSES,
|
||||
transformTicks = 10,
|
||||
quirks = arrayOf(blink),
|
||||
condition = { it.isBattling },
|
||||
transformedParts = arrayOf(
|
||||
shurikenleft.createTransformation().withVisibility(visibility = false),
|
||||
shurikenright.createTransformation().withVisibility(visibility = false)
|
||||
),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("greninja", "battle_idle")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getFaintAnimation(state: PosableState) = if (state.isPosedIn(standing, walk, battleidle, swim, float, sleep)) bedrockStateful("greninja", "faint") else null
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen6
|
||||
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BimanualFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BipedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import com.cobblemon.mod.common.entity.PoseType.Companion.MOVING_POSES
|
||||
import com.cobblemon.mod.common.entity.PoseType.Companion.STATIONARY_POSES
|
||||
import com.cobblemon.mod.common.entity.PoseType.Companion.UI_POSES
|
||||
import net.minecraft.client.model.geom.ModelPart
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
class QuilladinModel (root: ModelPart) : PokemonPosableModel(root), HeadedFrame, BipedFrame, BimanualFrame {
|
||||
override val rootPart = root.registerChildWithAllChildren("quilladin")
|
||||
override val head = getPart("torso")
|
||||
override val rightArm = getPart("arm_right")
|
||||
override val leftArm = getPart("arm_left")
|
||||
override val leftLeg = getPart("leg_left")
|
||||
override val rightLeg = getPart("leg_right")
|
||||
|
||||
override var portraitScale = 2F
|
||||
override var portraitTranslation = Vec3(-0.3, 0.1, 0.0)
|
||||
|
||||
override var profileScale = 0.7F
|
||||
override var profileTranslation = Vec3(0.0, 0.7, 0.0)
|
||||
|
||||
lateinit var standing: Pose
|
||||
lateinit var walk: Pose
|
||||
lateinit var sleep: Pose
|
||||
|
||||
override val cryAnimation = CryProvider { bedrockStateful("quilladin", "cry") }
|
||||
|
||||
override fun registerPoses() {
|
||||
val blink = quirk { bedrockStateful("quilladin", "blink")}
|
||||
|
||||
standing = registerPose(
|
||||
poseName = "standing",
|
||||
poseTypes = STATIONARY_POSES + UI_POSES,
|
||||
quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("quilladin", "ground_idle")
|
||||
)
|
||||
)
|
||||
|
||||
walk = registerPose(
|
||||
poseName = "walk",
|
||||
poseTypes = MOVING_POSES,
|
||||
quirks = arrayOf(blink),
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("quilladin", "ground_walk")
|
||||
)
|
||||
)
|
||||
|
||||
sleep = registerPose(
|
||||
poseType = PoseType.SLEEP,
|
||||
transformTicks = 10,
|
||||
animations = arrayOf(bedrock("quilladin", "sleep"))
|
||||
)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -29,7 +29,7 @@ class VivillonModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame, B
|
||||
override var portraitTranslation = Vec3(-0.3, 0.2, 0.0)
|
||||
|
||||
override var profileScale = 0.7F
|
||||
override var profileTranslation = Vec3(0.0, 0.8, 0.0)
|
||||
override var profileTranslation = Vec3(0.1, 0.8, 0.0)
|
||||
|
||||
lateinit var standing: Pose
|
||||
lateinit var walk: Pose
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen7
|
||||
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPosableModel
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import com.cobblemon.mod.common.entity.PoseType.Companion.UI_POSES
|
||||
import net.minecraft.client.model.geom.ModelPart
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
class CrabominableModel (root: ModelPart) : PokemonPosableModel(root), HeadedFrame {
|
||||
override val rootPart = root.registerChildWithAllChildren("crabominable")
|
||||
override val head = getPart("head")
|
||||
|
||||
override var portraitScale = 1.0F
|
||||
override var portraitTranslation = Vec3(-0.55, 1.65, 0.0)
|
||||
|
||||
override var profileScale = 0.3F
|
||||
override var profileTranslation = Vec3(0.0, 1.2, 0.0)
|
||||
|
||||
lateinit var standing: Pose
|
||||
lateinit var walk: Pose
|
||||
|
||||
override fun registerPoses() {
|
||||
standing = registerPose(
|
||||
poseName = "standing",
|
||||
poseTypes = PoseType.STATIONARY_POSES + UI_POSES,
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("crabominable", "ground_idle")
|
||||
)
|
||||
)
|
||||
|
||||
walk = registerPose(
|
||||
poseName = "walk",
|
||||
poseTypes = PoseType.MOVING_POSES,
|
||||
animations = arrayOf(
|
||||
singleBoneLook(),
|
||||
bedrock("crabominable", "ground_idle"),
|
||||
bedrock("crabominable", "ground_walk")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// override fun getFaintAnimation(
|
||||
// pokemonEntity: PokemonEntity,
|
||||
// state: PosableState<PokemonEntity>
|
||||
// ) = if (state.isPosedIn(standing, walk)) bedrockStateful("crabominable", "faint") else null
|
||||
}
|
||||
+13
@@ -220,8 +220,10 @@ object VaryingModelRepository {
|
||||
|
||||
inbuilt("squirtle", ::SquirtleModel)
|
||||
inbuilt("wartortle", ::WartortleModel)
|
||||
inbuilt("caterpie", ::CaterpieModel)
|
||||
inbuilt("metapod", ::MetapodModel)
|
||||
inbuilt("butterfree", ::ButterfreeModel)
|
||||
inbuilt("weedle", ::WeedleModel)
|
||||
inbuilt("beedrill", ::BeedrillModel)
|
||||
inbuilt("raticate", ::RaticateModel)
|
||||
inbuilt("raticate_alolan", ::RaticateAlolanModel)
|
||||
@@ -309,6 +311,9 @@ object VaryingModelRepository {
|
||||
inbuilt("morelull", :: MorelullModel)
|
||||
inbuilt("shiinotic", :: ShiinoticModel)
|
||||
inbuilt("spiritomb", :: SpiritombModel)
|
||||
inbuilt("chespin", :: ChespinModel)
|
||||
inbuilt("quilladin", :: QuilladinModel)
|
||||
inbuilt("chesnaught", :: ChesnaughtModel)
|
||||
inbuilt("pineco", :: PinecoModel)
|
||||
inbuilt("lotad", :: LotadModel)
|
||||
inbuilt("lombre", :: LombreModel)
|
||||
@@ -336,6 +341,9 @@ object VaryingModelRepository {
|
||||
inbuilt("walkingwake", ::WalkingwakeModel)
|
||||
inbuilt("ironleaves", ::IronleavesModel)
|
||||
inbuilt("miltank", ::MiltankModel)
|
||||
inbuilt("froakie", ::FroakieModel)
|
||||
inbuilt("frogadier", ::FrogadierModel)
|
||||
inbuilt("greninja", ::GreninjaModel)
|
||||
inbuilt("grookey", ::GrookeyModel)
|
||||
inbuilt("thwackey", ::ThwackeyModel)
|
||||
inbuilt("rillaboom", ::RillaboomModel)
|
||||
@@ -388,6 +396,9 @@ object VaryingModelRepository {
|
||||
inbuilt("hoothoot", ::HoothootModel)
|
||||
inbuilt("wingull", ::WingullModel)
|
||||
inbuilt("pelipper", ::PelipperModel)
|
||||
inbuilt("shinx", ::ShinxModel)
|
||||
inbuilt("luxio", ::LuxioModel)
|
||||
inbuilt("luxray", ::LuxrayModel)
|
||||
inbuilt("numel", ::NumelModel)
|
||||
inbuilt("roggenrola", ::RoggenrolaModel)
|
||||
inbuilt("boldore", ::BoldoreModel)
|
||||
@@ -444,6 +455,7 @@ object VaryingModelRepository {
|
||||
inbuilt("bayleef", ::BayleefModel)
|
||||
inbuilt("meganium", ::MeganiumModel)
|
||||
inbuilt("crabrawler", ::CrabrawlerModel)
|
||||
inbuilt("crabominable", ::CrabominableModel)
|
||||
inbuilt("wimpod", ::WimpodModel)
|
||||
inbuilt("golisopod", ::GolisopodModel)
|
||||
inbuilt("ralts", ::RaltsModel)
|
||||
@@ -499,6 +511,7 @@ object VaryingModelRepository {
|
||||
inbuilt("woobat", ::WoobatModel)
|
||||
inbuilt("swoobat", ::SwoobatModel)
|
||||
inbuilt("frillish", ::FrillishModel)
|
||||
inbuilt("jellicent", ::JellicentModel)
|
||||
inbuilt("cubchoo", ::CubchooModel)
|
||||
inbuilt("beartic", ::BearticModel)
|
||||
inbuilt("dreepy", ::DreepyModel)
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
+27
-20
@@ -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
|
||||
@@ -319,37 +331,32 @@ class PokemonRenderer(
|
||||
val clientDelegate = entity.delegate as PokemonClientDelegate
|
||||
val pokemonPosition = entity.position().add(0.0, entity.bbHeight / 2.0 * clientDelegate.activeSendoutScale.toDouble(), 0.0)
|
||||
val recallBallOrigin = (beamTarget as? Player)?.let { RecallArmAnimationTracker.get(it.uuid)?.beamOrigin }
|
||||
|
||||
val beamSourcePosition = when {
|
||||
// cover the recall ball origin presence
|
||||
recallBallOrigin != null -> recallBallOrigin
|
||||
// cover the pokeball catching a specimen
|
||||
beamTarget is EmptyPokeBallEntity -> (beamTarget.delegate as PokeBallPosableState)
|
||||
.locatorStates["beam"]
|
||||
?.transformPosition(Vec3(0.0, 0.2, 0.45)) ?: beamTarget.position()
|
||||
// player recall
|
||||
beamTarget.uuid == Minecraft.getInstance().player?.uuid -> {
|
||||
var beamSourcePosition = if (recallBallOrigin != null) {
|
||||
recallBallOrigin
|
||||
} else if (beamTarget is EmptyPokeBallEntity) {
|
||||
(beamTarget.delegate as PokeBallPosableState).locatorStates["beam"]?.transformPosition(Vec3(0.0, 0.2, 0.45)) ?: beamTarget.position()
|
||||
} else {
|
||||
if (beamTarget.uuid == Minecraft.getInstance().player?.uuid) {
|
||||
val lookVec = beamTarget.lookAngle.yRot((PI / 2).toFloat()).multiply(1.0, 0.0, 1.0).normalize()
|
||||
beamTarget.getEyePosition(partialTicks).subtract(0.0, 0.4, 0.0).subtract(lookVec.scale(0.3))
|
||||
}
|
||||
// npc recall
|
||||
beamTarget is NPCEntity -> {
|
||||
} else if (beamTarget is NPCEntity) {
|
||||
val npcDelegate = beamTarget.delegate as NPCClientDelegate
|
||||
val baseScale = beamTarget.renderScale * beamTarget.hitboxScale
|
||||
|
||||
(npcDelegate.locatorStates["beam"]?.getOrigin()?.scale(baseScale.toDouble()))
|
||||
?: beamTarget.position().add(0.0, (beamTarget.bbHeight / 2.0) * baseScale, 0.0)
|
||||
}
|
||||
clientDelegate.sendOutPosition != null -> clientDelegate.sendOutPosition!!
|
||||
// ???
|
||||
else -> {
|
||||
} else {
|
||||
val lookVec = beamTarget.lookAngle.yRot((PI / 2 - (beamTarget.visualRotationYInDegrees - beamTarget.xRot).toRadians()).toFloat()).multiply(1.0, 0.0, 1.0).normalize()
|
||||
beamTarget.getEyePosition(partialTicks).subtract(0.0, 0.7, 0.0).subtract(lookVec.scale(0.4))
|
||||
}
|
||||
}
|
||||
if (clientDelegate.sendOutPosition != null) {
|
||||
beamSourcePosition = clientDelegate.sendOutPosition!!
|
||||
}
|
||||
|
||||
if (beamSourcePosition.distanceTo(pokemonPosition) > 20) return
|
||||
|
||||
if (beamSourcePosition.distanceTo(pokemonPosition) > 20) {
|
||||
return
|
||||
}
|
||||
var newOffset = offset.scale(2.0)
|
||||
// the further away the source position is, the smaller the newOffset should be. Max distance is 20 blocks
|
||||
val distance = beamSourcePosition.distanceTo(entity.position())
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
package com.cobblemon.mod.common.command
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonSounds
|
||||
import com.cobblemon.mod.common.api.permission.CobblemonPermissions
|
||||
import com.cobblemon.mod.common.api.storage.pc.link.PCLinkManager
|
||||
import com.cobblemon.mod.common.api.storage.pc.link.PermissiblePcLink
|
||||
@@ -53,8 +54,8 @@ object PcCommand {
|
||||
throw SimpleCommandExceptionType(lang("command.pc.invalid-box", box, pc.boxes.size).red()).create()
|
||||
}
|
||||
PCLinkManager.addLink(PermissiblePcLink(pc, player, CobblemonPermissions.PC))
|
||||
OpenPCPacket(pc, box = if (box != null) (box - 1) else null, silent = true).sendToPlayer(player)
|
||||
OpenPCPacket(pc, box = if (box != null) (box - 1) else null).sendToPlayer(player)
|
||||
return Command.SINGLE_SUCCESS
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -11,7 +11,6 @@ package com.cobblemon.mod.common.command.argument
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.pokemon.FormData
|
||||
import com.cobblemon.mod.common.pokemon.Species
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import com.google.common.base.Splitter
|
||||
import com.mojang.brigadier.StringReader
|
||||
import com.mojang.brigadier.arguments.ArgumentType
|
||||
@@ -39,7 +38,7 @@ class FormArgumentType : ArgumentType<FormData> {
|
||||
}
|
||||
speciesBeginning++
|
||||
val speciesToken = reader.string.substring(speciesBeginning, formTokenBeginning)
|
||||
val species = PokemonSpecies.getByIdentifier(speciesToken.asIdentifierDefaultingNamespace()) ?: PokemonSpecies.species.first()
|
||||
val species = PokemonSpecies.getByName(speciesToken) ?: PokemonSpecies.species.first()
|
||||
val result = species.forms.union(listOf(species.standardForm)).first { it.formOnlyShowdownId().lowercase() == formToken }
|
||||
return result
|
||||
}
|
||||
@@ -58,7 +57,7 @@ class FormArgumentType : ArgumentType<FormData> {
|
||||
curIdx++
|
||||
val speciesToken = builder.input.substring(curIdx, builder.start - 1)
|
||||
|
||||
val species = PokemonSpecies.getByIdentifier(speciesToken.asIdentifierDefaultingNamespace()) ?: PokemonSpecies.species.first()
|
||||
val species = PokemonSpecies.getByName(speciesToken) ?: PokemonSpecies.species.first()
|
||||
val forms = species.forms.union(listOf(species.standardForm)).filter { it.formOnlyShowdownId().lowercase().startsWith(formToken) }
|
||||
forms.forEach { str ->
|
||||
builder.suggest(str.formOnlyShowdownId())
|
||||
|
||||
@@ -222,16 +222,16 @@ class CobblemonConfig {
|
||||
@CobblemonConfigField(Category.Spawning, lang = "shiny_rate", SERVER)
|
||||
var shinyRate = 8192F
|
||||
|
||||
@CobblemonConfigField(Category.Spawning, lang = "fossil_machine_alpha_chance", SERVER)
|
||||
@CobblemonConfigField(Category.Pokemon, lang = "fossil_machine_alpha_chance", SERVER)
|
||||
var fossilMachineAlphaChance = 20
|
||||
|
||||
@CobblemonConfigField(Category.Spawning, lang = "fossil_machine_shiny_chance", SERVER)
|
||||
@CobblemonConfigField(Category.Pokemon, lang = "fossil_machine_shiny_chance", SERVER)
|
||||
var fossilMachineShinyChance = 100
|
||||
|
||||
@CobblemonConfigField(Category.Spawning, lang = "honey_slather_alpha_chance", SERVER)
|
||||
@CobblemonConfigField(Category.Pokemon, lang = "honey_slather_alpha_chance", SERVER)
|
||||
var honeySlatherAlphaChance = 100
|
||||
|
||||
@CobblemonConfigField(Category.Spawning, lang = "honey_slather_shiny_chance", SERVER)
|
||||
@CobblemonConfigField(Category.Pokemon, lang = "honey_slather_shiny_chance", SERVER)
|
||||
var honeySlatherShinyChance = 4000
|
||||
|
||||
@CobblemonConfigField(Category.Pokemon, lang = "shiny_notice_particles_distance", CLIENT)
|
||||
@@ -392,21 +392,6 @@ class CobblemonConfig {
|
||||
@CobblemonConfigField(Category.Spawning, lang = "data_monitor_shiny_rate", SERVER)
|
||||
var monitorShinyRate = 200
|
||||
|
||||
@CobblemonConfigField(Category.World, lang = "type_gem_max_connected", SERVER)
|
||||
var typeGemMaxConnected = 16
|
||||
|
||||
@CobblemonConfigField(Category.World, lang = "type_gem_min_growth", SERVER)
|
||||
var typeGemMinGrowth = 8
|
||||
|
||||
@CobblemonConfigField(Category.World, lang = "type_gem_growth_chance", SERVER)
|
||||
var typeGemGrowthChance = 0.1f
|
||||
|
||||
@CobblemonConfigField(Category.World, lang = "type_gem_initial_expansion_cycles", SERVER)
|
||||
var typeGemInitialExpansionCycles = 100
|
||||
|
||||
@CobblemonConfigField(Category.World, lang = "type_gem_tick_chance", SERVER)
|
||||
var typeGemTickChance = 1.0f
|
||||
|
||||
fun clone(): CobblemonConfig {
|
||||
val newConfig = CobblemonConfig()
|
||||
CobblemonConfig::class.memberProperties.forEach { property ->
|
||||
|
||||
@@ -30,7 +30,7 @@ object CobblemonSchemas {
|
||||
private val SAME: (Int, Schema) -> Schema = ::Schema
|
||||
private val RESULT: DataFixerBuilder.Result = this.create()
|
||||
|
||||
const val DATA_VERSION = 3
|
||||
const val DATA_VERSION = 2
|
||||
|
||||
/**
|
||||
* The Cobblemon [DataFixer].
|
||||
@@ -85,8 +85,6 @@ object CobblemonSchemas {
|
||||
builder.addFixer(RaisedPPStagesFix(schema1))
|
||||
val schema2 = builder.addSchema(2, SAME)
|
||||
builder.addFixer(HypertrainIVFix(schema2))
|
||||
val schema3 = builder.addSchema(3, SAME)
|
||||
builder.addFixer(TypeGemGrowthTypeFix(schema3))
|
||||
}
|
||||
|
||||
private class CobblemonDataFixerCodec<R>(private val baseCodec: Codec<R>, private val typeReference: TypeReference) : Codec<R> {
|
||||
|
||||
-101
@@ -1,101 +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.datafixer.fix
|
||||
|
||||
import com.mojang.datafixers.DSL
|
||||
import com.mojang.datafixers.DataFix
|
||||
import com.mojang.datafixers.TypeRewriteRule
|
||||
import com.mojang.datafixers.schemas.Schema
|
||||
import com.mojang.serialization.Dynamic
|
||||
import net.minecraft.util.datafix.fixes.References
|
||||
|
||||
/**
|
||||
* Merges the old `should_grow` and `stunted` type gem cluster properties into `growth_type`.
|
||||
*/
|
||||
class TypeGemGrowthTypeFix(outputSchema: Schema) : DataFix(outputSchema, false) {
|
||||
|
||||
override fun makeRule(): TypeRewriteRule {
|
||||
return fixTypeEverywhereTyped("TypeGemGrowthTypeFix", inputSchema.getType(References.CHUNK)) { chunk ->
|
||||
chunk.update(DSL.remainderFinder(), ::fixChunk)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixChunk(chunk: Dynamic<*>): Dynamic<*> {
|
||||
return chunk.update("sections") { sections ->
|
||||
sections.createList(sections.asStream().map(::fixSection))
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixSection(section: Dynamic<*>): Dynamic<*> {
|
||||
return section.update("block_states") { blockStates ->
|
||||
blockStates.update("palette") { palette ->
|
||||
palette.createList(palette.asStream().map(::fixPaletteEntry))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixPaletteEntry(entry: Dynamic<*>): Dynamic<*> {
|
||||
if (entry.get("Name").asString("") !in CLUSTER_IDS) return entry
|
||||
|
||||
return entry.update("Properties") { properties ->
|
||||
if (properties.get(GROWTH_TYPE).result().isPresent) return@update properties
|
||||
|
||||
val shouldGrow = properties.get(SHOULD_GROW).asString(OLD_SHOULD_GROW_DEFAULT) == "true"
|
||||
val stunted = properties.get(STUNTED).asString(OLD_STUNTED_DEFAULT) == "true"
|
||||
|
||||
properties
|
||||
.remove(SHOULD_GROW)
|
||||
.remove(STUNTED)
|
||||
.set(GROWTH_TYPE, properties.createString(growthTypeFor(shouldGrow, stunted)))
|
||||
}
|
||||
}
|
||||
|
||||
private fun growthTypeFor(shouldGrow: Boolean, stunted: Boolean): String {
|
||||
return when {
|
||||
stunted -> GROWTH_TYPE_STUNTED
|
||||
shouldGrow -> GROWTH_TYPE_FULL
|
||||
else -> GROWTH_TYPE_NONE
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SHOULD_GROW = "should_grow"
|
||||
private const val STUNTED = "stunted"
|
||||
private const val GROWTH_TYPE = "growth_type"
|
||||
|
||||
private const val OLD_SHOULD_GROW_DEFAULT = "true"
|
||||
private const val OLD_STUNTED_DEFAULT = "false"
|
||||
|
||||
private const val GROWTH_TYPE_FULL = "full"
|
||||
private const val GROWTH_TYPE_STUNTED = "stunted"
|
||||
private const val GROWTH_TYPE_NONE = "none"
|
||||
|
||||
// ID's as they were when this fix was made.
|
||||
private val CLUSTER_IDS = setOf(
|
||||
"cobblemon:normal_gem_cluster",
|
||||
"cobblemon:fire_gem_cluster",
|
||||
"cobblemon:water_gem_cluster",
|
||||
"cobblemon:electric_gem_cluster",
|
||||
"cobblemon:grass_gem_cluster",
|
||||
"cobblemon:ice_gem_cluster",
|
||||
"cobblemon:fighting_gem_cluster",
|
||||
"cobblemon:poison_gem_cluster",
|
||||
"cobblemon:ground_gem_cluster",
|
||||
"cobblemon:flying_gem_cluster",
|
||||
"cobblemon:psychic_gem_cluster",
|
||||
"cobblemon:bug_gem_cluster",
|
||||
"cobblemon:rock_gem_cluster",
|
||||
"cobblemon:ghost_gem_cluster",
|
||||
"cobblemon:dragon_gem_cluster",
|
||||
"cobblemon:dark_gem_cluster",
|
||||
"cobblemon:steel_gem_cluster",
|
||||
"cobblemon:fairy_gem_cluster"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
+15
-3
@@ -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
|
||||
|
||||
@@ -269,7 +269,6 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
|
||||
attemptCatch(pokemonEntity)
|
||||
},
|
||||
ifCanceled = {
|
||||
captureFuture.complete(false)
|
||||
drop()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -1192,7 +1229,7 @@ open class PokemonEntity(
|
||||
itemStack.hurtAndBreak(1, player, EquipmentSlot.MAINHAND)
|
||||
return InteractionResult.SUCCESS
|
||||
} else if (itemStack.`is`(Items.BOWL)) {
|
||||
if (pokemon.aspects.any { it.contains("mooshtank") && !it.contains("mooshtank-false") }) {
|
||||
if (pokemon.aspects.any { it.contains("mooshtank") }) {
|
||||
player.playSound(SoundEvents.MOOSHROOM_MILK, 1.0f, 1.0f)
|
||||
// if the Mooshtank ate a Flower beforehand
|
||||
if (pokemon.lastFlowerFed != ItemStack.EMPTY && pokemon.aspects.any { it.contains("mooshtank-brown") }) {
|
||||
@@ -1234,8 +1271,7 @@ open class PokemonEntity(
|
||||
) {
|
||||
if (pokemon.aspects.any { it.contains("mooshtank-brown") }) {
|
||||
player.playSound(SoundEvents.MOOSHROOM_EAT, 1.0f, 1.0f)
|
||||
pokemon.lastFlowerFed = itemStack.copyWithCount(1)
|
||||
if (!player.isCreative && !level().isClientSide) itemStack.shrink(1)
|
||||
pokemon.lastFlowerFed = itemStack
|
||||
return InteractionResult.sidedSuccess(level().isClientSide)
|
||||
}
|
||||
} else if (!player.isShiftKeyDown && StashHandler.interactMob(player, pokemon, itemStack)) {
|
||||
@@ -1916,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)
|
||||
@@ -2742,4 +2787,4 @@ open class PokemonEntity(
|
||||
}
|
||||
|
||||
fun hasRider() = this.passengers.any { it is LivingEntity }
|
||||
}
|
||||
}
|
||||
+5
-2
@@ -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)
|
||||
}
|
||||
+97
@@ -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)
|
||||
}
|
||||
}
|
||||
+66
@@ -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)
|
||||
}
|
||||
}
|
||||
+258
-40
@@ -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.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user