Compare commits

..
Author SHA1 Message Date
jackowes cdb37c70f6 Fix up the steer force renderer and begin moving steering forces to an arbiter. Stepping stones to a future better system but its a start. 2026-04-08 23:26:32 -05:00
jackowes e052947d7a more obs avoid changes 2026-04-08 00:34:21 -05:00
jackowes 8ee18f7da4 dumb changes to go back to single block 2026-04-07 21:54:34 -05:00
jackowes 0ac418ec2c initial testing for object avoidance 2026-04-07 20:48:17 -05:00
jackowes 240804e201 Follow paths in smoothlier. Reynolds path following algorithm for steered entities 2026-04-05 23:17:55 -05:00
jackowes d3e1f6be9d Start of running ai 2026-04-05 14:33:43 -05:00
jackowes 6d7760ce30 Some cleanup and also a not so egregious implementation of steered behaviours with vanillas current path following logic. 2026-04-05 11:41:45 -05:00
jackowes 75e951191a squash merging the long way 2026-04-04 11:50:11 -05:00
jackowes 2157e9c09e - Bunch of moveControl testing and changes to steered behaviour logic.
- Added a few more tests mons to the new moveControl
2025-12-12 10:44:51 -06:00
jackowes 74ef1a61f9 - Fix pitch_tilt lerp to be 0.8 instead of 0.22
- Make it so players are affected by the custom push logic.
- Move base steering functions to extensions package
- Move the pokemonMoveControl over to steering logic temporarily
2025-12-05 18:26:49 -06:00
jackowes 23526824e8 - Attempted to smooth yrot turning
- weight influenced entity pushes
- make the predator seek its head to the entity position
2025-12-05 00:21:26 -06:00
jackowes 859e586503 - Pursue and avoidance tests
- Tried to hook up riding animations outside of riding but I think I forgot something.
2025-11-21 14:18:35 -06:00
jackowes a9b6c07ac9 - Add an avoid target 2025-11-20 00:35:55 -06:00
jackowes 82548e0624 - Fix to steering wander. 2025-11-19 23:56:52 -06:00
jackowes 552a405e2e - attempt to remedy line jumping 2025-11-16 09:35:20 -06:00
jackowes 08918956db Merge branch 'main' into manfred-herd-test
# Conflicts:
#	common/src/main/kotlin/com/cobblemon/mod/common/CobblemonActivities.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/CobblemonMemories.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/CobblemonNetwork.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/CobblemonSensors.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/entity/pokemon/PokemonEntity.kt
#	common/src/main/resources/assets/cobblemon/lang/en_us.json
2025-11-15 15:29:50 -06:00
jackowes c139a1fea9 Bunch of improvements to the debugRenderer. Still a one frame relocation of the render position 2025-09-19 00:42:02 -05:00
jackowes 040ae26f22 Merge branch 'main' into manfred-herd-test
# Conflicts:
#	common/src/main/kotlin/com/cobblemon/mod/common/api/riding/behaviour/types/liquid/DolphinBehaviour.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/entity/ai/FollowHerdLeaderTask.kt
2025-09-18 16:17:02 -05:00
jackowes 4c9b4a4f1f Add rudimentary obstacle avoidance debug renderer. 2025-09-15 01:22:57 -05:00
jackowes 7d34f83a8f some odd boid changes that might need to be changed back? cohesion not strong enough? 2025-09-13 10:12:15 -05:00
jackowes 8d89a37d93 unfinised wander 2025-09-09 21:29:21 -05:00
jackowes 65c5748c77 boids improvements 2025-09-08 20:18:54 -05:00
jackowes d4821c15bd full boids alg 1.0 2025-09-07 20:12:45 -05:00
jackowes c753ca54f1 start of boid nonsense. 2025-08-29 16:29:55 -05:00
jackowes ef581d1aa4 Merge remote-tracking branch 'origin/implement_pokemon' into manfred-herd-test 2025-08-28 13:08:07 -05:00
jackowes 1f4224b029 some random herd tests 2025-08-28 13:05:03 -05:00
494 changed files with 8115 additions and 3484 deletions

No files matched your search

+36 -115
View File
@@ -1,137 +1,58 @@
# Changelog
## [1.7.2 (January 25th, 2026)](#1-7-2)
## [1.7.4 (MONTH Xth, 2026)](#1-7-4)
### Additions
- Added Saccharine wood furniture for Adorn.
- Added the Partner Mark, which party Pokémon have a very small chance to earn as the player walks, provided they have enough friendship.
- Added decorative block models for existing items: Potion, Super Potion, Hyper Potion, Max Potion, Full Restore, Antidote, Awakening, Burn Heal, Ice Heal, Paralyze Heal, Full Heal, Ether, Elixir, Max Ether, Max Elixir, X Accuracy, X Attack, X Defence, X Special Attack, X Special Defence, X Speed, Dire Hit, Guard Spec, Weakness Policy, Blunder Policy, Cleanse Tag, and Spell Tag.
- Added decorative sign blocks called Plaques.
- Added ride style labels to ride stats in Pokédex, as well as ride style icons in the summary GUI.
- Added more seasonings for sinister tea:
- Mental Herb - Mental Restoration I for 10 seconds, which staves off Insomnia.
- White Herb - Removes negative effects.
- Milk, Moomoo Milk - Removes effects.
- Added dispenser support for:
- Applying a Honey Bottle to Saccharine Logs.
- Washing honey off of Saccharine Slathered Logs.
- Applying and removing honey on Saccharine Leaves.
- Added riding statistics: `Distance by Pokémon on Land`, `Distance by Pokémon in Air` and `Distance by Pokémon in Liquid`.
- Added config options to award experience to fainted Pokémon for opponents they helped defeat (`awardExperienceToFaintedPokemon`) and to still award experience for defeated enemies even after battle loss/forfeit (`awardExperienceOnBattleLoss`)
- Party Overlay Exp Gained Animation will now display the amount of EXP the Pokémon gained
- Added a toast to notify players of how they can scroll through their party. Did you know you can hold R and scroll instead of using arrow keys??!
- Added new layer property that makes a layer scroll in a direction. "scrolling": {"speedU": 0.1, "speedV": 0.1}
- Added missing crops to Botany Pots integration
### New rideable Pokémon
- Honchkrow
- Mantine
### Pokémon Added
#### Gen 5
- Throh
- Sawk
#### Gen 6
- Litleo
- Pyroar
- Spritzee
- Aromatisse
- Swirlix
- Slurpuff
### Model updates for the following Pokémon
- Falinks
- Cottonee
- Whimsicott
- Ferroseed
- Ferrothorn
- Chingling
- Chimecho
### Animation updates for the following Pokémon
- Mantine (ride animations)
- Flygon (ride jump animation)
- Honchkrow (ride animations)
- Croagunk
- Toxicroak
### Changes
- Added a new config option, `Enable In-Flight Dismounting` (default: off), which lets you dismount while riding a Pokémon in the air. This is the textbook definition of caveat emptor.
- Ride sounds have been separated into stereo for passengers and mono for other players. Riding will now sound more spacious.
- Battle AI now uses a smarter threshold for switching, reducing unnecessary switches.
- AI will always use the most damaging move when at low HP and when it is not switching, improving endgame decision-making.
- Items given from interacting with Saccharine Leaves are placed in the active hotbar slot if possible.
- Added `label` as an alternate key to the `tag` property.
- Restructured Botany Pots integration folder structure
- Decrease default pastured mon wander radius to 32.
- Factor in the weight of pokemon when calculating pushing forces between hitboxes.
- Remade the Starter Selection Screen with new assets
- Refactored berry trees to be less impactful on TPS (more performant)
### Fixes
- Fixed crashing when viewing another player in spectator mode. Some said this was reason to release this update a month ago. They were probably right.
- Fixed crashing when sending a Pokémon out after editing its form.
- Fixed all Pokémon being saved to chunks and never despawning even when they should be.
- Fixed Poké Snacks crashing if there was no available spawn.
- Fixed Poké Snack / Bait effects so that EV Yield, Type, and Egg Group filters now all apply together when weighting spawns. And you thought you were going crazy.
- Fixed Furfrou not being trimmable on NeoForge specifically.
- Fixed honey from Saccharine Leaves being harvestable at less than the max age.
- Fixed possible error on world generation when Combee are placed in naturally generated bee nests.
- Fixed Galarian Weezing crashing the world if it decides to blink the wrong way. The same could happen to you!
- Fixed Galarian Mr. Mime incorrectly being rideable. Some would argue that regular Mr. Mime shouldn't be rideable, but that would make them wrong.
- Fixed missing apricorn textures for Adorn blocks.
- Fixed Apricorn block model rotation for counters/sinks/cupboards/drawers to match that of most recent version of Adorn.
- Fixed item interaction sometimes not working properly when playing on servers.
- Fixed Mint Leaves not being usable for filling the Resurrection Machine.
- Fixed Super Potion recipes incorrectly using Hondew Berry instead of Aguav Berry. They look similar, to be fair.
- Fixed Wishiwashi not schooling properly.
- Fixed battle item duplication issue. How did it work? Wouldn't you like to know, weather boy.
- Fixed the ordering of Aprijuice's riding stat boosts so they always have the same order.
- Fixed the movesets of certain Pokémon being incorrectly sanitized upon reload, causing some forms to keep losing moves.
- Fixed selected leading Pokémon in battle not being used when levels are set.
- Fixed the bottom half of pasture blocks having an incorrect hit-box when facing north or south.
- Fixed gimmicks not being usable in double and triple battles.
- Fixed gimmick buttons showing when already used in the same turn in double and triple battles.
- Fixed mochi items consuming two items at a time.
- Fixed Fresh Start Mochi's EV reset not syncing health changes properly.
- Fixed Pokémon spawn influences not being specific to regional forms.
- Fixed memory leak from battles not being cleaned up after ending.
- Fixed orientation not getting reset after crashing while flying and getting back on the Pokémon.
- Fixed shiny Pokémon not respecting the `silent` flag on spawn.
- Fixed 'Plain' prefix not being applied to all low-quality Aprijuice.
- Fixed Pokémon interactions not playing sound effects when the volume was not specified.
- Fixed Pokémon interaction cooldowns taking longer than intended when the Pokémon was in the player’s party.
- Fixed Pokémon interactions occasionally crashing the game when spamming an interaction with a 0 cooldown.
- Fixed Pokémon fullness decreasing more than intended when the Pokémon was pastured.
- Fixed Scraggy line, Milcery line, and Morpeko spawning on unnatural blocks.
- Fixed invulnerability-bypassed damage being resisted by invulnerable NPC entities.
- Fixed Fabric network packets not being handled immediately, fixing incompatibilities with Supplementaries, Prometheus, Sleep Tight, and others.
- Fixed killer not being set early enough on a wild Pokémon entity from `WinInstruction`.
- Fixed requirements such as `biomeCondition` crashing the game when used to define Pokémon interactions.
- Fixed an issue that prevented registering custom dispenser behaviours (no longer overrides vanilla behaviour).
- Fixed an issue navigation that prevented Combee from pathing into and out of Saccharine Leaves.
- Added Saccharine Boats and Saccharine Boat with Chests to relevant boat tags.
- Fixed Soothe Bell not being properly tagged as a held item.
- Fixed `/calculateseatpositions` expecting a locator format that even we don't use. It needs an underscore after "seat".
- Fixed the `run_script` Molang function to not fail if the environment's context is null.
- Fixed the Molang functions `date_local_time`, `date_of`, `date_is_after` to use the correct date format.
- Fixes https://gitlab.com/cable-mc/cobblemon/-/issues/1943
- Fix occasional crash on retrieving revived fossils
- Fixed a compatibility issue with World Game Rules which caused some log spam in specific cases.
- Fixed improper validation of Pokémon when performing a trade.
- Fixed issue where particle like beam can disappear when you don't see the beginning of the beam or you look away
- Fixes https://gitlab.com/cable-mc/cobblemon/-/issues/1943
- Fix occasional crash on retrieving revived fossils
- Fix brushing not working on Neoforge
- [Various Submarine Ride Fixes](https://gitlab.com/cable-mc/cobblemon/-/merge_requests/2157)
- Fix Pastured mons despawning and running away.
- Fix Apricorn trees crashing when other mods bonemeal it
- [Various UFO Ride Improvements](https://gitlab.com/cable-mc/cobblemon/-/merge_requests/2156)
- Fixed NeoForge brewing stand unexpectedly crashing users when shift clicking items within it's menu
- Fixed NPCs switching pokemon very often during battles
- Fixed Campfire pot causing crashes under certain circumstances
- Fixed Type Gems not being consumed upon use
- Fixed Ancient Pokeball's to their respective modifiers
- Fixed a Server warning when recalling Pokemon
### Developer
- Changed the `owner` parameter in the `OwnerQueryRequirement` interface from `ServerPlayer` to `Player`. This method is now also called on the client to verify whether a Pokémon interaction succeeded, so make sure to update your implementations to handle both server and client contexts.
- Added the `pnx` to the `BattleFaintedEvent` and `FormeChangeEvent`.
- Added register methods for custom instructions to `ShowdownInterpreter`.
- Changed callback operations in `BattlePokemon` to allow multiple callbacks and be mutable.
- Added `display_name`, `description`, and `max_pp` functions to the `MoveTemplate` struct.
- Changed the implementations of `AIBattleActor` to be open classes to allow extension.
- Changed the resource location of Poké Ball icons in the GUI to use the balls namespace instead of Cobblemon's, allowing mods to properly use their own namespace for these textures.
- Added `PokeSnackSpawnerFactory` which allows influence registration on PokeSnack encounters
- Added `FishingSpawnerFactory` which allows influence registration on fishing encounters
- Added `BattleFleeAttemptEvent` fired from `PokemonBattle.checkFleeAttempt`, allowing developers to intercept and control flee attempts
### Molang & Datapacks
- Ride sounds can now be set as exclusive to passengers.
- Ride sounds no longer play when submerged unless specified with a new setting.
- Exception handling has been added to `run_molang`, resolving some crashes caused by malformed MoLang expressions.
- Added the `create_itemstack`, `has_inventory_space`, `set_inventory_slot`, and `give_item` MoLang functions for item and inventory utility.
- Added the `get_move_from_id` MoLang function for general move info queries without requiring a Moveset object to access.
- Added support for optional message variants in battle activate instructions for more context-specific battle text.
- Example: `this.add('-activate', pokemon, 'ability: example', '[msg]message1');` will parse to lang key `cobblemon.battle.activate.example.message1`.
- q.item.is_enchanted()
- q.item.has_enchantment(minecraft:sharpness, 3)
- Added `has_chosen_starter` & `get_starter_uuid` as available Molang functions
- Added `marks`, `has_mark`, & `remove_marks` as available Molang functions for pokemon
### Localization
- Updated translations for:
- French
- Canadian French
- Simplified Chinese
- Ukrainian
- Brazilian Portuguese
- Spanish
- Korean
- Japanese
+137
View File
@@ -0,0 +1,137 @@
# Changelog
## [1.7.2 (January 25th, 2026)](#1-7-2)
### Additions
- Added Saccharine wood furniture for Adorn.
- Added the Partner Mark, which party Pokémon have a very small chance to earn as the player walks, provided they have enough friendship.
- Added decorative block models for existing items: Potion, Super Potion, Hyper Potion, Max Potion, Full Restore, Antidote, Awakening, Burn Heal, Ice Heal, Paralyze Heal, Full Heal, Ether, Elixir, Max Ether, Max Elixir, X Accuracy, X Attack, X Defence, X Special Attack, X Special Defence, X Speed, Dire Hit, Guard Spec, Weakness Policy, Blunder Policy, Cleanse Tag, and Spell Tag.
- Added decorative sign blocks called Plaques.
- Added ride style labels to ride stats in Pokédex, as well as ride style icons in the summary GUI.
- Added more seasonings for sinister tea:
- Mental Herb - Mental Restoration I for 10 seconds, which staves off Insomnia.
- White Herb - Removes negative effects.
- Milk, Moomoo Milk - Removes effects.
- Added dispenser support for:
- Applying a Honey Bottle to Saccharine Logs.
- Washing honey off of Saccharine Slathered Logs.
- Applying and removing honey on Saccharine Leaves.
- Added riding statistics: `Distance by Pokémon on Land`, `Distance by Pokémon in Air` and `Distance by Pokémon in Liquid`.
### New rideable Pokémon
- Honchkrow
- Mantine
### Pokémon Added
#### Gen 5
- Throh
- Sawk
#### Gen 6
- Litleo
- Pyroar
- Spritzee
- Aromatisse
- Swirlix
- Slurpuff
### Model updates for the following Pokémon
- Falinks
- Cottonee
- Whimsicott
- Ferroseed
- Ferrothorn
- Chingling
- Chimecho
### Animation updates for the following Pokémon
- Mantine (ride animations)
- Flygon (ride jump animation)
- Honchkrow (ride animations)
- Croagunk
- Toxicroak
### Changes
- Added a new config option, `Enable In-Flight Dismounting` (default: off), which lets you dismount while riding a Pokémon in the air. This is the textbook definition of caveat emptor.
- Ride sounds have been separated into stereo for passengers and mono for other players. Riding will now sound more spacious.
- Battle AI now uses a smarter threshold for switching, reducing unnecessary switches.
- AI will always use the most damaging move when at low HP and when it is not switching, improving endgame decision-making.
- Items given from interacting with Saccharine Leaves are placed in the active hotbar slot if possible.
- Added `label` as an alternate key to the `tag` property.
### Fixes
- Fixed crashing when viewing another player in spectator mode. Some said this was reason to release this update a month ago. They were probably right.
- Fixed crashing when sending a Pokémon out after editing its form.
- Fixed all Pokémon being saved to chunks and never despawning even when they should be.
- Fixed Poké Snacks crashing if there was no available spawn.
- Fixed Poké Snack / Bait effects so that EV Yield, Type, and Egg Group filters now all apply together when weighting spawns. And you thought you were going crazy.
- Fixed Furfrou not being trimmable on NeoForge specifically.
- Fixed honey from Saccharine Leaves being harvestable at less than the max age.
- Fixed possible error on world generation when Combee are placed in naturally generated bee nests.
- Fixed Galarian Weezing crashing the world if it decides to blink the wrong way. The same could happen to you!
- Fixed Galarian Mr. Mime incorrectly being rideable. Some would argue that regular Mr. Mime shouldn't be rideable, but that would make them wrong.
- Fixed missing apricorn textures for Adorn blocks.
- Fixed Apricorn block model rotation for counters/sinks/cupboards/drawers to match that of most recent version of Adorn.
- Fixed item interaction sometimes not working properly when playing on servers.
- Fixed Mint Leaves not being usable for filling the Resurrection Machine.
- Fixed Super Potion recipes incorrectly using Hondew Berry instead of Aguav Berry. They look similar, to be fair.
- Fixed Wishiwashi not schooling properly.
- Fixed battle item duplication issue. How did it work? Wouldn't you like to know, weather boy.
- Fixed the ordering of Aprijuice's riding stat boosts so they always have the same order.
- Fixed the movesets of certain Pokémon being incorrectly sanitized upon reload, causing some forms to keep losing moves.
- Fixed selected leading Pokémon in battle not being used when levels are set.
- Fixed the bottom half of pasture blocks having an incorrect hit-box when facing north or south.
- Fixed gimmicks not being usable in double and triple battles.
- Fixed gimmick buttons showing when already used in the same turn in double and triple battles.
- Fixed mochi items consuming two items at a time.
- Fixed Fresh Start Mochi's EV reset not syncing health changes properly.
- Fixed Pokémon spawn influences not being specific to regional forms.
- Fixed memory leak from battles not being cleaned up after ending.
- Fixed orientation not getting reset after crashing while flying and getting back on the Pokémon.
- Fixed shiny Pokémon not respecting the `silent` flag on spawn.
- Fixed 'Plain' prefix not being applied to all low-quality Aprijuice.
- Fixed Pokémon interactions not playing sound effects when the volume was not specified.
- Fixed Pokémon interaction cooldowns taking longer than intended when the Pokémon was in the player’s party.
- Fixed Pokémon interactions occasionally crashing the game when spamming an interaction with a 0 cooldown.
- Fixed Pokémon fullness decreasing more than intended when the Pokémon was pastured.
- Fixed Scraggy line, Milcery line, and Morpeko spawning on unnatural blocks.
- Fixed invulnerability-bypassed damage being resisted by invulnerable NPC entities.
- Fixed Fabric network packets not being handled immediately, fixing incompatibilities with Supplementaries, Prometheus, Sleep Tight, and others.
- Fixed killer not being set early enough on a wild Pokémon entity from `WinInstruction`.
- Fixed requirements such as `biomeCondition` crashing the game when used to define Pokémon interactions.
- Fixed an issue that prevented registering custom dispenser behaviours (no longer overrides vanilla behaviour).
- Fixed an issue navigation that prevented Combee from pathing into and out of Saccharine Leaves.
- Added Saccharine Boats and Saccharine Boat with Chests to relevant boat tags.
- Fixed Soothe Bell not being properly tagged as a held item.
- Fixed `/calculateseatpositions` expecting a locator format that even we don't use. It needs an underscore after "seat".
- Fixed the `run_script` Molang function to not fail if the environment's context is null.
- Fixed the Molang functions `date_local_time`, `date_of`, `date_is_after` to use the correct date format.
### Developer
- Changed the `owner` parameter in the `OwnerQueryRequirement` interface from `ServerPlayer` to `Player`. This method is now also called on the client to verify whether a Pokémon interaction succeeded, so make sure to update your implementations to handle both server and client contexts.
- Added the `pnx` to the `BattleFaintedEvent` and `FormeChangeEvent`.
- Added register methods for custom instructions to `ShowdownInterpreter`.
- Changed callback operations in `BattlePokemon` to allow multiple callbacks and be mutable.
- Added `display_name`, `description`, and `max_pp` functions to the `MoveTemplate` struct.
- Changed the implementations of `AIBattleActor` to be open classes to allow extension.
- Changed the resource location of Poké Ball icons in the GUI to use the balls namespace instead of Cobblemon's, allowing mods to properly use their own namespace for these textures.
### Molang & Datapacks
- Ride sounds can now be set as exclusive to passengers.
- Ride sounds no longer play when submerged unless specified with a new setting.
- Exception handling has been added to `run_molang`, resolving some crashes caused by malformed MoLang expressions.
- Added the `create_itemstack`, `has_inventory_space`, `set_inventory_slot`, and `give_item` MoLang functions for item and inventory utility.
- Added the `get_move_from_id` MoLang function for general move info queries without requiring a Moveset object to access.
- Added support for optional message variants in battle activate instructions for more context-specific battle text.
- Example: `this.add('-activate', pokemon, 'ability: example', '[msg]message1');` will parse to lang key `cobblemon.battle.activate.example.message1`.
### Localization
- Updated translations for:
- French
- Canadian French
- Simplified Chinese
- Ukrainian
- Brazilian Portuguese
- Spanish
- Korean
- Japanese
+22
View File
@@ -0,0 +1,22 @@
# Changelog
## [1.7.3 (January 31st, 2026)](#1-7-3)
### Fixes
- Fixed mounted Pokémon not playing sounds.
- Fixed players failing to join servers randomly.
- Fixed some Pokémon having no stamina for flying.
- Fixed `/pctake` not respecting the actual size of the PC.
### Developer
- Poké Ball GUI textures now respect the ball's namespace, enabling proper texture loading for custom balls from other mods.
### Molang & Datapacks
- Added a `chance` requirement type for Pokémon interactions.
### Localization
- Updated translations for:
- French
- Canadian French
- Korean
- Portuguese
- Simplified Chinese
- Spanish
+1 -1
View File
@@ -55,7 +55,7 @@ dependencies {
// modCompileOnly(libs.flywheelFabric)
// Showdown
modCompileOnly(libs.graal)
modCompileOnly(libs.graal.core)
// Data Storage
modCompileOnly(libs.bundles.mongo)
@@ -15,6 +15,8 @@ import com.cobblemon.mod.common.api.events.CobblemonEvents;
import com.cobblemon.mod.common.api.events.item.LeftoversCreatedEvent;
import com.cobblemon.mod.common.api.orientation.OrientationController;
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies;
import com.cobblemon.mod.common.api.riding.RidingStyle;
import com.cobblemon.mod.common.api.riding.behaviour.types.air.HoverBehaviour;
import com.cobblemon.mod.common.api.storage.party.PartyStore;
import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore;
import com.cobblemon.mod.common.api.tags.CobblemonItemTags;
@@ -59,6 +61,7 @@ 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.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@@ -169,6 +172,32 @@ public abstract class PlayerMixin extends LivingEntity implements ScannableEntit
}
}
@Redirect(
method = "getDestroySpeed",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/player/Player;onGround()Z"
)
)
private boolean cobblemon$considerHoverOnGround(Player player) {
// If riding a Hover Mount, return true to bypass the 5x slowdown
if (player.getVehicle() instanceof PokemonEntity vehicle) {
var ridingController = vehicle.getRidingController();
if (ridingController == null) player.onGround();
var activeContext = ridingController.getContext();
if (activeContext == null) player.onGround();
if (activeContext.getStyle() == RidingStyle.AIR &&
activeContext.getBehaviour().equals(HoverBehaviour.Companion.getKEY())) {
return true;
}
}
// Otherwise, return the actual value
return player.onGround();
}
@Inject(
method = "eat",
at = @At(
@@ -101,7 +101,7 @@ public abstract class ServerEntityMixin {
cobblemon$broadcast(new ClientboundUpdateDriverInputPacket(driverInput, entity.getId()));
}
private void cobblemon$broadcast(NetworkPacket<?> packet) {
public void cobblemon$broadcast(NetworkPacket<?> packet) {
if (!(entity.level() instanceof ServerLevel level)) return;
ChunkMap chunkMap = level.getChunkSource().chunkMap;
Int2ObjectMap<?> entityMap = ((ChunkMapAccessor) chunkMap).getEntityMap();
@@ -35,6 +35,7 @@ import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import com.cobblemon.mod.common.client.gui.party.PartyTutorialToasts;
import static com.google.common.primitives.Floats.min;
import static net.minecraft.util.Mth.lerp;
@@ -85,11 +86,13 @@ public abstract class MouseHandlerMixin {
CobblemonClient.INSTANCE.getStorage().shiftSelected(false);
ci.cancel();
PartySendBinding.INSTANCE.actioned();
PartyTutorialToasts.INSTANCE.onHoldKeyScrollSwitchedSlot();
} else if (i < 0) {
accumulatedScrollY -= i;
CobblemonClient.INSTANCE.getStorage().shiftSelected(true);
ci.cancel();
PartySendBinding.INSTANCE.actioned();
PartyTutorialToasts.INSTANCE.onHoldKeyScrollSwitchedSlot();
}
}
}
@@ -310,7 +313,7 @@ public abstract class MouseHandlerMixin {
var playerRotater = (RidePassenger)player;
playerRotater.cobblemon$setRideXRot(playerRotater.cobblemon$getRideXRot() + f);
playerRotater.cobblemon$setRideYRot(playerRotater.cobblemon$getRideYRot() + g);
playerRotater.cobblemon$setRideXRot(Mth.clamp(playerRotater.cobblemon$getRideXRot(), -90.0F, 90.0F));
playerRotater.cobblemon$setRideXRot(Mth.clamp(playerRotater.cobblemon$getRideXRot(), -89.0F, 89.0F));
playerRotater.cobblemon$setRideYRot(Mth.clamp(playerRotater.cobblemon$getRideYRot(), -105.0F, 105.0F));
if (player.getVehicle() != null) {
player.getVehicle().onPassengerTurned(player);
@@ -14,9 +14,14 @@ import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.pokemon.Pokemon;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef;
import com.llamalad7.mixinextras.sugar.ref.LocalFloatRef;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
@@ -89,4 +94,61 @@ public abstract class EntityMixin {
cir.setReturnValue(customEyePos);
}
@WrapOperation(
method = "push(Lnet/minecraft/world/entity/Entity;)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;push(DDD)V", ordinal = 0)
)
public void cobblemon$customPusherLogic(Entity pusher, double x, double y, double z,
Operation<Void> original,
@Local(argsOnly = true) Entity pushee,
@Local(name = "e") double e, @Local(name = "d") double d,
@Share("pusherIsPlayerOrPokemon") LocalBooleanRef pusherIsPlayerOrPokemon,
@Share("pusheeIsPlayerOrPokemon") LocalBooleanRef pusheeIsPlayerOrPokemon,
@Share("pusheeReceivedForce")LocalFloatRef pusheeReceivedForce) {
pusherIsPlayerOrPokemon.set(pusher instanceof PokemonEntity || pusher instanceof Player);
pusheeIsPlayerOrPokemon.set(pushee instanceof PokemonEntity || pushee instanceof Player);
if (!pusheeIsPlayerOrPokemon.get() || !pusherIsPlayerOrPokemon.get()) {
original.call(pusher, x, y, z);
return;
}
/**********************************************
* Custom portion of the code:
* Handle collisions with weight in mind. Use
* the weight value of pokemon to determine
* which side gets what portion of the total
* force.
*********************************************/
// Weights in hectograms
// Check to see if the pusher or pushee is a player. 200ish pounds. Steve's like a 6'6 miner
var pusherWeight = pusher instanceof PokemonEntity pusherPokemon ? pusherPokemon.getPokemon().getSpecies().getWeight() : 900F;
var pusheeWeight = pushee instanceof PokemonEntity pusheePokemon ? pusheePokemon.getPokemon().getSpecies().getWeight() : 900F;
// Calculate the portion of force received for both entities. Bigger receives less from the
// collision than the smaller one.
var totalWeight = pusherWeight + pusheeWeight;
var pusherReceivedForce = pusheeWeight / totalWeight * 2F;
pusheeReceivedForce.set((pusherWeight / totalWeight) * 2F);
original.call(pusher, -d * pusherReceivedForce, 0.0, -e * pusherReceivedForce);
}
@WrapOperation(
method = "push(Lnet/minecraft/world/entity/Entity;)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;push(DDD)V", ordinal = 1)
)
public void cobblemon$customPusheeLogic(Entity pusher, double x, double y, double z, Operation<Void> original,
@Local(name = "e") double e, @Local(name = "d") double d,
@Share("pusherIsPlayerOrPokemon") LocalBooleanRef pusherIsPlayerOrPokemon,
@Share("pusheeIsPlayerOrPokemon") LocalBooleanRef pusheeIsPlayerOrPokemon,
@Share("pusheeReceivedForce")LocalFloatRef pusheeReceivedForce) {
if (!pusheeIsPlayerOrPokemon.get() || !pusherIsPlayerOrPokemon.get()) {
original.call(pusher, x, y, z);
return;
}
original.call(pusher, d * pusheeReceivedForce.get(), 0.0, e * pusheeReceivedForce.get());
}
}
@@ -65,7 +65,11 @@ import com.cobblemon.mod.common.api.storage.player.adapter.PlayerDataMongoBacken
import com.cobblemon.mod.common.api.storage.player.factory.CachedPlayerDataStoreFactory
import com.cobblemon.mod.common.api.tags.CobblemonEntityTypeTags
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.battles.*
import com.cobblemon.mod.common.battles.BagItems
import com.cobblemon.mod.common.battles.BattleFormat
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.BattleSide
import com.cobblemon.mod.common.battles.ShowdownThread
import com.cobblemon.mod.common.battles.actor.PokemonBattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.block.dispenser.DispenserBehaviorRegistry
@@ -76,7 +80,12 @@ import com.cobblemon.mod.common.config.LastChangedVersion
import com.cobblemon.mod.common.config.constraint.IntConstraint
import com.cobblemon.mod.common.config.starter.StarterConfig
import com.cobblemon.mod.common.data.CobblemonDataProvider
import com.cobblemon.mod.common.events.*
import com.cobblemon.mod.common.events.AdvancementHandler
import com.cobblemon.mod.common.events.CallbackHandler
import com.cobblemon.mod.common.events.EntityCallbackHandler
import com.cobblemon.mod.common.events.PokedexHandler
import com.cobblemon.mod.common.events.ServerTickHandler
import com.cobblemon.mod.common.events.StatHandler
import com.cobblemon.mod.common.net.messages.client.settings.ServerSettingsPacket
import com.cobblemon.mod.common.permission.LaxPermissionValidator
import com.cobblemon.mod.common.platform.events.PlatformEvents
@@ -90,11 +99,23 @@ import com.cobblemon.mod.common.pokemon.feature.SlowpokeTailRegrowthSpeciesFeatu
import com.cobblemon.mod.common.pokemon.feature.SlowpokeTailRegrowthSpeciesFeatureProvider
import com.cobblemon.mod.common.pokemon.feature.TagSeasonResolver
import com.cobblemon.mod.common.pokemon.helditem.CobblemonHeldItemManager
import com.cobblemon.mod.common.pokemon.properties.*
import com.cobblemon.mod.common.pokemon.properties.AspectPropertyType
import com.cobblemon.mod.common.pokemon.properties.BattleCloneProperty
import com.cobblemon.mod.common.pokemon.properties.FreezeFrameProperty
import com.cobblemon.mod.common.pokemon.properties.HiddenAbilityPropertyType
import com.cobblemon.mod.common.pokemon.properties.LabelProperty
import com.cobblemon.mod.common.pokemon.properties.NoAIProperty
import com.cobblemon.mod.common.pokemon.properties.UnaspectPropertyType
import com.cobblemon.mod.common.pokemon.properties.UncatchableProperty
import com.cobblemon.mod.common.pokemon.stat.CobblemonStatProvider
import com.cobblemon.mod.common.starter.CobblemonStarterHandler
import com.cobblemon.mod.common.util.*
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.ifDedicatedServer
import com.cobblemon.mod.common.util.isLaterVersion
import com.cobblemon.mod.common.util.party
import com.cobblemon.mod.common.util.requestWallpapers
import com.cobblemon.mod.common.util.server
import com.cobblemon.mod.common.world.feature.CobblemonPlacedFeatures
import com.cobblemon.mod.common.world.feature.ore.CobblemonOrePlacedFeatures
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
@@ -102,6 +123,17 @@ import com.mongodb.ConnectionString
import com.mongodb.MongoClientSettings
import com.mongodb.client.MongoClient
import com.mongodb.client.MongoClients
import java.io.File
import java.io.FileReader
import java.io.FileWriter
import java.io.PrintWriter
import java.util.UUID
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
import kotlin.properties.Delegates
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaField
import net.minecraft.client.Minecraft
import net.minecraft.commands.synchronization.SingletonArgumentInfo
import net.minecraft.resources.ResourceKey
@@ -114,17 +146,6 @@ import net.minecraft.world.level.Level
import net.minecraft.world.level.storage.LevelResource
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import java.io.File
import java.io.FileReader
import java.io.FileWriter
import java.io.PrintWriter
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
import kotlin.properties.Delegates
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaField
object Cobblemon {
const val MODID = CobblemonBuildDetails.MOD_ID
@@ -165,7 +186,7 @@ object Cobblemon {
val statistics: CobblemonStats = CobblemonStats
@JvmStatic
val builtinPacks = listOf<CobblemonPack>(
val builtinPacks = listOf(
CobblemonPack(id = "adorncompatibility", name = "Adorn Compatibility", packType = PackType.CLIENT_RESOURCES, activationBehaviour = ResourcePackActivationBehaviour.ALWAYS_ENABLED, neededMods = setOf("adorn")),
CobblemonPack(id = "gyaradosjump", name = "Gyarados Jump Patterns", packType = PackType.CLIENT_RESOURCES, activationBehaviour = ResourcePackActivationBehaviour.DEFAULT_ENABLED),
CobblemonPack(id = "regionbiasforms", name = "Region Bias Forms", packType = PackType.CLIENT_RESOURCES, activationBehaviour = ResourcePackActivationBehaviour.DEFAULT_ENABLED),
@@ -451,7 +472,7 @@ object Cobblemon {
server()?.getLevel(dimension)
} else {
val mc = Minecraft.getInstance()
return mc.singleplayerServer?.getLevel(dimension) ?: mc.level
mc.singleplayerServer?.getLevel(dimension) ?: mc.level
}
}
@@ -28,6 +28,8 @@ object CobblemonActivities {
@JvmField
val POKEMON_HERD = register(Activity("pokemon_herd"))
@JvmField
val POKEMON_BOIDS = register(Activity("pokemon_boid"))
@JvmField
val POKEMON_POLLINATION = register(Activity("pokemon_pollination"))
@JvmStatic
@@ -21,6 +21,7 @@ import net.minecraft.core.UUIDUtil
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import java.util.*
import net.minecraft.world.entity.player.Player
object CobblemonMemories {
val memories = mutableMapOf<String, MemoryModuleType<*>>()
@@ -92,6 +93,9 @@ object CobblemonMemories {
val DISABLE_WALK_TO_WANTED_ITEM = register<Boolean>("disable_walk_to_wanted_item")
@JvmField
val TIME_TRYING_TO_REACH_WANTED_ITEM = register<Int>("time_trying_to_wanted_item")
/** Is the nearest player holding a diamond for debugging purposes? */
@JvmField
val DIAMOND_DEBUGGER = register<Player>("diamond_debugger")
@JvmStatic
fun <U> register(id: String, codec: Codec<U>): MemoryModuleType<U> {
@@ -21,12 +21,16 @@ import com.cobblemon.mod.common.client.net.callback.partymove.OpenPartyMoveCallb
import com.cobblemon.mod.common.client.net.cooking.ToggleCookingPotLidHandler
import com.cobblemon.mod.common.client.net.data.CobblemonMechanicsSyncHandler
import com.cobblemon.mod.common.client.net.data.DataRegistrySyncPacketHandler
import com.cobblemon.mod.common.client.net.data.PokemonInteractionsSyncHandler
import com.cobblemon.mod.common.client.net.data.RideSettingsSyncHandler
import com.cobblemon.mod.common.client.net.debug.OpenRidingStatsDebugGUIHandler
import com.cobblemon.mod.common.client.net.debug.S2CUpdateObsAvoidHandler
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.*
import com.cobblemon.mod.common.client.net.effect.PokeSnackBlockParticlesHandler
import com.cobblemon.mod.common.client.net.effect.RunPosableMoLangHandler
import com.cobblemon.mod.common.client.net.effect.SaccharineLogBlockParticlesHandler
import com.cobblemon.mod.common.client.net.effect.SpawnSnowstormEntityParticleHandler
import com.cobblemon.mod.common.client.net.effect.SpawnSnowstormParticleHandler
import com.cobblemon.mod.common.client.net.gui.ExpGainedDataPacketHandler
import com.cobblemon.mod.common.client.net.gui.InteractPokemonUIPacketHandler
import com.cobblemon.mod.common.client.net.gui.PokedexUIPacketHandler
@@ -34,7 +38,11 @@ import com.cobblemon.mod.common.client.net.gui.SummaryUIPacketHandler
import com.cobblemon.mod.common.client.net.npc.CloseNPCEditorHandler
import com.cobblemon.mod.common.client.net.npc.OpenNPCEditorHandler
import com.cobblemon.mod.common.client.net.orientation.ClientboundUpdateOrientationHandler
import com.cobblemon.mod.common.client.net.pasture.*
import com.cobblemon.mod.common.client.net.pasture.ClosePastureHandler
import com.cobblemon.mod.common.client.net.pasture.OpenPastureHandler
import com.cobblemon.mod.common.client.net.pasture.PokemonPasturedHandler
import com.cobblemon.mod.common.client.net.pasture.PokemonUnpasturedHandler
import com.cobblemon.mod.common.client.net.pasture.UpdatePastureConflictFlagHandler
import com.cobblemon.mod.common.client.net.pokedex.ServerConfirmedRegisterHandler
import com.cobblemon.mod.common.client.net.pokemon.update.ClientboundUpdateRidingStateHandler
import com.cobblemon.mod.common.client.net.pokemon.update.PokemonUpdatePacketHandler
@@ -52,7 +60,14 @@ import com.cobblemon.mod.common.client.net.storage.party.SetPartyPokemonHandler
import com.cobblemon.mod.common.client.net.storage.party.SetPartyReferenceHandler
import com.cobblemon.mod.common.client.net.storage.pc.*
import com.cobblemon.mod.common.client.net.toast.ToastPacketHandler
import com.cobblemon.mod.common.client.net.trade.*
import com.cobblemon.mod.common.client.net.trade.TradeAcceptanceChangedHandler
import com.cobblemon.mod.common.client.net.trade.TradeCancelledHandler
import com.cobblemon.mod.common.client.net.trade.TradeCompletedHandler
import com.cobblemon.mod.common.client.net.trade.TradeOfferExpiredHandler
import com.cobblemon.mod.common.client.net.trade.TradeOfferNotificationHandler
import com.cobblemon.mod.common.client.net.trade.TradeProcessStartedHandler
import com.cobblemon.mod.common.client.net.trade.TradeStartedHandler
import com.cobblemon.mod.common.client.net.trade.TradeUpdatedHandler
import com.cobblemon.mod.common.net.PacketRegisterInfo
import com.cobblemon.mod.common.net.messages.client.CalculateSeatPositionsPacket
import com.cobblemon.mod.common.net.messages.client.OpenBehaviourEditorPacket
@@ -66,15 +81,24 @@ 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.ClientboundUpdateObsAvoidPacket
import com.cobblemon.mod.common.net.messages.client.debug.RequestOpenRidingStatsDebugGUIPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueClosedPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueOpenedPacket
import com.cobblemon.mod.common.net.messages.client.effect.*
import com.cobblemon.mod.common.net.messages.client.effect.PokeSnackBlockParticlesPacket
import com.cobblemon.mod.common.net.messages.client.effect.RunPosableMoLangPacket
import com.cobblemon.mod.common.net.messages.client.effect.SaccharineLogBlockParticlesPacket
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormEntityParticlePacket
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormParticlePacket
import com.cobblemon.mod.common.net.messages.client.npc.CloseNPCEditorPacket
import com.cobblemon.mod.common.net.messages.client.npc.OpenNPCEditorPacket
import com.cobblemon.mod.common.net.messages.client.orientation.ClientboundUpdateDriverInputPacket
import com.cobblemon.mod.common.net.messages.client.orientation.ClientboundUpdateOrientationPacket
import com.cobblemon.mod.common.net.messages.client.pasture.*
import com.cobblemon.mod.common.net.messages.client.pasture.ClosePasturePacket
import com.cobblemon.mod.common.net.messages.client.pasture.OpenPasturePacket
import com.cobblemon.mod.common.net.messages.client.pasture.PokemonPasturedPacket
import com.cobblemon.mod.common.net.messages.client.pasture.PokemonUnpasturedPacket
import com.cobblemon.mod.common.net.messages.client.pasture.UpdatePastureConflictFlagPacket
import com.cobblemon.mod.common.net.messages.client.pokedex.ServerConfirmedRegisterPacket
import com.cobblemon.mod.common.net.messages.client.pokemon.update.*
import com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution.AddEvolutionPacket
@@ -94,19 +118,37 @@ import com.cobblemon.mod.common.net.messages.client.storage.party.InitializePart
import com.cobblemon.mod.common.net.messages.client.storage.party.MoveClientPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.party.SetPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.party.SetPartyReferencePacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.*
import com.cobblemon.mod.common.net.messages.client.storage.pc.ClosePCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.InitializePCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.MoveClientPCPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.OpenPCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.RenamePCBoxPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.SetPCBoxPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.SetPCPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.ChangePCBoxWallpaperPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.RequestPCBoxWallpapersPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.SetPCBoxWallpapersPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.UnlockPCBoxWallpaperPacket
import com.cobblemon.mod.common.net.messages.client.toast.ToastPacket
import com.cobblemon.mod.common.net.messages.client.trade.*
import com.cobblemon.mod.common.net.messages.client.trade.TradeAcceptanceChangedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeCancelledPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeCompletedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeOfferExpiredPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeOfferNotificationPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeProcessStartedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeStartedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeUpdatedPacket
import com.cobblemon.mod.common.net.messages.client.ui.ExpGainedDataPacket
import com.cobblemon.mod.common.net.messages.client.ui.InteractPokemonUIPacket
import com.cobblemon.mod.common.net.messages.client.ui.PokedexUIPacket
import com.cobblemon.mod.common.net.messages.client.ui.SummaryUIPacket
import com.cobblemon.mod.common.net.messages.server.*
import com.cobblemon.mod.common.net.messages.server.battle.*
import com.cobblemon.mod.common.net.messages.server.battle.BattleSelectActionsPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamLeavePacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamRequestPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamResponsePacket
import com.cobblemon.mod.common.net.messages.server.battle.RemoveSpectatorPacket
import com.cobblemon.mod.common.net.messages.server.battle.SpectateBattlePacket
import com.cobblemon.mod.common.net.messages.server.behaviour.DamageOnCollisionPacket
import com.cobblemon.mod.common.net.messages.server.behaviour.SetEntityBehaviourPacket
import com.cobblemon.mod.common.net.messages.server.block.AdjustBlockEntityViewerCountPacket
@@ -131,7 +173,11 @@ import com.cobblemon.mod.common.net.messages.server.pasture.UnpasturePokemonPack
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.FinishScanningPacket
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.StartScanningPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.interact.InteractPokemonPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.*
import com.cobblemon.mod.common.net.messages.server.pokemon.update.ServerboundUpdateRidingStatePacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetActiveMarkPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetItemHiddenPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetMarkingsPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetNicknamePacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.evolution.AcceptEvolutionPacket
import com.cobblemon.mod.common.net.messages.server.riding.DismountPokemonPacket
import com.cobblemon.mod.common.net.messages.server.riding.ServerboundUpdateDriverInputPacket
@@ -139,14 +185,25 @@ import com.cobblemon.mod.common.net.messages.server.riding.ServerboundUpdateRide
import com.cobblemon.mod.common.net.messages.server.starter.RequestStarterScreenPacket
import com.cobblemon.mod.common.net.messages.server.storage.SwapPCPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.party.MovePartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.party.PartyTutorialPacket
import com.cobblemon.mod.common.net.messages.server.storage.party.ReleasePartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.party.SwapPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.pc.*
import com.cobblemon.mod.common.net.messages.server.trade.*
import com.cobblemon.mod.common.net.messages.server.trade.AcceptTradeRequestPacket
import com.cobblemon.mod.common.net.messages.server.trade.CancelTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.ChangeTradeAcceptancePacket
import com.cobblemon.mod.common.net.messages.server.trade.OfferTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.PerformTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.UpdateTradeOfferPacket
import com.cobblemon.mod.common.net.serverhandling.ChallengeHandler
import com.cobblemon.mod.common.net.serverhandling.ChallengeResponseHandler
import com.cobblemon.mod.common.net.serverhandling.RequestInteractionsHandler
import com.cobblemon.mod.common.net.serverhandling.battle.*
import com.cobblemon.mod.common.net.serverhandling.battle.BattleSelectActionsHandler
import com.cobblemon.mod.common.net.serverhandling.battle.RemoveSpectatorHandler
import com.cobblemon.mod.common.net.serverhandling.battle.SpectateBattleHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamLeaveHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamRequestHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamRequestResponseHandler
import com.cobblemon.mod.common.net.serverhandling.behaviour.DamageOnCollisionPacketHandler
import com.cobblemon.mod.common.net.serverhandling.behaviour.SetEntityBehaviourHandler
import com.cobblemon.mod.common.net.serverhandling.block.AdjustBlockEntityViewerCountHandler
@@ -165,6 +222,7 @@ import com.cobblemon.mod.common.net.serverhandling.dialogue.InputToDialogueHandl
import com.cobblemon.mod.common.net.serverhandling.evolution.AcceptEvolutionHandler
import com.cobblemon.mod.common.net.serverhandling.npc.SaveNPCHandler
import com.cobblemon.mod.common.net.serverhandling.orientation.ServerboundUpdateOrientationHandler
import com.cobblemon.mod.common.net.serverhandling.storage.party.PartyTutorialPacketHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.PasturePokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.SetPastureConflictHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.UnpastureAllPokemonHandler
@@ -172,7 +230,11 @@ import com.cobblemon.mod.common.net.serverhandling.pasture.UnpasturePokemonHandl
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.FinishScanningHandler
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.StartScanningHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.interact.InteractPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.*
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.ServerboundUpdateRidingStateHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetActiveMarkHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetItemHiddenHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetMarkingsHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetNicknameHandler
import com.cobblemon.mod.common.net.serverhandling.riding.DismountPokemonPacketHandler
import com.cobblemon.mod.common.net.serverhandling.riding.DriverInputPacketHandler
import com.cobblemon.mod.common.net.serverhandling.riding.ServerboundUpdateRiderRotationHandler
@@ -186,9 +248,18 @@ import com.cobblemon.mod.common.net.serverhandling.storage.party.MovePartyPokemo
import com.cobblemon.mod.common.net.serverhandling.storage.party.ReleasePCPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.storage.party.SwapPartyPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.storage.pc.*
import com.cobblemon.mod.common.net.serverhandling.trade.*
import com.cobblemon.mod.common.net.serverhandling.trade.AcceptTradeRequestHandler
import com.cobblemon.mod.common.net.serverhandling.trade.CancelTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.ChangeTradeAcceptanceHandler
import com.cobblemon.mod.common.net.serverhandling.trade.OfferTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.PerformTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.UpdateTradeOfferHandler
import com.cobblemon.mod.common.util.server
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.Entity
import com.cobblemon.mod.common.mixin.accessor.ChunkMapAccessor
import com.cobblemon.mod.common.mixin.accessor.TrackedEntityAccessor
/**
* Registers Cobblemon network packets.
@@ -211,6 +282,19 @@ object CobblemonNetwork {
fun sendToAllPlayers(packet: NetworkPacket<*>) = sendPacketToPlayers(server()!!.playerList.players, packet)
@JvmStatic
fun sendPacketToPlayers(players: Iterable<ServerPlayer>, packet: NetworkPacket<*>) = players.forEach { sendPacketToPlayer(it, packet) }
fun sendPacketToTrackingPlayers(entity: Entity, packet: NetworkPacket<*>) {
val level = entity.level() as? ServerLevel ?: return
val chunkMap = level.chunkSource.chunkMap
val entityTracker = (chunkMap as ChunkMapAccessor).entityMap[entity.id]
if (entityTracker is TrackedEntityAccessor) {
entityTracker.seenBy.forEach { connection ->
sendPacketToPlayer(connection.player, packet)
}
}
}
val s2cPayloads = generateS2CPacketInfoList()
val c2sPayloads = generateC2SPacketInfoList()
@@ -332,7 +416,7 @@ object CobblemonNetwork {
// Data registries
list.add(PacketRegisterInfo(AbilityRegistrySyncPacket.ID, AbilityRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(CobblemonMechanicsSyncPacket.ID, CobblemonMechanicsSyncPacket::decode, CobblemonMechanicsSyncHandler))
list.add(PacketRegisterInfo(PokemonInteractionsSyncPacket.ID, PokemonInteractionsSyncPacket::decode, PokemonInteractionsSyncHandler))
list.add(PacketRegisterInfo(PokemonInteractionsSyncPacket.ID, PokemonInteractionsSyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(MovesRegistrySyncPacket.ID, MovesRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(BerryRegistrySyncPacket.ID, BerryRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(SpeciesRegistrySyncPacket.ID, SpeciesRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
@@ -418,6 +502,7 @@ object CobblemonNetwork {
// Debug / cheats
list.add(PacketRegisterInfo(CalculateSeatPositionsPacket.ID, CalculateSeatPositionsPacket::decode, CalculateSeatPositionsHandler))
list.add(PacketRegisterInfo(ClientboundUpdateObsAvoidPacket.ID, ClientboundUpdateObsAvoidPacket::decode, S2CUpdateObsAvoidHandler))
// Riding
list.add(PacketRegisterInfo(ClientboundUpdateRidingStatePacket.ID, ClientboundUpdateRidingStatePacket::decode, ClientboundUpdateRidingStateHandler))
@@ -460,7 +545,7 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(ReleasePCPokemonPacket.ID, ReleasePCPokemonPacket::decode, ReleasePCPokemonHandler))
list.add(PacketRegisterInfo(UnlinkPlayerFromPCPacket.ID, UnlinkPlayerFromPCPacket::decode, UnlinkPlayerFromPCHandler))
list.add(PacketRegisterInfo(RequestRenamePCBoxPacket.Companion.ID, RequestRenamePCBoxPacket.Companion::decode, RequestRenamePCBoxHandler))
list.add(PacketRegisterInfo(RequestRenamePCBoxPacket.ID, RequestRenamePCBoxPacket::decode, RequestRenamePCBoxHandler))
list.add(PacketRegisterInfo(PCBoxWallpapersPacket.ID, PCBoxWallpapersPacket::decode, PCBoxWallpapersHandler))
list.add(PacketRegisterInfo(RequestChangePCBoxWallpaperPacket.ID, RequestChangePCBoxWallpaperPacket::decode, RequestChangePCBoxWallpaperHandler))
list.add(PacketRegisterInfo(MarkPCBoxWallpapersSeenPacket.ID, MarkPCBoxWallpapersSeenPacket::decode, MarkPCBoxWallpapersSeenHandler))
@@ -518,6 +603,9 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(PartyPokemonMoveSelectedPacket.ID, PartyPokemonMoveSelectedPacket::decode, PartyPokemonMoveSelectedHandler))
list.add(PacketRegisterInfo(PartyMoveSelectCancelledPacket.ID, PartyMoveSelectCancelledPacket::decode, PartyMoveSelectCancelledHandler))
//Party move tutorial toasts packets
list.add(PacketRegisterInfo(PartyTutorialPacket.ID, PartyTutorialPacket::decode, PartyTutorialPacketHandler))
// Dialogue packets
list.add(PacketRegisterInfo(EscapeDialoguePacket.ID, EscapeDialoguePacket::decode, EscapeDialogueHandler))
list.add(PacketRegisterInfo(InputToDialoguePacket.ID, InputToDialoguePacket::decode, InputToDialogueHandler))
@@ -49,6 +49,9 @@ object CobblemonSensors {
@JvmField
val NEARBY_SWEET_BERRY_BUSH = register("nearby_sweet_berry_bush", ::SweetBerryBushSensor)
@JvmField
val PLAYER_DEBUGGING = register("player_debugging", ::PlayerDebuggingSensor)
@JvmField
val POKEMON_NEARBY_WANTED_ITEM = register("pokemon_nearby_wanted_item", ::PokemonItemSensor)
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai.config.task
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.CobblemonSensors
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
import com.cobblemon.mod.common.api.ai.WrapperLivingEntityTask
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.tasks.WakeUpTask
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.BehaviorControl
class BoidsTaskConfig : SingleTaskConfig {
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) = emptyList<MoLangConfigVariable>()
override fun createTask(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): BehaviorControl<in LivingEntity>? {
if (entity !is PokemonEntity) {
return null
}
behaviourConfigurationContext.addMemories(CobblemonMemories.POKEMON_DROWSY, CobblemonMemories.POKEMON_SLEEPING)
behaviourConfigurationContext.addSensors(CobblemonSensors.POKEMON_DROWSY)
return WrapperLivingEntityTask(WakeUpTask.create(), PokemonEntity::class.java)
}
}
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai.config.task
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.BoidsMoveControl
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.BehaviorControl
import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder
import net.minecraft.world.entity.ai.behavior.declarative.Trigger
class SwitchFromBoidTaskConfig : SingleTaskConfig {
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) = emptyList<MoLangConfigVariable>()
override fun createTask(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): BehaviorControl<in LivingEntity>? {
behaviourConfigurationContext.addMemories(CobblemonMemories.DIAMOND_DEBUGGER)
return BehaviorBuilder.create {
it.group(
it.absent(CobblemonMemories.DIAMOND_DEBUGGER)
).apply(it) { _ ->
Trigger { world, entity, time ->
if(entity !is PokemonEntity) return@Trigger false
if (entity.moveControl !is PokemonMoveControl) {
entity.moveControl = PokemonMoveControl(entity)
}
entity.brain.updateActivityFromSchedule(world.dayTime, world.gameTime)
return@Trigger true
}
}
}
}
}
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai.config.task
import com.cobblemon.mod.common.CobblemonActivities
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.CobblemonSensors
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.BoidsMoveControl
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormEntityParticlePacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.BehaviorControl
import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder
import net.minecraft.world.entity.ai.behavior.declarative.Trigger
import net.minecraft.world.entity.player.Player
class SwitchToBoidTaskConfig : SingleTaskConfig {
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) = emptyList<MoLangConfigVariable>()
override fun createTask(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): BehaviorControl<in LivingEntity>? {
behaviourConfigurationContext.addMemories(CobblemonMemories.DIAMOND_DEBUGGER)
behaviourConfigurationContext.addSensors(CobblemonSensors.PLAYER_DEBUGGING)
return BehaviorBuilder.create {
it.group(
it.present(CobblemonMemories.DIAMOND_DEBUGGER)
).apply(it) { _ ->
Trigger { world, entity, time ->
if(entity !is PokemonEntity) return@Trigger false
val player = entity.brain.getMemory(CobblemonMemories.DIAMOND_DEBUGGER).orElse(null)
if (entity.moveControl !is BoidsMoveControl && player is LivingEntity) {
entity.moveControl = BoidsMoveControl(entity, player)
} else {
return@Trigger false
}
entity.brain.setActiveActivityIfPossible(CobblemonActivities.POKEMON_BOIDS)
// SpawnSnowstormEntityParticlePacket(
// cobblemonResource("evo_particles"),
// entity.id,
// listOf("shiny_particles", "middle")).sendToPlayersAround(
// entity.x,
// entity.y,
// entity.z,
// 64.0,
// entity.level().dimension()
// )
return@Trigger true
}
}
}
}
}
@@ -124,6 +124,8 @@ interface TaskConfig {
cobblemonResource("target_entity") to TargetEntityTaskConfig::class.java,
cobblemonResource("memory_aspect") to MemoryAspectTaskConfig::class.java,
cobblemonResource("activity_change") to ActivityChangeTaskConfig::class.java,
cobblemonResource("switch_from_boid") to SwitchFromBoidTaskConfig::class.java,
cobblemonResource("switch_to_boid") to SwitchToBoidTaskConfig::class.java,
)
}
@@ -17,6 +17,7 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangV
import com.cobblemon.mod.common.entity.OmniPathingEntity
import com.cobblemon.mod.common.entity.ai.CobblemonRandomSurfacePos
import com.cobblemon.mod.common.entity.ai.CobblemonWalkTarget
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.mainThreadRuntime
import com.cobblemon.mod.common.util.resolveFloat
@@ -184,6 +185,12 @@ class WanderTaskConfig : SingleTaskConfig {
if (targetVec == null) continue
// Ignore pathing targets outside of the pasture zone if the pokemon is pastured.
val tethering = (entity as? PokemonEntity)?.tethering
if (tethering?.canRoamTo(BlockPos.containing(targetVec)) == false) {
continue
}
pos = applyHeightConstraints(
pos = BlockPos.containing(targetVec),
minimumHeight = minimumHeight,
@@ -22,18 +22,21 @@ 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.BattleFledEvent
import com.cobblemon.mod.common.api.events.battles.BattleFleeAttemptEvent
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.pokemon.stats.BattleEvSource
import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.white
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.battles.ActiveBattlePokemon
import com.cobblemon.mod.common.battles.BattleCaptureAction
import com.cobblemon.mod.common.battles.BattleFormat
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.BattleSide
import com.cobblemon.mod.common.battles.FleeAttemptActionResponse
import com.cobblemon.mod.common.battles.ForfeitActionResponse
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.battles.dispatch.BattleDispatch
@@ -48,7 +51,9 @@ import com.cobblemon.mod.common.entity.npc.NPCBattleActor
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.battle.BattleEndPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMakeChoicePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMessagePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleQueueRequestPacket
import com.cobblemon.mod.common.pokemon.evolution.progress.DefeatEvolutionProgress
import com.cobblemon.mod.common.pokemon.evolution.progress.LastBattleCriticalHitsEvolutionProgress
import com.cobblemon.mod.common.pokemon.requirements.DefeatRequirement
@@ -137,6 +142,9 @@ open class PokemonBattle(
val captureActions = mutableListOf<BattleCaptureAction>()
val majorBattleActions = hashMapOf<UUID, BattleMessage>()
/** Monotonically increasing counter to track faint ordering within this battle. */
var faintCounter = 0
val minorBattleActions = hashMapOf<UUID, BattleMessage>()
val contextManager = ContextManager()
@@ -244,12 +252,23 @@ open class PokemonBattle(
fun end() {
ended = true
val awardToFainted = Cobblemon.config.awardExperienceToFaintedPokemon
val awardOnLoss = Cobblemon.config.awardExperienceOnBattleLoss
this.actors.forEach { actor ->
val faintedPokemons = actor.pokemonList.filter { it.health <= 0 }
actor.getSide().getOppositeSide().actors.forEach { opponent ->
val opponentNonFaintedPokemons = opponent.pokemonList.filter { it.health > 0 }
val opponentPokemons = if (awardToFainted) opponent.pokemonList else opponent.pokemonList.filter { it.health > 0 }
faintedPokemons.forEach { faintedPokemon ->
for (opponentPokemon in opponentNonFaintedPokemons) {
for (opponentPokemon in opponentPokemons) {
// Faint-order check: if awarding XP to fainted pokemon, only award for enemies that fainted before this pokemon did
if (awardToFainted) {
val enemyFaintedAt = faintedPokemon.faintedAt ?: continue
val opponentFaintedAt = opponentPokemon.faintedAt
if (opponentFaintedAt != null && opponentFaintedAt <= enemyFaintedAt) {
continue
}
}
val facedFainted = opponentPokemon.facedOpponents.contains(faintedPokemon)
val pokemon = opponentPokemon.effectedPokemon
if (facedFainted) {
@@ -270,7 +289,8 @@ open class PokemonBattle(
else -> continue
}
val experience = Cobblemon.experienceCalculator.calculate(opponentPokemon, faintedPokemon, multiplier)
if (experience > 0 && actor.pokemonList.all { it.health <= 0 }) {
val enemyTeamWiped = actor.pokemonList.all { it.health <= 0 }
if (experience > 0 && (enemyTeamWiped || awardOnLoss)) {
opponent.awardExperience(opponentPokemon, experience)
}
Cobblemon.evYieldCalculator.calculate(opponentPokemon, faintedPokemon).forEach { (stat, amount) ->
@@ -505,6 +525,7 @@ open class PokemonBattle(
fun checkForInputDispatch() {
if (checkForfeit()) return // ignore actors that are still choosing, their choices don't matter anymore
if (checkFleeAttempt()) return
val readyToInput = (actors.any { !it.mustChoose && it.responses.isNotEmpty() } && actors.none { it.mustChoose })
if (readyToInput && captureActions.isEmpty()) {
actors.filter { it.responses.isNotEmpty() }.forEach { it.writeShowdownResponse() }
@@ -512,6 +533,20 @@ open class PokemonBattle(
}
}
private fun checkFleeAttempt(): Boolean {
val runner = actors.find { it.responses.any { it is FleeAttemptActionResponse } } as? PlayerBattleActor ?: return false
CobblemonEvents.BATTLE_FLEE_ATTEMPT.post(BattleFleeAttemptEvent(this, runner))
runner.responses.clear()
runner.mustChoose = true
runner.request?.let { runner.sendUpdate(BattleQueueRequestPacket(it)) }
runner.sendUpdate(BattleMakeChoicePacket())
runner.entity?.sendSystemMessage(battleLang("run_prompt").white())
return true
}
/** Forces Showdown to end the battle when a [BattleActor] chooses to forfeit. */
private fun checkForfeit(): Boolean {
val forfeit = actors.find { it.responses.any { it is ForfeitActionResponse } }
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.api.events
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.BattleVictoryEvent
import com.cobblemon.mod.common.api.events.battles.instruction.FormeChangeEvent
@@ -110,6 +111,9 @@ object CobblemonEvents {
@JvmField
val BATTLE_STARTED_POST = EventObservable<BattleStartedEvent.Post>()
@JvmField
val BATTLE_FLEE_ATTEMPT = EventObservable<BattleFleeAttemptEvent>()
@JvmField
val BATTLE_FLED = EventObservable<BattleFledEvent>()
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.battles
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.util.asArrayValue
/**
* Event fired when a [PokemonBattle] is fled by a [PlayerBattleActor].
*
* @author saultsc
* @since March 31th 2026
*/
data class BattleFleeAttemptEvent(
override val battle: PokemonBattle,
val player: PlayerBattleActor
) : BattleEvent {
val context = mutableMapOf<String, MoValue>(
"battle" to battle.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 }
)
}
@@ -59,40 +59,32 @@ object PokemonInteractions : JsonDataRegistry<PokemonInteractionSet> {
.setPrettyPrinting()
.create()
val speciesInteractions = mutableListOf<PokemonInteractionSet>()
val generalInteractions = mutableListOf<PokemonInteractionSet>()
val interactions = mutableListOf<PokemonInteractionSet>()
override fun sync(player: ServerPlayer) {
PokemonInteractionsSyncPacket(
speciesInteractions,
generalInteractions,
).sendToPlayer(player)
PokemonInteractionsSyncPacket(interactions).sendToPlayer(player)
}
override fun reload(data: Map<ResourceLocation, PokemonInteractionSet>) {
speciesInteractions.clear()
generalInteractions.clear()
val split = data.entries.partition { it.value.requirements.any { requirement -> requirement is PokemonPropertiesRequirement && requirement.target.species != null } }
speciesInteractions.addAll(split.first.map {it.value})
generalInteractions.addAll(split.second.map {it.value})
Cobblemon.LOGGER.info("Loaded {} Pokémon interaction sets", data.size)
interactions.clear()
val split = data.entries
.partition {
it.value.requirements.any { requirement -> requirement is PokemonPropertiesRequirement && requirement.target.species != null }
}
// Put the species specific interactions first as they take priority
interactions.addAll(split.first.map {it.value})
interactions.addAll(split.second.map {it.value})
Cobblemon.LOGGER.info("Loaded {} Pokémon interaction sets", interactions.size)
}
fun findInteraction(pokemon: PokemonEntity): PokemonInteraction? {
val setCheck: (PokemonInteractionSet) -> Boolean = { it.requirements.all { req -> req.check(pokemon.pokemon) }}
val interactionCheck: (PokemonInteraction) -> Boolean = { it.requirements.all { req -> req.check(pokemon.pokemon) } && !pokemon.pokemon.isOnInteractionCooldown(it.grouping) }
// species-specific interactions take priority
val validInteractions = speciesInteractions
return interactions
.filter(setCheck)
.flatMap { it.interactions }
.filter(interactionCheck)
.toMutableList()
if (validInteractions.isEmpty()) { // if all species-specific interactions are absent/on cooldown, fallback to more generic ones
validInteractions.addAll(generalInteractions.filter(setCheck).flatMap { it.interactions }
.filter(interactionCheck)
)
}
return validInteractions.randomOrNull()
.firstOrNull(interactionCheck)
}
}
@@ -43,17 +43,9 @@ data class PokemonInteractionSet(
companion object {
fun decode(buffer: FriendlyByteBuf): PokemonInteractionSet {
val requirements = buffer.readList { it ->
CobblemonRequirementAdapter.decode(it)
}
val interactions = buffer.readList { it ->
PokemonInteraction.decode(it)
}
return PokemonInteractionSet(
requirements,
interactions,
)
val requirements = buffer.readList(CobblemonRequirementAdapter::decode)
val interactions = buffer.readList(PokemonInteraction::decode)
return PokemonInteractionSet(requirements, interactions)
}
}
}
@@ -145,6 +145,7 @@ import net.minecraft.world.entity.item.ItemEntity
import net.minecraft.world.entity.monster.Monster
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.enchantment.EnchantmentHelper
import net.minecraft.world.level.ClipContext
import net.minecraft.world.level.GameType
import net.minecraft.world.level.Level
@@ -871,6 +872,8 @@ object MoLangFunctions {
DoubleValue(value)
}
map.put("get_starter_uuid") { _ -> Cobblemon.playerDataManager.getGenericData(player).starterUUID?.let{ StringValue(it.toString()) } ?: DoubleValue.ZERO }
map.put("has_chosen_starter") { _ -> DoubleValue(Cobblemon.playerDataManager.getGenericData(player).starterSelected) }
}
map
}
@@ -891,6 +894,15 @@ object MoLangFunctions {
map.put("is_of") { params -> DoubleValue(holder.`is`(params.getString(0).asIdentifierDefaultingNamespace())) }
map.put("is_in") { params -> DoubleValue(holder.`is`(TagKey.create(Registries.ITEM, params.getString(0).replace("#", "").asIdentifierDefaultingNamespace()))) }
map.put("is_food") { params -> DoubleValue(stack.has(DataComponents.FOOD)) }
map.put("is_enchanted") { params -> DoubleValue(stack.hasFoil()) }
map.put("has_enchantment") { params ->
val enchantmentId = params.getString(0).asIdentifierDefaultingNamespace()
val minLevel = params.getIntOrNull(1) ?: 1
val enchantmentRegistry = registryAccess.registryOrThrow(Registries.ENCHANTMENT)
val holder = enchantmentRegistry.getHolder(enchantmentId).orElse(null) ?: return@put DoubleValue.ZERO
val level = EnchantmentHelper.getItemEnchantmentLevel(holder, stack)
return@put DoubleValue(level >= minLevel)
}
return@mutableListOf map
}
)
@@ -1830,6 +1842,30 @@ object MoLangFunctions {
map.put("remove_cosmetic_item") { _ ->
pokemon.removeCosmeticItem()
}
map.put("marks") { _ -> pokemon.marks.asArrayValue { StringValue(it.identifier.toString()) } }
map.put("has_mark") { params ->
var hasMark = false
val identifier = params.getString(0).asIdentifierDefaultingNamespace()
val mark = Marks.getByIdentifier(identifier)
if (mark != null) {
hasMark = pokemon.marks.contains(mark)
}
DoubleValue(hasMark)
}
map.put("remove_marks") { params ->
var removedMark = false
for (param in params.params) {
val identifier = param.asString().asIdentifierDefaultingNamespace()
val mark = Marks.getByIdentifier(identifier)
if (mark != null) {
pokemon.exchangeMark(mark, false)
removedMark = true
}
}
DoubleValue(removedMark)
}
map.put("add_marks") { params ->
var appliedMark = false
for (param in params.params) {
@@ -2023,9 +2059,13 @@ object MoLangFunctions {
}
}
map.put("is_sprinting") {
pokemonEntity.ifRidingAvailableSupply(DoubleValue.ZERO) { behaviour, settings, state ->
val moValue = behaviour.asMoLangValue(settings, state, pokemonEntity)
return@ifRidingAvailableSupply moValue.functions.get("sprinting")?.apply(MoParams.EMPTY) as? MoValue ?: DoubleValue.ZERO
if (pokemonEntity.hasControllingPassenger()) { // If its being ridden then the riding state sets the animation
pokemonEntity.ifRidingAvailableSupply(DoubleValue.ZERO) { behaviour, settings, state ->
val moValue = behaviour.asMoLangValue(settings, state, pokemonEntity)
return@ifRidingAvailableSupply moValue.functions.get("sprinting")?.apply(MoParams.EMPTY) as? MoValue ?: DoubleValue.ZERO
}
} else {
pokemonEntity.isPokemonSprinting
}
}
map.put("is_drifting") {
@@ -2815,8 +2855,8 @@ object MoLangFunctions {
}
variable
}
else -> null
} ?: throw IllegalArgumentException("Invalid NBT element type: ${nbt.type}")
else -> VariableStruct()
}
}
}
@@ -289,11 +289,11 @@ object PokeBalls : JsonDataRegistry<PokeBall> {
createDefault("ancient_great_ball", MultiplierModifier(1.5F), ancient = true)
createDefault("ancient_ultra_ball", MultiplierModifier(2F), ancient = true)
createDefault("ancient_heavy_ball", throwPower = 0.75f, ancient = true)
createDefault("ancient_leaden_ball", throwPower = 0.75f, ancient = true)
createDefault("ancient_gigaton_ball", throwPower = 0.75f, ancient = true)
createDefault("ancient_leaden_ball", MultiplierModifier(1.5F), throwPower = 0.75f, ancient = true)
createDefault("ancient_gigaton_ball", MultiplierModifier(2F), throwPower = 0.75f, ancient = true)
createDefault("ancient_feather_ball", throwPower = 2.5f, ancient = true)
createDefault("ancient_wing_ball", throwPower = 2.5f, ancient = true)
createDefault("ancient_jet_ball", throwPower = 2.5f, ancient = true)
createDefault("ancient_wing_ball", MultiplierModifier(1.5F), throwPower = 2.5f, ancient = true)
createDefault("ancient_jet_ball", MultiplierModifier(2F), throwPower = 2.5f, ancient = true)
createDefault("ancient_origin_ball", GuaranteedModifier(), ancient = true)
// Luxury ball effect, low priority as it must be triggered before soothe bell as of gen 4
CobblemonEvents.FRIENDSHIP_UPDATED.subscribe(priority = Priority.LOW) { event ->
@@ -314,11 +314,11 @@ object PokemonSpecies : JsonDataRegistry<Species> {
val result = mutableMapOf<String, String>()
this.species.forEach {species ->
val baseSpecies = ShowdownSpecies(species, null)
result[ShowdownIdentifiable.REGEX.replace(baseSpecies.name, "")] = this.gson.toJson(baseSpecies)
result[ShowdownIdentifiable.REGEX.replace(baseSpecies.name.lowercase(), "")] = this.gson.toJson(baseSpecies)
species.forms.forEach { form ->
if (form != species.standardForm) {
val formSpecies = ShowdownSpecies(species, form)
result[ShowdownIdentifiable.REGEX.replace(formSpecies.name, "")] = this.gson.toJson(formSpecies)
result[ShowdownIdentifiable.REGEX.replace(formSpecies.name.lowercase(), "")] = this.gson.toJson(formSpecies)
}
}
}
@@ -119,7 +119,10 @@ class RidingController(
private fun canTransitionToLand(): Boolean {
if ((entity.isUnderWater)) return false
if ((entity.isEyeInFluid(FluidTags.WATER) || entity.isEyeInFluid(FluidTags.LAVA))) return false
return entity.onGround()
// If the currently in the air but has no land style then don't transition and render the ride useless.
// Hover is a good example for this.
val shouldStayInAir = entity.pokemon.riding.behaviours?.get(RidingStyle.LAND) == null && context?.style == RidingStyle.AIR
return entity.onGround() && !shouldStayInAir
}
private fun canTransitionToLiquid(): Boolean {
@@ -213,10 +213,23 @@ class HoverBehaviour : RidingBehaviour<HoverSettings, HoverState> {
val accel = topSpeed / (vehicle.runtime.resolveDouble(settings.accelerationExpr ?: globalHover.accelerationExpr!!) * 20.0)
var newVelocity = state.rideVelocity.get()
val unrotateVel = vehicle.deltaMovement.yRot(vehicle.yRot)
val vehicleFrontVec = Vec3(0.0, 0.0, 1.0)
val faceVel = Vec3(vehicle.deltaMovement.x, 0.0, vehicle.deltaMovement.z)
val d = vehicle.deltaMovement.horizontalDistance()
val v2 = Vec3(faceVel.dot(Vec3(1.0, 0.0, 0.0)) * d, newVelocity.y, faceVel.dot(Vec3(0.0, 0.0, 1.0)) * d)
// Check for collisions and if found, update the speed to reflect it
if (vehicle.verticalCollision || vehicle.horizontalCollision) {
state.speed.set(vehicle.deltaMovement.length())
if (vehicle.horizontalCollision) {
val smackingIntoWallSpeed = max( vehicle.deltaMovement.horizontalDistance(), 0.2)
newVelocity = Vec3(newVelocity.x, 0.0, newVelocity.z).normalize().scale(smackingIntoWallSpeed).add(0.0, newVelocity.y, 0.0)
}
if (vehicle.verticalCollision) {
newVelocity = Vec3(newVelocity.x, 0.0, newVelocity.z)
}
// align the velocity vector to be in local vehicle space
@@ -60,7 +60,7 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
val poseProvider = PoseProvider<SubmarineSettings, SubmarineState>(PoseType.FLOAT)
.with(PoseOption(PoseType.SWIM) { _, state, entity -> state.rideVelocity.get().z > 0.05 && !state.onSurface.get()})
.with(PoseOption(PoseType.STAND) { _, state, entity -> state.rideVelocity.get().z <= 0.05 && state.onSurface.get()})
.with(PoseOption(PoseType.STAND) { _, state, entity -> state.rideVelocity.get().z > 0.05 && state.onSurface.get()})
.with(PoseOption(PoseType.SWIM) { _, state, entity -> state.rideVelocity.get().z > 0.05 && state.onSurface.get()})
override fun isActive(settings: SubmarineSettings, state: SubmarineState, vehicle: PokemonEntity): Boolean {
return vehicle.isInWater || !vehicle.onGround()
@@ -191,11 +191,17 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
* Calculate Forward Velocity
*********************************************/
val currZVel = state.rideVelocity.get().z
val newZVel = when {
var newZVel = when {
driver.zza > 0.0 -> min(topSpeed, currZVel + accel)
driver.zza < 0.0 -> max( -strafeSpeed, currZVel - strafeAccel) //TODO: Might need to be applied in addition to the normal braking force?
else -> lerp(currZVel, 0.0, 0.05)
driver.zza < 0.0 -> max( -strafeSpeed, currZVel - strafeAccel)
else -> lerp(currZVel, 0.0, 0.02)
}
// If trying to brake then add the friction to the braking force
if (driver.zza != 0.0f && driver.zza.sign.toDouble() != newZVel.sign) {
newZVel = lerp(newZVel, 0.0, 0.02)
}
currVel = Vec3(currVel.x, currVel.y, newZVel)
/*********************************************
@@ -206,14 +212,20 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
val gravity = (9.8 / ( 20.0)) * 0.2 * 0.4
val terminalYVel = 2.0
val driverYInput = if(driver.jumping) 1.0 else if (driver.isShiftKeyDown) -1.0 else 0.0
val newYVel = when {
var newYVel = when {
(!vehicle.isInWater) -> max(-terminalYVel, currYVel - gravity)
//(vehicle.isInWater && !vehicle.isUnderWater && currYVel > 0.001) -> lerp(currYVel, 0.0, 0.1) // Make sure that yVel is above a threshold before doing this otherwise it'll block submerging.
//driverYInput != 0.0 && currYVel > 0.001 && driverYInput.sign != currYVel.sign -> lerp(currYVel, 0.0, 0.05)
driverYInput > 0.0 -> min(vertTopSpeed, currYVel + strafeAccel)
driverYInput < 0.0 -> max(-vertTopSpeed, currYVel - strafeAccel)
else -> lerp(currYVel, 0.0, 0.05)
else -> lerp(currYVel, 0.0, 0.02)
}
// If trying to brake then add the friction to the braking force
if (driverYInput != 0.0 && driverYInput != newYVel.sign) {
newYVel = lerp(newYVel, 0.0, 0.02)
}
currVel = Vec3(currVel.x, newYVel, currVel.z)
/*********************************************
@@ -221,11 +233,17 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
*********************************************/
val currXVel = state.rideVelocity.get().x
val horzTopSpeed = strafeSpeed
val newXVel = when {
var newXVel = when {
driver.xxa > 0.0 -> min(horzTopSpeed, currXVel + strafeAccel)
driver.xxa < 0.0 -> max( -horzTopSpeed, currXVel - strafeAccel)
else -> lerp(currXVel, 0.0, 0.01)
else -> lerp(currXVel, 0.0, 0.02)
}
// If trying to brake then add the friction to the braking force
if (driver.xxa != 0.0f && driver.xxa.sign.toDouble() != newZVel.sign) {
newXVel = lerp(newXVel, 0.0, 0.02)
}
currVel = Vec3(newXVel, currVel.y, currVel.z)
// Set the new velocity
@@ -305,81 +323,6 @@ class SubmarineBehaviour : RidingBehaviour<SubmarineSettings, SubmarineState> {
}
private fun correctOrientation(
settings: SubmarineSettings,
state: SubmarineState,
vehicle: PokemonEntity,
controller: OrientationController,
driver: Player,
deltaTime: Double
) {
// TODO: remove if not needed
// // Calculate correcting roll force.
// // If the ride is moving then have the correction rate be an equal proportion of:
// // - how rolled are you
// // - how long has it been since last mouse input (higher is stronger correction)
// // Also the correction is proportional as a whole to how NOT pitched are you so you don't spin endlessly when looking up or down
// val howRolledAmI = sqrt(abs((cos(controller.roll.toRadians()) - 1.0f) * 0.5f)) // sqrt so it trends towards one pretty quickly
// val maxRollCorrectionRate =
// if (state.rideVelocity.get().length() < 0.2) 10.0f * abs(cos(controller.pitch.toRadians()))
// else 10.0f * cos(controller.pitch.toRadians()).pow(2) * (
// howRolledAmI*1.0f +
// (state.noInputTime.get() - rollCorrectionTimer).coerceIn(0.0, 1.0).pow(2).toFloat()*0.5f
// )
// val maxRollForce = maxRollCorrectionRate / 40.0f
// val rollArrivalDeg = 60.0f
// val desiredRoll = 0.0f
//
// // Calculate the signed error (how far and in what direction)
// val rollError = Mth.wrapDegrees(desiredRoll.angleDifference(controller.roll))
//
// // Use the absolute error to calculate the magnitude of the influence
// val influenceMagnitude = (min(rollArrivalDeg, abs(rollError)) / rollArrivalDeg)
// val desiredRollForce = maxRollCorrectionRate * influenceMagnitude * -rollError.sign
// var steeredRollForce = desiredRollForce - state.currRollCorrectionForce.get()
//
// // Ensure the 'steering' correction force doesn't exceed our maxForce
// if (abs(steeredRollForce) > maxRollForce) {
// steeredRollForce = steeredRollForce.sign * maxRollForce
// }
//
// // Apply the 'steered' force (its really not a steering force I need a better name)
// state.currRollCorrectionForce.set(
// state.currRollCorrectionForce.get() + steeredRollForce
// )
//Calculate correcting pitch force.
// val rollCorrectionTimer = vehicle.runtime.resolveDouble(settings.timeToRollCorrect ?: globalSubmarine.timeToRollCorrect!!)
// val maxPitchCorrectionRate = 5.0f * (state.noInputTime.get() - rollCorrectionTimer).coerceIn(0.0, 1.0).pow(2).toFloat()*0.5f
// val maxPitchForce = maxPitchCorrectionRate / 40.0f
// val pitchArrivalDeg = 45.0f
// val desiredPitch = 0.0f
//
// if(state.rideVelocity.get().length() < 0.1 && state.noInputTime.get() > rollCorrectionTimer) {
// val pitchErrror = Mth.wrapDegrees(desiredPitch.angleDifference(controller.pitch))
// val arrivalInfluence = (min(pitchArrivalDeg,abs(pitchErrror)) / pitchArrivalDeg)
// val desiredPitchForce = maxPitchCorrectionRate * arrivalInfluence * -pitchErrror.sign
// var steeredPitchForce = desiredPitchForce - state.currPitchCorrectionForce.get()
//
// // Ensure the 'steering' correction force doesn't exceed our maxForce
// if (abs(steeredPitchForce) > maxPitchForce) {
// steeredPitchForce = steeredPitchForce.sign * maxPitchForce
// }
//
// // Apply the 'steered' force (its really not a steering force I need a better name)
// state.currPitchCorrectionForce.set(
// state.currPitchCorrectionForce.get() + steeredPitchForce
// )
// } else {
// state.currPitchCorrectionForce.set(
// //reduce rollCorrection gradually
// lerp(state.currPitchCorrectionForce.get(), 0.0, deltaTime * 0.98)
// )
// }
}
override fun rotationOnMouseXY(
settings: SubmarineSettings,
state: SubmarineState,
@@ -54,8 +54,6 @@ class RidingAnimationData(val ride: PokemonEntity)
fun update() {
val activeRide = ride.hasControllingPassenger()
if (!activeRide) return
var currRot = Vec3.ZERO
var angDelta = Vec3.ZERO
@@ -153,14 +151,16 @@ class RidingAnimationData(val ride: PokemonEntity)
/******************************************************
* Update springs
*****************************************************/
val driver = ride.controllingPassenger
val vertInput = when {
driver != null && driver.jumping-> 1.0
driver != null && driver.isShiftKeyDown -> -1.0
else -> 0.0
if (activeRide) {
val driver = ride.controllingPassenger
val vertInput = when {
driver != null && driver.jumping-> 1.0
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)
}
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)
@@ -44,6 +44,7 @@ import com.cobblemon.mod.common.api.spawning.preset.BestSpawnerConfig
import com.cobblemon.mod.common.api.spawning.preset.PokemonSpawnDetailPreset
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import com.cobblemon.mod.common.api.spawning.spawner.BasicSpawner
import com.cobblemon.mod.common.api.spawning.spawner.FishingSpawnerFactory
import com.cobblemon.mod.common.api.spawning.spawner.FixedAreaSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory
@@ -139,19 +140,6 @@ object BestSpawner {
fun onServerStarted(server: MinecraftServer) {
CobblemonSpawnPools.onServerLoad(server)
fishingSpawner = BasicSpawner(
name = "fishing",
spawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL
).also {
it.influences.add(
BucketMultiplyingInfluence(
multipliers = mapOf(
"uncommon" to 2.25f,
"rare" to 5.5f,
"ultra-rare" to 5.5f,
)
)
)
}
fishingSpawner = FishingSpawnerFactory.createSharedSpawner()
}
}
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnRules
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.BucketMultiplyingInfluence
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence.Companion.TYPICAL_VARIATION
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.util.mutableLazy
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.item.ItemStack
import com.cobblemon.mod.common.api.spawning.BestSpawner;
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
/**
* This isn't really a factory, one FishingSpawner is created as a field on [BestSpawner] on
* server start. However, this emulates the systems that PlayerSpawner uses so extenders can
* add influences that affect all fishing encounters.
*
* Responsible for creating [BasicSpawner]s for Fishing with whatever appropriate settings.
* You can swap over the spawn pool and the influences here, and it will apply to all newly-generated
* [BasicSpawner]s for Fishing. Has a global every-cast influence list, as well as a context-sensitive
* influence list.
*
* @author Andy
* @since January 23rd, 2026
*/
object FishingSpawnerFactory {
var spawnPool: SpawnPool by mutableLazy {
CobblemonSpawnPools.WORLD_SPAWN_POOL
}
data class Context(
val player: ServerPlayer,
val rodStack: ItemStack,
val lureLevel: Int,
val luckOfTheSeaLevel: Int,
val world: ServerLevel,
val pos: BlockPos,
val spawner: BasicSpawner
)
/**
* List of influences for a new cast of the fishing rod. Context is provided at
* build time in [buildPositionInfluences]
*/
var positionInfluenceBuilders = mutableListOf<(ctx: Context) -> SpawningInfluence?>(
{ ctx -> PlayerLevelRangeInfluence(ctx.player, TYPICAL_VARIATION) },
{ ctx -> BucketNormalizingInfluence(tier = ctx.lureLevel + ctx.luckOfTheSeaLevel) }
)
fun createSharedSpawner(): BasicSpawner {
return BasicSpawner(
name = "fishing",
spawnPool = spawnPool
).also { spawner ->
spawner.influences += BucketMultiplyingInfluence(
multipliers = mapOf(
"uncommon" to 2.25f,
"rare" to 5.5f,
"ultra-rare" to 5.5f,
)
)
}
}
/**
* Builds the per-cast influences for [FishingSpawnablePosition]
*/
fun buildPositionInfluences(ctx: Context): MutableList<SpawningInfluence> {
return positionInfluenceBuilders.mapNotNullTo(mutableListOf()) { it(ctx) }
}
}
@@ -0,0 +1,101 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.api.fishing.SpawnBait
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnRules
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.BucketMultiplyingInfluence
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawnBaitInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.rules.SpawnRule
import com.cobblemon.mod.common.util.mutableLazy
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
/**
* Responsible for creating [FixedAreaSpawner]s for PokeSnacks with whatever appropriate settings.
* You can swap over the spawn pool and the influences here, and it will apply to all newly-generated
* [FixedAreaSpawner]s for PokeSnacks.
*
* @author Andy
* @since January 23rd, 2026
*/
object PokeSnackSpawnerFactory {
var spawnPool: SpawnPool by mutableLazy {
CobblemonSpawnPools.WORLD_SPAWN_POOL
}
data class Context(
val world: ServerLevel,
val pos: BlockPos,
val horizontalRadius: Int,
val verticalRadius: Int,
val maxPokemonPerChunk: Float,
val name: String,
val baitEffects: List<SpawnBait.Effect>
)
/**
* The list of influences for a new [FixedAreaSpawner] for a PokeSnack.
* The influences will be added to the spawner in the order they are added to this list.
*/
var influenceBuilders = mutableListOf<(ctx: Context) -> SpawningInfluence?>(
{ ctx ->
val stackedLureTier = ctx.baitEffects
.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }
.sumOf { it.value }
.toInt()
if (stackedLureTier <= 0) null else BucketNormalizingInfluence(
tier = stackedLureTier,
gradient = 0.2F,
firstTier = 1.2F
)
},
{ _ ->
BucketMultiplyingInfluence(
mapOf(
"uncommon" to 2.25f,
"rare" to 5.5f,
"ultra-rare" to 5.5f,
)
)
},
{ ctx ->
SpawnBaitInfluence(effects = ctx.baitEffects)
},
)
fun create(ctx: Context): FixedAreaSpawner {
val influences = influenceBuilders.mapNotNull { it(ctx) }
return FixedAreaSpawner(
name = ctx.name,
spawnPool = spawnPool,
world = ctx.world,
position = ctx.pos,
horizontalRadius = ctx.horizontalRadius,
verticalRadius = ctx.verticalRadius,
maxPokemonPerChunk = ctx.maxPokemonPerChunk
).also { spawner ->
spawner.influences.addAll(influences)
// TODO(Andy): PokeSnacks currently bypass pokemon spawn rules, should that be true?
// Adding this rule will change that
spawner.influences.addAll(
CobblemonSpawnRules.rules.values
.filter(SpawnRule::enabled)
.flatMap(SpawnRule::components)
)
}
}
}
@@ -28,6 +28,7 @@ data class GeneralPlayerData(
var starterUUID: UUID?,
var keyItems: MutableSet<ResourceLocation>,
var battleTheme: ResourceLocation?,
var partySelectTutorialDone: Boolean = false,
val extraData: MutableMap<String, PlayerDataExtension>,
) : InstancedPlayerData {
var advancementData: PlayerAdvancementData = PlayerAdvancementData()
@@ -53,7 +54,8 @@ data class GeneralPlayerData(
starterSelected,
starterUUID,
advancementData.totalBattleVictoryCount == 0,
battleTheme
battleTheme,
partySelectTutorialDone
)
}
@@ -36,7 +36,8 @@ class PlayerDataJsonBackend: JsonBackedPlayerDataStoreBackend<GeneralPlayerData>
starterUUID = null,
keyItems = Cobblemon.config.defaultKeyItems,
extraData = mutableMapOf(),
battleTheme = CobblemonSounds.PVP_BATTLE.location
battleTheme = CobblemonSounds.PVP_BATTLE.location,
partySelectTutorialDone = false
)}
override val gson = GsonBuilder()
@@ -37,7 +37,8 @@ class PlayerDataMongoBackend(mongoClient: MongoClient, databaseName: String, col
starterUUID = null,
keyItems = Cobblemon.config.defaultKeyItems,
extraData = mutableMapOf(),
battleTheme = CobblemonSounds.PVP_BATTLE.location
battleTheme = CobblemonSounds.PVP_BATTLE.location,
partySelectTutorialDone = false
)}
override val gson = GsonBuilder()
@@ -34,7 +34,8 @@ data class ClientGeneralPlayerData(
var starterSelected: Boolean = false,
var starterUUID: UUID? = null,
var showChallengeLabel: Boolean = true,
val battleTheme: ResourceLocation? = null
val battleTheme: ResourceLocation? = null,
var partySelectTutorialDone: Boolean = false
) : ClientInstancedPlayerData {
override fun encode(buf: RegistryFriendlyByteBuf) {
@@ -46,6 +47,7 @@ data class ClientGeneralPlayerData(
buf.writeNullable(starterUUID) { pb, value -> pb.writeString(value.toString()) }
buf.writeNullable(resetStarters) { pb, value -> pb.writeBoolean(value) }
buf.writeNullable(battleTheme) {pb, value -> pb.writeIdentifier(value)}
buf.writeBoolean(partySelectTutorialDone)
}
companion object {
fun decode(buffer: RegistryFriendlyByteBuf): SetClientPlayerDataPacket {
@@ -57,6 +59,7 @@ data class ClientGeneralPlayerData(
val starterUUID = buffer.readNullable { it.readString() }?.let { UUID.fromString(it) }
val resetStarterPrompt = buffer.readNullable { it.readBoolean() }
val battleTheme = buffer.readNullable { it.readIdentifier() }
val partySelectTutorialDone = buffer.readBoolean()
val data = ClientGeneralPlayerData(
resetStarterPrompt,
promptStarter,
@@ -64,7 +67,8 @@ data class ClientGeneralPlayerData(
starterSelected,
starterUUID,
showChallengeLabel,
battleTheme
battleTheme,
partySelectTutorialDone
)
//Weird to do this, but since the flag doesn't get passed to the decoded obj, do it here
//Should be fine, as long as decode doesn't get run on the server for some reason
@@ -94,7 +94,8 @@ enum class ShowdownActionResponseType(val loader: (RegistryFriendlyByteBuf) -> S
PASS({ PassActionResponse }),
SHIFT({ ShiftActionResponse()}),
HEAL_ITEM({ HealItemActionResponse("potion") }),
FORFEIT({ ForfeitActionResponse() });
FORFEIT({ ForfeitActionResponse() }),
FLEE_ATTEMPT({ FleeAttemptActionResponse() }),
}
abstract class ShowdownActionResponse(val type: ShowdownActionResponseType) {
@@ -280,6 +281,11 @@ class ForfeitActionResponse : ShowdownActionResponse(ShowdownActionResponseType.
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?) = "forfeit"
}
class FleeAttemptActionResponse : ShowdownActionResponse(ShowdownActionResponseType.FLEE_ATTEMPT) {
override fun isValid(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?, forceSwitch: Boolean) = true
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?) = "flee-attempt"
}
class ShowdownMoveset {
lateinit var moves: List<InBattleMove>
var trapped = false
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.battles
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.battles.runner.ShowdownService
import net.minecraft.CrashReport
import java.util.*
import java.util.concurrent.CountDownLatch
@@ -37,8 +38,15 @@ class ShowdownThread : Thread("Cobblemon Showdown") {
override fun run() {
LOGGER.info("Starting showdown service...")
ShowdownService.service.openConnection()
LOGGER.info("Showdown has been started!")
this.latch.countDown()
try {
ShowdownService.service.openConnection()
LOGGER.info("Showdown has been started!")
} catch (e: Exception) {
LOGGER.error("Failed to start showdown service!", e)
} finally {
this.latch.countDown()
throw RuntimeException("Showdown service failed to initialize...")
}
}
}
@@ -31,7 +31,6 @@ import kotlin.collections.associateWith
import kotlin.collections.partition
import kotlin.math.absoluteValue
/**
* AI that tries to choose the best move for the given situations. Based off of the Pokemon Trainer Tournament Simulator Github
* https://github.com/cRz-Shadows/Pokemon_Trainer_Tournament_Simulator/blob/main/pokemon-showdown/sim/examples/Simulation-test-1.ts#L330
@@ -49,7 +48,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
private val hpWeightConsideration = 0.25 // how much HP difference is a consideration for switchins
private val hpFractionCoefficient = 0.4 // how much HP differences should be taken into account for switch ins
private val boostWeightCoefficient = 1 // the amount of boosts considered a baseline to be removed
private val switchOutMatchupThreshold = 0 // todo change this to get it feeling just right (-7 never switches)
private val switchOutMatchupThreshold = -2 // todo change this to get it feeling just right (-7 never switches)
private val selfKoMoveMatchupThreshold = 0.3
private val trickRoomThreshold = 85
private val recoveryMoveThreshold = 0.50
@@ -219,19 +218,14 @@ class StrongBattleAI(skill: Int) : BattleAI {
.filter { it.second.canBeSentOut() }
if (forceSwitch || activeBattlePokemon.isGone()) {
if (battle.turn == 1) {
val switchTo = activeBattlePokemon.actor.pokemonList.filter { it.canBeSentOut() }.randomOrNull()
?: return DefaultActionResponse()
switchTo.willBeSwitchedIn = true
return SwitchActionResponse(switchTo.uuid)
}
else {
val bestEstimation = availableSwitches.maxByOrNull { estimateMatchup(activeBattlePokemon, aiSide, battle, it.first) }
?: return PassActionResponse
// When forced to switch, pick the Pokemon with the best matchup estimation
val bestSwitchScore = availableSwitches.maxOfOrNull { estimateMatchup(activeBattlePokemon, aiSide, battle, it.first) }
?: return PassActionResponse
val bestSwitch = availableSwitches.firstOrNull { estimateMatchup(activeBattlePokemon, aiSide, battle, it.first) == bestSwitchScore }
?: return PassActionResponse
bestEstimation.second.willBeSwitchedIn = true
return SwitchActionResponse(bestEstimation.second.uuid)
}
bestSwitch.second.willBeSwitchedIn = true
return SwitchActionResponse(bestSwitch.second.uuid)
}
if (moveset == null) {
return PassActionResponse
@@ -259,9 +253,9 @@ class StrongBattleAI(skill: Int) : BattleAI {
if (!checkSkillLevel()){
val move = availableMoves
.filter { it.first.target.targetList(activeBattlePokemon)?.isEmpty() != true }
.randomOrNull()
?: return MoveActionResponse("struggle")
.filter { it.first.target.targetList(activeBattlePokemon)?.isEmpty() != true }
.randomOrNull()
?: return MoveActionResponse("struggle")
return chooseMove(move.first, activeBattlePokemon)
}
@@ -270,7 +264,6 @@ class StrongBattleAI(skill: Int) : BattleAI {
if (checkSwitchOutSkill() && shouldSwitchOut(aiSide, battle, activeBattlePokemon, moveset)) {
considerSwitching(activeBattlePokemon, activeTrackerPokemon, opponents, availableMoves, availableSwitches, battle, aiSide)
}
activeTrackerPokemon.firstTurn = false
// If HP is below 30% and not switching out, always use the most damaging move
if (activeTrackerPokemon.currentHpPercent < 0.3 && !shouldSwitchOut(aiSide, battle, activeBattlePokemon, moveset)) {
@@ -296,8 +289,6 @@ class StrongBattleAI(skill: Int) : BattleAI {
return chooseMove(fakeOut.first, activeBattlePokemon, targets.firstOrNull { validTarget != null && validTarget.id == it.battlePokemon!!.uuid})
}
activeTrackerPokemon.firstTurn = false
// Explosion/Self destruct
availableMoves.firstOrNull {
@@ -315,7 +306,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Deal with non-weather related field changing effects
for (move in availableMoves) {
// Tailwind
if (move.first.id == "tailwind" && move.first.id != activeTracker.alliedSide.tailwindCondition &&
availableSwitches.size > 2) {
@@ -324,21 +315,21 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Trick room
if (move.first.id == "trickroom" && move.first.id != activeTracker.currentRoom
&& availableSwitches.count { statEstimationActive(it.first, Stats.SPEED) <= trickRoomThreshold } >= 2) {
&& availableSwitches.count { statEstimationActive(it.first, Stats.SPEED) <= trickRoomThreshold } >= 2) {
return chooseMove(move.first, activeBattlePokemon)
}
// todo find a way to get list of active screens
// Aurora veil
if (move.first.id == "auroraveil" && move.first.id != activeTracker.alliedSide.screenCondition
&& activeTracker.currentWeather in listOf("hail", "snow")) {
&& activeTracker.currentWeather in listOf("hail", "snow")) {
return chooseMove(move.first, activeBattlePokemon)
}
// todo find a way to get list of active screens
// Light Screen
if (move.first.id == "lightscreen" && move.first.id != activeTracker.alliedSide.screenCondition
&& opponents.any { (it.species?.baseStats?.get(Stats.SPECIAL_ATTACK) ?: 0) > (it.species?.baseStats?.get(Stats.ATTACK) ?: 0) }
&& opponents.any { (it.species?.baseStats?.get(Stats.SPECIAL_ATTACK) ?: 0) > (it.species?.baseStats?.get(Stats.ATTACK) ?: 0) }
&& availableSwitches.size > 1) {
return chooseMove(move.first, activeBattlePokemon)
}
@@ -356,13 +347,13 @@ class StrongBattleAI(skill: Int) : BattleAI {
for (move in availableMoves) {
// Setup
if (nOppRemainingMons >= 3 && move.first.id in AIUtility.entryHazards
&& !activeTracker.opponentSide.sideHazards.contains(move.first.id)) {
&& !activeTracker.opponentSide.sideHazards.contains(move.first.id)) {
return chooseMove(move.first, activeBattlePokemon)
}
// Removal
if (nRemainingMons >= 2 && move.first.id in AIUtility.antiHazardsMoves
&& activeTracker.alliedSide.sideHazards.isNotEmpty()) {
&& activeTracker.alliedSide.sideHazards.isNotEmpty()) {
return chooseMove(move.first, activeBattlePokemon)
}
}
@@ -372,10 +363,10 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Court Change
for (move in availableMoves) {
if (move.first.id == "courtchange"
&& (!AIUtility.entryHazards.none { it in activeTracker.alliedSide.sideHazards }
|| setOf("tailwind", "lightscreen", "reflect").any { it in activeTracker.opponentSide.sideHazards })
&& setOf("tailwind", "lightscreen", "reflect").none { it in activeTracker.opponentSide.sideHazards }
&& AIUtility.entryHazards.none { it in activeTracker.opponentSide.sideHazards }) {
&& (!AIUtility.entryHazards.none { it in activeTracker.alliedSide.sideHazards }
|| setOf("tailwind", "lightscreen", "reflect").any { it in activeTracker.opponentSide.sideHazards })
&& setOf("tailwind", "lightscreen", "reflect").none { it in activeTracker.opponentSide.sideHazards }
&& AIUtility.entryHazards.none { it in activeTracker.opponentSide.sideHazards }) {
return chooseMove(move.first, activeBattlePokemon)
}
}
@@ -383,7 +374,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Strength Sap
for (move in availableMoves) {
if (move.first.id == "strengthsap" && activeTrackerPokemon.currentHpPercent < 0.5
&& activeTrackerPokemon.species!!.baseStats.getOrDefault(Stats.ATTACK, 0) > 80) {
&& activeTrackerPokemon.species!!.baseStats.getOrDefault(Stats.ATTACK, 0) > 80) {
return chooseMove(move.first, activeBattlePokemon)
}
}
@@ -391,10 +382,10 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Belly Drum
for (move in availableMoves) {
if (move.first.id == "bellydrum"
&& (activeTrackerPokemon.currentHpPercent > 0.6
&& activeTrackerPokemon.pokemon!!.heldItem().item == CobblemonItems.SITRUS_BERRY
|| activeTrackerPokemon.currentHpPercent > 0.8)
&& activeTrackerPokemon.boosts.getOrDefault(Stats.ATTACK, 0) < 1) {
&& (activeTrackerPokemon.currentHpPercent > 0.6
&& activeTrackerPokemon.pokemon!!.heldItem().item == CobblemonItems.SITRUS_BERRY
|| activeTrackerPokemon.currentHpPercent > 0.8)
&& activeTrackerPokemon.boosts.getOrDefault(Stats.ATTACK, 0) < 1) {
return chooseMove(move.first, activeBattlePokemon)
}
}
@@ -404,8 +395,8 @@ class StrongBattleAI(skill: Int) : BattleAI {
for (move in availableMoves) {
AIUtility.weatherSetupMoves[move.first.id]?.let { requiredWeather ->
if (activeTracker.currentWeather != requiredWeather.lowercase() &&
!(activeTracker.currentWeather == "primordialsea" && requiredWeather == "raindance") &&
!(activeTracker.currentWeather == "desolateland" && requiredWeather == "sunnyday")) {
!(activeTracker.currentWeather == "primordialsea" && requiredWeather == "raindance") &&
!(activeTracker.currentWeather == "desolateland" && requiredWeather == "sunnyday")) {
return chooseMove(move.first, activeBattlePokemon)
}
}
@@ -431,8 +422,8 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Stall out side conditions
if ((activeTracker.opponentSide.screenCondition != null || activeTracker.opponentSide.tailwindCondition != null) &&
(activeTracker.alliedSide.screenCondition == null || activeTracker.alliedSide.tailwindCondition == null) ||
opponents.any { it.currentStatus == null } &&
activeTrackerPokemon.protectCount == 0 && opponents.none { it.currentAbility?.name == "unseenfist"}) {
opponents.any { it.currentStatus == null } &&
activeTrackerPokemon.protectCount == 0 && opponents.none { it.currentAbility?.name == "unseenfist"}) {
activeTrackerPokemon.protectCount = 3
return chooseMove(move.first, activeBattlePokemon)
}
@@ -460,10 +451,10 @@ class StrongBattleAI(skill: Int) : BattleAI {
activeTrackerPokemon.firstTurn = false
val move = availableMoves
.filter { it.first.canBeUsed() }
.filter { it.first.mustBeUsed() || it.first.target.targetList(activeBattlePokemon)?.isEmpty() != true }
.randomOrNull()
?: return MoveActionResponse("struggle")
.filter { it.first.canBeUsed() }
.filter { it.first.mustBeUsed() || it.first.target.targetList(activeBattlePokemon)?.isEmpty() != true }
.randomOrNull()
?: return MoveActionResponse("struggle")
return chooseMove(move.first, activeBattlePokemon)
}
@@ -520,6 +511,10 @@ class StrongBattleAI(skill: Int) : BattleAI {
return false
val activeTrackerPokemon = activeTracker.alliedSide.activePokemon.first { it.id == activeBattlePokemon.battlePokemon!!.uuid }
// Don't switch out if we just switched in
if (activeTrackerPokemon.firstTurn)
return false
val actorTracker = activeTracker.alliedSide.actors.first { activeTrackerPokemon in it.activePokemon }
val availableSwitches = actorTracker.party.filter { it.currentHp!! > 0 }
val currentAbility = activeTrackerPokemon.pokemon!!.ability
@@ -532,7 +527,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Only switch if a switch-in is significantly better than the current Pokémon
val currentScore = estimateMatchup(activeBattlePokemon, side, battle)
val bestSwitchScore = availableSwitches.maxOfOrNull { estimateMatchup(activeBattlePokemon, side, battle, it) } ?: currentScore
val improvementThreshold = currentScore.absoluteValue * 0.25 + 2
val improvementThreshold = currentScore.absoluteValue * 0.5 + 3
if (bestSwitchScore <= currentScore + improvementThreshold) {
return false
}
@@ -567,7 +562,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
if (availableSwitches.isEmpty() || availableSwitches.none { estimateMatchup(activeBattlePokemon, side, battle, it) > 0 })
return false
// ...and a 'good' reason to switch out
if (opponentActiveTracker.any { it.boosts.getOrDefault(Stats.ACCURACY, 0) <= accuracySwitchThreshold } ||
@@ -995,6 +990,4 @@ class StrongBattleAI(skill: Int) : BattleAI {
return MoveActionResponse(move.id, chosenTarget.getPNX())
}
}
}
}
@@ -132,6 +132,7 @@ class TrackerActor(val uuid: UUID) {
actor.activePokemon.filter { newActive -> activePokemon.none { it.id == newActive.battlePokemon?.uuid } }.forEach {
val active = party.firstOrNull { trackMon -> trackMon.id == it.battlePokemon?.uuid } ?: return@forEach
party.remove(active)
active.firstTurn = true // Reset firstTurn when switching in
activePokemon.add(active)
}
val swappedOut = activePokemon.filter { trackMon -> actor.activePokemon.none { it.battlePokemon?.uuid == trackMon.id } }
@@ -145,6 +146,7 @@ class TrackerActor(val uuid: UUID) {
actor.activePokemon.filter { newActive -> activePokemon.none { it.id == newActive.battlePokemon?.uuid } }.forEach {
val active = party.firstOrNull { trackMon -> trackMon.id == it.battlePokemon?.uuid } ?: return@forEach
party.remove(active)
active.firstTurn = true // Reset firstTurn when switching in
activePokemon.add(active)
}
val swappedOut = activePokemon.filter { trackMon -> actor.activePokemon.none { it.battlePokemon?.uuid == trackMon.id } }
@@ -51,6 +51,7 @@ class FaintInstruction(battle: PokemonBattle, val message: BattleMessage) : Inte
preamble.thenAccept {
faintingPokemon.effectedPokemon.currentHealth = 0
faintingPokemon.faintedAt = battle.faintCounter++
faintingPokemon.sendUpdate()
CobblemonEvents.BATTLE_FAINTED.post(BattleFaintedEvent(battle, pokemon, context, pnx))
battle.getActorAndActiveSlotFromPNX(pnx).second.battlePokemon = null
@@ -110,6 +110,9 @@ open class BattlePokemon(
/** A set of all the BattlePokemon that they faced during the battle (for exp purposes) */
val facedOpponents = mutableSetOf<BattlePokemon>()
/** The faint sequence number within the battle, or null if this pokemon has not fainted. */
var faintedAt: Int? = null
/**
* The [HeldItemManager] backing this [BattlePokemon].
*/
@@ -133,7 +133,10 @@ class ApricornBlock(settings: Properties, val apricorn: Apricorn) : HorizontalDi
override fun isBonemealSuccess(world: Level, random: RandomSource, pos: BlockPos, state: BlockState) = true
override fun performBonemeal(world: ServerLevel, random: RandomSource, pos: BlockPos, state: BlockState) {
world.setBlock(pos, state.setValue(AGE, state.getValue(AGE) + 1), 2)
val curAge = state.getValue(AGE)
val newAge = curAge + 1
if (newAge > MAX_AGE) return
world.setBlock(pos, state.setValue(AGE, newAge), UPDATE_CLIENTS)
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
@@ -186,17 +189,17 @@ class ApricornBlock(settings: Properties, val apricorn: Apricorn) : HorizontalDi
* This uses [Block.dropResources] to handle the drops.
* It will also reset the [BlockState] of this block at the given location to the start of growth.
*
* @param world The [World] the apricorn is in.
* @param world The [Level] the apricorn is in.
* @param state The [BlockState] of the apricorn.
* @param pos The [BlockPos] of the apricorn.
* @return The [BlockState] after harvest.
*/
fun harvest(world: Level, state: BlockState, pos: BlockPos): BlockState {
// Uses loot tables, to change the drops use 'data/cobblemon/loot_tables/blocks/<color>_apricorn.json'
Block.dropResources(state, world, pos)
dropResources(state, world, pos)
// Don't use default as we want to keep the facing
val resetState = state.setValue(AGE, MIN_AGE)
world.setBlock(pos, resetState, Block.UPDATE_CLIENTS)
world.setBlock(pos, resetState, UPDATE_CLIENTS)
return resetState
}
@@ -81,7 +81,32 @@ class BerryBlock(private val berryIdentifier: ResourceLocation, settings: Proper
override fun isBonemealSuccess(world: Level, random: RandomSource, pos: BlockPos, state: BlockState) = !this.isMaxAge(state)
override fun <T : BlockEntity> getTicker(world: Level, blockState: BlockState, blockWithEntityType: BlockEntityType<T>): BlockEntityTicker<T>? = createTickerHelper(blockWithEntityType, CobblemonBlockEntities.BERRY, BerryBlockEntity.TICKER)
// Use Random Tick instead of BlockEntityTicker for performance optimization
override fun <T : BlockEntity> getTicker(world: Level, blockState: BlockState, blockWithEntityType: BlockEntityType<T>): BlockEntityTicker<T>? = null
// Enable Random Tick: only immature and non-rooted berries need to tick
override fun isRandomlyTicking(state: BlockState): Boolean {
return state.getValue(AGE) < FRUIT_AGE && !state.getValue(IS_ROOTED)
}
// Random Tick handler: uses time compensation to ensure correct growth speed
@Deprecated("Deprecated in Java")
override fun randomTick(state: BlockState, world: ServerLevel, pos: BlockPos, random: RandomSource) {
val blockEntity = world.getBlockEntity(pos) as? BerryBlockEntity ?: return
// processTick calculates elapsed time and updates timer, returns whether growth is needed
if (blockEntity.processTick(world, pos, state) && !world.getBlockTicks().hasScheduledTick(pos, this)) {
// Schedule growth for next tick to avoid tick scheduler conflicts during randomTick
world.scheduleTick(pos, this, 1)
}
}
// Scheduled tick handler: performs the actual growth
@Deprecated("Deprecated in Java")
override fun tick(state: BlockState, world: ServerLevel, pos: BlockPos, random: RandomSource) {
if (state.getValue(IS_ROOTED)) return
growHelper(world, random, pos, state)
}
init {
registerDefaultState(stateDefinition.any()
@@ -151,12 +151,14 @@ class CampfireBlock(settings: Properties, val isSoul: Boolean) : BaseEntityBlock
override fun placeLiquid(level: LevelAccessor, pos: BlockPos, state: BlockState, fluidState: FluidState): Boolean {
val blockEntity = level.getBlockEntity(pos)
if (fluidState.type === Fluids.WATER && blockEntity is CampfireBlockEntity) {
if (!level.isClientSide) {
removePotItem(blockEntity, state, level as Level, pos, null, true)
level.playSoundServer(pos.center, SoundEvents.GENERIC_EXTINGUISH_FIRE, SoundSource.BLOCKS, 1.0F, 1.0F)
}
if (level is Level) {
if (!level.isClientSide) {
removePotItem(blockEntity, state, level, pos, null, true)
level.playSoundServer(pos.center, SoundEvents.GENERIC_EXTINGUISH_FIRE, SoundSource.BLOCKS, 1.0F, 1.0F)
}
if (level.isClientSide) for (i in 0..19) MCCampfireBlock.makeParticles(level as Level, pos, false, true)
if (level.isClientSide) for (i in 0..19) MCCampfireBlock.makeParticles(level, pos, false, true)
}
level.scheduleTick(pos, fluidState.type, fluidState.type.getTickDelay(level))
return true
@@ -65,6 +65,9 @@ class BerryBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Cobblemon
private val growthPoints = arrayListOf<ResourceLocation>()
var mulchVariant = MulchVariant.NONE
// For Random Tick compensation: records the game time of last tick
var lastTickTime: Long = 0L
/**
* The idea behind the growth point sequence is it's a 16-long string of
* hexadecimal numbers. They represent the order of the growth points that
@@ -267,6 +270,7 @@ class BerryBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Cobblemon
} catch (ignored: ResourceLocationException) {}
}
this.mulchDuration = nbt.getInt(MULCH_DURATION)
this.lastTickTime = nbt.getLong(LAST_TICK_TIME)
this.wasLoading = false
if (nbt.contains(GROWTH_POINTS_SEQUENCE)) {
growthPointSequence = nbt.getString(GROWTH_POINTS_SEQUENCE)
@@ -287,6 +291,7 @@ class BerryBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Cobblemon
nbt.putInt(MULCH_DURATION, mulchDuration)
nbt.putString(GROWTH_POINTS_SEQUENCE, growthPointSequence)
nbt.putString(MULCH_VARIANT, mulchVariant.toString())
nbt.putLong(LAST_TICK_TIME, lastTickTime)
}
override fun setChanged() {
@@ -346,24 +351,39 @@ class BerryBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Cobblemon
return
}
/**
* Process Random Tick compensation logic.
* Calculates elapsed time since last tick and batch-decrements stageTimer.
*/
fun processTick(world: ServerLevel, pos: BlockPos, state: BlockState): Boolean {
if (state.getValue(BerryBlock.IS_ROOTED)) return false
val currentTime = world.gameTime
val elapsed = if (lastTickTime > 0L) {
(currentTime - lastTickTime).toInt().coerceAtLeast(1)
} else {
1 // First tick, only decrement by 1
}
lastTickTime = currentTime
// Batch decrement the timer
if (stageTimer > 0) {
stageTimer = (stageTimer - elapsed).coerceAtLeast(0)
}
// Return whether growth is needed
return stageTimer <= 0
}
//Using a similar approach to ComputerCraft, implement this client side
interface RenderState : AutoCloseable {
var needsRebuild: Boolean
}
companion object {
internal val TICKER = BlockEntityTicker<BerryBlockEntity> { world, pos, state, blockEntity ->
if (world.isClientSide) return@BlockEntityTicker
if (state.getValue(BerryBlock.IS_ROOTED)) return@BlockEntityTicker
if (blockEntity.stageTimer >= 0) {
blockEntity.stageTimer--
}
if (blockEntity.stageTimer <= 0) {
(state.block as BerryBlock).growHelper(world as ServerLevel, world.random, pos, state)
}
}
//private const val LIFE_CYCLES = "life_cycles"
private const val GROWTH_POINTS = "GrowthPoints"
private const val LAST_TICK_TIME = "LastTickTime"
private const val GROWTH_POINTS_SEQUENCE = "GrowthPointsSequence"
private const val GROWTH_TIMER = "GrowthTimer"
private const val STAGE_TIMER = "StageTimer"
@@ -29,6 +29,7 @@ import com.cobblemon.mod.common.api.spawning.influence.SpawnBaitInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.spawner.FixedAreaSpawner
import com.cobblemon.mod.common.api.spawning.spawner.PokeSnackSpawnerFactory
import com.cobblemon.mod.common.block.PokeSnackBlock
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.item.components.BaitEffectsComponent
@@ -62,41 +63,22 @@ open class PokeSnackBlockEntity(pos: BlockPos, state: BlockState) :
}
val spawner: FixedAreaSpawner by lazy {
FixedAreaSpawner(
name = "poke_snack_spawner_${level?.dimension()?.location()}}_$blockPos",
spawnPool = CobblemonSpawnPools.WORLD_SPAWN_POOL,
world = level as ServerLevel,
position = blockPos,
val server = level as ServerLevel
val baitEffects = getBaitEffects()
val ctx = PokeSnackSpawnerFactory.Context(
world = server,
pos = blockPos,
horizontalRadius = RADIUS,
verticalRadius = RADIUS,
maxPokemonPerChunk = Cobblemon.config.pokeSnackPokemonPerChunk
).also {
it.influences.add(this)
maxPokemonPerChunk = Cobblemon.config.pokeSnackPokemonPerChunk,
name = "poke_snack_spawner_${server.dimension().location()}_$blockPos",
baitEffects = baitEffects
)
val baitEffects = getBaitEffects()
val stackedLureTier = baitEffects
.filter { it.type == SpawnBait.Effects.RARITY_BUCKET }
.sumOf { it.value }
.toInt()
if (stackedLureTier > 0) {
it.influences += BucketNormalizingInfluence(
tier = stackedLureTier,
gradient = 0.2F,
firstTier = 1.2F
)
}
it.influences += BucketMultiplyingInfluence(
mapOf(
"uncommon" to 2.25f,
"rare" to 5.5f,
"ultra-rare" to 5.5f,
)
)
it.influences += SpawnBaitInfluence(effects = baitEffects)
PokeSnackSpawnerFactory.create(ctx).also { spawner ->
spawner.influences.add(this)
}
}
@@ -128,11 +128,11 @@ class FossilMultiblockStructure (
if(pokemon != null) {
pokemon.caughtBall = ballType
player.party().add(pokemon)
this.fossilState.growthState = "Taken"
player.playSound(CobblemonSounds.FOSSIL_MACHINE_RETRIEVE_POKEMON, 1.0F, 1.0F)
CobblemonEvents.FOSSIL_REVIVED.post(FossilRevivedEvent(pokemon, player))
CobblemonCriteria.RESURRECT_POKEMON.trigger(player, pokemon)
player.party().add(pokemon)
}
// Turn the monitor off
@@ -27,6 +27,7 @@ import com.cobblemon.mod.common.client.gui.PartyOverlayDataControl
import com.cobblemon.mod.common.client.gui.RideControlsOverlay
import com.cobblemon.mod.common.client.gui.battle.BattleOverlay
import com.cobblemon.mod.common.client.gui.cookingpot.CookingPotScreen
import com.cobblemon.mod.common.client.gui.party.PartyTutorialToasts
import com.cobblemon.mod.common.client.particle.BedrockParticleOptionsRepository
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.client.render.DeferredRenderer
@@ -128,6 +129,8 @@ object CobblemonClient {
battleOverlay.onLogout()
ClientTaskTracker.clear()
checkedStarterScreen = false
overlay.resetAttachedToast()
PartyTutorialToasts.reset()
// CobblemonDataProvider.canReload = true
DeferredRenderer.clearAll()
ClientPlayerIcon.clear()
@@ -165,13 +168,12 @@ object CobblemonClient {
PlatformEvents.CLIENT_TICK_POST.subscribe { event ->
val player = event.client.player
if (player != null) {
var selectedItem = player.inventory.getItem(player.inventory.selected)
val selectedItem = player.inventory.getItem(player.inventory.selected)
if (pokedexUsageContext.scanningGuiOpen &&
!(selectedItem.`is`(CobblemonItemTags.POKEDEX)) &&
!(player.offhandItem.`is`(CobblemonItemTags.POKEDEX) &&
player.isUsingItem == true &&
player.usedItemHand == InteractionHand.OFF_HAND
)
player.isUsingItem &&
player.usedItemHand == InteractionHand.OFF_HAND)
) {
// Stop using Pokédex in main hand if player switches to a different slot in hotbar
pokedexUsageContext.stopUsing(PokedexUsageContext.OPEN_SCANNER_BUFFER_TICKS + 1)
@@ -216,7 +218,7 @@ object CobblemonClient {
}
fun registerColors() {
this.implementation.registerBlockColors(BlockColor { blockState, view, blockPos, tintIndex ->
this.implementation.registerBlockColors(BlockColor { _, view, blockPos, _ ->
blockPos?.let { pos ->
view?.getBlockEntity(pos)?.let { blockEntity ->
if (blockEntity is TintBlockEntity) return@BlockColor blockEntity.getTint()
@@ -12,16 +12,17 @@ import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.gui.drawPosablePortrait
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.darkGray
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.battle.BattleGUI
import com.cobblemon.mod.common.client.gui.party.PartyTutorialToasts
import com.cobblemon.mod.common.client.gui.toast.CobblemonToast
import com.cobblemon.mod.common.client.keybind.boundKey
import com.cobblemon.mod.common.client.keybind.keybinds.HidePartyBinding
import com.cobblemon.mod.common.client.keybind.keybinds.SummaryBinding
import com.cobblemon.mod.common.client.keybind.keybinds.*
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.getDepletableRedGreen
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
@@ -150,6 +151,9 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
val portraitFrameOffsetY = 2
val selectedSlot = CobblemonClient.storage.selectedSlot
val partySize = party.slots.count { it != null }
PartyTutorialToasts.onOverlayRender(partySize)
party.forEachIndexed { index, pokemon ->
if (pokemon != null) {
val selectedOffsetX = if (selectedSlot == index) 6 else 0
@@ -226,7 +230,7 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
var levelScrollOffset = 0F
var levelScrollPositionOffset = 0F
var levelScrollSize = 17F
if (expGainedData != null && expGainedData.oldLevel != null) {
if (expGainedData?.oldLevel != null) {
val ticksWithDelta = (expGainedData.ticks + partialDeltaTicks) - (PartyOverlayDataControl.BAR_UPDATE_BEFORE_TIME + PartyOverlayDataControl.BAR_FLASH_TIME)
val transition = ticksWithDelta / PartyOverlayDataControl.LEVEL_UP_PORTRAIT_TIME
if ((0F..<1F).contains(transition)) {
@@ -400,10 +404,48 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
blue = expBlue
)
//Handling EXP Text
var expGained = 0
var expTextTransparency = 0F
var expTextOffset = 0F
if (expGainedData != null) {
expGained = expGainedData.expGained
val ticksWithDelta = expGainedData.ticks + partialDeltaTicks
if (ticksWithDelta <= PartyOverlayDataControl.EXP_POPUP_TIME.fadeIn) {
val transition = ticksWithDelta / PartyOverlayDataControl.EXP_POPUP_TIME.fadeIn
val step = (floor(transition * 3)) + 1
expTextTransparency = step * 0.25F
expTextOffset = 4 - step
} else if (ticksWithDelta <= PartyOverlayDataControl.EXP_POPUP_TIME.fadeIn + PartyOverlayDataControl.EXP_POPUP_TIME.hold) {
expTextTransparency = 1F
expTextOffset = 0F
} else {
val adjustedTicks = ticksWithDelta - (PartyOverlayDataControl.EXP_POPUP_TIME.fadeIn + PartyOverlayDataControl.EXP_POPUP_TIME.hold)
val transition = adjustedTicks / PartyOverlayDataControl.EXP_POPUP_TIME.fadeOut
val step = (floor(transition * 3)) + 1
expTextTransparency = 1F - (step * 0.25F)
expTextOffset = -step
}
}
if (expTextTransparency > 0) {
drawScaledText(
context = context,
text = lang("ui.exp.number", expGained.toString()).bold(),
x = panelX + selectedOffsetX + 57F,
y = indexY + 17F + expTextOffset,
scale = SCALE,
shadow = true,
opacity = expTextTransparency
)
}
//Handling Popups
var ticksForMove = -1F
var ticksForEvo = -1F
//Handling Popups
if (expGainedData != null) {
val ticksWithDelta = expGainedData.ticks + partialDeltaTicks
val ticks = ticksWithDelta - (PartyOverlayDataControl.BAR_UPDATE_BEFORE_TIME + PartyOverlayDataControl.BAR_FLASH_TIME)
@@ -434,7 +476,7 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
return 0F to 1F
} else {
val adjustedPopupTicks = ticks - (PartyOverlayDataControl.POPUP_TIME.fadeIn + PartyOverlayDataControl.POPUP_TIME.hold)
val transition = adjustedPopupTicks / PartyOverlayDataControl.POPUP_TIME.fadeIn
val transition = adjustedPopupTicks / PartyOverlayDataControl.POPUP_TIME.fadeOut
val step = (floor(transition * 3)) + 1
return (-step) to (1F - (step * 0.25F))
}
@@ -448,7 +490,7 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
blitk(
matrixStack = matrices,
texture = newEvoPopup,
x = (panelX + selectedOffsetX + 56.5F) / SCALE,
x = (panelX + selectedOffsetX + (if (expTextTransparency > 0 && movePopupTransparency > 0) 78F else 56.5F)) / SCALE,
y = (indexY + 4 + evoPopupYOffset) / SCALE,
width = 37,
height = 20,
@@ -461,7 +503,7 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
matrixStack = matrices,
texture = newMovesPopup,
x = (panelX + selectedOffsetX + 56.5F) / SCALE,
y = (indexY + 17 + movePopupYOffset) / SCALE,
y = (indexY + (if (expTextTransparency > 0) 4 else 17) + movePopupYOffset) / SCALE,
width = 37,
height = 20,
scale = SCALE,
@@ -19,10 +19,10 @@ object PartyOverlayDataControl {
const val BAR_UPDATE_BEFORE_TIME = 15
const val BAR_UPDATE_AFTER_TIME = 15
const val BAR_FLASH_TIME = 2
const val BAR_UPDATE_NO_LEVEL_TIME = BAR_UPDATE_BEFORE_TIME + BAR_UPDATE_AFTER_TIME + BAR_FLASH_TIME
val POPUP_TIME = FadeTimings(3, 40, 3)
const val LEVEL_UP_PORTRAIT_TIME = 10
val BAR_UPDATE_NO_LEVEL_TIME = BAR_UPDATE_BEFORE_TIME + BAR_UPDATE_AFTER_TIME + BAR_FLASH_TIME
val EXP_POPUP_TIME = FadeTimings(3, BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME + POPUP_TIME.total() - 6, 3)
private enum class RenderTarget {
NONE,
@@ -35,21 +35,19 @@ object PartyOverlayDataControl {
private var barFillupSound: SoundInstance? = null
private var levelUpJingleSound: SoundInstance? = null
private var levelUpJingleSoundType: SoundEvent? = null
fun pokemonGainedExp(pokemonUuid : UUID, oldLevel: Int?, expGained: Int, countOfMovesLearned: Int, countOfEvosUnlocked: Int = 0) {
var time = BAR_UPDATE_NO_LEVEL_TIME
if (countOfMovesLearned > 0 || countOfEvosUnlocked > 0) {
time = BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME + POPUP_TIME.total()
}
val data = overlayData[pokemonUuid]
val newData = ExpGainedData(oldLevel, expGained, countOfMovesLearned, countOfEvosUnlocked).also { it.ticksMax = time }
val newData = ExpGainedData(oldLevel, expGained, countOfMovesLearned, countOfEvosUnlocked).also { it.ticksMax = BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME + POPUP_TIME.total() }
if (data != null) {
if (data.movesGainedData != null) {
if (data.movesGainedData != null) { //Absorb Data
newData.countOfMovesLearned += data.movesGainedData!!.movesCount
data.movesGainedData = null
}
if (data.evoGainedData != null) {
if (data.evoGainedData != null) { //Absorb Data
newData.countOfEvosUnlocked += data.evoGainedData!!.evolutionCount
data.evoGainedData = null
}
val expData = data.expGainedData
@@ -72,8 +70,7 @@ object PartyOverlayDataControl {
val data = overlayData[pokemonUuid]
val newData = EvolutionGainedData(evoCount).also { it.ticksMax = POPUP_TIME.total() }
if (data != null) {
if (data.expGainedData != null && data.expGainedData!!.ticks < BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME) {
data.expGainedData!!.ticksMax = BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME + POPUP_TIME.total()
if (data.expGainedData != null) {
data.expGainedData!!.countOfEvosUnlocked += evoCount
return
}
@@ -93,8 +90,7 @@ object PartyOverlayDataControl {
val data = overlayData[pokemonUuid]
val newData = MovesGainedData(movesCount).also { it.ticksMax = POPUP_TIME.total() }
if (data != null) {
if (data.expGainedData != null && data.expGainedData!!.ticks < BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME) {
data.expGainedData!!.ticksMax = BAR_UPDATE_BEFORE_TIME + BAR_FLASH_TIME + POPUP_TIME.total()
if (data.expGainedData != null) {
data.expGainedData!!.countOfMovesLearned += movesCount
return
}
@@ -137,13 +133,28 @@ object PartyOverlayDataControl {
fun tick(paused: Boolean) {
val deletion = mutableListOf<UUID>()
if (paused) return
if (levelUpJingleSound != null && !Minecraft.getInstance().soundManager.isActive(levelUpJingleSound!!)) {
levelUpJingleSound = null
levelUpJingleSoundType = null
}
overlayData.forEach { (id, data) ->
if (data.expGainedData != null) { // We update this in order and render these in order, and as such we don't tick others
val expGainedData = data.expGainedData!!
expGainedData.ticks += 1
if (expGainedData.ticks == BAR_UPDATE_BEFORE_TIME && expGainedData.oldLevel != null) {
stopSound(levelUpJingleSound)
levelUpJingleSound = playSound(CobblemonSounds.LEVELUP)
if (expGainedData.countOfEvosUnlocked > 0) {
stopSound(levelUpJingleSound)
levelUpJingleSound = playSound(CobblemonSounds.EVOLUTION_NOTIFICATION)
levelUpJingleSoundType = CobblemonSounds.EVOLUTION_NOTIFICATION
} else {
if (levelUpJingleSoundType != CobblemonSounds.EVOLUTION_NOTIFICATION) {
stopSound(levelUpJingleSound)
levelUpJingleSound = playSound(CobblemonSounds.LEVELUP)
levelUpJingleSoundType = CobblemonSounds.LEVELUP
}
}
}
if (expGainedData.ticks >= expGainedData.ticksMax) {
data.expGainedData = null
@@ -166,6 +177,7 @@ object PartyOverlayDataControl {
}
if (getCurrentRenderTarget(id) == RenderTarget.NONE) deletion.add(id)
}
deletion.forEach {
overlayData.remove(it)
}
@@ -216,7 +228,7 @@ object PartyOverlayDataControl {
//SOUNDS
private fun playSound(soundEvent: SoundEvent): SoundInstance? {
if (!PartyOverlay.canRender()) return null
val soundInstance = SimpleSoundInstance.forUI(soundEvent, 1F)
val soundInstance = SimpleSoundInstance.forUI(soundEvent, 1F, 1F)
Minecraft.getInstance().soundManager.play(soundInstance)
return soundInstance
}
@@ -47,6 +47,7 @@ fun drawProfilePokemon(
a: Float = 1F,
headYaw: Float = 0f,
headPitch: Float = 0f,
blockLight: Int = 13
) = drawProfilePokemon(
species = renderablePokemon.species.resourceIdentifier,
matrixStack = matrixStack,
@@ -63,6 +64,7 @@ fun drawProfilePokemon(
a = a,
headYaw = headYaw,
headPitch = headPitch,
blockLight = blockLight
)
fun drawProfilePokemon(
@@ -82,6 +84,7 @@ fun drawProfilePokemon(
a: Float = 1F,
headYaw: Float = 0f,
headPitch: Float = 0f,
blockLight: Int = 13
) {
RenderSystem.applyModelViewMatrix()
matrixStack.scale(scale, scale, -scale)
@@ -138,7 +141,7 @@ fun drawProfilePokemon(
val light1 = Vector3f(-1F, 1F, 1.0F)
val light2 = Vector3f(1.3F, -1F, 1.0F)
RenderSystem.setShaderLights(light1, light2)
val packedLight = LightTexture.pack(11, 7)
val packedLight = LightTexture.pack(blockLight, 0)
val colour = toHex(r, g, b, a)
model.withLayerContext(bufferSource, state, VaryingModelRepository.getLayers(species, state)) {
@@ -0,0 +1,153 @@
/*
* 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
import com.cobblemon.mod.common.api.gui.blitk
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.gui.components.events.GuiEventListener
import net.minecraft.client.gui.narration.NarrationElementOutput
import net.minecraft.network.chat.CommonComponents
import net.minecraft.resources.ResourceLocation
import kotlin.math.floor
open class VariableObjectSelectionList(x: Int, y: Int, width: Int, height: Int): AbstractWidget(x, y, width, height, CommonComponents.EMPTY) {
val children = mutableListOf<Entry>()
var listHeight = 0
var scrollAmount = 0.0
fun updateScroll(offset: Double) {
setScroll(scrollAmount + offset)
}
fun getMaxScroll(): Int {
return (listHeight - height).coerceAtLeast(0)
}
fun setScroll(scroll: Double) {
scrollAmount = scroll.coerceIn(0.0, getMaxScroll().toDouble())
}
fun removeEntry(index: Int) {
listHeight -= children[index].height
children.removeAt(index)
updatePositions()
}
fun removeEntry(entry: Entry) {
listHeight -= entry.height
children.remove(entry)
updatePositions()
}
fun removeLastEntry() {
listHeight -= children.last().height
children.removeLast()
}
fun updatePositions() {
var stackHeight = 0
children.forEach {
it.scrolledY = stackHeight
stackHeight += it.height
}
updateScroll(0.0)
}
fun addEntry(entry: Entry) {
children.add(entry)
entry.scrolledY = listHeight
listHeight += entry.height
}
override fun renderWidget(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
var stackHeight = -scrollAmount.toInt()
guiGraphics.enableScissor(x, y, x + width, y + height)
children.forEachIndexed { index, entry ->
if (stackHeight in 1..<height || stackHeight + entry.height in 1..<height) {
entry.render(guiGraphics, index, x, y + stackHeight, mouseX, mouseY, partialTick)
}
stackHeight += entry.height
}
guiGraphics.disableScissor()
}
fun getEntryAtPosition(mouseX: Int, mouseY: Int): Entry? {
if (mouseX in x..(x + width) && mouseY in y..(y + height)) {
var stackHeight = -scrollAmount
children.forEach {
if (mouseY.toDouble() in (y + stackHeight)..(y + stackHeight + it.height)) {
return it
}
stackHeight += it.height
}
}
return null
}
fun ensureVisible(index: Int) {
val entry = children[index]
if (entry.scrolledY - scrollAmount < 0) {
setScroll(entry.scrolledY.toDouble())
} else if (entry.scrolledY + entry.height > height) {
setScroll(entry.scrolledY.toDouble() + entry.height)
}
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
updateScroll(-verticalAmount * (listHeight / children.size / 2))
return true
}
override fun mouseClicked(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
val entry = getEntryAtPosition(pMouseX.toInt(), pMouseY.toInt())
if (entry != null) {
return entry.mouseClicked(pMouseX, pMouseY, pButton)
}
return false
}
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
return children.any {
it.mouseReleased(pMouseX, pMouseY, pButton)
} || super.mouseReleased(pMouseX, pMouseY, pButton)
}
fun getRowTop(entry: Entry): Int {
var stackHeight = -scrollAmount.toInt()
children.forEach {
if (it == entry) {
return y + stackHeight
}
stackHeight += it.height
}
return y
}
override fun defaultButtonNarrationText(pNarrationElementOutput: NarrationElementOutput) {}
override fun updateWidgetNarration(narrationElementOutput: NarrationElementOutput) {}
abstract inner class Entry(var height: Int): GuiEventListener {
var scrolledY: Int = 0
private var widgetFocused = false
override fun setFocused(focused: Boolean) {
this.widgetFocused = focused
}
override fun isFocused(): Boolean = widgetFocused
abstract fun render(guiGraphics: GuiGraphics, index: Int, x: Int, y: Int, mouseX: Int, mouseY: Int, partialTick: Float)
}
inner class EmptySeparator(height: Int): Entry(height) {
override fun render(guiGraphics: GuiGraphics, index: Int, x: Int, y: Int, mouseX: Int, mouseY: Int, partialTick: Float) {}
}
}
@@ -0,0 +1,100 @@
/*
* 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
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.gui.narration.NarrationElementOutput
import net.minecraft.client.sounds.SoundManager
import net.minecraft.network.chat.CommonComponents
import net.minecraft.resources.ResourceLocation
class VariableObjectSelectionScrollbar(var parent: VariableObjectSelectionList, x: Int, y: Int, width: Int, height: Int): AbstractWidget(x, y, width, height, CommonComponents.EMPTY) {
companion object {
private val scrollerResource: ResourceLocation = ResourceLocation("minecraft", "textures/gui/sprites/widget/scroller.png")
private val scrollerBackgroundResource: ResourceLocation = ResourceLocation("minecraft", "textures/gui/sprites/widget/scroller_background.png")
private const val SCROLLER_HEIGHT = 32
}
private var scrolling = false
private var scrollHoldOffset = 0.0
fun scrollbarVisible(): Boolean = parent.getMaxScroll() > 0
override fun renderWidget(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
if (scrollbarVisible()) {
val scrollPosition = (parent.scrollAmount / parent.getMaxScroll()) * (height - SCROLLER_HEIGHT)
blitk(
matrixStack = guiGraphics.pose(),
texture = scrollerBackgroundResource,
x = x,
y = y,
width = width,
height = height,
)
blitk(
matrixStack = guiGraphics.pose(),
texture = scrollerResource,
x = x,
y = y + scrollPosition,
width = width,
height = SCROLLER_HEIGHT,
textureWidth = 6,
textureHeight = SCROLLER_HEIGHT
)
}
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, scrollX: Double, scrollY: Double): Boolean {
return parent.mouseScrolled(mouseX, mouseY, scrollX, scrollY)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (mouseX.toInt() in x..(x + width) && mouseY.toInt() in y..(y + height)) {
if (button == 0) {
var scrollPosition = (parent.scrollAmount / parent.getMaxScroll()) * (height - SCROLLER_HEIGHT)
if (mouseY !in (y + scrollPosition)..(y + scrollPosition + SCROLLER_HEIGHT)) {
val scrollRatio =
((mouseY - y - SCROLLER_HEIGHT / 2) / (height - SCROLLER_HEIGHT)).coerceIn(0.0, 1.0)
parent.setScroll(scrollRatio * parent.getMaxScroll())
scrollPosition = (parent.scrollAmount / parent.getMaxScroll()) * (height - SCROLLER_HEIGHT)
}
scrollHoldOffset = mouseY - y - scrollPosition
scrolling = true
return true
}
}
return false
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, dragX: Double, dragY: Double): Boolean {
if (button == 0 && scrolling) {
val scrollRatio = ((mouseY - y - scrollHoldOffset) / (height - SCROLLER_HEIGHT)).coerceIn(0.0,1.0)
parent.setScroll(scrollRatio * parent.getMaxScroll())
return true
}
return false
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (button == 0 && scrolling) {
scrolling = false
return true
}
return false
}
override fun playDownSound(handler: SoundManager) {}
override fun updateWidgetNarration(narrationElementOutput: NarrationElementOutput) {}
}
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.client.gui.battle.subscreen
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
import com.cobblemon.mod.common.battles.FleeAttemptActionResponse
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.battle.SingleActionRequest
import com.cobblemon.mod.common.client.gui.battle.BattleGUI
@@ -65,7 +66,7 @@ class BattleGeneralActionSelection(
addOption(rank++, battleLang("ui.run"), BattleGUI.runResource) {
CobblemonClient.battle?.minimised = true
Minecraft.getInstance().player?.displayClientMessage(battleLang("run_prompt"), false)
battleGUI.selectAction(request, FleeAttemptActionResponse())
playDownSound(Minecraft.getInstance().soundManager)
}
} else {
@@ -0,0 +1,99 @@
/*
* 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.party
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.api.text.darkGray
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.gui.toast.CobblemonToast
import com.cobblemon.mod.common.client.keybind.boundKey
import com.cobblemon.mod.common.client.keybind.keybinds.DownShiftPartyBinding
import com.cobblemon.mod.common.client.keybind.keybinds.PartySendBinding
import com.cobblemon.mod.common.client.keybind.keybinds.UpShiftPartyBinding
import com.cobblemon.mod.common.net.messages.server.storage.party.PartyTutorialPacket
import com.cobblemon.mod.common.util.lang
import com.mojang.blaze3d.platform.InputConstants
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.components.toasts.Toast
import net.minecraft.client.gui.components.toasts.AdvancementToast
import net.minecraft.network.chat.Component
import net.minecraft.util.Mth
object PartyTutorialToasts {
private var toastAttached = false
val upKey = UpShiftPartyBinding.boundKey()
val downKey = DownShiftPartyBinding.boundKey()
val upDisplay =
if (upKey.type == InputConstants.Type.KEYSYM && upKey.value == InputConstants.KEY_UP)
Component.literal("↑")
else upKey.displayName
val downDisplay =
if (downKey.type == InputConstants.Type.KEYSYM && downKey.value == InputConstants.KEY_DOWN)
Component.literal("↓")
else downKey.displayName
private val toast = CobblemonToast(
Mth.createInsecureUUID(),
CobblemonItems.POKEDEX_RED.defaultInstance,
lang("ui.party.party_select_title").red(),
lang(
"ui.party.party_select_description",
upDisplay,
downDisplay,
PartySendBinding.boundKey().displayName
).darkGray(),
AdvancementToast.BACKGROUND_SPRITE,
-1F,
0
)
fun reset() {
toastAttached = false
}
fun onOverlayRender(partySize: Int) {
if (partySize <= 1) return
if (!CobblemonClient.clientPlayerData.partySelectTutorialDone) {
showToast()
}
}
fun onArrowKeySwitchedSlot() {
if (CobblemonClient.clientPlayerData.partySelectTutorialDone) return
if (CobblemonClient.storage.party.count { it != null } < 2) return
PartyTutorialPacket().sendToServer()
hideToast()
}
fun onHoldKeyScrollSwitchedSlot() {
if (CobblemonClient.clientPlayerData.partySelectTutorialDone) return
if (CobblemonClient.storage.party.count { it != null } < 2) return
PartyTutorialPacket().sendToServer()
hideToast()
}
private fun showToast() {
val minecraft = Minecraft.getInstance()
if (!toastAttached) {
toast.nextVisibility = Toast.Visibility.SHOW
minecraft.toasts.addToast(toast)
toastAttached = true
}
}
private fun hideToast() {
if (toastAttached) toast.nextVisibility = Toast.Visibility.HIDE
}
}
@@ -40,6 +40,7 @@ import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.isInventoryKeyPressed
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.toAssetPath
import com.mojang.blaze3d.platform.InputConstants
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
@@ -463,7 +464,7 @@ class PCGUI(
)
// Poké Ball
val ballResource = cobblemonResource("textures/item/poke_balls/" + pokemon.caughtBall.name.path + ".png")
val ballResource = pokemon.caughtBall.name.toAssetPath("textures/item/poke_balls/", ".png")
blitk(
matrixStack = matrices,
texture = ballResource,
@@ -375,7 +375,8 @@ class PokemonInfoWidget(val pX: Int, val pY: Int, val updateForm: (PokedexForm)
matrixStack = matrices,
partialTicks = delta,
rotation = Quaternionf().fromEulerXYZDegrees(rotationVector),
state = state
state = state,
blockLight = 15
)
matrices.popPose()
@@ -15,18 +15,19 @@ import com.cobblemon.mod.common.api.gui.ColourLibrary
import com.cobblemon.mod.common.api.gui.MultiLineLabelK
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.api.types.ElementalType
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.client.gui.ExitButton
import com.cobblemon.mod.common.client.gui.TypeIcon
import com.cobblemon.mod.common.client.gui.VariableObjectSelectionScrollbar
import com.cobblemon.mod.common.client.gui.startselection.widgets.CategoryList
import com.cobblemon.mod.common.client.gui.startselection.widgets.ExitButton
import com.cobblemon.mod.common.client.gui.startselection.widgets.preview.ArrowButton
import com.cobblemon.mod.common.client.gui.startselection.widgets.preview.SelectionButton
import com.cobblemon.mod.common.client.gui.startselection.widgets.SelectionButton
import com.cobblemon.mod.common.client.gui.startselection.widgets.preview.StarterRoundabout
import com.cobblemon.mod.common.client.gui.summary.Summary.Companion.PARTY
import com.cobblemon.mod.common.client.gui.summary.widgets.ModelWidget
import com.cobblemon.mod.common.client.gui.summary.widgets.type.DualTypeWidget
import com.cobblemon.mod.common.client.gui.summary.widgets.type.SingleTypeWidget
import com.cobblemon.mod.common.client.gui.summary.widgets.type.TypeWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.config.starter.RenderableStarterCategory
import com.cobblemon.mod.common.net.messages.server.SelectStarterPacket
@@ -34,13 +35,14 @@ import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.math.toRGB
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.toasts.Toast
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.io.FileNotFoundException
/**
* Starterselection Screen Thingy
@@ -49,32 +51,31 @@ import net.minecraft.network.chat.Component
* @since 2022-06-18
*/
class StarterSelectionScreen(private val categories: List<RenderableStarterCategory>): Screen("cobblemon.ui.starter.title".asTranslated()), CobblemonRenderable {
companion object {
// Size of UI at scale 1
private const val BASE_WIDTH = 200
private const val BASE_HEIGHT = 175
private const val BASE_WIDTH = 239
private const val BASE_HEIGHT = 197
// Resources
private val base = cobblemonResource("textures/gui/starterselection/starterselection_base.png")
private val baseUnderlay = cobblemonResource("textures/gui/starterselection/starterselection_base_underlay.png")
private val baseFrame = cobblemonResource("textures/gui/starterselection/starterselection_base_frame.png")
private val baseResource = cobblemonResource("textures/gui/starterselection/base.png")
private val backgroundResource = cobblemonResource("textures/gui/starterselection/background.png")
private val backgroundBallResource = cobblemonResource("textures/gui/starterselection/background_poke_ball.png")
// Type Backgrounds
private val singleTypeBackground = cobblemonResource("textures/gui/starterselection/starterselection_type_slot1.png")
private val doubleTypeBackground = cobblemonResource("textures/gui/starterselection/starterselection_type_slot2.png")
private val platformResource = cobblemonResource("textures/gui/starterselection/platform_base.png")
private val platformShadow = cobblemonResource("textures/gui/pokedex/platform_shadow.png")
private val typeSpacerResource = cobblemonResource("textures/gui/starterselection/type_spacer_single.png")
private val ballResource = cobblemonResource("textures/item/poke_balls/poke_ball.png")
}
private var currentSelection = 0
private lateinit var currentCategory: RenderableStarterCategory
var currentSelection = 0
lateinit var currentCategory: RenderableStarterCategory
private lateinit var modelWidget: ModelWidget
private lateinit var currentPokemon: RenderablePokemon
private lateinit var typeWidget: TypeWidget
private lateinit var starterRoundaboutCenter: StarterRoundabout
private lateinit var starterRoundaboutLeft: StarterRoundabout
private lateinit var starterRoundaboutRight: StarterRoundabout
override fun renderBlurredBackground(delta: Float) { }
var ticksElapsed = 0
var currentBallBackgroundFrame = 0
override fun renderBlurredBackground(delta: Float) {}
override fun renderMenuBackground(context: GuiGraphics) {}
@@ -95,55 +96,24 @@ class StarterSelectionScreen(private val categories: List<RenderableStarterCateg
return
}
addRenderableWidget(
CategoryList(
paneWidth = 71, paneHeight = BASE_HEIGHT - 11,
topOffset = 6,
entryHeight = 20, entryWidth = 57,
categories = categories,
listX = x - 2, listY = y + 8,
starterSelectionScreen = this
)
)
val rightButton = ArrowButton(
pX = x + 183, pY = y + 151,
pWidth = 9, pHeight = 14,
right = true
) {
right()
}
val leftButton = ArrowButton(
pX = x + 72, pY = y + 151,
pWidth = 9, pHeight = 14,
right = false
) {
left()
}
addRenderableWidget(rightButton)
addRenderableWidget(leftButton)
currentCategory = categories.first()
currentPokemon = currentCategory.pokemon[currentSelection]
with(currentPokemon) {
modelWidget = ModelWidget(
pX = x + 85, pY = y + 50,
pWidth = 102, pHeight = 100,
pX = x + 6, pY = y + 17,
pWidth = 118, pHeight = 100,
pokemon = this,
baseScale = 2.0f,
baseScale = 2.7f,
playCryOnClick = true,
offsetY = -12.0
)
typeWidget = typeWidget(this, x, y)
}
addRenderableWidget(modelWidget)
val selectionButton = SelectionButton(
pX = x + 106, pY = y + 124,
pX = x + 13, pY = y + 180,
pWidth = SelectionButton.BUTTON_WIDTH, pHeight = SelectionButton.BUTTON_HEIGHT
) {
CobblemonNetwork.sendToServer(
@@ -152,182 +122,214 @@ class StarterSelectionScreen(private val categories: List<RenderableStarterCateg
selected = currentSelection
)
)
Minecraft.getInstance().setScreen(null)
playSound(CobblemonSounds.GUI_CLICK)
onClose()
}
addRenderableWidget(selectionButton)
starterRoundaboutCenter = StarterRoundabout(
pX = x + 119, pY = height / 2 + 84,
pWidth = StarterRoundabout.MODEL_WIDTH, pHeight = StarterRoundabout.MODEL_HEIGHT,
pokemon = currentPokemon,
rotationVector = this.modelWidget.rotationVector
)
starterRoundaboutLeft = StarterRoundabout(
pX = x + 89, pY = height / 2 + 84,
pWidth = StarterRoundabout.MODEL_WIDTH, pHeight = StarterRoundabout.MODEL_HEIGHT,
pokemon = currentCategory.pokemon[leftOfCurrentSelection()],
clickAction = { _, _ -> this.left() },
rotationVector = this.modelWidget.rotationVector
)
starterRoundaboutRight = StarterRoundabout(
pX = x + 149, pY = height / 2 + 84,
pWidth = StarterRoundabout.MODEL_WIDTH, pHeight = StarterRoundabout.MODEL_HEIGHT,
pokemon = currentCategory.pokemon[rightOfCurrentSelection()],
clickAction = { _, _ -> this.right() },
rotationVector = this.modelWidget.rotationVector
)
addRenderableWidget(starterRoundaboutLeft)
addRenderableWidget(starterRoundaboutCenter)
addRenderableWidget(starterRoundaboutRight)
// Add Exit Button
addRenderableWidget(
ExitButton(
pX = x + 181, pY = y + 2,
pWidth = 16, pHeight = 12,
pXTexStart = 0, pYTexStart = 0, pYDiffText = 0
) {
Minecraft.getInstance().setScreen(null)
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
ExitButton(pX = x + 210, pY = y + 181) {
playSound(CobblemonSounds.GUI_CLICK)
onClose()
}
)
val categoryList = CategoryList(
paneWidth = 97, paneHeight = 145,
categories = categories,
listX = x + 134, listY = y + 27,
starterSelectionScreen = this
)
addRenderableWidget(categoryList)
addRenderableWidget(VariableObjectSelectionScrollbar(
parent = categoryList,
x = x + 231,
y = y + 27,
width = 3,
height = 145
))
}
override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val matrices = context.pose()
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
// Render Underlay
//Background
blitk(
matrixStack = matrices,
texture = baseUnderlay,
x = x, y = y,
width = BASE_WIDTH, height = BASE_HEIGHT
texture = backgroundResource,
x = x + 6,
y = y + 17,
width = 118,
height = 100
)
// Render Base
blitk(
matrixStack = matrices,
texture = base,
x = x, y = y,
width = BASE_WIDTH, height = BASE_HEIGHT
texture = backgroundBallResource,
x = x + 10.5,
y = y + 12.5,
width = 109,
height = 109,
textureHeight = 1744,
vOffset = currentBallBackgroundFrame * 109
)
// Render Frame
val (r, g, b) = currentPokemon.form.primaryType.hue.toRGB()
//Platform
blitk(
matrixStack = matrices,
texture = baseFrame,
red = r,
green = g,
blue = b,
x = x, y = y,
width = BASE_WIDTH, height = BASE_HEIGHT
texture = platformResource,
x = x + 8.5,
y = y + 88,
width = 113,
height = 32
)
// Render Text
val typePlatform = getPlatformResource(currentPokemon.form.primaryType)
if (typePlatform != null) {
blitk(
matrixStack = matrices,
texture = typePlatform,
x = x + 8.5,
y = y + 82,
width = 113,
height = 30
)
}
blitk(
matrixStack = matrices,
texture = platformShadow,
x = (x + 43) / 0.5F,
y = (y + 93.5) / 0.5F,
width = 90,
height = 20,
scale = 0.5F
)
//Base
blitk(
matrixStack = matrices,
texture = baseResource,
x = x,
y = y,
width = BASE_WIDTH,
height = BASE_HEIGHT
)
//Type Icon
TypeIcon(
x = x + 65,
y = y + 120,
type = currentPokemon.form.primaryType,
secondaryType = currentPokemon.form.secondaryType,
centeredX = true
).render(context)
if (currentPokemon.form.secondaryType == null) {
blitk(
matrixStack = matrices,
texture = typeSpacerResource,
x = x + 47,
y = y + 123,
width = 36,
height = 12
)
}
//Name
blitk(
matrixStack = matrices,
texture = ballResource,
x = (x + 4) / 0.5F,
y = (y + 3) / 0.5F,
width = 16,
height = 16,
scale = 0.5F
)
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = currentPokemon.species.translatedName.bold(),
x = x + 14,
y = y + 2,
shadow = true
)
// Pokédex Number
// Add preceding zeroes if Pokédex number is less than 4 digits
var dexNo = currentPokemon.species.nationalPokedexNumber.toString()
while (dexNo.length < 4) {
dexNo = "0$dexNo"
}
dexNo = "#$dexNo"
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = dexNo.text().bold(),
x = x + 79,
y = y + 1,
shadow = true
)
//"Title"
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = lang("ui.starter.title").bold(),
x = x + 125, y = y + 3F,
centered = true,
scale = 1.4F,
maxCharacterWidth = 120,
shadow = true
x = x + 172,
y = y + 11,
shadow = true,
centered = true
)
// Render Name
val pokemonName = currentPokemon.species.translatedName
val scale = 0.8F
drawScaledText(
context = context,
text = pokemonName,
centered = true,
scale = scale,
maxCharacterWidth = 50,
x = x + 94,
y = y + 19.5,
shadow = false
)
//Description Text
val smallTextScale = 0.5F
// Render Description
val scale2 = 0.60F
matrices.pushPose()
matrices.scale(scale2, scale2, 1F)
// TODO use all pokedex lines across multiple clickable pages in this screen
matrices.scale(smallTextScale, smallTextScale, 1F)
MultiLineLabelK.create(
component = currentPokemon.form.pokedex.first().asTranslated(),
width = 127,
maxLines = 4
width = 114 / smallTextScale,
maxLines = 5
).renderLeftAligned(
context = context,
x = (x + 119) / scale2 + 4, y = (y + 18) / scale2 + 4.0,
ySpacing = (8.0 / scale2) - 1.25,
colour = ColourLibrary.WHITE, shadow = false
x = (x + 8) / smallTextScale,
y = (y + 143) / smallTextScale,
ySpacing = 5.5 / smallTextScale,
colour = ColourLibrary.WHITE,
shadow = true
)
matrices.popPose()
// Render the type background
blitk(
matrixStack = matrices,
texture = currentPokemon.form.secondaryType?.let { doubleTypeBackground } ?: singleTypeBackground,
x = (currentPokemon.form.secondaryType?.let { x + 76.75 } ?: (x + 85.25)), y = y + 29.4,
width = currentPokemon.form.secondaryType?.let { 35.25 } ?: 19, height = 19.25
)
// Render the type widget
typeWidget.render(context, mouseX, mouseY, delta)
// Render the rest
super.render(context, mouseX, mouseY, delta)
}
fun changeCategory(category: RenderableStarterCategory) {
fun getPlatformResource(type: ElementalType): ResourceLocation? {
return try {
cobblemonResource("textures/gui/starterselection/starter_platform_base_${type.showdownId}.png")
} catch (error: FileNotFoundException) {
null
}
}
fun selectPokemon(category: RenderableStarterCategory, slot: Int) {
currentCategory = category
currentSelection = 0
updateSelection()
}
private fun right() {
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
currentSelection = rightOfCurrentSelection()
updateSelection()
}
private fun rightOfCurrentSelection() : Int = if (currentSelection + 1 <= currentCategory.pokemon.size - 1) currentSelection + 1 else 0
private fun left() {
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
currentSelection = leftOfCurrentSelection()
updateSelection()
}
private fun leftOfCurrentSelection() : Int = if (currentSelection - 1 == -1) currentCategory.pokemon.size - 1 else currentSelection - 1
private fun updateSelection() {
currentSelection = slot
currentPokemon = currentCategory.pokemon[currentSelection].also {
modelWidget.pokemon = it
typeWidget = typeWidget(it, (width - BASE_WIDTH) / 2, (height - BASE_HEIGHT) / 2)
}
starterRoundaboutLeft.pokemon = currentCategory.pokemon[leftOfCurrentSelection()]
starterRoundaboutCenter.pokemon = currentPokemon
starterRoundaboutRight.pokemon = currentCategory.pokemon[rightOfCurrentSelection()]
}
private fun typeWidget(pokemon: RenderablePokemon, x: Int, y: Int) : TypeWidget {
return pokemon.form.secondaryType?.let {
DualTypeWidget(
pX = x + 77, pY = y + 30,
pWidth = 18, pHeight = 18,
pMessage = Component.literal("What?"),
mainType = pokemon.form.primaryType, secondaryType = it
)
} ?: SingleTypeWidget(
pX = x + 85, pY = y + 30,
pWidth = 18, pHeight = 18,
type = pokemon.form.primaryType,
renderText = false
)
override fun tick() {
ticksElapsed++
val delay = 3
if (ticksElapsed % delay == 0) currentBallBackgroundFrame++
if (currentBallBackgroundFrame == 16) currentBallBackgroundFrame = 0
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
@@ -340,4 +342,8 @@ class StarterSelectionScreen(private val categories: List<RenderableStarterCateg
}
override fun isPauseScreen() = true
fun playSound(soundEvent: SoundEvent) {
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(soundEvent, 1.0F))
}
}
@@ -8,126 +8,212 @@
package com.cobblemon.mod.common.client.gui.startselection.widgets
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.client.gui.PokemonGUIAnimationStyle
import com.cobblemon.mod.common.client.gui.VariableObjectSelectionList
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.startselection.StarterSelectionScreen
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.config.starter.RenderableStarterCategory
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.ObjectSelectionList
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.network.chat.Component
import org.joml.Quaternionf
import org.joml.Vector3f
class CategoryList(
private val paneWidth: Int,
private val paneHeight: Int,
topOffset: Int,
private val entryWidth: Int,
entryHeight: Int,
private val categories: List<RenderableStarterCategory>,
val listX: Int,
val listY: Int,
private val minecraft: Minecraft = Minecraft.getInstance(),
private val starterSelectionScreen: StarterSelectionScreen
) : ObjectSelectionList<CategoryList.Category>(
minecraft,
) : VariableObjectSelectionList(
listX,
listY,
paneWidth,
paneHeight,
topOffset,
entryHeight
paneHeight
), CobblemonRenderable {
companion object {
private const val CATEGORY_BUTTON_WIDTH = 51.5f
private const val CATEGORY_BUTTON_HEIGHT = 16f
private const val ENTRY_X_OFFSET = 10f
private val categoryResource = cobblemonResource("textures/gui/starterselection/starterselection_slot.png")
private val headerResource = cobblemonResource("textures/gui/starterselection/selection_container_header.png")
private val footerResource = cobblemonResource("textures/gui/starterselection/selection_container_footer.png")
private val edgeResource = cobblemonResource("textures/gui/starterselection/selection_container_edge.png")
private val containerResource = cobblemonResource("textures/gui/starterselection/selection_container.png")
private val buttonResource = cobblemonResource("textures/gui/starterselection/starter_selection.png")
private val buttonActiveResource = cobblemonResource("textures/gui/starterselection/starter_selection_active.png")
const val MARGIN = 4
const val HEADER_HEIGHT = 7
const val BUTTON_SEPERATION = 3
const val CONTAINER_BUTTON_SIZE = 25
const val CONTAINER_WIDTH = 89
}
init {
this.x = listX
this.y = listY
this.correctSize()
createEntries().forEach { addEntry(it) }
//this.setRenderBackground(false)
addEntry(EmptySeparator(MARGIN))
createEntries().forEach {
addEntry(it)
addEntry(EmptySeparator(MARGIN))
}
}
private fun createEntries() = categories.map {
Category(it)
private fun createEntries() = categories.map { category ->
val rows = (category.pokemon.size - 1) / 3 + 1
val height = CONTAINER_BUTTON_SIZE * rows + BUTTON_SEPERATION * (rows + 1) + HEADER_HEIGHT + 3
Category(category, height)
}
override fun renderListBackground(context: GuiGraphics) {}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
super.renderWidget(context, mouseX, mouseY, delta)
correctSize()
}
private fun correctSize() {
this.setRectangle(this.paneWidth, this.paneHeight, this.listX, this.listY)
}
private fun scale(n: Int): Int = (this.minecraft.window.guiScale * n).toInt()
override fun getRowWidth() = this.entryWidth
override fun getScrollbarPosition(): Int {
return this.listX + this.width - 5
}
inner class Category(private val category: RenderableStarterCategory) : Entry<Category>() {
inner class Category(private val category: RenderableStarterCategory, height: Int) : Entry(height) {
override fun render(
context: GuiGraphics,
guiGraphics: GuiGraphics,
index: Int,
y: Int,
x: Int,
entryWidth: Int,
entryHeight: Int,
y: Int,
mouseX: Int,
mouseY: Int,
hovered: Boolean,
tickDelta: Float
partialTick: Float
) {
val matrices = context.pose()
val isHovered = mouseX >= x && mouseY >= y && mouseX < x + entryWidth && mouseY < y + (entryHeight - 1)
if (isHovered) {
blitk(
matrixStack = matrices,
x = x + 2.5f, y = y,
texture = categoryResource,
width = CATEGORY_BUTTON_WIDTH, height = CATEGORY_BUTTON_HEIGHT,
red = 0.75f, green = 0.75f, blue = 0.75f
)
} else
blitk(
matrixStack = matrices,
x = x + 2.5f, y = y,
texture = categoryResource,
width = CATEGORY_BUTTON_WIDTH, height = CATEGORY_BUTTON_HEIGHT
)
drawScaledText(
context = context,
text = category.displayNameText,
x = x + 28,
y = y + 4.5F,
scale = 1F,
maxCharacterWidth = 50,
shadow = true,
centered = true
val rows = (category.pokemon.size - 1) / 3 + 1
val matrices = guiGraphics.pose()
//Header
blitk(
matrixStack = matrices,
texture = headerResource,
x = x + MARGIN,
y = y,
width = CONTAINER_WIDTH,
height = HEADER_HEIGHT
)
val smallTextScale = 0.5F
drawScaledText(
context = guiGraphics,
text = category.displayNameText.bold(),
x = x + MARGIN + CONTAINER_WIDTH / 2,
y = y + 2,
shadow = true,
centered = true,
scale = smallTextScale
)
//Container
var stackHeight = HEADER_HEIGHT
blitk(
matrixStack = matrices,
texture = edgeResource,
x = x + MARGIN,
y = y + stackHeight,
width = CONTAINER_WIDTH,
height = 1
)
stackHeight += 1
val containerInnerHeight = CONTAINER_BUTTON_SIZE * rows + BUTTON_SEPERATION * (rows + 1)
blitk(
matrixStack = matrices,
texture = containerResource,
x = x + MARGIN,
y = y + stackHeight,
width = CONTAINER_WIDTH,
height = containerInnerHeight
)
stackHeight += containerInnerHeight
blitk(
matrixStack = matrices,
texture = edgeResource,
x = x + MARGIN,
y = y + stackHeight,
width = CONTAINER_WIDTH,
height = 1
)
stackHeight += 1
//Footer
blitk(
matrixStack = matrices,
texture = footerResource,
x = x + MARGIN,
y = y + stackHeight,
width = CONTAINER_WIDTH,
height = 1
)
//Buttons
for (index in 0..<category.pokemon.size) {
val row = index / 3
val column = index % 3
val startX = x + MARGIN + 5 + column * (CONTAINER_BUTTON_SIZE + 2)
val startY = y + HEADER_HEIGHT + 1 + BUTTON_SEPERATION + row * (BUTTON_SEPERATION + CONTAINER_BUTTON_SIZE)
val active = starterSelectionScreen.currentCategory == category && starterSelectionScreen.currentSelection == index
val hovered = getEntryAtPosition(mouseX, mouseY) == this && mouseX in startX..(startX + CONTAINER_BUTTON_SIZE) && mouseY in startY..(startY + CONTAINER_BUTTON_SIZE)
blitk(
matrixStack = matrices,
texture = if (active) buttonActiveResource else buttonResource,
x = startX,
y = startY,
width = CONTAINER_BUTTON_SIZE,
height = CONTAINER_BUTTON_SIZE,
textureHeight = CONTAINER_BUTTON_SIZE * 2,
vOffset = if (hovered) CONTAINER_BUTTON_SIZE else 0
)
matrices.pushPose()
matrices.translate(startX + (CONTAINER_BUTTON_SIZE / 2.0), startY.toDouble() - 6, 0.0)
matrices.scale(3.5F, 3.5F, 1F)
val state = FloatingState()
drawProfilePokemon(
species = category.pokemon[index].species.resourceIdentifier,
matrixStack = matrices,
rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, 35F, 0F)),
state = state,
scale = 4.5F,
partialTicks = 0F
)
matrices.popPose()
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
starterSelectionScreen.changeCategory(category = category)
minecraft.soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
return true
val entryY = getRowTop(this)
val hoveredButton = getButtonHovered(x, entryY, mouseX, mouseY)
if (hoveredButton != -1) {
starterSelectionScreen.selectPokemon(category, hoveredButton)
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
return true
}
return false
}
override fun getNarration(): Component {
return Component.literal("Yes")
private fun getButtonHovered(x: Int, y: Int, mouseX: Double, mouseY: Double): Int {
for (index in 0..<category.pokemon.size) {
val row = index / 3
val column = index % 3
val startX = x + MARGIN + 5 + column * (CONTAINER_BUTTON_SIZE + 2)
val startY = y + HEADER_HEIGHT + 1 + BUTTON_SEPERATION + row * (BUTTON_SEPERATION + CONTAINER_BUTTON_SIZE)
if (getEntryAtPosition(mouseX.toInt(), mouseY.toInt()) == this && mouseX.toInt() in startX..(startX + CONTAINER_BUTTON_SIZE) && mouseY.toInt() in startY..(startY + CONTAINER_BUTTON_SIZE)) {
return index
}
}
return -1
}
}
}
@@ -6,11 +6,13 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.startselection.widgets.preview
package com.cobblemon.mod.common.client.gui.startselection.widgets
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.gui.ColourLibrary
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.util.cobblemonResource
@@ -26,45 +28,39 @@ class SelectionButton(
pX: Int, pY: Int,
pWidth: Int, pHeight: Int,
onPress: OnPress
): Button(pX, pY, pWidth, pHeight, Component.literal("SelectionButton"), onPress, DEFAULT_NARRATION), CobblemonRenderable {
): Button(pX, pY, pWidth, pHeight, Component.literal("SelectionButton"), onPress, DEFAULT_NARRATION),
CobblemonRenderable {
companion object {
private val buttonTexture = cobblemonResource("textures/gui/starterselection/starterselection_button.png")
const val BUTTON_WIDTH = 56
const val BUTTON_HEIGHT = 12
private const val SCALE = 0.7f
private val buttonResource = cobblemonResource("textures/gui/starterselection/choose_button.png")
const val BUTTON_WIDTH = 106
const val BUTTON_HEIGHT = 14
const val TEXT_HEIGHT = 10
}
override fun playDownSound(soundManager: SoundManager) {
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
}
override fun playDownSound(soundManager: SoundManager) {}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val matrices = context.pose()
if (isHovered)
blitk(
matrixStack = matrices,
texture = buttonTexture,
x = x + 0.6, y = y + 0.6,
width = BUTTON_WIDTH - 0.25, height = BUTTON_HEIGHT - 0.25,
red = 0.75f, green = 0.75f, blue = 0.75f
)
else
blitk(
matrixStack = matrices,
texture = buttonTexture,
x = x + 0.6, y = y + 0.6,
width = BUTTON_WIDTH - 0.25, height = BUTTON_HEIGHT - 0.25
)
blitk(
matrixStack = matrices,
texture = buttonResource,
x = x,
y = y,
width = BUTTON_WIDTH,
height = BUTTON_HEIGHT,
textureHeight = BUTTON_HEIGHT * 2,
vOffset = if (isHovered) BUTTON_HEIGHT else 0
)
drawScaledText(
context = context,
text = lang("ui.starter.choosebutton"),
x = x + BUTTON_WIDTH / 2, y = y + BUTTON_HEIGHT / 2 - 2.4,
colour = ColourLibrary.WHITE,
centered = true,
maxCharacterWidth = 68,
scale = SCALE,
shadow = true
font = CobblemonResources.DEFAULT_LARGE,
text = lang("ui.starter.choosebutton").bold(),
x = x + BUTTON_WIDTH / 2,
y = y + BUTTON_HEIGHT / 2 - TEXT_HEIGHT / 2,
shadow = true,
centered = true
)
}
}
@@ -45,6 +45,7 @@ import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.isInventoryKeyPressed
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.toAssetPath
import com.mojang.blaze3d.platform.InputConstants
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
@@ -558,7 +559,7 @@ class Summary private constructor(party: Collection<Pokemon?>, private val edita
}
// Poké Ball
val ballResource = cobblemonResource("textures/item/poke_balls/" + selectedPokemon.caughtBall.name.path + ".png")
val ballResource = selectedPokemon.caughtBall.name.toAssetPath("textures/item/poke_balls/", ".png")
blitk(
matrixStack = matrices,
texture = ballResource,
@@ -31,6 +31,7 @@ class ModelWidget(
var offsetY: Double = 0.0,
val playCryOnClick: Boolean = false,
val shouldFollowCursor: Boolean = false,
val blockLight: Int = 13
): SoundlessWidget(pX, pY, pWidth, pHeight, Component.literal("Summary - ModelWidget")) {
companion object {
@@ -115,6 +116,7 @@ class ModelWidget(
partialTicks = partialTicks,
headYaw = currentYawAndPitch.first,
headPitch = currentYawAndPitch.second,
blockLight = blockLight
)
heldItemRenderer.renderOnModel(
@@ -19,11 +19,11 @@ import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.net.messages.server.RequestMoveSwapPacket
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import java.math.RoundingMode
import java.text.DecimalFormat
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component
import java.math.RoundingMode
import java.text.DecimalFormat
class MovesWidget(
pX: Int, pY: Int,
@@ -20,6 +20,8 @@ import net.minecraft.resources.ResourceLocation
import net.minecraft.util.Mth
import java.util.*
import kotlin.math.min
import net.minecraft.client.gui.Font
import net.minecraft.util.FormattedCharSequence
@Environment(EnvType.CLIENT)
class CobblemonToast(
@@ -38,11 +40,18 @@ class CobblemonToast(
private var lastTime = 0L
internal var nextVisibility: Toast.Visibility = Toast.Visibility.SHOW
fun getLines(font: Font, maxWidth: Int): List<FormattedCharSequence> {
return font.split(this.description, maxWidth)
}
override fun render(context: GuiGraphics, manager: ToastComponent, startTime: Long): Toast.Visibility {
context.blitSprite(this.frameTexture, 0, 0, this.width(), this.height())
val maxWidth = 125
val textRenderer = manager.minecraft.font
context.blitSprite(this.frameTexture, 0, 0, this.width(), this.height() + (getLines(textRenderer, maxWidth).size - 1) * 10)
context.drawString(textRenderer, this.title, 30, 7, this.title.style.color?.value ?: -1, false)
context.drawString(textRenderer, this.description, 30, 18, this.description.style.color?.value ?: -1, false)
context.drawWordWrap(textRenderer, this.description, 30, 18, maxWidth, this.description.style.color?.value ?: -1)
context.renderFakeItem(this.icon, 8, 8)
if (this.hasProgressBar()) {
context.fill(3, 28, 157, 29, -1)
@@ -32,6 +32,7 @@ import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.toAssetPath
import com.mojang.blaze3d.platform.InputConstants
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
@@ -584,7 +585,7 @@ class TradeGUI(
// Poké Ball
val nameXOffset = if (isOpposing) 75 else 0
val ballResource = cobblemonResource("textures/item/poke_balls/" + pokemon.caughtBall.name.path + ".png")
val ballResource = pokemon.caughtBall.name.toAssetPath("textures/item/poke_balls/", ".png")
blitk(
matrixStack = context.pose(),
texture = ballResource,
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.client.keybind.keybinds
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.gui.party.PartyTutorialToasts
import com.cobblemon.mod.common.client.keybind.CobblemonKeyBinding
import com.cobblemon.mod.common.client.keybind.KeybindCategories
import com.mojang.blaze3d.platform.InputConstants
@@ -21,5 +22,6 @@ object DownShiftPartyBinding : CobblemonKeyBinding(
) {
override fun onPress() {
CobblemonClient.storage.shiftSelected(true)
PartyTutorialToasts.onArrowKeySwitchedSlot()
}
}
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.client.keybind.keybinds
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.gui.party.PartyTutorialToasts
import com.cobblemon.mod.common.client.keybind.CobblemonKeyBinding
import com.cobblemon.mod.common.client.keybind.KeybindCategories
import com.mojang.blaze3d.platform.InputConstants
@@ -21,5 +22,6 @@ object UpShiftPartyBinding : CobblemonKeyBinding(
) {
override fun onPress() {
CobblemonClient.storage.shiftSelected(false)
PartyTutorialToasts.onArrowKeySwitchedSlot()
}
}
@@ -1,27 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.data
import com.cobblemon.mod.common.api.interaction.PokemonInteractions
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.net.messages.client.data.PokemonInteractionsSyncPacket
import net.minecraft.client.Minecraft
object PokemonInteractionsSyncHandler : ClientNetworkPacketHandler<PokemonInteractionsSyncPacket> {
override fun handle(
packet: PokemonInteractionsSyncPacket,
client: Minecraft,
) {
PokemonInteractions.speciesInteractions.clear()
PokemonInteractions.speciesInteractions.addAll(packet.speciesInteractions)
PokemonInteractions.generalInteractions.clear()
PokemonInteractions.generalInteractions.addAll(packet.generalInteractions)
}
}
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.debug
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.render.debug.LineState
import com.cobblemon.mod.common.client.render.debug.SteerForceRenderer
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
import net.minecraft.client.Minecraft
object S2CUpdateObsAvoidHandler : ClientNetworkPacketHandler<ClientboundUpdateObsAvoidPacket> {
override fun handle(packet: ClientboundUpdateObsAvoidPacket, client: Minecraft) {
client.executeIfPossible {
//ObjectAvoidanceRenderer.avoidanceLines[packet.entityId] = packet.lines
val lineState = SteerForceRenderer.lineStates.getOrPut(packet.entityId) { LineState() }
lineState.update(packet.lines)
}
}
}
@@ -309,6 +309,7 @@ class SnowstormParticle(
boundingBox = AABB.ofSize(Vec3(x, y, z), radius, radius, radius)
if (dx == 0.0 && dy == 0.0 && dz == 0.0) {
updatePosition()
expandBoundingBoxForCulling(radius)
return
}
@@ -366,6 +367,16 @@ class SnowstormParticle(
}
}
updatePosition()
expandBoundingBoxForCulling(radius)
}
private fun expandBoundingBoxForCulling(collisionRadius: Double) {
val visualSizeX = abs(runtime.resolveDouble(storm.effect.particle.sizeX))
val visualSizeY = abs(runtime.resolveDouble(storm.effect.particle.sizeY))
val visualExtent = maxOf(visualSizeX, visualSizeY)
if (visualExtent > collisionRadius) {
boundingBox = AABB.ofSize(Vec3(x, y, z), visualExtent, visualExtent, visualExtent)
}
}
fun updatePosition() {
@@ -290,8 +290,14 @@ class ModelLayer {
val emissive: Boolean = false
val translucent: Boolean = false
val translucent_cull: Boolean = false
val scrolling: ScrollingTextureSettings? = null
}
class ScrollingTextureSettings(
val speedU: Float = 0.01F,
val speedV: Float = 0.01F
)
enum class SpriteType {
PORTRAIT,
PROFILE
@@ -0,0 +1,116 @@
/*
* 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.debug
import com.cobblemon.mod.common.api.net.Encodable
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.blaze3d.vertex.VertexConsumer
import net.minecraft.client.Camera
import net.minecraft.client.Minecraft
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.util.Mth
import net.minecraft.world.phys.Vec3
/**
* Data class currently used for storing line data for rendering in [SteerForceRenderer]
*
* @author Jackson
* @since September 18th, 2025
*/
data class EntityDebugLine(
val vector: Vec3, // Direction/magnitude in world space, relative to entity origin
val r: Float,
val g: Float,
val b: Float,
val a: Float,
) : Encodable {
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeVec3(vector)
buffer.writeFloat(r)
buffer.writeFloat(g)
buffer.writeFloat(b)
buffer.writeFloat(a)
}
companion object {
operator fun invoke(vector: Vec3, r: Float = 1f, g: Float = 1f, b: Float = 1f, a: Float = 1f): EntityDebugLine {
return EntityDebugLine(vector, r.coerceIn(0f,1f), g.coerceIn(0f,1f), b.coerceIn(0f,1f), a.coerceIn(0f,1f))
}
fun decode(buffer: RegistryFriendlyByteBuf): EntityDebugLine {
val vector = buffer.readVec3()
return EntityDebugLine(vector, buffer.readFloat(), buffer.readFloat(), buffer.readFloat(), buffer.readFloat())
}
}
}
class LineState {
/**
* The comparison of the previous and current lines relies on the order being consistent
*/
var previousLines: List<EntityDebugLine> = emptyList()
var currentLines: List<EntityDebugLine> = emptyList()
var lastReceivedLineTick: Long? = null
fun tick() {
previousLines = currentLines
}
fun update(newLines: List<EntityDebugLine>) {
lastReceivedLineTick = Minecraft.getInstance().level?.gameTime
currentLines = newLines
if (previousLines.isEmpty()) previousLines = newLines
}
// entityPos: interpolated world position of the entity this tick
fun draw(vc: VertexConsumer, pose: PoseStack.Pose, partialTick: Float, camera: Camera, entity: PokemonEntity, partialTicks: Float) {
previousLines.zip(currentLines).forEach { (prev, curr) ->
val iVec = prev.vector.lerp(curr.vector, partialTick.toDouble())
val iR = prev.r.lerp(curr.r, partialTick)
val iG = prev.g.lerp(curr.g, partialTick)
val iB = prev.b.lerp(curr.b, partialTick)
val iA = prev.a.lerp(curr.a, partialTick)
// Interpolated world position — same method used everywhere in the renderer
val worldStart = Vec3(
Mth.lerp(partialTicks.toDouble(), entity.xOld, entity.x),
Mth.lerp(partialTicks.toDouble(), entity.yOld, entity.y),
Mth.lerp(partialTicks.toDouble(), entity.zOld, entity.z)
)
val worldEnd = worldStart.add(iVec)
val relStart = worldStart.subtract(camera.position)
val relEnd = worldEnd.subtract(camera.position)
// Normal is the normalized direction of the segment
val dir = iVec.normalize()
val nx = dir.x.toFloat()
val ny = dir.y.toFloat()
val nz = dir.z.toFloat()
vc.addVertex(pose.pose(), relStart.x.toFloat(), relStart.y.toFloat(), relStart.z.toFloat())
.setColor(iR, iG, iB, iA)
.setNormal(pose, nx, ny, nz)
vc.addVertex(pose.pose(), relEnd.x.toFloat(), relEnd.y.toFloat(), relEnd.z.toFloat())
.setColor(iR, iG, iB, iA)
.setNormal(pose, nx, ny, nz)
}
}
fun reset() {
previousLines = emptyList()
currentLines = emptyList()
}
}
private fun Float.lerp(end: Float, t: Float): Float = this + (end - this) * t.coerceIn(0f, 1f)
@@ -0,0 +1,58 @@
/*
* 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.debug
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.mojang.blaze3d.vertex.PoseStack
import net.minecraft.client.Camera
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.client.renderer.RenderType
object SteerForceRenderer {
// Lines to render attached to the uuid of entities.
val lineStates = mutableMapOf<Int, LineState>()
fun registerTickHandler() {
// Advance line States
PlatformEvents.CLIENT_TICK_PRE.subscribe {
lineStates.forEach { uuid, lineState ->
lineState.tick()
}
}
}
fun render(poseStack: PoseStack, multiBufferSource: MultiBufferSource, partialTicks: Float, camera: Camera) {
val matrix = poseStack.last().pose()
val level = Minecraft.getInstance().level ?: return
val currTick = level.gameTime
val ticksUntilStale = 3
val keysToRemove = lineStates.keys.filter { entityId ->
val entity = level.getEntity(entityId) as? PokemonEntity
val lastRecTick = lineStates[entityId]?.lastReceivedLineTick
entity == null || (lastRecTick != null && (currTick - lastRecTick) > ticksUntilStale)
}
keysToRemove.forEach { lineStates.remove(it) }
// One buffer for all independent line segments
val lineBuffer = multiBufferSource.getBuffer(RenderType.lines())
lineStates.forEach { entityId, lineState ->
val entity = level.getEntity(entityId) as? PokemonEntity ?: return@forEach
lineState.draw(lineBuffer, poseStack.last(), partialTicks, camera, entity, partialTicks)
}
}
}
@@ -20,6 +20,7 @@ import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
import com.cobblemon.mod.common.client.render.AnimatedModelTextureSupplier
import com.cobblemon.mod.common.client.render.ModelLayer
import com.cobblemon.mod.common.client.render.ScrollingTextureSettings
import com.cobblemon.mod.common.client.render.models.blockbench.animation.*
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockActiveAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockAnimationRepository
@@ -48,6 +49,7 @@ import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.blaze3d.vertex.VertexConsumer
import com.mojang.blaze3d.vertex.VertexFormat
import com.mojang.math.Axis
import net.minecraft.Util
import net.minecraft.client.model.geom.ModelPart
import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.client.renderer.RenderStateShard
@@ -453,15 +455,16 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
val provider = bufferProvider
if (provider != null) {
for (layer in currentLayers) {
var renderLayer : RenderType
if (layer.texture is AnimatedModelTextureSupplier && layer.texture.interpolation) {
//Handle Interpolation
val texture = layer.texture.interpolatedTexture(currentState ?: FloatingState()) ?: continue
renderLayer = makeLayer(DynamicStateShard(texture), layer.emissive, layer.translucent, layer.translucent_cull)
}
else {
val texture = layer.texture?.invoke(currentState ?: FloatingState()) ?: continue
renderLayer = getLayer(texture, layer.emissive, layer.translucent, layer.translucent_cull)
val texture = layer.texture?.invoke(currentState ?: FloatingState()) ?: continue
val scrolling = layer.scrolling
val renderLayer: RenderType = if (scrolling != null) {
getScrollingLayer(texture, scrolling)
} else if (layer.texture is AnimatedModelTextureSupplier && layer.texture.interpolation) {
val interpolatedTexture = layer.texture.interpolatedTexture(currentState ?: FloatingState()) ?: continue
makeLayer(DynamicStateShard(interpolatedTexture), layer.emissive, layer.translucent, layer.translucent_cull)
} else {
getLayer(texture, layer.emissive, layer.translucent, layer.translucent_cull)
}
val consumer = provider.getBuffer(renderLayer)
val tint = layer.tint
@@ -500,7 +503,7 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
!emissive && translucent && translucentCull -> RenderStateShard.RENDERTYPE_ENTITY_TRANSLUCENT_CULL_SHADER
!emissive && translucent -> RenderStateShard.RENDERTYPE_ENTITY_TRANSLUCENT_SHADER
!emissive && !translucent -> RenderStateShard.RENDERTYPE_ENTITY_CUTOUT_SHADER
else -> RenderStateShard.RENDERTYPE_ENTITY_TRANSLUCENT_EMISSIVE_SHADER // This one should be changed to maybe a custom shader? Translucent stuffs with things
else -> RenderStateShard.RENDERTYPE_ENTITY_TRANSLUCENT_EMISSIVE_SHADER
}
)
.setTextureState(texture)
@@ -533,6 +536,12 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
}
}
fun getScrollingLayer(texture: ResourceLocation, scrolling: ScrollingTextureSettings): RenderType {
val gameTime = Util.getMillis().toFloat() / 1000f
val offsetU = (gameTime * scrolling.speedU) % 1.0f
val offsetV = (gameTime * scrolling.speedV) % 1.0f
return RenderType.breezeWind(texture, offsetU, offsetV)
}
/** Applies the given pose's [ModelPartTransformation]s to the model, if there is a matching pose. */
fun applyPose(state: PosableState, pose: Pose, intensity: Float) = pose.transformedParts.forEach { it.apply(state, intensity) }
@@ -8,6 +8,7 @@
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen1
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PitchTiltAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.createTransformation
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BiWingedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BipedFrame
@@ -198,6 +199,7 @@ class PidgeottoModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame,
transformTicks = 10,
quirks = arrayOf(blink, flyQuirk1, flyQuirk2),
animations = arrayOf(
PitchTiltAnimation(bone = getPart("body"), maxChangePerTick = 1F),
singleBoneLook(),
bedrock("pidgeotto", "air_fly")
)
@@ -8,11 +8,11 @@
package com.cobblemon.mod.common.command
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.permission.CobblemonPermissions
import com.cobblemon.mod.common.api.storage.pc.PCPosition
import com.cobblemon.mod.common.api.storage.pc.POKEMON_PER_BOX
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.util.*
import com.mojang.brigadier.Command
import com.mojang.brigadier.CommandDispatcher
@@ -35,7 +35,7 @@ object PcTakeCommand {
.then(
Commands.argument("box", IntegerArgumentType.integer(1))
.then(
Commands.argument("slot", IntegerArgumentType.integer(1, 30))
Commands.argument("slot", IntegerArgumentType.integer(1, POKEMON_PER_BOX))
.executes(::execute)
)
)
@@ -60,8 +60,8 @@ object PcTakeCommand {
val slot = IntegerArgumentType.getInteger(context, "slot")
val pc = target.pc()
if (box > POKEMON_PER_BOX) {
context.source.sendFailure(commandLang("pctake.too_many_boxes", POKEMON_PER_BOX))
if (box > pc.boxes.size) {
context.source.sendFailure(commandLang("pctake.too_many_boxes", pc.boxes.size))
return 0
}
@@ -146,6 +146,12 @@ class CobblemonConfig {
@CobblemonConfigField(Category.Battles, lang = "experience_share_multiplier", SERVER)
var experienceShareMultiplier = .5
@CobblemonConfigField(Category.Battles, lang = "award_experience_to_fainted_pokemon", SERVER)
var awardExperienceToFaintedPokemon = false
@CobblemonConfigField(Category.Battles, lang = "award_experience_on_battle_loss", SERVER)
var awardExperienceOnBattleLoss = false
@CobblemonConfigField(Category.Battles, lang = "lucky_egg_multiplier", SERVER)
var luckyEggMultiplier = 1.5
@@ -247,7 +253,7 @@ class CobblemonConfig {
var pastureBlockUpdateTicks = 40
@CobblemonConfigField(Category.World, lang = "pasture_max_wander_distance", SERVER)
var pastureMaxWanderDistance = 64
var pastureMaxWanderDistance = 32
@CobblemonConfigField(Category.World, lang = "pasture_max_per_chunk", SERVER)
var pastureMaxPerChunk = 4F
@@ -126,6 +126,14 @@ class StarterConfig {
PokemonProperties.parse("Fuecoco level=10"),
PokemonProperties.parse("Quaxly level=10"),
)
),
StarterCategory(
name = "Special",
displayName = "cobblemon.starterselection.category.special",
pokemon = mutableListOf(
PokemonProperties.parse("Pikachu level=10"),
PokemonProperties.parse("Eevee level=10")
)
)
)
}
@@ -90,8 +90,10 @@ interface MoLangScriptingEntity {
behavioursAreCustom = nbt.getBoolean(DataKeys.SCRIPTED_BEHAVIOURS_ARE_CUSTOM)
behaviours.clear()
behaviours.addAll(nbt.getList(DataKeys.SCRIPTED_BEHAVIOURS, ListTag.TAG_STRING.toInt()).map { ResourceLocation.parse(it.asString) })
data = MoLangFunctions.readMoValueFromNBT(nbt.getCompound(DataKeys.SCRIPTED_DATA)) as VariableStruct
config = if (nbt.contains(DataKeys.SCRIPTED_CONFIG)) MoLangFunctions.readMoValueFromNBT(nbt.getCompound(DataKeys.SCRIPTED_CONFIG)) as VariableStruct else VariableStruct()
val dataKey = if (nbt.contains(DataKeys.SCRIPTED_DATA)) DataKeys.SCRIPTED_DATA else "Data" // migrate old use of "Data"
data = MoLangFunctions.readMoValueFromNBT(nbt.getCompound(dataKey)) as VariableStruct
val configKey = if (nbt.contains(DataKeys.SCRIPTED_CONFIG)) DataKeys.SCRIPTED_CONFIG else "Config" // migrate old use of "Config"
config = if (nbt.contains(configKey)) MoLangFunctions.readMoValueFromNBT(nbt.getCompound(configKey)) as VariableStruct else VariableStruct()
}
fun registerFunctionsForScripting(struct: QueryStruct) {
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.entity.ai
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.entity.OmniPathingEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.ai.OmniPathNodeMaker
import com.cobblemon.mod.common.util.deleteNode
import com.cobblemon.mod.common.util.getWaterAndLavaIn
@@ -35,6 +34,7 @@ import net.minecraft.world.phys.Vec3
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.acos
import kotlin.math.max
/**
* A navigator designed to work with the [OmniPathNodeMaker], allowing a path that can cross land, water, and air
@@ -49,6 +49,8 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
var cachedCurrentNode: Node? = null
var currentNodeDistance = 0F
var segmentIndex: Int = 0
var pathFollowTarget: Vec3? = null
data class NavigationContext(
val onHit: (damage: DamageSource) -> Unit = {},
@@ -160,6 +162,65 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
}
doStuckDetection(vec3d)
// Gather the steer target from the segment pathing
// TODO: this shouldn't live here
val currentPath = path ?: return
if (!currentPath.isDone) {
segmentIndex = segmentIndex.coerceIn(0, max(currentPath.nodeCount - 2, 0))
pathFollowTarget = getPathSteerTarget(currentPath, mob)
}
}
/**
* Gets a steering target for the current path
* This video explains it well: https://www.youtube.com/watch?v=rlZYT-uvmGQ
* Uses Reynolds path following for velocity steered entities
*/
fun getPathSteerTarget(
path: Path,
mob: Mob
): Vec3 {
// If this is the final segment then just seek towards the end
if (segmentIndex >= path.nodeCount - 1) return path.getNode(path.nodeCount - 1).asVec3().add(0.5, 0.0, 0.5)
// Determine future position to use in steering calculations
val lookAheadDist = 1.0 //TODO: Scale to look ahead a certain amount of ticks probably
val futurePos = mob.position().add(mob.deltaMovement.normalize().scale(lookAheadDist))
// Gather the current segment points
var pointS1 = path.getNode(segmentIndex).asVec3().add(0.5, 0.0, 0.5) // Add the 0.5's to get the blockface center as blockpos is on a corner
var pointS2 = path.getNode(segmentIndex + 1).asVec3().add(0.5, 0.0, 0.5)
// Project the future point onto the current segment
var vecA = futurePos.subtract(pointS1)
var vecB = pointS2.subtract(pointS1)
var normB = vecB.normalize()
val lenB = vecB.length()
var projAB = max(vecA.dot(normB), 0.0) // make at least 0 as negative means we are projecting behind the segment which we don't want
// If the projection goes off the front of the segment then advance to the next segment and recalculate the projection
if (projAB > lenB && segmentIndex < path.nodeCount - 2) {
segmentIndex++
pointS1 = path.getNode(segmentIndex).asVec3().add(0.5, 0.0, 0.5)
pointS2 = path.getNode(segmentIndex + 1).asVec3().add(0.5, 0.0, 0.5)
vecA = futurePos.subtract(pointS1)
vecB = pointS2.subtract(pointS1)
normB = vecB.normalize()
projAB = max(vecA.dot(normB), 0.0)
}
// Get the projected point on the segment
val projPoint = pointS1.add(normB.scale(projAB ))
// Determine if the future position is too far from the path by getting its distance from its projected position on the current path segment
val futureDistFromPath = projPoint.distanceTo(futurePos)
val acceptableDist = mob.bbWidth / 3.0 //TODO: figure out what this really needs to be
val pushForward = 0.1 // Advance the projected point along the segment to avoid going backwards
val seekPoint = if (futureDistFromPath < acceptableDist) futurePos.add(normB.scale(pushForward)) else projPoint.add(normB.scale(pushForward))
return seekPoint
}
fun isAirborne(world: Level, pos: BlockPos) =
@@ -223,6 +284,8 @@ 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
segmentIndex = 0
pathFollowTarget = null
this.moveTo(x, y, z, speed)
}
@@ -277,6 +340,8 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
}
override fun moveTo(path: Path?, speed: Double): Boolean {
segmentIndex = 0
pathFollowTarget = null
if (path != null && path.nodeCount > 0) {
val node = path.getNode(0)!!
// pokemonEntity.discard()
@@ -399,6 +464,8 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
this.cachedCurrentNode = null
path = null
nodeEvaluator.done()
segmentIndex = 0
pathFollowTarget = null
// In case a path is cancelled instead of completed, check if we should stop flying
if (pather.couldStopFlying() && !isAirborne(entity.level(), entity.blockPosition())) {
pather.setFlying(false)
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.entity.ai.steering
import com.cobblemon.mod.common.CobblemonNetwork.sendPacketToTrackingPlayers
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
import net.minecraft.world.phys.Vec3
// Runs a list of SteerRules in priority order, consuming a force budget.
// High priority rules get their full contribution first. Lower priority
// rules only get what budget remains.
class SteerArbitrator(
val entity: PokemonEntity,
val maxForce: Double,
val rules: MutableList<SteerRule> = mutableListOf()
) {
private var sortedRules: List<SteerRule> = emptyList()
private var dirty = true
fun addRule(rule: SteerRule) { rules.add(rule); dirty = true }
fun removeRule(rule: SteerRule) { rules.remove(rule); dirty = true }
fun clearRules() { rules.clear(); dirty = true }
fun calculate(debug: Boolean = false): Vec3 {
if (dirty) {
sortedRules = rules.sortedByDescending { it.priority }
dirty = false
}
var remaining = maxForce
var result = Vec3.ZERO
val debugLines = if (debug) mutableListOf<EntityDebugLine>() else null
for (rule in sortedRules) {
if (remaining <= 0.0) break
val force = rule.calculate() ?: continue
val scaled = force.scale(rule.weight.toDouble())
val len = scaled.length()
if (len <= 0.0) continue
val contribution = if (len <= remaining) {
remaining -= len
scaled
} else {
val partial = remaining
remaining = 0.0
scaled.normalize().scale(partial)
}
result = result.add(contribution)
debugLines?.let {
val (r, g, b) = rule.debugColor
it.add(EntityDebugLine(contribution.scale(50.0), r, g, b, 1.0f))
}
}
debugLines?.let {
if (result.lengthSqr() > 0.0)
it.add(EntityDebugLine(result.scale(50.0), 1f, 1f, 1f, 1.5f))
if (it.isNotEmpty())
sendPacketToTrackingPlayers(entity, ClientboundUpdateObsAvoidPacket(entity.id, it))
}
return result
}
}
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.entity.ai.steering
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import net.minecraft.world.phys.Vec3
// A per-tick steering force contributor evaluated by SteerArbitrator.
// Return null from calculate() if the rule has nothing to say this tick.
interface SteerRule {
val entity: PokemonEntity
// Higher = evaluated first, consumes force budget before lower priority rules.
// 90-100: obstacle avoidance
// 60-89: threat / boundary
// 30-59: social (separation, alignment, cohesion)
// 0-29: goal / wander
val priority: Int
// Multiplied against the rule output before budget accounting.
val weight: Float
// RGB 0-1 for the debug line drawn by SteerArbitrator.
val debugColor: Triple<Float, Float, Float> get() = Triple(1f, 1f, 1f)
fun calculate(): Vec3?
}
@@ -0,0 +1,76 @@
/*
* 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.ai.steering.rules
import com.cobblemon.mod.common.entity.ai.steering.SteerRule
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.entity.pokemon.extensions.steerToTarget
import net.minecraft.world.phys.Vec3
import kotlin.math.max
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.math.cos
// Forward AABB projection obstacle avoidance. Stateful — one instance per entity.
class ObstacleAvoidRule(
override val entity: PokemonEntity,
override val priority: Int = 100,
override val weight: Float = 1.0f,
override val debugColor: Triple<Float, Float, Float> = Triple(1f, 0f, 0f),
val maxForce: Double = 0.02,
val lookAheadTicks: Int = 10,
val smoothingAlpha: Double = 0.25
) : SteerRule {
private var prevForce: Vec3 = Vec3.ZERO
override fun calculate(): Vec3? {
val topSpeed = (entity.moveControl as? PokemonMoveControl)?.steerSpeed ?: 0.1
val forwardVec = if (entity.deltaMovement.horizontalDistanceSqr() < 0.001) {
val yaw = Math.toRadians(entity.yRot.toDouble())
Vec3(-sin(yaw), 0.0, cos(yaw))
} else {
Vec3(entity.deltaMovement.x, 0.0, entity.deltaMovement.z).normalize()
}
val lookAheadDist = max(entity.deltaMovement.length() * lookAheadTicks, entity.bbWidth / 2.0)
val collBox = entity.boundingBox.move(forwardVec.scale(lookAheadDist))
val shapes = entity.level().getBlockCollisions(entity, collBox)
val nearestShape = shapes.minByOrNull { shape ->
shape.bounds().center.distanceToSqr(entity.position())
}
if (nearestShape == null) {
prevForce = prevForce.lerp(Vec3.ZERO, smoothingAlpha)
return if (prevForce.lengthSqr() < 1e-6) null else prevForce
}
val obsCenter = nearestShape.bounds().center
val toObs = obsCenter.subtract(entity.position())
val projVec = forwardVec.scale(toObs.dot(forwardVec))
val lateralComponent = toObs.subtract(projVec)
val avoidForce = if (lateralComponent.lengthSqr() < 0.001) {
// Dead ahead — turn right
val rightVec = forwardVec.cross(Vec3(0.0, 1.0, 0.0))
entity.steerToTarget(entity.position().add(rightVec), topSpeed, maxForce, horzOnly = true)
} else {
var avoidanceVec = Vec3(lateralComponent.x * -1.0, 0.0, lateralComponent.z * -1.0)
val avoidStrength = sqrt(1.0 - (avoidanceVec.length() / (entity.bbWidth / 2.0 + 0.5)).coerceIn(0.0, 0.7))
val avoidancePoint = entity.position().add(avoidanceVec)
entity.steerToTarget(avoidancePoint, topSpeed, maxForce * avoidStrength, horzOnly = true)
}
prevForce = prevForce.lerp(avoidForce, smoothingAlpha)
return prevForce
}
}
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.entity.ai.steering.rules
import com.cobblemon.mod.common.entity.ai.steering.SteerRule
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.entity.pokemon.extensions.steerToTarget
import net.minecraft.world.phys.Vec3
// Steers toward a target position. Reads speed from PokemonMoveControl.steerSpeed.
// Return null from targetProvider to deactivate for a tick.
class SeekRule(
override val entity: PokemonEntity,
override val priority: Int = 10,
override val weight: Float = 1.0f,
override val debugColor: Triple<Float, Float, Float> = Triple(0f, 1f, 1f),
val maxForce: Double,
val horzOnly: Boolean = false,
val arrivalRange: Double? = null,
val targetProvider: (PokemonEntity) -> Vec3?
) : SteerRule {
override fun calculate(): Vec3? {
val target = targetProvider(entity) ?: return null
val speed = (entity.moveControl as? PokemonMoveControl)?.steerSpeed ?: 0.1
return entity.steerToTarget(target, speed, maxForce, arrivalRange, horzOnly)
}
}
@@ -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.ai.steering.rules
import com.cobblemon.mod.common.entity.ai.steering.SteerRule
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.entity.pokemon.extensions.steerToTarget
import net.minecraft.world.phys.Vec3
import kotlin.math.sin
// Steers toward a point directly ahead of the entity's current velocity/yaw.
// Useful as a baseline test rule.
class WalkForwardRule(
override val entity: PokemonEntity,
override val priority: Int = 10,
override val weight: Float = 1.0f,
override val debugColor: Triple<Float, Float, Float> = Triple(0f, 1f, 0f),
val maxForce: Double = 0.02
) : SteerRule {
override fun calculate(): Vec3? {
val speed = (entity.moveControl as? PokemonMoveControl)?.steerSpeed ?: 0.1
val forwardVec = if (entity.deltaMovement.horizontalDistanceSqr() < 0.001) {
val yaw = Math.toRadians(entity.yRot.toDouble())
Vec3(-sin(yaw), 0.0, cos(yaw))
} else {
Vec3(entity.deltaMovement.x, 0.0, entity.deltaMovement.z).normalize()
}
val pointAhead = entity.position().add(forwardVec)
return entity.steerToTarget(pointAhead, speed, maxForce, horzOnly = true)
}
}
private fun cos(d: Double) = kotlin.math.cos(d)
private fun sin(d: Double) = kotlin.math.sin(d)
@@ -0,0 +1,71 @@
/*
* 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.ai.steering.rules
import com.cobblemon.mod.common.entity.ai.steering.SteerRule
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.entity.pokemon.extensions.steerToTarget
import net.minecraft.util.Mth
import net.minecraft.world.phys.Vec3
import kotlin.math.abs
import kotlin.math.sin
import kotlin.random.Random
// Reynolds wander — projects a sphere ahead and offsets a point on it each tick.
// Stateful: holds current wander angles, one instance per entity.
// https://slsdo.github.io/steering-behaviors/
class WanderRule(
override val entity: PokemonEntity,
override val priority: Int = 10,
override val weight: Float = 1.0f,
override val debugColor: Triple<Float, Float, Float> = Triple(1f, 0.5f, 0f),
val maxForce: Double = 0.02,
val wanderDisp: Double = 15.0,
val centerDistance: Double = 4.0,
val wanderRadius: Double = 4.0,
val horzOnly: Boolean = false //TODO: all of these should be checks if flying or walking
) : SteerRule {
var currHorzWander = 0.0
var currVertWander = 0.0
override fun calculate(): Vec3? {
val speed = (entity.moveControl as? PokemonMoveControl)?.steerSpeed ?: 0.1
val normVec = if (entity.deltaMovement.lengthSqr() > 0.0001) {
entity.lookAngle.normalize()
} else {
Vec3(
Random.nextDouble(-1.0, 1.0),
Random.nextDouble(-1.0, 1.0),
Random.nextDouble(-1.0, 1.0)
).normalize()
}
var wanderOff = normVec
// Clamp vertical wander when getting too steep
val currPitchS = sin(Mth.atan2(wanderOff.y, wanderOff.horizontalDistance()))
if (currPitchS >= 0.5f || currPitchS <= -0.5f) currVertWander = 0.0
wanderOff = wanderOff.xRot(Math.toRadians(currVertWander).toFloat())
wanderOff = wanderOff.yRot(Math.toRadians(currHorzWander).toFloat())
val wanderCenter = entity.position().add(normVec.scale(centerDistance))
val wanderPoint = wanderCenter.add(wanderOff.scale(wanderRadius))
// Advance wander angles
currVertWander = Mth.wrapDegrees(currVertWander + Random.nextDouble(-wanderDisp, wanderDisp))
currHorzWander = Mth.wrapDegrees(currHorzWander + Random.nextDouble(-wanderDisp, wanderDisp))
val force = entity.steerToTarget(wanderPoint, speed, maxForce)
return if (horzOnly) Vec3(force.x, 0.0, force.z) else force
}
}
@@ -27,6 +27,7 @@ import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluenc
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence.Companion.TYPICAL_VARIATION
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
import com.cobblemon.mod.common.api.spawning.spawner.FishingSpawnerFactory
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.client.sound.EntitySoundTracker
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
@@ -762,7 +763,6 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
lureLevel: Int
) {
val spawner = BestSpawner.fishingSpawner
val bucketInfluence = BucketNormalizingInfluence(tier = lureLevel + luckOfTheSeaLevel)
val spawnCause = FishingSpawnCause(
spawner = spawner,
@@ -771,14 +771,21 @@ class PokeRodFishingBobberEntity(type: EntityType<out PokeRodFishingBobberEntity
lureLevel = lureLevel,
)
val spawnablePosition = FishingSpawnablePosition(
cause = spawnCause,
val ctx = FishingSpawnerFactory.Context(
player = player,
rodStack = rodItemStack,
lureLevel = lureLevel,
luckOfTheSeaLevel = luckOfTheSeaLevel,
world = level() as ServerLevel,
pos = position().toBlockPos(),
influences = mutableListOf(
PlayerLevelRangeInfluence(player, TYPICAL_VARIATION),
bucketInfluence
)
spawner = spawner
)
val spawnablePosition = FishingSpawnablePosition(
cause = spawnCause,
world = ctx.world,
pos = ctx.pos,
influences = FishingSpawnerFactory.buildPositionInfluences(ctx)
)
val result = spawner.calculateSpawnActionForPosition(
@@ -164,6 +164,7 @@ import net.minecraft.world.item.ItemUtils
import net.minecraft.world.item.Items
import net.minecraft.world.level.Level
import net.minecraft.world.level.LightLayer
import net.minecraft.world.level.block.HorizontalDirectionalBlock
import net.minecraft.world.level.block.SuspiciousEffectHolder
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.gameevent.GameEvent
@@ -174,10 +175,7 @@ import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import java.util.*
import java.util.concurrent.CompletableFuture
import kotlin.math.PI
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
import kotlin.math.*
@Suppress("unused")
open class PokemonEntity(
@@ -213,6 +211,7 @@ open class PokemonEntity(
@JvmStatic var RIDE_BOOSTS = SynchedEntityData.defineId(PokemonEntity::class.java, RideBoostsDataSerializer)
@JvmStatic var RIDE_STAMINA = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.FLOAT)
@JvmStatic var SCALE_MODIFIER = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.FLOAT)
@JvmStatic val IS_SPRINTING = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.BOOLEAN)
const val BATTLE_LOCK = "battle"
const val EVOLUTION_LOCK = "evolving"
@@ -292,6 +291,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
@@ -460,6 +462,7 @@ open class PokemonEntity(
builder.define(RIDE_BOOSTS, emptyMap())
builder.define(RIDE_STAMINA, 1F)
builder.define(SCALE_MODIFIER, 1F)
builder.define(IS_SPRINTING, false)
}
override fun onSyncedDataUpdated(data: EntityDataAccessor<*>) {
@@ -622,8 +625,12 @@ open class PokemonEntity(
isPokemonFlying = flyDist - flyDistO > 0.005F
isPokemonWalking = walkDist - walkDistO > 0.005F
if (passengers.isNotEmpty() && level().isClientSide) {
rideSoundManager.tick()
// TODO: Figure out a better way to allow for this. Or maybe just always do the animations data accumulation its really not that bad
if (this.pokemon.species.name.lowercase() == "charizard" || (passengers.isNotEmpty() && level().isClientSide)) {
// This is so ugly why am I doing it like this... even if its for a test this is low
if (passengers.isNotEmpty() && level().isClientSide) {
rideSoundManager.tick()
}
ridingAnimationData.update()
} else if (!passengers.isNotEmpty() && level().isClientSide) {
rideSoundManager.stop()
@@ -677,13 +684,10 @@ open class PokemonEntity(
yBodyRot = (spawnDirection * 1000F).toInt() / 1000F
}
if (this.tethering != null && !this.tethering!!.box.contains(this.x, this.y, this.z)) {
this.tethering = null
this.pokemon.recall()
}
jumping = false
checkPastureTether()
//This is so that pokemon in the pasture block are ALWAYS in sync with the pokemon box
//Before, pokemon entities in pastures would hold an old ref to a pokemon obj and changes to that would not appear to the underlying file
if (this.tethering != null && age % 20 == 0) {
@@ -705,6 +709,40 @@ open class PokemonEntity(
schedulingTracker.update(1 / 20F)
}
// Check to see if the entity needs recalled or repositioned if pastured.
fun checkPastureTether() {
// Teleport a pastured mon back to the pasture block if they are out of range
if (this.tethering != null && !this.tethering!!.box.contains(this.x, this.y, this.z)) {
// Gather the ideal position (behind the pastureBlock)
val pastureBlockPos = this.tethering!!.pasturePos
val state = this.level().getBlockState(pastureBlockPos)
// Check to ensure we are working with a pastureblock here and it hasn't been broken
if (state.block == CobblemonBlocks.PASTURE) {
val direction = state.getValue(HorizontalDirectionalBlock.FACING)
val width = this.boundingBox.xsize
val tpPos = pastureBlockPos.offset(direction.normal.multiply(ceil(width).toInt() + 1)).center
val heightAllowance = 4.0 // Allow spawning up to x blocks above the pastureBlock y level
// Attempt tp and recall if it fails
if (!this.randomTeleport(tpPos.x, tpPos.y + heightAllowance, tpPos.z, false)) {
// If the teleport was not successful then recall the mon
this.tethering = null
this.pokemon.recall()
} else {
// Stop any now invalid navigation post teleport
this.navigation.stop()
}
} else {
// If the block is destroyed or something else went wrong.
this.tethering = null
this.pokemon.recall()
}
}
}
override fun customServerAiStep() {
this.getBrain().tick(level() as ServerLevel, this)
// PokemonBrain.updateActivities(this)
@@ -2119,7 +2157,9 @@ open class PokemonEntity(
override fun stopSeenByPlayer(player: ServerPlayer) {
if (this.ownerUUID == player.uuid && tethering == null) {
// queuedToDespawn = true
this.remove(RemovalReason.DISCARDED)
if (this.removalReason != RemovalReason.DISCARDED) {
this.remove(RemovalReason.DISCARDED)
}
return
}
}
@@ -0,0 +1,547 @@
/*
* 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.CobblemonActivities
import com.cobblemon.mod.common.CobblemonNetwork.sendPacketToTrackingPlayers
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.extensions.*
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
import com.cobblemon.mod.common.util.math.geometry.toRadians
import com.cobblemon.mod.common.util.resolveDouble
import net.minecraft.util.Mth
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.control.MoveControl
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.level.ClipContext
import net.minecraft.world.phys.HitResult
import net.minecraft.world.phys.Vec3
import kotlin.math.*
import kotlin.random.Random
class BoidsMoveControl(val pokemonEntity: PokemonEntity, val target: LivingEntity) : MoveControl(pokemonEntity) {
companion object {
const val VERY_CLOSE = 2.500000277905201E-3
const val MAX_SPEED = 0.3 / 2
const val MAX_FORCE = 0.02
const val PREDATOR_MAX_SPEED = 0.6
// const val MAX_SPEED = 0.2
// const val MAX_FORCE = 0.013
}
// TODO: might need to hold its own velocity so minecraft doesn't do its nonsense to it?
// Can handle collisions ourselves as well
var speed: Double? = null
var drawLines: MutableList<EntityDebugLine> = mutableListOf()
// wander vars
var currHorzWander = 0.0
var currVertWander = 0.0
val wanderDisp = 15.0 // The degrees of change from tick to tick in wander angle
override fun tick() {
// Clear since the info is sent and is now stale
drawLines.clear()
// Steering constants
val orbitRadius = 16.0
val targetPos = target.position().add(0.0, 4.0, 0.0)
val arrivalRange = 2.0 // in blocks
val species = pokemonEntity.pokemon.species.name
// Set this to ensure the flying pose is active
// ignore all the friction stuff from outside
speed?.let { pokemonEntity.setDeltaMovement(pokemonEntity.deltaMovement.normalize().scale(it)) }
// Constrain to rectangular boundary
val cubeWidth = 16.0
val cubeHeight = 16.0
val cubeCenter = target.position().add(0.0, cubeHeight / 2.0, 0.0)
//forceRectBoundary(cubeCenter, cubeWidth, cubeHeight)
// Apply boid forces
// Obstacle Avoidance
val whiskerRange = 1.0
val avoidanceAngle = 20.0 // x degrees to the left, right, up, and down
//val obsAvoidForce = whisObsAvoid(whiskerRange, avoidanceAngle)
// if (obsAvoidForce != null) {
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(obsAvoidForce.scale(0.5))
// }
// TODO: obviously don't hardcode each pokemon. Remove this after testing is finished.
if (pokemonEntity.pokemon.species.name.lowercase() == "charizard") {
pokemonEntity.setFlying(true)
pokemonEntity.resetFallDistance()
seekPrey(pokemonEntity)
} else if (pokemonEntity.pokemon.species.name.lowercase() == "garchomp") {
pokemonEntity.setFlying(false)
val walkSpeed = pokemonEntity.runtime.resolveDouble(pokemonEntity.behaviour.moving.walk.walkSpeed) * 1.5
val steerForce = pokemonEntity.steerToTarget(target.position(), walkSpeed, walkSpeed / 12)
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(steerForce.x, 0.0, steerForce.z)
// speed?.let { this.mob.setZza(speed!!.toFloat()) }
} else if (pokemonEntity.pokemon.species.name.lowercase() == "skiddo") {
pokemonEntity.setFlying(false)
//wander(pokemonEntity,1.0)
// val steerForce = pokemonEntity.steerToTargetForce(target.position(), MAX_SPEED, MAX_FORCE)
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(steerForce.x, 0.0, steerForce.z)
aviodPredators(pokemonEntity, includePlayer = true)
boid(isFlyingOrSwimming = false)
} else {
pokemonEntity.setFlying(true)
pokemonEntity.resetFallDistance()
wander(pokemonEntity,0.8)
boid()
aviodPredators(pokemonEntity, includePlayer = true)
// Force towards ground
forceRectBoundary(pokemonEntity, cubeCenter, cubeWidth, cubeHeight)
}
// val avoidForce = avoidTarget(target.position(), MAX_SPEED, MAX_FORCE, 10.0)
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(avoidForce.scale(1.0))
// Apply steering force to current movement
// val dm = pokemonEntity.deltaMovement
// val w_pos = pokemonEntity.position().add(Vec3(dm.x, 0.0, dm.z))
// val steerForce = steerToTargetForce(w_pos, MAX_SPEED, MAX_FORCE)
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(steerForce)
// Take note of speed again to avoid minecraft's friction logic
speed = pokemonEntity.deltaMovement.length() * 0.98
// Test out what this setting of speed does.
//speed?.let { this.mob.speed = speed!!.toFloat() }
// Limit upwards speed to be half of max speed. The boids kept bumping into the top of the box more so than birds
// would. Honestly birds probably mostly want to move in the x,z plane anyhow
// pokemonEntity.deltaMovement = Vec3(
// pokemonEntity.deltaMovement.x,
// min(pokemonEntity.deltaMovement.y, MAX_SPEED * 0.5),
// pokemonEntity.deltaMovement.y
// ).normalize().scale(speed!!)
pokemonEntity.orientEntityToTravel(0.4f)
if (false && drawLines.isNotEmpty()) { //TODO: Re-enable
sendPacketToTrackingPlayers(pokemonEntity, ClientboundUpdateObsAvoidPacket(pokemonEntity.id, drawLines))
}
}
private fun aviodPredators(entity: PokemonEntity, includePlayer: Boolean = false) {
val fearRadius = 5.0
val brain = entity.brain
val predators = brain.getMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES).map { nearbyEntities ->
nearbyEntities.filterIsInstance<PokemonEntity>().filter { pokemon ->
pokemon.brain.activeNonCoreActivity.orElse(null) == CobblemonActivities.POKEMON_BOIDS &&
pokemon.pokemon.species.name.lowercase() == "charizard" &&
entity.position().subtract(pokemon.position()).length() <= fearRadius
}
}.orElse(emptyList())
var totalEvasionVec = Vec3.ZERO
var predatorCount = predators.size
// If the player should be included in avoidance then add the player as a predator.
if (includePlayer) {
totalEvasionVec = totalEvasionVec.add(entity.avoidTarget(target.position(), MAX_SPEED * 1.2, MAX_FORCE * 2, fearRadius))
predatorCount += 1
}
// Average evasion forces for all predators in the area and apply it to the prey entity
if (predatorCount > 0) {
for (predator in predators) {
totalEvasionVec = totalEvasionVec.add(entity.avoidTarget(predator.position(), MAX_SPEED * 1.2, MAX_FORCE * 2, fearRadius))
}
totalEvasionVec = totalEvasionVec.scale(1.0 / predatorCount)
if (entity.isFlying() || entity.isSwimming) {
entity.deltaMovement = entity.deltaMovement.add(totalEvasionVec)
} else {
entity.deltaMovement = entity.deltaMovement.add(Vec3(totalEvasionVec.x, 0.0, totalEvasionVec.z))
}
}
}
/*************************************************************************************************************
Seek the closest prey and pursue
*************************************************************************************************************/
private fun seekPrey(predator: PokemonEntity) {
val SeekRadius = 20.0
val brain = predator.brain
// Do calcs for head height as it looks better this way than a charizard flying crotch first at a group of pidgeys
val kindaHeadHeight = predator.position()
val prey = brain.getMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES).map { nearbyEntities ->
nearbyEntities.filterIsInstance<PokemonEntity>().filter { pokemon ->
pokemon.brain.activeNonCoreActivity.orElse(null) == CobblemonActivities.POKEMON_BOIDS &&
(pokemon.pokemon.species.name.lowercase() == "pidgey" || pokemon.pokemon.species.name.lowercase() == "skiddo") &&
kindaHeadHeight.subtract(pokemon.position()).length() <= SeekRadius
}
}.orElse(emptyList())
// Find the closest prey and seek towards it
val nearestPrey = prey.minByOrNull { pokePrey ->
kindaHeadHeight.subtract(pokePrey.position()).length()
}
nearestPrey?.let { prey ->
// Do the actual attack
// TODO: make this not dumb
if(kindaHeadHeight.subtract(nearestPrey.position()).length() < 0.5 + predator.bbWidth + nearestPrey.bbWidth) {
//predator.doHurtTarget(nearestPrey)
}
// Steer towards the target
val steerForce = predator.steerToTarget(prey.position(), PREDATOR_MAX_SPEED, MAX_FORCE)
predator.deltaMovement = predator.deltaMovement.add(steerForce)
}
}
/*************************************************************************************************************
Whisker Obstacle Avoidance
This function ray casts in a cone in front of the entity
*************************************************************************************************************/
private fun whisObsAvoid(range: Double, lookWidth: Double): Vec3? {
// --- 1. Build the Orientation Matrix ---
val forward = pokemonEntity.lookAngle.scale(range)
// val tempUp = if (abs(forward.y) > 0.999) Vec3(0.0, 0.0, 1.0) else Vec3(0.0, 1.0, 0.0)
// val right = forward.cross(tempUp).normalize()
// val up = right.cross(forward).normalize()
//
// val orientation = Matrix3f()
// .setColumn(0, Vector3f(right.x.toFloat(), right.y.toFloat(), right.z.toFloat()))
// .setColumn(1, Vector3f(up.x.toFloat(), up.y.toFloat(), up.z.toFloat()))
// .setColumn(2, Vector3f(forward.x.toFloat(), forward.y.toFloat(), forward.z.toFloat()))
// --- 2. Define Whiskers in Simple, Local Space ---
val radLookWidth = lookWidth.toRadians().toFloat()
// val localWhiskers: List<Vector3f> = listOf(
// Vector3f(0f, 0f, 1f), // Forward
//// Vector3f(sin(radLookWidth), 0f, cos(radLookWidth)), // Left
//// Vector3f(-sin(radLookWidth), 0f, cos(radLookWidth)), // Right
//// Vector3f(0f, sin(radLookWidth), cos(radLookWidth)), // Up
//// Vector3f(0f, -sin(radLookWidth), cos(radLookWidth)) // Down
// )
//
// // --- 3. Transform Local Whiskers to World Space using the Matrix ---
// val whiskerOffsets = localWhiskers.map { localVec ->
// orientation.transform(localVec, Vector3f()).toVec3d().scale(range) // Transform and store in a new vector
// }
// Obtain all points to be raycasted to
val scaledLookVec = pokemonEntity.deltaMovement.normalize().scale(range)
val lookPoints: List<Vec3> = listOf(
forward,
// forward.yRot(radLookWidth),
// forward.yRot(-radLookWidth),
// forward.xRot(90.toRadians()),
// forward.xRot((-90).toRadians()),
)
// iterate through each point and raycast to it starting from the entities pos (may need to be from somewhere else)
// If a position is hit then add its avoidance force to the totalAvoidance forces.
var totalAvoidance: Vec3? = null
val level = pokemonEntity.level()
val startPos = pokemonEntity.position()
for (point in lookPoints) {
val endPos = startPos.add(point)
val hit = level.clip(
ClipContext(
startPos,
endPos,
ClipContext.Block.OUTLINE, // If this is being weird then go back to COLLIDER
ClipContext.Fluid.ANY, // Birds should probably avoid fluid
pokemonEntity
)
)
// Add the whisker to the debug lines to be drawn
drawLines.add(EntityDebugLine(endPos, 1.0f, 0.0f, 0.0f))
//visualizeRay(level, startPos, endPos, hit.type != HitResult.Type.MISS)
if (hit.type != HitResult.Type.MISS) {
// Get the aviodance vector as the vector from the block detection point
// to the pokemon.
// Also inversely scale the size of the vector based on the distance the pokemon
// is to the object. This means close obstacles are avoided more strongly
val hitDist = hit.location.distanceTo(pokemonEntity.eyePosition)
val avoidVec = startPos.subtract(endPos).normalize() //.scale(1.0 - (hitDist / range))
// Add avoid vec to total
totalAvoidance = totalAvoidance?.add(avoidVec) ?: avoidVec
}
}
// // Scale all forces down to the max foce
// if (totalAvoidance != null) {
// // Cap the max total avoidance desire
// if (totalAvoidance.length() > MAX_SPEED) totalAvoidance = totalAvoidance.normalize().scale(MAX_SPEED)
// // Convert to Reynolds steering force
// totalAvoidance = totalAvoidance.subtract(pokemonEntity.deltaMovement)
// // Cap the max total avoidance force
// if (totalAvoidance.length() > MAX_FORCE) totalAvoidance = totalAvoidance.normalize().scale(MAX_FORCE)
// } // Is this next one even cleaner?
return totalAvoidance?.let { desiredVelocity ->
// Cap the desired velocity (using lengthSqr is more efficient than length)
val cappedVelocity = if (desiredVelocity.lengthSqr() > MAX_SPEED * MAX_SPEED) {
desiredVelocity.normalize().scale(MAX_SPEED)
} else {
desiredVelocity
}
// Convert to Reynolds steering force
val steeringForce = cappedVelocity.subtract(pokemonEntity.deltaMovement)
// Cap the final steering force
if (steeringForce.lengthSqr() > MAX_FORCE * MAX_FORCE) {
steeringForce.normalize().scale(MAX_FORCE)
} else {
steeringForce
}
}
}
private fun wander(entity: PokemonEntity, forceScale: Double = 1.0) {
// Wander behaviour
/*************************************************************************************************************
* The wander behaviour is based of Reynolds wander behaviour which can be seen in the below link under wander
* This version however is adapted to 3dimensions.
* https://slsdo.github.io/steering-behaviors/
*************************************************************************************************************/
// Get curr steering positions in left/right and up/down offsets
val normVec = if(entity.deltaMovement.lengthSqr() > 0.0001) {
entity.lookAngle.normalize()
} else {
Vec3(
Random.nextDouble(-1.0, 1.0),
Random.nextDouble(-1.0, 1.0),
Random.nextDouble(-1.0, 1.0)
).normalize()
}
var wanderOff = normVec
val currPitchS = sin(Mth.atan2(wanderOff.y, wanderOff.horizontalDistance()))
if (currPitchS >= 0.5f || currPitchS <= -0.5f) { //TODO: make this y limit class wide
this.currVertWander = 0.0
}
wanderOff = wanderOff.xRot(this.currVertWander.toRadians())
wanderOff = wanderOff.yRot(this.currHorzWander.toRadians())
// Base the center off the current direction of travel
val centerDistance = 4.0 // How many blocks the wander 'sphere' is in front of the entity
val wanderCenter = entity.position().add(normVec.scale(centerDistance))
// Get the wander circle's radius
val wanderRadius = 4.0
val wanderPoint = wanderCenter.add(wanderOff.scale(wanderRadius))
// change wander points
val randVert = Random.nextDouble(-wanderDisp, wanderDisp)
val randHorz = Random.nextDouble(-wanderDisp, wanderDisp)
currVertWander = Mth.wrapDegrees(randVert + currVertWander)
currHorzWander = Mth.wrapDegrees(randHorz + currHorzWander)
drawLines.add(EntityDebugLine(wanderPoint.subtract(pokemonEntity.position()), 1.0f, 0.0f, 0.0f, 1.0f)) // Add WanderPoint
drawLines.add(EntityDebugLine(wanderCenter.subtract(pokemonEntity.position()), 0.0f, 1.0f, 0.0f, 1.0f)) // Add wander center
val wanderForce = entity.steerToTarget(wanderPoint, MAX_SPEED, MAX_FORCE)
if(pokemonEntity.isFlying() || pokemonEntity.isSwimming) {
entity.deltaMovement = pokemonEntity.deltaMovement.add(wanderForce.scale(forceScale))
} else {
entity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(wanderForce.x, 0.0, wanderForce.z).scale(forceScale))
}
}
private fun boid(isFlyingOrSwimming: Boolean = true) {
// TODO: maybe move this to a sensor that lives spins up on the boid activity creation
// Get a list of other boid entities in this entity's vicinity
val brain = pokemonEntity.brain
val boids = brain.getMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES).map { nearbyEntities ->
nearbyEntities.filterIsInstance<PokemonEntity>().filter { pokemon ->
pokemon.brain.activeNonCoreActivity.orElse(null) == CobblemonActivities.POKEMON_BOIDS
}
}.orElse(emptyList())
// separation
val separationForce = separation(boids)?.scale(0.8)
separationForce?.let { force ->
if( isFlyingOrSwimming) {
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(force)
} else {
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(force.x, 0.0, force.z))
}
}
// alignment
val alignmentForce = alignment(boids)?.scale(0.0)
alignmentForce?.let { force ->
if( isFlyingOrSwimming) {
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(force)
} else {
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(force.x, 0.0, force.z))
}
}
// cohesion
val cohesionForce = cohesion(boids)?.scale(0.6)
cohesionForce?.let { force ->
if( isFlyingOrSwimming) {
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(force)
} else {
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(force.x, 0.0, force.z))
}
}
}
private fun separation(boids: List<PokemonEntity>): Vec3? {
var seperationVec: Vec3? = null
// Gather the total separation vec of all close entities
boids.forEach { boid ->
val toTargetVec = pokemonEntity.position().subtract(boid.position())
val desiredSeperation = boid.bbWidth * 2.0 // Base the seperation distance on the model width
val currDist = toTargetVec.length()
if (currDist < desiredSeperation) {
if (seperationVec == null) seperationVec = Vec3.ZERO
// Make the seperation force inversely proprotional to the serperation distance
// Also, scale the force by the bbwidth of the model so
seperationVec = seperationVec.add(toTargetVec.normalize().scale((1.0 - (currDist / desiredSeperation)) ))
}
}
if (seperationVec != null) {
//if (seperationVec.length() > MAX_SPEED) seperationVec = seperationVec.normalize().scale(MAX_SPEED)
seperationVec = seperationVec.normalize().scale(MAX_SPEED)
seperationVec = seperationVec.subtract(pokemonEntity.deltaMovement)
if (seperationVec.length() > MAX_FORCE) {
seperationVec = seperationVec.normalize().scale(MAX_FORCE)
}
}
return seperationVec
}
private fun alignment(boids: List<PokemonEntity>): Vec3? {
val neighborDist = 4.0
var summedVel: Vec3? = null
var alignmentVec: Vec3? = null
var boidCount = 0
// Gather the average position of the boid
boids.forEach { boid ->
val vecToOther = boid.position().subtract(this.pokemonEntity.position())
val distToOther = vecToOther.length()
val widthOfViewInDeg = 180
val isInSightLine = pokemonEntity.deltaMovement.normalize().dot(vecToOther) > cos((widthOfViewInDeg / 2.0).toRadians())
if (isInSightLine && distToOther < neighborDist) {
if (summedVel == null) summedVel = Vec3.ZERO
summedVel = summedVel.add(boid.deltaMovement.normalize())
boidCount++
}
}
if (summedVel != null) {
// Get the average vel/alignment by dividing by the number of boids sampled
val averageVel = summedVel.scale(1.0 / boidCount)
//alignmentVec = if (averageVel.length() > MAX_SPEED) averageVel.normalize().scale(MAX_SPEED) else averageVel
alignmentVec = averageVel.normalize().scale(MAX_SPEED)
alignmentVec = alignmentVec.subtract(pokemonEntity.deltaMovement)
if (alignmentVec.length() > MAX_FORCE) {
alignmentVec = alignmentVec.normalize().scale(MAX_FORCE)
}
}
return alignmentVec
}
private fun cohesion(boids: List<PokemonEntity>): Vec3? {
//val neighborDist = 5.0
var summedPos: Vec3? = null
var cohesionVec: Vec3? = null
var boidCount = 0
val cohesionRadius = 8.0
// Gather the average position of the boid
boids.forEach { boid ->
val vecToOther = boid.position().subtract(this.pokemonEntity.position())
val distToOther = vecToOther.length()
val widthOfViewInDeg = 360
val isInSightLine = pokemonEntity.deltaMovement.normalize().dot(vecToOther) > cos((widthOfViewInDeg / 2.0).toRadians())
if (isInSightLine && distToOther < cohesionRadius) { // Only use members that are within a certain radius and eyeline
if (summedPos == null) summedPos = Vec3.ZERO
summedPos = summedPos.add(boid.position())
boidCount++
}
}
if (summedPos != null) {
// Get the average position by dividing by the number of boids sampled
val averagePos = summedPos.scale(1.0 / boidCount)
// Get the vector towards that average position and scale
cohesionVec = averagePos.subtract(pokemonEntity.position())
//if (cohesionVec.length() > MAX_SPEED) cohesionVec = cohesionVec.normalize().scale(MAX_SPEED)
cohesionVec = cohesionVec.normalize().scale(MAX_SPEED)
cohesionVec = cohesionVec.subtract(pokemonEntity.deltaMovement)
if (cohesionVec.length() > MAX_FORCE) {
cohesionVec = cohesionVec.normalize().scale(MAX_FORCE)
}
}
return cohesionVec
}
private fun forceRectBoundary(entity: PokemonEntity, basePos: Vec3, rectWidth: Double, rectHeight: Double) {
val maxBoundaryForce = MAX_FORCE //* 2.0
val correctiveForce = getRectCorrectiveForce(basePos, rectWidth, rectHeight)
if (correctiveForce != null) {
//val correctiveForce = steerToTargetForce(correctivePos, speedLimit, maxForce)
var steer = correctiveForce.subtract(entity.deltaMovement)
if (steer.length() > maxBoundaryForce) {
steer = steer.normalize().scale(maxBoundaryForce)
}
entity.setDeltaMovement(entity.deltaMovement.add(steer))
}
}
// Returns the desired force to correct the entity back into the rectangle area
private fun getRectCorrectiveForce( basePos: Vec3, rectWidth: Double, rectHeight: Double): Vec3? {
val entityPos = pokemonEntity.position()
val posFromBase = entityPos.subtract(basePos)
val cubeHalfWidth = rectWidth / 2.0 // Useful as the basePoint is in the middle of the cube
val cubeHalfHeight = rectHeight / 2.0
// Check the three possible faces for exceeding the boundary (x, y, and z faces of cube)
val xExceed = max(0.0, abs(posFromBase.x) - cubeHalfWidth)
val yExceed = max(0.0, abs(posFromBase.y) - cubeHalfHeight)
val zExceed = max(0.0, abs(posFromBase.z) - cubeHalfWidth)
return if ( xExceed == 0.0 && yExceed == 0.0 && zExceed == 0.0) {
null
} else {
var desiredPos = pokemonEntity.position().add(pokemonEntity.deltaMovement) // Add delta movement so we aren't slowing this guy down
if (xExceed != 0.0) desiredPos = Vec3( basePos.x, desiredPos.y, desiredPos.z)
if (yExceed != 0.0) desiredPos = Vec3( desiredPos.x, basePos.y, desiredPos.z)
if (zExceed != 0.0) desiredPos = Vec3( desiredPos.x, desiredPos.y, basePos.z)
return desiredPos.subtract(pokemonEntity.position()).normalize().scale(MAX_SPEED)
}
}
}
//val desiredScale = if (distToTarget > slowDownRadius) speedLimit else (distToTarget/slowDownRadius).pow(2) * speedLimit
//desiredVec = desiredVec.normalize().scale(desiredScale)
@@ -9,30 +9,47 @@
package com.cobblemon.mod.common.entity.pokemon.ai
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonNetwork.sendPacketToTrackingPlayers
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addStandardFunctions
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine.Companion.invoke
import com.cobblemon.mod.common.entity.PlatformType
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.ai.OmniPathNavigation
import com.cobblemon.mod.common.entity.ai.steering.SteerArbitrator
import com.cobblemon.mod.common.entity.ai.steering.rules.ObstacleAvoidRule
import com.cobblemon.mod.common.entity.ai.steering.rules.SeekRule
import com.cobblemon.mod.common.entity.ai.steering.rules.WanderRule
import com.cobblemon.mod.common.entity.ai.steering.rules.WalkForwardRule
import com.cobblemon.mod.common.entity.pokemon.PokemonBehaviourFlag
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.extensions.*
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
import com.cobblemon.mod.common.util.getWaterAndLavaIn
import com.cobblemon.mod.common.util.math.geometry.toDegrees
import com.cobblemon.mod.common.util.math.geometry.toRadians
import com.cobblemon.mod.common.util.resolveFloat
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.tags.BlockTags
import net.minecraft.tags.FluidTags
import net.minecraft.util.Mth
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.attributes.Attributes
import net.minecraft.world.entity.ai.control.MoveControl
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 kotlin.math.abs
import kotlin.math.cos
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.random.Random
class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemonEntity) {
companion object {
@@ -50,6 +67,41 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
var bankUpwardsBlocksPerTick = 0F
var bankRightDegreesPerTick = 1F
var obsCorrectDir = 0F
var drawLines: MutableList<EntityDebugLine> = mutableListOf()
var prevForce: Vec3? = null
var steerSpeed = this.gatherTopSpeed(pokemonEntity)
private val steerArbitrator by lazy {
val steerArbitrator = SteerArbitrator(
entity = pokemonEntity,
maxForce = 0.02,
rules = mutableListOf(
ObstacleAvoidRule(
entity = pokemonEntity,
debugColor = Triple(1f, 0f, 0f),
),
SeekRule(
entity = pokemonEntity,
priority = 10,
maxForce = 0.02,
debugColor = Triple(0f, 1f, 0f),
horzOnly = true,
targetProvider = { e ->
(e.navigation as? OmniPathNavigation)?.pathFollowTarget //TODO: maybe change this seems silly. Probably want a rule that checks explicitly?
}
),
WanderRule(
entity = pokemonEntity,
debugColor = Triple(0f, 1f, 1f),
horzOnly = true
)
)
)
steerArbitrator
}
fun startBanking(
forwardBlocksPerTick: Float = 0F,
upwardsBlocksPerTick: Float = 0F,
@@ -73,6 +125,7 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
}
override fun tick() {
mob.setDiscardFriction(false) // ignored while trying to move but if stopped or doing something else this needs reenabled
if (banking && operation != Operation.WAIT) {
banking = false
}
@@ -102,6 +155,7 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
val baseSpeed = mob.getAttributeValue(Attributes.MOVEMENT_SPEED).toFloat() * this.speedModifier.toFloat()
val adjustedSpeed = baseSpeed * mediumSpeed
if (banking) {
pokemonEntity.yRot += bankRightDegreesPerTick
pokemonEntity.travel(Vec3(0.0, bankUpwardsBlocksPerTick.toDouble(), bankForwardBlocksPerTick.toDouble()).scale(mediumSpeed.toDouble()))
@@ -110,11 +164,142 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
}
if (bankDurationTicks == 0) {
banking = false
operation = Operation.WAIT
}
}
// Here we call the wander on every pokemon no matter what then they wander then leave
// Just ignore all operatations and do whatever is being tested.
if (true) {
// val horzOnly = true
//
// var topSpeed = gatherTopSpeed(pokemonEntity)
// var maxForce = 0.02 // Arbitrarily chosen as of right now.
// pokemonEntity.isPokemonSprinting = false
//
// // grab forward travel vector direction
// val forwardVec = if (pokemonEntity.deltaMovement.horizontalDistanceSqr() < 0.001) {
// // No meaningful velocity so derive from yaw orientation instead
// val yaw = Math.toRadians(pokemonEntity.yRot.toDouble())
// Vec3(-sin(yaw), 0.0, cos(yaw))
// } else {
// if (horzOnly) Vec3(pokemonEntity.deltaMovement.x, 0.0, pokemonEntity.deltaMovement.z).normalize()
// else pokemonEntity.deltaMovement.normalize()
// }
// val pointAhead = pokemonEntity.position().add(forwardVec)
// val aheadForce = pokemonEntity.steerToTarget(pointAhead, topSpeed, maxForce, horzOnly = horzOnly)
//
//
//// val player = pokemonEntity.level().getNearestPlayer(pokemonEntity, 10.0) ?: return
//// // Flee direction — away from player, flattened to XZ
//// val toPlayerVec = pokemonEntity.position().subtract(player.position())
////
//// if (toPlayerVec.length() < 6.0) {
//// pokemonEntity.isPokemonSprinting = true
////// topSpeed = 0.4 // hardcoded high value
////// maxForce = 0.04
//// topSpeed *= 2.0f
//// maxForce *= 2
//// }
////
//// val fleePoint = pokemonEntity.position().add(toPlayerVec)
//// val aheadForce = pokemonEntity.steerToTarget(fleePoint, topSpeed, maxForce, horzOnly = horzOnly)
//
// // For now just move forward by seeking towards a point ahead
// val lookAheadTicks = 10 // How far in the future of this entities current movement to look ahead for collisions
// val lookAheadDist = max(pokemonEntity.deltaMovement.length() * lookAheadTicks, pokemonEntity.bbWidth / 2.0) // At least always look ahead the width of the mons own hitbox
//
// // Get the collision box
// val inflationAmount = 0.0 //pokemonEntity.bbWidth / 5.0 //TODO: is this needed? it seems to help a bit keeping them off walls but would stop them from entering gaps they truly could
// val collBox = pokemonEntity.boundingBox.move(forwardVec.scale(lookAheadDist)).inflate(inflationAmount, 0.0, inflationAmount) // Inflate a bit
// val shapes = pokemonEntity.level().getBlockCollisions(pokemonEntity, collBox)
//
// // Gather the nearest shape
// val nearestShape = shapes.minByOrNull { shape ->
// shape.bounds().center.distanceToSqr(pokemonEntity.position())
// }
//
// var avoidancForce = Vec3.ZERO
//
// if (nearestShape != null) {
// var hasLeftForce = false
// var hasRightForce = false
// val right = Vec3(-forwardVec.z, 0.0, forwardVec.x) //TODO: expand to 3 dimensions
//
// val obsCenter = nearestShape.bounds().center
// val toObs = obsCenter.subtract(pokemonEntity.position())
//
// // Get the lateral avoidance point to seek to (the avoidance force really)
// // It is the negated vector of toObs - Proj(toObs->forwardVec)
// val projVec = forwardVec.scale(toObs.dot(forwardVec)) // Can do this because forwardVec is normalized
// val lateralComponent = toObs.subtract(projVec) // TODO: moved back so that two opposing forces turn the dude around
//
// avoidancForce = if (lateralComponent.lengthSqr() < 0.001) { // If its dead ahead then just turn right
// val rightVec = forwardVec.cross(Vec3(0.0,1.0,0.0))
// obsCorrectDir = 1F
// pokemonEntity.steerToTarget(pokemonEntity.position().add(rightVec), topSpeed, maxForce, horzOnly = horzOnly)
// } else {
// // Scale the strength of the avoidance vec based off how close to being dead center it is
//
// var avoidanceVec = lateralComponent.scale(-1.0)
// if (horzOnly) avoidanceVec = Vec3(avoidanceVec.x, 0.0, avoidanceVec.z)
// //var avoidStrength = (abs(toObs.dot(avoidanceVec)) / (pokemonEntity.bbWidth / 2.0) ).coerceIn(0.0, 1.0)
//
// // 0.5 to add half the block width allowance since if it catches the corner of a block it still gets the center
// var avoidStrength = sqrt(1 - (avoidanceVec.length() / (pokemonEntity.bbWidth / 2.0 + 0.5) ).coerceIn(0.0, 0.7))
// // Track which side this force is pushing toward
// //val newObsCorrectDir = if (avoidanceVec.normalize().dot(right) > 0) 1F else -1f
//
// // If the new direction is opposite the last direction then invert it
//// if (obsCorrectDir == 0F) {
//// obsCorrectDir = newObsCorrectDir
//// } else if (newObsCorrectDir != obsCorrectDir) {
//// avoidanceVec = avoidanceVec.scale(-1.0)
//// //avoidStrength = 1.0
//// }
//
// val avoidancePoint = pokemonEntity.position().add(avoidanceVec)
// pokemonEntity.steerToTarget(avoidancePoint, topSpeed, maxForce * avoidStrength, horzOnly = horzOnly)
// }
// } else {
// obsCorrectDir = 0F
// }
//
// // accumulte forces
// // TODO: steertoTarget also checks stuff like this. Should that be different? The forces could compound here
// val accumForce = aheadForce.scale(0.5).add(avoidancForce.scale(1.0))
// var finalForce = if( avoidancForce == Vec3.ZERO) aheadForce
// else avoidancForce
//
// prevForce?.let { finalForce = it.lerp(finalForce, 0.1) }
// prevForce = finalForce
//
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(finalForce)
//
// // Must be called to make sure we have complete control over our entities velocity.
// mob.speed = 0f
// mob.setZza(0f)
// mob.setXxa(0f)
// mob.setDiscardFriction(true)
// pokemonEntity.orientEntityToTravel(1.0f)
// pokemonEntity.setFlying(false)
//
// drawLines.add(EntityDebugLine(forwardVec))
// if (drawLines.isNotEmpty()) {
// sendPacketToTrackingPlayers(pokemonEntity, ClientboundUpdateObsAvoidPacket(pokemonEntity.id, drawLines.toList()))
// drawLines.clear()
// }
if (operation == Operation.STRAFE) {
steerSpeed = gatherTopSpeed(pokemonEntity)
val force = steerArbitrator.calculate(debug = true)
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(force)
mob.speed = 0f
mob.setZza(0f)
mob.setXxa(0f)
mob.setDiscardFriction(true)
pokemonEntity.orientEntityToTravel(1.0f)
} else if (operation == Operation.STRAFE) {
var movingDistanceTotal = Mth.sqrt(strafeForwards * strafeForwards + strafeRight * strafeRight)
if (movingDistanceTotal < 1.0f) {
movingDistanceTotal = 1.0f
@@ -161,10 +346,14 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
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
// NEW
pokemonEntity.orientEntityToTravel(1.0f)
// OLD
// 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)
@@ -191,15 +380,101 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
val scale = min(adjustedSpeed.toDouble(), fullDistance.length())
mob.deltaMovement = direction.scale(scale)
val maxForce = 0.02 // 0.02
// Gather the point to be steered to
val nav = pokemonEntity.navigation
nav.pathFollowTarget?.let { rt ->
wantedX = rt.x
wantedZ = rt.z
wantedY = rt.y
}
// + 0.05 for dealing with swimming out of water, they otherwise get stuck on the lip
val steerForce = pokemonEntity.steerToTarget(Vec3(wantedX, refine(wantedY + 0.05), wantedZ), scale, maxForce)
mob.deltaMovement = mob.deltaMovement.add(steerForce)
//mob.deltaMovement = direction.scale(scale)
xDist = fullDistance.x
zDist = fullDistance.z
yDist = fullDistance.y
} 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
var forwardSpeed = adjustedSpeed
var forceDiv = 1 / 5.0 // Portion of max speed that force can apply every tick
var minForce = 0.02 // If its not at least this it breaks quite a few things. looking at you snorlax
val finalGoal: BlockPos? = pokemonEntity.navigation.path?.target
val distanceToFinalGoal = finalGoal?.let {
pokemonEntity.position().distanceTo(it.center).toFloat()
} ?: 0.0f
if (distanceToFinalGoal > 8f) { //TODO: scale this to hitbox size
pokemonEntity.isPokemonSprinting = true
forwardSpeed *= 1.4f
forceDiv = 1 / 10.0
minForce *= 1.0 //TODO: figure out what to actually do for this
} else {
pokemonEntity.isPokemonSprinting = false
}
// New way
val blockFriction = mob.level()
.getBlockState(mob.getBlockPosBelowThatAffectsMyMovement())
.block.getFriction()
val frictionInfluencedSpeed = if (mob.onGround()) {
forwardSpeed * (0.21600002f / (blockFriction * blockFriction * blockFriction))
} else {
0.02f // getFlyingSpeed() default for non-player controlled. Also hopefully our checks above this caught this case
}
val baseForce = (forwardSpeed * 0.98) * frictionInfluencedSpeed
// We find where the equilibrium between vanilla friction and the input force usually is to get a topSpeed for this mon.
// Could be replaced in the future with direct topSpeed calculations in the pokemon species form
val frictionAppliedAfter = blockFriction * 0.91
val topSpeed = baseForce / (1 - frictionAppliedAfter)
val currSpeed = pokemonEntity.deltaMovement.length()
//TODO: is this needed?
// if (!pokemonEntity.isPokemonSprinting && currSpeed > topSpeed) { // TODO: this might mess up some stuff like tnt pushing mobs around
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.normalize().scale(lerp(currSpeed, topSpeed, 0.1))
// }
// Get a force that looks natural
val maxForce = minForce //max(baseForce * forceDiv, minForce)
// Gather the point to be steered to
val nav = pokemonEntity.navigation
nav.pathFollowTarget?.let { rt ->
wantedX = rt.x
wantedZ = rt.z
wantedY = rt.y
}
val steerForce = pokemonEntity.steerToTarget(
Vec3(wantedX, wantedY, wantedZ),
topSpeed, // Is never reached as minecraft relies on top speed being obtained when force=friction
maxForce,
horzOnly = true
)
// If this happens a lot then we have issues
val mdt = LivingEntity.MIN_MOVEMENT_DISTANCE
if ( abs(steerForce.x) < mdt || abs(steerForce.y) < mdt || abs(steerForce.z) < mdt) {
//Cobblemon.LOGGER.info("INFO: Steer Force dimension smaller than minimum movement distance value") //TODO: figure this out
}
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(steerForce)
//pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(0.0103766, 0.0, 0.0))
mob.speed = 0f
mob.setZza(0f)
mob.setXxa(0f)
mob.setDiscardFriction(true)
// Old way
// mob.speed = forwardSpeed
}
if (!verticalHandled) {
@@ -280,6 +555,41 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
}
}
fun gatherTopSpeed( pokemonEntity: PokemonEntity): Double {
val behaviour = pokemonEntity.behaviour
val mediumSpeed = runtime.resolveFloat(if (pokemonEntity.getCurrentPoseType() in setOf(PoseType.FLY, PoseType.HOVER)) {
behaviour.moving.fly.flySpeedHorizontal
} else if (pokemonEntity.isEyeInFluid(FluidTags.WATER) || pokemonEntity.isEyeInFluid(FluidTags.LAVA)) {
behaviour.moving.swim.swimSpeed
} else {
behaviour.moving.walk.walkSpeed
}) * 2.5F //TODO: idk why this is 2.5 but I think we're stuck with it
val baseSpeed = mob.getAttributeValue(Attributes.MOVEMENT_SPEED).toFloat() * 0.4f //TODO: does this ever need to not be hardcoded?
val adjustedSpeed = baseSpeed * mediumSpeed
val forwardSpeed = adjustedSpeed
val blockFriction = mob.level()
.getBlockState(mob.getBlockPosBelowThatAffectsMyMovement())
.block.getFriction()
val frictionInfluencedSpeed = if (mob.onGround()) {
forwardSpeed * (0.21600002f / (blockFriction * blockFriction * blockFriction))
} else {
0.02f // getFlyingSpeed() default for non-player controlled
}
// Force applied every tick through the usual vanilla route
val baseForce = (forwardSpeed * 0.98) * frictionInfluencedSpeed
// We find where the equilibrium between vanilla friction and the input force usually is to get a topSpeed for this mon.
// Could be replaced in the future with direct topSpeed calculations in the pokemon species form
val frictionAppliedAfter = blockFriction * 0.91
val topSpeed = baseForce / (1 - frictionAppliedAfter)
return topSpeed
}
/*
* 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.
@@ -0,0 +1,56 @@
/*
* 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.sensors
import com.cobblemon.mod.common.CobblemonMemories
import com.google.common.collect.ImmutableSet
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.sensing.Sensor
import net.minecraft.world.item.Items
import net.minecraft.world.entity.player.Player
class PlayerDebuggingSensor : Sensor<LivingEntity>(SCAN_INTERVAL) {
override fun doTick(level: ServerLevel, entity: LivingEntity) {
var diamondHolder: LivingEntity? = null
diamondHolder = entity.brain.getMemory(CobblemonMemories.DIAMOND_DEBUGGER).orElse(null)?.let {
if(it.mainHandItem.`is`(Items.DIAMOND)) {
it
} else {
null
}
}
if (diamondHolder == null) {
val nearEntities = entity.brain.getMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES).orElse(mutableListOf())
diamondHolder = nearEntities?.find { entity ->
entity.mainHandItem.`is`(Items.DIAMOND)
}
}
if (diamondHolder != null && diamondHolder is Player) {
entity.brain.setMemory(CobblemonMemories.DIAMOND_DEBUGGER, diamondHolder)
} else {
entity.brain.eraseMemory(CobblemonMemories.DIAMOND_DEBUGGER)
}
}
override fun requires(): Set<MemoryModuleType<*>> {
return setOf(
CobblemonMemories.DIAMOND_DEBUGGER,
MemoryModuleType.NEAREST_LIVING_ENTITIES
)
}
companion object {
const val SCAN_INTERVAL = 10
}
}
@@ -0,0 +1,40 @@
//
///*
// * 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.tasks;
//
//import com.cobblemon.mod.common.CobblemonMemories
//import com.cobblemon.mod.common.api.pokemon.status.Statuses
//import com.cobblemon.mod.common.api.storage.party.PartyStore
//import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
//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
//
//object BoidsTask {
//fun create(): OneShot<PokemonEntity> {
// return BehaviorBuilder.create {
// it.group(
// it.absent(CobblemonMemories.POKEMON_DROWSY)
// ).apply(it) { _ ->
// Trigger { world, entity, _ ->
// if (entity.pokemon.status?.status == Statuses.SLEEP && entity.pokemon.storeCoordinates.get()?.store !is PartyStore) {
// entity.pokemon.status = null
// entity.brain.eraseMemory(CobblemonMemories.POKEMON_SLEEPING)
// entity.brain.useDefaultActivity()
// return@Trigger true
// } else {
// return@Trigger false
// }
// }
// }
// }
//}
//}
@@ -0,0 +1,111 @@
package com.cobblemon.mod.common.entity.pokemon.extensions
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.math.geometry.toDegrees
import net.minecraft.util.Mth
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.phys.Vec3
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.sign
/*************************************************************************************************************
Steering to Target
Detailed in: https://www.red3d.com/cwr/steer/gdc99/
*************************************************************************************************************/
fun PokemonEntity.steerToTarget(
targetPos: Vec3,
maxSpeed: Double,
maxForce: Double,
arrivalRange: Double? = null,
horzOnly: Boolean = false
): Vec3 {
val cPos = if (horzOnly) Vec3(this.position().x, 0.0, this.position().z) else this.position()
val tPos = if (horzOnly) Vec3(targetPos.x, 0.0, targetPos.z) else targetPos
// Ensure we can't have a negative range here
val absArrivalRange = arrivalRange?.let { abs(arrivalRange) }
// Calculate the vector from the pokemons current position to the
// target position. Set its desired magnitude to max speed or a
// portion of it if within the arrivalRange
val toTargetVec = tPos.subtract(cPos)
val distToTarget = toTargetVec.length()
val desiredSpeed = if (absArrivalRange != null && distToTarget < absArrivalRange) (distToTarget/absArrivalRange) * maxSpeed else maxSpeed
val desiredVec = toTargetVec.normalize().scale(desiredSpeed)
var steer = if (horzOnly) desiredVec.subtract(Vec3(this.deltaMovement.x, 0.0, this.deltaMovement.z))
else desiredVec.subtract(this.deltaMovement)
if (steer.length() > maxForce) {
steer = steer.normalize().scale(maxForce)
}
return steer
}
/*************************************************************************************************************
Steering Target Avoidance
Detailed in: https://www.red3d.com/cwr/steer/gdc99/
*************************************************************************************************************/
fun PokemonEntity.avoidTarget(
targetPos: Vec3,
maxSpeed: Double,
maxForce: Double,
avoidanceRange: Double? = null
): Vec3 {
// Calculate the vector from the targets current position to the
// pokemon position. Set its desired magnitude to max speed scaled
// linearly from 1.0 when right next to the target and 0.0 when on the
// very edge of the avoidance Range
var fromTargetVec = this.position().subtract(targetPos)
// Calculate distance to the target
val distToTarget = fromTargetVec.length()
if (distToTarget < 1.0E-4) {fromTargetVec = Vec3(0.0, 1.0, 0.0)} // Ensure the avoidance vector can't be ZERO
// Return no force otherwise a desired vector will be calculated that slows the velocity.
if (avoidanceRange != null && distToTarget >= avoidanceRange) return Vec3.ZERO
// Scale the avoidance force linearly
val desiredSpeed = when {
avoidanceRange == null -> maxSpeed // If no avoidance range then just always flee as fast as you can
avoidanceRange == 0.0 -> maxSpeed // Avoid weird divide by zero nonsense
distToTarget < avoidanceRange -> (1.0 - (distToTarget / avoidanceRange)) * maxSpeed
else -> 0.0
}
//TODO: determine if this should be scaled by distance. Probably not
val desiredVec = fromTargetVec.normalize() //.scale(desiredSpeed)
// Steer the pokemon towards the desired vector
var steer = desiredVec.subtract(this.deltaMovement)
if (steer.length() > maxForce) {
steer = steer.normalize().scale(maxForce)
}
return steer
}
/*************************************************************************************************************
Lerp the mobs yRot to its direction of travel
*************************************************************************************************************/
fun PokemonEntity.orientEntityToTravel(k: Float = 1.0f) {
val interpAmount = k.coerceIn(0.0f, 1.0f) //TODO: This probably shouldn't be a lerp like this.
if (this.deltaMovement.length() < 0.01) return //LivingEntity.MIN_MOVEMENT_DISTANCE) return
// Ensure the entity is facing the direction it is moving in both pitch and yaw
val normVec = this.deltaMovement.normalize()
val targetAngle = Mth.atan2(normVec.z, normVec.x).toDegrees() - 90.0f
val currAngle = this.yRot
val angleDiff = Mth.wrapDegrees(targetAngle - currAngle)
val maxPerTickRotation = 30.0f // set really high to ignore for now 15.0f // TODO: is something like this needed or should it be a clientside spring or something?
val effRot = min(abs(angleDiff), maxPerTickRotation) * sign(angleDiff)
this.yRot = currAngle + (effRot * interpAmount)
//entity.xRot = asin(normVec.y.toFloat()).toDegrees()
}
@@ -31,7 +31,7 @@ class BaitEffectsComponent(
}
override fun equals(other: Any?): Boolean {
return other is BaitEffectsComponent && effects.size == other.effects.size && effects.all { it in other.effects }
return other is BaitEffectsComponent && effects == other.effects
}
override fun hashCode(): Int {
@@ -49,9 +49,7 @@ class FoodColourComponent(
}
override fun equals(other: Any?): Boolean {
return other is FoodColourComponent &&
colours.size == other.colours.size &&
colours.mapIndexed { index, value -> value == other.colours[index] }.all { it }
return other is FoodColourComponent && colours == other.colours // List == List in kotlin checks for element order and index.
}
fun getColoursAsARGB(): List<Int> {
@@ -20,11 +20,12 @@ class BerryRegistrySyncPacket(berries: Collection<Berry>) : DataRegistrySyncPack
}
override val id = ID
override fun encodeEntry(buffer: RegistryFriendlyByteBuf, entry: Berry) {
entry.encode(buffer)
}
override fun decodeEntry(buffer: RegistryFriendlyByteBuf): Berry? = Berry.decode(buffer)
override fun decodeEntry(buffer: RegistryFriendlyByteBuf): Berry = Berry.decode(buffer)
override fun synchronizeDecoded(entries: Collection<Berry>) {
Berries.reload(entries.associateBy { it.identifier })
@@ -9,40 +9,30 @@
package com.cobblemon.mod.common.net.messages.client.data
import com.cobblemon.mod.common.api.interaction.PokemonInteractionSet
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.interaction.PokemonInteractions
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
class PokemonInteractionsSyncPacket(
val speciesInteractions: List<PokemonInteractionSet>,
val generalInteractions: List<PokemonInteractionSet>,
) : NetworkPacket<PokemonInteractionsSyncPacket> {
entries: Collection<PokemonInteractionSet>,
) : DataRegistrySyncPacket<PokemonInteractionSet, PokemonInteractionsSyncPacket>(entries) {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeCollection(speciesInteractions) { it, speciesInteraction ->
speciesInteraction.encode(it)
}
buffer.writeCollection(generalInteractions) { it, generalInteraction ->
generalInteraction.encode(it)
}
override fun encodeEntry(buffer: RegistryFriendlyByteBuf, entry: PokemonInteractionSet) {
entry.encode(buffer)
}
override fun decodeEntry(buffer: RegistryFriendlyByteBuf): PokemonInteractionSet {
return PokemonInteractionSet.decode(buffer)
}
override fun synchronizeDecoded(entries: Collection<PokemonInteractionSet>) {
PokemonInteractions.interactions.clear()
PokemonInteractions.interactions.addAll(entries)
}
companion object {
val ID = cobblemonResource("pokemon_interactions_sync")
fun decode(buffer: RegistryFriendlyByteBuf): PokemonInteractionsSyncPacket {
val speciesInteractions = buffer.readList { it ->
PokemonInteractionSet.decode(it)
}
val generalInteractions = buffer.readList { it ->
PokemonInteractionSet.decode(it)
}
return PokemonInteractionsSyncPacket(
speciesInteractions,
generalInteractions,
)
}
fun decode(buffer: RegistryFriendlyByteBuf): PokemonInteractionsSyncPacket = PokemonInteractionsSyncPacket(emptyList()).apply { decodeBuffer(buffer) }
}
}
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.debug
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeCollection
import com.cobblemon.mod.common.util.writeString
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.phys.Vec3
/**
* Packet sent to the client to update the obstacle avoidance lines to be rendered
* while debugging
*
* @author Jackson
* @since September 15th, 2025
*/
class ClientboundUpdateObsAvoidPacket(val entityId: Int, val lines: List<EntityDebugLine>) : NetworkPacket<ClientboundUpdateObsAvoidPacket> {
override val id: ResourceLocation = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeInt(entityId)
buffer.writeCollection(lines) { _, linePair ->
linePair.encode(buffer)
}
}
companion object {
val ID = cobblemonResource("obstacle_avoidance_lines")
fun decode(buffer: RegistryFriendlyByteBuf): ClientboundUpdateObsAvoidPacket {
val entityId = buffer.readInt()
val lines = buffer.readList { buf ->
EntityDebugLine.decode(buf as RegistryFriendlyByteBuf)
}
return ClientboundUpdateObsAvoidPacket(entityId, lines)
}
}
}
Loaded 100 of 494 files, more files were not shown because too many files have changed in this diff. Show more