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
76 changed files with 2401 additions and 99 deletions

No files matched your search

+7
View File
@@ -2,6 +2,7 @@
## [1.7.4 (MONTH Xth, 2026)](#1-7-4)
### Additions
- 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}
@@ -37,15 +38,21 @@
- [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
- 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
- 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:
+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)
@@ -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();
@@ -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> {
@@ -23,6 +23,7 @@ import com.cobblemon.mod.common.client.net.data.CobblemonMechanicsSyncHandler
import com.cobblemon.mod.common.client.net.data.DataRegistrySyncPacketHandler
import com.cobblemon.mod.common.client.net.data.RideSettingsSyncHandler
import com.cobblemon.mod.common.client.net.debug.OpenRidingStatsDebugGUIHandler
import com.cobblemon.mod.common.client.net.debug.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.PokeSnackBlockParticlesHandler
@@ -80,6 +81,7 @@ import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyMoveCallba
import com.cobblemon.mod.common.net.messages.client.cooking.SeasoningRegistrySyncPacket
import com.cobblemon.mod.common.net.messages.client.cooking.ToggleCookingPotLidPacket
import com.cobblemon.mod.common.net.messages.client.data.*
import com.cobblemon.mod.common.net.messages.client.debug.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
@@ -253,7 +255,11 @@ 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.
@@ -276,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()
@@ -483,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))
@@ -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,
)
}
@@ -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 }
)
}
@@ -1842,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) {
@@ -2035,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") {
@@ -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)
}
}
}
@@ -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)
@@ -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...")
}
}
}
@@ -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].
*/
@@ -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
@@ -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,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)
}
}
}
@@ -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)
}
}
}
@@ -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")
)
@@ -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
@@ -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
}
}
@@ -175,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(
@@ -214,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"
@@ -293,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
@@ -461,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<*>) {
@@ -623,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()
@@ -2151,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()
}
@@ -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)
}
}
}
@@ -77,7 +77,8 @@ object PokemonBrain {
CobblemonSensors.NEARBY_GROWABLE_CROPS,
CobblemonSensors.NEARBY_BEE_HIVE,
CobblemonSensors.NEARBY_FLOWER,
CobblemonSensors.NEARBY_SWEET_BERRY_BUSH
CobblemonSensors.NEARBY_SWEET_BERRY_BUSH,
CobblemonSensors.PLAYER_DEBUGGING
// CobblemonSensors.BATTLING_POKEMON,
// CobblemonSensors.NPC_BATTLING
@@ -179,6 +180,7 @@ object PokemonBrain {
MemoryModuleType.HURT_BY_ENTITY,
MemoryModuleType.NEAREST_PLAYERS,
MemoryModuleType.NEAREST_VISIBLE_PLAYER,
MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER,
MemoryModuleType.ANGRY_AT,
MemoryModuleType.ATTACK_COOLING_DOWN,
CobblemonMemories.POKEMON_DROWSY,
@@ -201,7 +203,8 @@ object PokemonBrain {
CobblemonMemories.HERD_LEADER,
CobblemonMemories.HERD_SIZE,
CobblemonMemories.ATTACK_TARGET_DATA,
CobblemonMemories.NEARBY_SWEET_BERRY_BUSH
CobblemonMemories.NEARBY_SWEET_BERRY_BUSH,
CobblemonMemories.DIAMOND_DEBUGGER
)
private fun coreTasks(pokemon: Pokemon) = buildList<Pair<Int, BehaviorControl<in PokemonEntity>>> {
@@ -200,6 +200,10 @@
{
"condition": "q.is_holding_item",
"animation": "q.bedrock('charizard', 'hold_item')"
},
{
"condition": "!q.is_ridden",
"animation": "q.pitch_tilt('body', 0.8)"
}
],
"quirks": [
@@ -217,9 +221,10 @@
"q.look('neck4', 0.35, 0.35, 5, -5, 15, -15)",
"q.look('head', 0.4, 0.25, 5, -5, 10, -10)",
"q.bedrock('charizard', 'air_fly')",
"q.bedrock('charizard', 'ride_air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('charizard', 'ride_air_fly')"
"condition": "!q.is_ridden",
"animation": "q.pitch_tilt('body', 0.8)"
},
{
"condition": "q.is_holding_item",
@@ -65,7 +65,9 @@
},
"hover": {
"poseTypes": ["HOVER"],
"animations": ["q.look('neck')", "q.bedrock('pidgeot', 'air_idle')"],
"animations": [
"q.look('neck')", "q.bedrock('pidgeot', 'air_idle')"
],
"transformedParts": [
{
"part": "wing_open_left",
@@ -91,7 +93,14 @@
"fly": {
"poseTypes": ["FLY"],
"condition": "!q.is_gliding",
"animations": ["q.look('neck')", "q.bedrock('pidgeot', 'air_idle')"],
"animations": [
"q.look('neck')",
"q.bedrock('pidgeot', 'air_idle')",
{
"condition": "!q.is_ridden",
"animation": "q.pitch_tilt('body', 0.8)"
}
],
"transformedParts": [
{
"part": "wing_open_left",
@@ -97,7 +97,7 @@
"item.cobblemon.npc_editor": "NPC's Trainer Card",
"item.cobblemon.link_cable": "Link Cable",
"item.cobblemon.link_cable.tooltip": "Evolves pokemon that require trading",
"item.cobblemon.link_cable.tooltip": "Evolves Pokémon that require trading",
"item.cobblemon.dragon_scale": "Dragon Scale",
"item.cobblemon.dragon_scale.tooltip": "Evolves Seadra into Kingdra when held while trading",
"item.cobblemon.kings_rock": "Kings Rock",
@@ -423,7 +423,7 @@
"item.cobblemon.razor_fang.tooltip_1": "When the holder successfully inflicts damage, the target may also flinch",
"item.cobblemon.razor_fang.tooltip_2": "Evolves Gligar into Gliscor when held while leveling up during the night",
"item.cobblemon.red_card": "Red Card",
"item.cobblemon.red_card.tooltip_1": "Pokemon that damage the holder are forced to switch out",
"item.cobblemon.red_card.tooltip_1": "Pokémon that damage the holder are forced to switch out",
"item.cobblemon.red_card.tooltip_2": "Consumed after use",
"item.cobblemon.ring_target": "Ring Target",
"item.cobblemon.ring_target.tooltip": "The holder's type immunities are removed",
@@ -489,7 +489,7 @@
"item.cobblemon.white_herb.tooltip_1": "Restores any lowered stat in battle",
"item.cobblemon.white_herb.tooltip_2": "Consumed after use",
"item.cobblemon.bright_powder": "Bright Powder",
"item.cobblemon.bright_powder.tooltip": "Reduces the accuracy of enemy pokemon's moves by 10%",
"item.cobblemon.bright_powder.tooltip": "Reduces the accuracy of enemy Pokémon's moves by 10%",
"item.cobblemon.metal_powder": "Metal Powder",
"item.cobblemon.metal_powder.tooltip": "Raises an untransformed Ditto's Defense and Sp. Def by 50%",
"item.cobblemon.quick_powder": "Quick Powder",
@@ -2494,6 +2494,10 @@
"cobblemon.config.ui.allow_experience_from_pvp.tooltip": "Whether to allow Exp. gain from PvP battles.",
"cobblemon.config.ui.experience_share_multiplier": "Experience Share Multiplier",
"cobblemon.config.ui.experience_share_multiplier.tooltip": "Multiplier of how much of the total Exp is given to a Pokémon holding an Exp. Share.",
"cobblemon.config.ui.award_experience_to_fainted_pokemon": "Award Exp. to Fainted Pokémon",
"cobblemon.config.ui.award_experience_to_fainted_pokemon.tooltip": "Whether fainted Pokémon receive Exp. for opponents they helped defeat before fainting.",
"cobblemon.config.ui.award_experience_on_battle_loss": "Award Exp. on Battle Loss",
"cobblemon.config.ui.award_experience_on_battle_loss.tooltip": "Whether Pokémon receive Exp. for opponents they defeated even if the battle is lost. Requires 'Award Exp. to Fainted Pokémon' to be enabled for fainted Pokémon to benefit.",
"cobblemon.config.ui.lucky_egg_multiplier": "Lucky Egg Multiplier",
"cobblemon.config.ui.lucky_egg_multiplier.tooltip": "Multiplier of how much of the total Exp is given to a Pokémon holding a Lucky Egg.",
"cobblemon.config.ui.allow_spectating": "Allow Spectating",
@@ -2604,7 +2608,7 @@
"cobblemon.config.ui.summary_profile_animations.always_animate.tooltip": "Entire party animates unless fainted",
"cobblemon.config.ui.third_person_view_bobbing": "Third Person View Bobbing",
"cobblemon.config.ui.third_person_view_bobbing.tooltip": "If true then use default Minecraft behavior to follow directly behind the player's head. If false then follow a fixed offset from the pokemon.",
"cobblemon.config.ui.third_person_view_bobbing.tooltip": "If true then use default Minecraft behavior to follow directly behind the player's head. If false then follow a fixed offset from the Pokémon.",
"cobblemon.config.ui.invert_pitch": "Invert Pitch",
"cobblemon.config.ui.invert_pitch.tooltip": "Invert the direction for pitch (up and down).",
"cobblemon.config.ui.invert_yaw": "Invert Yaw",
@@ -3893,6 +3897,9 @@
"cobblemon.behaviour.pokemon_bee.name": "Pollinates Flowers",
"cobblemon.behaviour.pokemon_bee.desc": "Pollinates flowers and adds honey to hives and Saccharine tree leaves.",
"cobblemon.behaviour.pokemon_boids.name": "Boids",
"cobblemon.behaviour.pokemon_boids.desc": "Boids Around.",
"cobblemon.behaviour.pokemon_picks_up_items.name": "Picks Up Items",
"cobblemon.behaviour.pokemon_picks_up_items.desc": "The Pokémon will pick up items that it cares about.",
@@ -0,0 +1,25 @@
{
"name": "cobblemon.behaviour.pokemon_boids.name",
"description": "cobblemon.behaviour.pokemon_boids.desc",
"entityType": "cobblemon:pokemon",
"configurations": [
{
"type": "add_tasks_to_activity",
"activity": "idle",
"tasksByPriority": {
"0": [
"switch_to_boid"
]
}
},
{
"type": "add_tasks_to_activity",
"activity": "cobblemon:pokemon_boid",
"tasksByPriority": {
"0": [
"switch_from_boid"
]
}
}
]
}
@@ -219,6 +219,26 @@
"rouseChance": 0.0042
}
},
"ai": [
{
"type": "add_tasks_to_activity",
"activity": "core",
"tasksByPriority": {
"0": [
"switch_to_boid"
]
}
},
{
"type": "add_tasks_to_activity",
"activity": "cobblemon:pokemon_boid",
"tasksByPriority": {
"0": [
"switch_from_boid"
]
}
}
],
"drops": {
"amount": 4,
"entries": [
@@ -89,6 +89,26 @@
"willFlee": false
}
},
"ai": [
{
"type": "add_tasks_to_activity",
"activity": "core",
"tasksByPriority": {
"0": [
"switch_to_boid"
]
}
},
{
"type": "add_tasks_to_activity",
"activity": "cobblemon:pokemon_boid",
"tasksByPriority": {
"0": [
"switch_from_boid"
]
}
}
],
"drops": {
"amount": 6,
"entries": [
@@ -110,6 +110,24 @@
}
]
}
},
{
"type": "add_tasks_to_activity",
"activity": "core",
"tasksByPriority": {
"0": [
"switch_to_boid"
]
}
},
{
"type": "add_tasks_to_activity",
"activity": "cobblemon:pokemon_boid",
"tasksByPriority": {
"0": [
"switch_from_boid"
]
}
}
],
"drops": {
@@ -121,6 +121,26 @@
]
}
},
"ai": [
{
"type": "add_tasks_to_activity",
"activity": "core",
"tasksByPriority": {
"0": [
"switch_to_boid"
]
}
},
{
"type": "add_tasks_to_activity",
"activity": "cobblemon:pokemon_boid",
"tasksByPriority": {
"0": [
"switch_from_boid"
]
}
}
],
"drops": {
"amount": 3,
"entries": [
@@ -246,6 +246,26 @@
"rouseChance": 0.0042
}
},
"ai": [
{
"type": "add_tasks_to_activity",
"activity": "core",
"tasksByPriority": {
"0": [
"switch_to_boid"
]
}
},
{
"type": "add_tasks_to_activity",
"activity": "cobblemon:pokemon_boid",
"tasksByPriority": {
"0": [
"switch_from_boid"
]
}
}
],
"drops": {
"amount": 2,
"entries": [
@@ -175,6 +175,26 @@
"rouseChance": 0.0042
}
},
"ai": [
{
"type": "add_tasks_to_activity",
"activity": "core",
"tasksByPriority": {
"0": [
"switch_to_boid"
]
}
},
{
"type": "add_tasks_to_activity",
"activity": "cobblemon:pokemon_boid",
"tasksByPriority": {
"0": [
"switch_from_boid"
]
}
}
],
"drops": {
"amount": 3,
"entries": [
@@ -51,7 +51,7 @@
"riding": {
"behaviours": {
"LAND": {
"key": "cobblemon:land/minekart",
"key": "cobblemon:land/horse",
"rideSounds": [],
"stats": {
"ACCELERATION": "0-5",
@@ -3,6 +3,7 @@
"values": [
"#cobblemon:berries",
"#cobblemon:herbs",
"#cobblemon:type_gems",
"minecraft:snowball"
]
}
@@ -1,6 +1,7 @@
{
"replace": false,
"values": [
"#cobblemon:berries"
"#cobblemon:berries",
"#cobblemon:type_gems"
]
}
@@ -3,6 +3,7 @@
"values": [
"#cobblemon:berries",
"#cobblemon:herbs",
"#cobblemon:type_gems",
"minecraft:snowball"
]
}
@@ -29,7 +29,7 @@
"required": false
},
{
"id": "biomeswevegone:black_forest",
"id": "biomeswevegone:ironwood_gour",
"required": false
},
{
@@ -38,11 +38,15 @@
"required": false
},
{
"id": "biomeswevegone:aspen_boreal",
"id": "biomeswevegone:howling_peaks",
"required": false
},
{
"id": "biomeswevegone:howling_peaks",
"id": "biomeswevegone:overgrowth_woodlands",
"required": false
},
{
"id": "biomeswevegone:temperate_grove",
"required": false
},
{
@@ -78,6 +78,10 @@
"id": "biomeswevegone:coconino_meadow",
"required": false
},
{
"id": "biomeswevegone:forgotten_forest",
"required": false
},
{
"id": "biomeswevegone:mojave_desert",
"required": false
@@ -91,7 +95,7 @@
"required": false
},
{
"id": "biomeswevegone:white_mangrove_marshes",
"id": "biomeswevegone:temperate_grove",
"required": false
},
{
@@ -59,6 +59,14 @@
"id": "biomeswevegone:coniferous_forest",
"required": false
},
{
"id": "biomeswevegone:crimson_tundra",
"required": false
},
{
"id": "biomeswevegone:eroded_borealis",
"required": false
},
{
"id": "biomeswevegone:frosted_coniferous_forest",
"required": false
@@ -72,11 +80,15 @@
"required": false
},
{
"id": "biomeswevegone:rose_fields",
"id": "biomeswevegone:maple_taiga",
"required": false
},
{
"id": "biomeswevegone:weeping_witch_forest",
"id": "biomeswevegone:redwood_thicket",
"required": false
},
{
"id": "biomeswevegone:rose_fields",
"required": false
},
{
@@ -50,6 +50,10 @@
"id": "biomesoplenty:seasonal_orchard",
"required": false
},
{
"id": "biomeswevegone:pumpkin_valley",
"required": false
},
{
"id": "blooming_biosphere:autumnal_forest",
"required": false
@@ -2,6 +2,10 @@
"replace": false,
"values": [
"minecraft:bamboo_jungle",
{
"id": "biomeswevegone:crag_gardens",
"required": false
},
{
"id": "wythers:bamboo_jungle_canyon",
"required": false
@@ -42,14 +42,6 @@
"id": "biomesoplenty:orchard",
"required": false
},
{
"id": "biomeswevegone:amaranth_fields",
"required": false
},
{
"id": "biomeswevegone:allium_fields",
"required": false
},
{
"id": "biomeswevegone:rose_fields",
"required": false
@@ -58,10 +50,6 @@
"id": "biomeswevegone:skyris_vale",
"required": false
},
{
"id": "biomeswevegone:orchard",
"required": false
},
{
"id": "terralith:blooming_plateau",
"required": false
@@ -18,10 +18,6 @@
"id": "biomesoplenty:mystic_grove",
"required": false
},
{
"id": "biomeswevegone:skyris_vale",
"required": false
},
{
"id": "terralith:amethyst_canyon",
"required": false
@@ -31,6 +31,10 @@
"id": "blooming_biosphere:rainforest",
"required": false
},
{
"id": "biomeswevegone:forgotten_forest",
"required": false
},
{
"id": "biomeswevegone:tropical_rainforest",
"required": false
@@ -41,6 +41,14 @@
"id": "biomesoplenty:rocky_shrubland",
"required": false
},
{
"id": "biomeswevegone:firecracker_chaparral",
"required": false
},
{
"id": "biomeswevegone:sierra_badlands",
"required": false
},
{
"id": "blooming_biosphere:chaparral",
"required": false
@@ -18,6 +18,18 @@
"id": "biomeswevegone:ebony_woods",
"required": false
},
{
"id": "biomeswevegone:overgrowth_woodlands",
"required": false
},
{
"id": "biomeswevegone:pale_bog",
"required": false
},
{
"id": "biomeswevegone:weeping_witch_forest",
"required": false
},
{
"id": "wythers:ancient_taiga",
"required": false
@@ -23,6 +23,10 @@
"id": "biomesoplenty:tundra",
"required": false
},
{
"id": "biomeswevegone:crimson_tundra",
"required": false
},
{
"id": "clifftree:bog",
"required": false
+11 -2
View File
@@ -59,15 +59,24 @@ dependencies {
modImplementation(libs.bundles.fabric.integrations.implementation)
modRuntimeOnly(libs.bundles.fabric.integrations.runtimeOnly)
modRuntimeOnly(libs.bundles.mongo)
bundle(libs.bundles.mongo)
// modImplementation(libs.flywheelFabric)
// include(libs.flywheelFabric)
listOf(
libs.fabric.kotlin,
libs.bundles.graal,
libs.bundles.mongo
).forEach {
include(it)
}
// Added to make graal available in dev
modRuntimeOnly(libs.bundles.graal)
include(libs.fabric.kotlin)
listOf(
libs.graal,
libs.molang
).forEach {
bundle(it)
@@ -17,6 +17,7 @@ import com.cobblemon.mod.common.client.CobblemonClient.reloadCodedAssets
import com.cobblemon.mod.common.client.keybind.CobblemonKeyBinds
import com.cobblemon.mod.common.client.pokedex.PokedexType
import com.cobblemon.mod.common.client.render.atlas.CobblemonAtlases
import com.cobblemon.mod.common.client.render.debug.SteerForceRenderer
import com.cobblemon.mod.common.client.render.item.CobblemonModelPredicateRegistry
import com.cobblemon.mod.common.item.PokedexItem
import com.cobblemon.mod.common.particle.CobblemonParticles
@@ -171,6 +172,24 @@ class CobblemonFabricClient: ClientModInitializer, CobblemonClientImplementation
)
}
WorldRenderEvents.LAST.register { context ->
val poseStack = context.matrixStack()
val multiBufferSource = context.consumers()
val camera = context.camera()
val partialTicks = context.tickCounter().getGameTimeDeltaPartialTick(true)
// Add a null check to ensure all required tools are present before rendering
if (poseStack != null && multiBufferSource != null && camera != null) {
SteerForceRenderer.render(
poseStack,
multiBufferSource,
partialTicks,
camera
)
}
SteerForceRenderer.registerTickHandler()
}
CobblemonModelPredicateRegistry.registerPredicates()
}
@@ -48,8 +48,6 @@ tasks {
configurations = listOf(bundle)
mergeServiceFiles()
relocate ("com.mongodb", "com.cobblemon.mod.relocations.mongodb")
relocate ("org.graalvm", "com.cobblemon.mod.relocations.graalvm")
relocate ("com.oracle", "com.cobblemon.mod.relocations.oracle")
}
+17 -4
View File
@@ -9,15 +9,20 @@ blossom = "2.1.0"
idea-ext = "1.1.7"
versioning = "3.1.0"
# Loom
# Minecraft Development
loom = "1.11-SNAPSHOT"
architectury-plugin = "3.4-SNAPSHOT"
moddev = "1.0.11"
# Common
molang = "1.1.20"
jei-api = "19.22.1.316"
# Graal
graal = "22.3.0"
icu4j = "71.1"
jei-api = "19.22.1.316"
regex = "22.3.0"
truffle = "22.3.0"
# Fabric
fabric-loader = "0.17.2"
@@ -48,6 +53,7 @@ classgraph = "4.8.179"
# General Buildscript Plugins
kotlin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
loom = { group = "dev.architectury", name = "architectury-loom", version.ref = "loom" }
neoforge-moddev = { group = "net.neoforged", name = "moddev-gradle", version.ref = "moddev" }
architectury = { group = "architectury-plugin", name = "architectury-plugin.gradle.plugin", version.ref = "architectury-plugin" }
shadow = { group = "com.github.johnrengelman", name = "shadow", version.ref = "shadow" }
blossom = { group = "net.kyori", name = "blossom", version.ref = "blossom" }
@@ -62,7 +68,12 @@ kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", vers
# Common
jei-api = { group = "mezz.jei", name = "jei-1.21.1-common-api", version.ref = "jei-api"}
molang = { group = "local.com.bedrockk", name = "molang", version.ref = "molang" }
graal = { group = "org.graalvm.js", name = "js", version.ref = "graal" }
# Graal
graal-core = { group = "org.graalvm.js", name = "js", version.ref = "graal" }
graal-sdk = { group = "org.graalvm.sdk", name = "graal-sdk", version.ref = "graal" }
graal-regex = { group = "org.graalvm.regex", name = "regex", version.ref = "graal"}
graal-truffle = { group = "org.graalvm.truffle", name = "truffle-api", version.ref = "graal" }
icu4j = { group = "com.ibm.icu", name = "icu4j", version.ref = "icu4j" }
# Fabric
@@ -78,7 +89,7 @@ mod-menu = { group = "com.terraformersmc", name = "modmenu", version.ref = "mod-
c2me-fabric = { group = "maven.modrinth", name = "c2me-fabric", version.ref = "c2me-fabric" }
# NeoForge
neoforge = { group = "net.neoforged", name = "neoforge", version.ref = "neoforge" }
neoforge-api = { group = "net.neoforged", name = "neoforge", version.ref = "neoforge" }
neo-kotlin-forge = { group = "thedarkcolour", name = "kotlinforforge-neoforge", version.ref = "neo-kotlin-for-forge" }
neoforge-debugutils = { group = "maven.modrinth", name = "debugutils", version.ref = "debugutils-forge"}
jei-neoforge = { group = "mezz.jei", name = "jei-1.21.1-neoforge", version.ref = "jei-api"}
@@ -104,3 +115,5 @@ fabric-integrations-runtimeOnly = [ "jei-fabric", "c2me-fabric" ]
neoforge-integrations-runtimeOnly = [ "jei-neoforge", "c2me-neoforge" ]
mongo = [ "mongo-driver-core", "mongo-driver-sync" ]
unitTesting = [ "junit-fabric", "junit-params", "mockito-core", "mock-kotlin", "classgraph" ]
graal = [ "graal-core", "graal-sdk", "graal-regex", "graal-truffle", "icu4j" ]
+11 -4
View File
@@ -33,13 +33,14 @@ repositories {
}
dependencies {
neoForge(libs.neoforge)
neoForge(libs.neoforge.api)
//shadowCommon group: 'commons-io', name: 'commons-io', version: '2.6'
//modImplementation(libs.flywheelForge)
//include(libs.flywheelForge)
modLocalRuntime(libs.neoforge.debugutils)
modRuntimeOnly(libs.bundles.neoforge.integrations.runtimeOnly)
modRuntimeOnly(libs.bundles.mongo)
forgeRuntimeLibrary(libs.bundles.mongo)
implementation(libs.neo.kotlin.forge)
@@ -52,15 +53,21 @@ dependencies {
testImplementation(project(":common", configuration = "namedElements"))
listOf(
libs.graal,
libs.bundles.graal,
libs.bundles.mongo
).forEach {
include(it)
}
modRuntimeOnly(libs.bundles.graal)
forgeRuntimeLibrary(libs.bundles.graal)
listOf(
libs.molang
).forEach {
forgeRuntimeLibrary(it)
bundle(it)
}
forgeRuntimeLibrary(libs.bundles.mongo)
bundle(libs.bundles.mongo)
}
tasks {
@@ -19,6 +19,7 @@ import com.cobblemon.mod.common.client.gui.config.CobblemonConfigScreen
import com.cobblemon.mod.common.client.keybind.CobblemonKeyBinds
import com.cobblemon.mod.common.client.pokedex.PokedexType
import com.cobblemon.mod.common.client.render.atlas.CobblemonAtlases
import com.cobblemon.mod.common.client.render.debug.SteerForceRenderer
import com.cobblemon.mod.common.client.render.item.CobblemonModelPredicateRegistry
import com.cobblemon.mod.common.client.render.shader.CobblemonShaders
import com.cobblemon.mod.common.item.PokedexItem
@@ -68,6 +69,7 @@ import net.neoforged.neoforge.client.event.RegisterParticleProvidersEvent
import net.neoforged.neoforge.client.event.RegisterShadersEvent
import net.neoforged.neoforge.client.event.RenderGuiEvent
import net.neoforged.neoforge.client.event.RenderGuiLayerEvent
import net.neoforged.neoforge.client.event.RenderLevelStageEvent
import net.neoforged.neoforge.client.gui.IConfigScreenFactory
import net.neoforged.neoforge.client.gui.VanillaGuiLayers
import net.neoforged.neoforge.common.NeoForge
@@ -102,6 +104,18 @@ object CobblemonNeoForgeClient : CobblemonClientImplementation {
modContainer.registerExtensionPoint(IConfigScreenFactory::class.java, object : IConfigScreenFactory {
override fun createScreen(modContainer: ModContainer, arg: Screen): Screen = CobblemonConfigScreen(arg)
})
NeoForge.EVENT_BUS.addListener<RenderLevelStageEvent> { event ->
if (event.stage == RenderLevelStageEvent.Stage.AFTER_PARTICLES) {
SteerForceRenderer.render(
event.poseStack,
Minecraft.getInstance().renderBuffers().bufferSource(),
event.partialTick.gameTimeDeltaTicks,
event.camera
)
}
}
SteerForceRenderer.registerTickHandler()
}
private fun onRegisterReloadListener(event: RegisterClientReloadListenersEvent) {