Improved land wandering, added path type filtering to pokemon pathing

This commit is contained in:
Paul
2025-03-24 16:15:57 +10:00
parent 9d38ad78bf
commit 7e4054ad8f
16 changed files with 168 additions and 110 deletions
+9
View File
@@ -0,0 +1,9 @@
- Nodemaker bug, it thinks jumps are possible from water up an entire block - they aint
https://gyazo.com/759b8285e7c07046ff2e54adf0d7a7ac
- Why is magikarp trying to path through ice
https://gyazo.com/ae230758c529f885eeb0dd91e60530d2
- We should use mixins to put the path node filtering from OmniPathNodeMaker in the base classes otherwise
NPC wandering will be jank too.
@@ -37,7 +37,7 @@ object CobblemonMemories {
val POKEMON_DROWSY = register("pokemon_drowsy", PrimitiveCodec.BOOL) val POKEMON_DROWSY = register("pokemon_drowsy", PrimitiveCodec.BOOL)
val POKEMON_SLEEPING = register("pokemon_sleeping", PrimitiveCodec.BOOL) val POKEMON_SLEEPING = register("pokemon_sleeping", PrimitiveCodec.BOOL)
val POKEMON_BATTLE = register<UUID>("pokemon_battle") val POKEMON_BATTLE = register<UUID>("pokemon_battle")
val PATH_COOLDOWN = register("path_cooldown", PrimitiveCodec.BOOL) val PATH_COOLDOWN = register<Boolean>("path_cooldown")
val TARGETED_BATTLE_POKEMON = register<UUID>("targeted_battle_pokemon") val TARGETED_BATTLE_POKEMON = register<UUID>("targeted_battle_pokemon")
val NEAREST_VISIBLE_ATTACKER = register<LivingEntity>("nearest_visible_attacker") val NEAREST_VISIBLE_ATTACKER = register<LivingEntity>("nearest_visible_attacker")
val NEARBY_GROWABLE_CROPS = register<BlockPos>("nearby_growable_crops") val NEARBY_GROWABLE_CROPS = register<BlockPos>("nearby_growable_crops")
@@ -67,7 +67,7 @@ class FindAirTaskConfig : SingleTaskConfig {
val blockPos = entity.closestPosition(iterable) { isAirPos(entity.level(), it) } val blockPos = entity.closestPosition(iterable) { isAirPos(entity.level(), it) }
?: return@Trigger false ?: return@Trigger false
walkTarget.set(WalkTarget(blockPos.above(), 0.35F, 1)) walkTarget.set(WalkTarget(blockPos.above(), 0.35F, 0))
return@Trigger true return@Trigger true
} }
} }
@@ -12,25 +12,17 @@ import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.api.ai.BrainConfigurationContext import com.cobblemon.mod.common.api.ai.BrainConfigurationContext
import com.cobblemon.mod.common.api.ai.asVariables import com.cobblemon.mod.common.api.ai.asVariables
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangValue import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangValue
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity import com.cobblemon.mod.common.entity.ai.CobblemonWalkTarget
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.canFit
import com.cobblemon.mod.common.util.toVec3d
import com.cobblemon.mod.common.util.withQueryValue import com.cobblemon.mod.common.util.withQueryValue
import net.minecraft.core.BlockPos import net.minecraft.core.BlockPos
import net.minecraft.tags.BlockTags
import net.minecraft.world.entity.LivingEntity import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.PathfinderMob import net.minecraft.world.entity.PathfinderMob
import net.minecraft.world.entity.ai.behavior.BehaviorControl import net.minecraft.world.entity.ai.behavior.BehaviorControl
import net.minecraft.world.entity.ai.behavior.BlockPosTracker
import net.minecraft.world.entity.ai.behavior.RandomStroll
import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder
import net.minecraft.world.entity.ai.behavior.declarative.Trigger import net.minecraft.world.entity.ai.behavior.declarative.Trigger
import net.minecraft.world.entity.ai.memory.MemoryModuleType import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.entity.ai.util.LandRandomPos import net.minecraft.world.entity.ai.util.LandRandomPos
import net.minecraft.world.level.block.state.BlockState import net.minecraft.world.level.pathfinder.PathType
import net.minecraft.world.phys.Vec3
class WanderTaskConfig : SingleTaskConfig { class WanderTaskConfig : SingleTaskConfig {
companion object { companion object {
@@ -76,69 +68,74 @@ class WanderTaskConfig : SingleTaskConfig {
pathCooldown.setWithExpiry(true, 40L) pathCooldown.setWithExpiry(true, 40L)
val targetVec = getLandTarget(entity) ?: return@Trigger true //LandRandomPos.getPos(entity, horizontalRange.resolveInt(), verticalRange.resolveInt()) ?: return@Trigger false // val targetVec = getLandTarget(entity) ?: return@Trigger true
walkTarget.set(WalkTarget(targetVec, speedMultiplier.resolveFloat(), 1)) val targetVec = LandRandomPos.getPos(entity, horizontalRange.resolveInt(), verticalRange.resolveInt()) ?: return@Trigger false
lookTarget.set(BlockPosTracker(targetVec.add(0.0, entity.eyeHeight.toDouble(), 0.0))) val pos = BlockPos.containing(targetVec)
walkTarget.set(CobblemonWalkTarget(pos, speedMultiplier.resolveFloat(), 0, { it !in listOf(PathType.WATER, PathType.WATER_BORDER) }))
lookTarget.erase()
return@Trigger true return@Trigger true
} }
} }
} }
} }
@Suppress("DEPRECATION", "MemberVisibilityCanBePrivate") /*
fun getLandTarget(entity: PathfinderMob): Vec3? { * This method is from the old wander goal. It is very verbose but reliably finds decent land wander targets. Its only advantage over
val roamDistanceCondition: (BlockPos) -> Boolean = if (entity is PokemonEntity) ({ entity.tethering?.canRoamTo(it) != false }) else ({ true }) * the LandRandomPos method (with our CobblemonWalkTarget) is that it doesn't lead entities in the direction of water.
val iterable: Iterable<BlockPos> = BlockPos.randomBetweenClosed(entity.random, 64, entity.blockX - 10, entity.blockY, entity.blockZ - 10, entity.blockX + 10, entity.blockY, entity.blockZ + 10) */
val condition: (BlockState, BlockPos) -> Boolean = { _, pos -> entity.canFit(pos) && roamDistanceCondition(pos) } // fun getLandTarget(entity: PathfinderMob): Vec3? {
val iterator = iterable.iterator() // val roamDistanceCondition: (BlockPos) -> Boolean = if (entity is PokemonEntity) ({ entity.tethering?.canRoamTo(it) != false }) else ({ true })
position@ // val iterable: Iterable<BlockPos> = BlockPos.randomBetweenClosed(entity.random, 64, entity.blockX - 10, entity.blockY, entity.blockZ - 10, entity.blockX + 10, entity.blockY, entity.blockZ + 10)
while (iterator.hasNext()) { // val condition: (BlockState, BlockPos) -> Boolean = { _, pos -> entity.canFit(pos) && roamDistanceCondition(pos) }
val pos = iterator.next().mutable() // val iterator = iterable.iterator()
var blockState = entity.level().getBlockState(pos) // position@
// while (iterator.hasNext()) {
val maxSteps = 16 // val pos = iterator.next().mutable()
var steps = 0 // var blockState = entity.level().getBlockState(pos)
var good = false //
if (!blockState.isSolid && !blockState.liquid()) { // val maxSteps = 16
pos.move(0, -1, 0) // var steps = 0
var previousWasAir = true // var good = false
while (steps++ < maxSteps && pos.y > entity.level().minBuildHeight) { // if (!blockState.isSolid && !blockState.liquid()) {
if (pos.y <= entity.level().minBuildHeight) { // pos.move(0, -1, 0)
continue@position // var previousWasAir = true
} // while (steps++ < maxSteps && pos.y > entity.level().minBuildHeight) {
blockState = entity.level().getBlockState(pos) // if (pos.y <= entity.level().minBuildHeight) {
if (blockState.isSolid && !blockState.`is`(BlockTags.LEAVES) && previousWasAir) { // continue@position
pos.move(0, 1, 0) // }
blockState = entity.level().getBlockState(pos) // blockState = entity.level().getBlockState(pos)
good = true // if (blockState.isSolid && !blockState.`is`(BlockTags.LEAVES) && previousWasAir) {
break // pos.move(0, 1, 0)
} else { // blockState = entity.level().getBlockState(pos)
previousWasAir = blockState.isAir // good = true
} // break
pos.move(0, -1, 0) // } else {
} // previousWasAir = blockState.isAir
} else { // }
var previousWasSolid = blockState.isSolid && !blockState.`is`(BlockTags.LEAVES) // pos.move(0, -1, 0)
pos.move(0, 1, 0) // }
while (steps++ < maxSteps) { // } else {
if (pos.y >= entity.level().maxBuildHeight) { // var previousWasSolid = blockState.isSolid && !blockState.`is`(BlockTags.LEAVES)
continue@position // pos.move(0, 1, 0)
} // while (steps++ < maxSteps) {
blockState = entity.level().getBlockState(pos) // if (pos.y >= entity.level().maxBuildHeight) {
if (blockState.isAir && previousWasSolid) { // continue@position
good = true // }
break // blockState = entity.level().getBlockState(pos)
} // if (blockState.isAir && previousWasSolid) {
previousWasSolid = blockState.isSolid && !blockState.`is`(BlockTags.LEAVES) // good = true
pos.move(0, 1, 0) // break
} // }
} // previousWasSolid = blockState.isSolid && !blockState.`is`(BlockTags.LEAVES)
// pos.move(0, 1, 0)
if (good && condition(blockState, pos)) { // }
return pos.toVec3d() // }
} //
} // if (good && condition(blockState, pos)) {
// return pos.toVec3d()
return null // }
} // }
//
// return null
// }
} }
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.entity.ai
import net.minecraft.core.BlockPos
import net.minecraft.world.entity.ai.behavior.BlockPosTracker
import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.level.pathfinder.PathType
/**
* This is a trick to allow us to instruct the node-maker to only consider specific node types,
* even if the entity can, in a pinch, use more.
*
* @author Hiroku
* @since March 24th, 2025
*/
class CobblemonWalkTarget(
pos: BlockPos,
speedModifier: Float,
completionRange: Int,
val nodeTypeFilter: (PathType) -> Boolean = { true },
) : WalkTarget(
BlockPosTracker(pos),
speedModifier,
completionRange
)
@@ -8,6 +8,7 @@
package com.cobblemon.mod.common.entity.ai package com.cobblemon.mod.common.entity.ai
import com.cobblemon.mod.common.pokemon.ai.OmniPathNodeMaker
import net.minecraft.core.BlockPos import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.PathfinderMob import net.minecraft.world.entity.PathfinderMob
@@ -18,6 +19,7 @@ import net.minecraft.world.entity.ai.memory.MemoryStatus
import net.minecraft.world.entity.ai.memory.WalkTarget import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.entity.ai.util.DefaultRandomPos import net.minecraft.world.entity.ai.util.DefaultRandomPos
import net.minecraft.world.level.pathfinder.Path import net.minecraft.world.level.pathfinder.Path
import net.minecraft.world.level.pathfinder.PathType
import net.minecraft.world.phys.Vec3 import net.minecraft.world.phys.Vec3
class FollowWalkTargetTask( class FollowWalkTargetTask(
@@ -102,9 +104,22 @@ class FollowWalkTargetTask(
} }
} }
private fun withNodeFilter(entity: PathfinderMob, nodeFilter: (PathType) -> Boolean, block: () -> Unit) {
val nodeEvaluator = entity.navigation.nodeEvaluator
if (nodeEvaluator is OmniPathNodeMaker) {
nodeEvaluator.nodeFilter = nodeFilter
block()
nodeEvaluator.nodeFilter = { true }
} else {
block()
}
}
private fun hasFinishedPath(entity: PathfinderMob, walkTarget: WalkTarget, time: Long): Boolean { private fun hasFinishedPath(entity: PathfinderMob, walkTarget: WalkTarget, time: Long): Boolean {
val blockPos = walkTarget.target.currentBlockPosition() val blockPos = walkTarget.target.currentBlockPosition()
withNodeFilter(entity, (walkTarget as? CobblemonWalkTarget)?.nodeTypeFilter ?: { true }) {
this.path = entity.navigation.createPath(blockPos, 0) this.path = entity.navigation.createPath(blockPos, 0)
}
this.speed = walkTarget.speedModifier this.speed = walkTarget.speedModifier
val brain = entity.brain val brain = entity.brain
if (hasReached(entity, walkTarget)) { if (hasReached(entity, walkTarget)) {
@@ -126,10 +141,12 @@ class FollowWalkTargetTask(
10, 10,
7, 7,
Vec3.atBottomCenterOf(blockPos), Vec3.atBottomCenterOf(blockPos),
1.5707963705062866 Math.PI / 2F
) )
if (vec3d != null) { if (vec3d != null) {
withNodeFilter(entity, (walkTarget as? CobblemonWalkTarget)?.nodeTypeFilter ?: { true }) {
this.path = entity.navigation.createPath(vec3d.x, vec3d.y, vec3d.z, 0) this.path = entity.navigation.createPath(vec3d.x, vec3d.y, vec3d.z, 0)
}
return this.path != null return this.path != null
} }
} }
@@ -12,13 +12,9 @@ import net.minecraft.server.level.ServerLevel
import net.minecraft.tags.FluidTags import net.minecraft.tags.FluidTags
import net.minecraft.world.entity.PathfinderMob import net.minecraft.world.entity.PathfinderMob
import net.minecraft.world.entity.ai.behavior.Behavior import net.minecraft.world.entity.ai.behavior.Behavior
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.MemoryStatus
class StayAfloatTask(private val chance: Float = 0F) : Behavior<PathfinderMob>( class StayAfloatTask(private val chance: Float = 0F) : Behavior<PathfinderMob>(
mapOf( mapOf()
MemoryModuleType.WALK_TARGET to MemoryStatus.VALUE_ABSENT,
)
) { ) {
override fun checkExtraStartConditions(world: ServerLevel, entity: PathfinderMob): Boolean { override fun checkExtraStartConditions(world: ServerLevel, entity: PathfinderMob): Boolean {
return entity.isInWater && entity.getFluidHeight(FluidTags.WATER) > entity.fluidJumpThreshold || entity.isInLava return entity.isInWater && entity.getFluidHeight(FluidTags.WATER) > entity.fluidJumpThreshold || entity.isInLava
@@ -59,6 +59,7 @@ class EatGrassTask(
override fun start(world: ServerLevel, entity: PokemonEntity, time: Long) { override fun start(world: ServerLevel, entity: PokemonEntity, time: Long) {
timer = 40 timer = 40
entity.brain.setMemoryWithExpiry(CobblemonMemories.PATH_COOLDOWN, true, 40L)
entity.playAnimation("eat") entity.playAnimation("eat")
world.broadcastEntityEvent(entity, 10.toByte()) world.broadcastEntityEvent(entity, 10.toByte())
} }
@@ -125,13 +125,9 @@ class Species : ClientDataSynchronizer<Species>, ShowdownIdentifiable {
) )
}, },
ApplyPresets().apply { ApplyPresets().apply {
presets.add(cobblemonResource("floats")) presets.addAll(listOf(cobblemonResource("floats"), cobblemonResource("avoids_water")))
condition = Either.left("!q.entity.behaviour.moving.swim.can_breathe_underwater".asExpression()) condition = Either.left("!q.entity.behaviour.moving.swim.can_breathe_underwater".asExpression())
}, },
ApplyPresets().apply {
presets.add(cobblemonResource("avoids_water"))
condition = Either.left("q.entity.behaviour.moving.swim.avoids_water".asExpression())
},
ApplyPresets().apply { ApplyPresets().apply {
condition = Either.left("!q.entity.is_in_party".asExpression()) condition = Either.left("!q.entity.is_in_party".asExpression())
presets.add(cobblemonResource("pokemon_sleeps")) presets.add(cobblemonResource("pokemon_sleeps"))
@@ -43,6 +43,8 @@ class OmniPathNodeMaker : NodeEvaluator() {
var canPathThroughFire: Boolean = false var canPathThroughFire: Boolean = false
var nodeFilter: (PathType) -> Boolean = { true }
override fun prepare(cachedWorld: PathNavigationRegion, entity: Mob) { override fun prepare(cachedWorld: PathNavigationRegion, entity: Mob) {
super.prepare(cachedWorld, entity) super.prepare(cachedWorld, entity)
nodePosToType.clear() nodePosToType.clear()
@@ -188,6 +190,10 @@ class OmniPathNodeMaker : NodeEvaluator() {
} }
fun isValidPathType(type: PathType): Boolean { fun isValidPathType(type: PathType): Boolean {
if (!nodeFilter(type)) {
return false
}
return when { return when {
(type == PathType.BREACH || type == PathType.WATER || type == PathType.WATER_BORDER) && canSwimInWater() -> true (type == PathType.BREACH || type == PathType.WATER || type == PathType.WATER_BORDER) && canSwimInWater() -> true
type == PathType.OPEN && canFly() -> true type == PathType.OPEN && canFly() -> true
@@ -939,6 +939,7 @@
"cobblemon.command.dialogue.invalid-dialogue": "Invalid dialogue identifier", "cobblemon.command.dialogue.invalid-dialogue": "Invalid dialogue identifier",
"cobblemon.command.pc.invalid-box": "Box number %1$s exceeds max boxes of %2$s.", "cobblemon.command.pc.invalid-box": "Box number %1$s exceeds max boxes of %2$s.",
"cobblemon.command.unlockablepcboxwallpaper.invalid-wallpaper": "Invalid wallpaper identifier", "cobblemon.command.unlockablepcboxwallpaper.invalid-wallpaper": "Invalid wallpaper identifier",
"cobblemon.command.brainedit.non_scriptable": "You are not looking at a MoLang scriptable entity.",
"cobblemon.ui.generic.yes": "Yes", "cobblemon.ui.generic.yes": "Yes",
"cobblemon.ui.generic.no": "No", "cobblemon.ui.generic.no": "No",
@@ -2823,6 +2824,9 @@
"cobblemon.brain_preset.avoids_water.name": "Avoids Water", "cobblemon.brain_preset.avoids_water.name": "Avoids Water",
"cobblemon.brain_preset.avoids_water.desc": "Tries to find air and then gets away from water.", "cobblemon.brain_preset.avoids_water.desc": "Tries to find air and then gets away from water.",
"cobblemon.brain_preset.pokemon_eats_grass.name": "Eats Grass",
"cobblemon.brain_preset.pokemon_eats_grass.desc": "The Pokémon occasionally eats grass underfoot.",
"cobblemon.move.10000000voltthunderbolt": "10,000,000 Volt Thunderbolt", "cobblemon.move.10000000voltthunderbolt": "10,000,000 Volt Thunderbolt",
"cobblemon.move.10000000voltthunderbolt.desc": "The user, Pikachu wearing a cap, powers up a jolt of electricity using its Z-Power and unleashes it. Critical hits land more easily.", "cobblemon.move.10000000voltthunderbolt.desc": "The user, Pikachu wearing a cap, powers up a jolt of electricity using its Z-Power and unleashes it. Critical hits land more easily.",
"cobblemon.move.absorb": "Absorb", "cobblemon.move.absorb": "Absorb",
@@ -0,0 +1,15 @@
{
"name": "cobblemon.brain_preset.pokemon_eats_grass.name",
"description": "cobblemon.brain_preset.pokemon_eats_grass.desc",
"configurations": [
{
"type": "add_tasks_to_activity",
"activity": "idle",
"tasksByPriority": {
"2": [
"eat_grass"
]
}
}
]
}
@@ -64,13 +64,8 @@
}, },
"ai": [ "ai": [
{ {
"type": "add_tasks_to_activity", "type": "apply_presets",
"activity": "idle", "preset": "cobblemon:eats_grass"
"tasksByPriority": {
"2": [
"eat_grass"
]
}
} }
], ],
"drops": { "drops": {
@@ -59,13 +59,8 @@
}, },
"ai": [ "ai": [
{ {
"type": "add_tasks_to_activity", "type": "apply_presets",
"activity": "idle", "presets": ["cobblemon:eats_grass"]
"tasksByPriority": {
"2": [
"eat_grass"
]
}
} }
], ],
"drops": { "drops": {
@@ -53,13 +53,8 @@
}, },
"ai": [ "ai": [
{ {
"type": "add_tasks_to_activity", "type": "apply_presets",
"activity": "idle", "presets": ["cobblemon:eats_grass"]
"tasksByPriority": {
"2": [
"eat_grass"
]
}
} }
], ],
"standingEyeHeight": 0.6, "standingEyeHeight": 0.6,