Compare commits

...
48 changed files with 1585 additions and 311 deletions

No files matched your search

@@ -8,26 +8,35 @@
package com.cobblemon.mod.common.api.ai.biggerbrain
import com.bedrockk.molang.Expression
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.api.PrioritizedList
import com.cobblemon.mod.common.api.Priority
import com.cobblemon.mod.common.api.ai.biggerbrain.distraction.Distraction
import com.cobblemon.mod.common.api.ai.biggerbrain.distraction.DistractionType
import com.cobblemon.mod.common.api.ai.biggerbrain.fixation.EntityFixation
import com.cobblemon.mod.common.api.ai.biggerbrain.fixation.LookAndMoveFixation
import com.cobblemon.mod.common.api.ai.biggerbrain.objective.Objective
import com.cobblemon.mod.common.api.ai.biggerbrain.objective.ObjectiveStatus
import com.cobblemon.mod.common.api.ai.biggerbrain.sense.Sense
import com.cobblemon.mod.common.api.ai.movement.TraversalPolicy
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangValue
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.net.messages.client.debug.UpdateTrackedBrainDataPacket
import com.cobblemon.mod.common.util.getMemorySafely
import com.cobblemon.mod.common.util.getPlayer
import com.cobblemon.mod.common.util.mainThreadRuntime
import com.cobblemon.mod.common.util.resolveFloat
import com.cobblemon.mod.common.util.withQueryValue
import net.minecraft.core.BlockPos
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.PathfinderMob
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.MemoryStatus
import net.minecraft.world.entity.ai.memory.WalkTarget
import java.util.UUID
/**
@@ -110,7 +119,8 @@ class BiggerBrain(val entity: LivingEntity) {
}
}
if (fixation?.tick(entity, this) == false) {
val f = fixation
if (f?.tick(entity, this) == false && fixation === f) {
fixation = null
}
@@ -190,4 +200,86 @@ class BiggerBrain(val entity: LivingEntity) {
fun <T> getMemory(memory: MemoryModuleType<T>): T? {
return entity.brain.getMemorySafely(memory).orElse(null)
}
fun tryMovementFixation(
blockPos: () -> BlockPos?,
speedMultiplier: Expression,
closeEnoughDistance: Int = 0,
traversalPolicy: () -> TraversalPolicy? = { null },
description : String,
distraction: Distraction,
lookAtWalkTarget: Boolean = true,
distractionTypes: Set<DistractionType> = setOf(DistractionType.FORCE),
onMovementStopped : ((entity: LivingEntity, brain: BiggerBrain, status: MovementStatus?) -> Unit)? = null,
) : Pair<EntityFixation?, ObjectiveStatus> {
if (entity !is PathfinderMob) {
return null to ObjectiveStatus.INVALID
}
val speedMultiplier = mainThreadRuntime.resolveFloat(speedMultiplier)
if (!canDistract(distraction)) {
return null to ObjectiveStatus.BLOCKED // We couldn't take over.
}
val pos = blockPos() ?: return null to ObjectiveStatus.INACTIVE // Couldn't resolve the walk target
val movementTracker = RegularMovementTracker.attempt(
entity = entity,
walkTarget = WalkTarget(
pos,
speedMultiplier,
closeEnoughDistance
),
policy = traversalPolicy(),
)
if (movementTracker != null && (fixation == null || (distraction != null && distract(distraction)))) {
val fixation = fixate(
LookAndMoveFixation(
distractionTypes = distractionTypes,
lookingTracker = if (lookAtWalkTarget) {
LookingTracker.fromNavigationPathOrWalkTarget(
entity,
movementTracker.walkTarget
)
} else {
null
},
movementTracker = movementTracker,
description = description,
onMovementComplete = onMovementStopped
)
)
return fixation to ObjectiveStatus.ACTIVE
}
return null to ObjectiveStatus.INACTIVE
}
inline fun <reified T : LivingEntity> checkObjectivePrerequisites(
cooldownTicks: Int = -1,
fixations: List<EntityFixation> = emptyList(),
ifIneligible: () -> Unit = {},
): ObjectiveStatus? {
val status = if (entity !is T) {
ObjectiveStatus.INVALID
} else if (fixations.isNotEmpty() && fixations.any { isFixation(it) }) {
ObjectiveStatus.FIXATED
} else if (cooldownTicks > 0) {
ObjectiveStatus.INACTIVE
} else null
if (status != null && status != ObjectiveStatus.INACTIVE) {
ifIneligible()
}
return status
}
inline fun <reified T : LivingEntity> checkObjectivePrerequisites(
cooldownTicks: Int = -1,
fixation: EntityFixation? = null,
ifIneligible: () -> Unit = {},
): ObjectiveStatus? {
return checkObjectivePrerequisites<T>(cooldownTicks, fixation?.let(::listOf) ?: emptyList(), ifIneligible)
}
}
@@ -8,7 +8,9 @@
package com.cobblemon.mod.common.api.ai.biggerbrain
import com.cobblemon.mod.common.api.ai.movement.TraversalPolicy
import com.cobblemon.mod.common.entity.ai.CobblemonWalkTarget
import com.cobblemon.mod.common.entity.ai.OmniPathNavigation
import com.cobblemon.mod.common.pokemon.ai.OmniPathNodeMaker
import com.cobblemon.mod.common.util.deleteNode
import java.util.function.Predicate
@@ -31,18 +33,33 @@ import net.minecraft.world.phys.Vec3
*/
class RegularMovementTracker(
val entity: PathfinderMob,
override val walkTarget: WalkTarget
override val walkTarget: WalkTarget,
/**
* The traversal rules for this movement. They stay attached to the path, including when it's recomputed or
* re-planned because the target moved. Null uses the legacy rules. Only honoured by [OmniPathNavigation].
*/
policy: TraversalPolicy? = null,
/** An already planned path to follow instead of planning one here. */
initialPath: Path? = null
) : MovementTracker {
companion object {
/**
* Attempts to create a tracker for the given entity and target. Returns null if the path
* couldn't be created.
*/
fun attempt(entity: PathfinderMob, walkTarget: WalkTarget): RegularMovementTracker? {
return RegularMovementTracker(entity, walkTarget).takeIf { it.isValid }
fun attempt(entity: PathfinderMob, walkTarget: WalkTarget, policy: TraversalPolicy? = null): RegularMovementTracker? {
return RegularMovementTracker(entity, walkTarget, policy).takeIf { it.isValid }
}
/** Creates a tracker that follows a path which was already planned, under the policy it was planned with. */
fun following(entity: PathfinderMob, walkTarget: WalkTarget, path: Path, policy: TraversalPolicy?): RegularMovementTracker? {
return RegularMovementTracker(entity, walkTarget, policy, path).takeIf { it.isValid }
}
}
/** The policy given explicitly, otherwise whatever the walk target asks for. */
val policy: TraversalPolicy? = policy ?: (walkTarget as? CobblemonWalkTarget)?.policy
var isValid = false
private set
var started = false
@@ -53,7 +70,11 @@ class RegularMovementTracker(
init {
val hasReached = hasReached(entity, walkTarget)
if (!hasReached && hasFinishedPath(entity, walkTarget)) {
if (!hasReached && initialPath != null) {
this.path = initialPath
this.lastKnownTargetPos = walkTarget.target.currentBlockPosition()
isValid = true
} else if (!hasReached && hasFinishedPath(entity, walkTarget)) {
this.lastKnownTargetPos = walkTarget.target.currentBlockPosition()
isValid = true
} else {
@@ -164,15 +185,25 @@ class RegularMovementTracker(
target: BlockPos
): Path? {
var path: Path? = null
if (walkTarget is CobblemonWalkTarget) {
val navigation = entity.navigation
if (policy != null && navigation is OmniPathNavigation) {
// The policy replaces node filtering; destination filtering still applies below.
var planned = navigation.planPath(target, 0, policy)
if (planned != null && walkTarget is CobblemonWalkTarget) {
while (planned.endNode?.type?.let { walkTarget.destinationNodeTypeFilter(it) } == false) {
planned.deleteNode(planned.nodeCount - 1)
}
if (planned.nodeCount == 0) {
planned = null
}
}
path = planned
} else if (walkTarget is CobblemonWalkTarget) {
withNodeFilter(
entity = entity,
nodeFilter = walkTarget.nodeTypeFilter,
) {
var resolvedPath = entity.navigation.createPath(
walkTarget.target.currentBlockPosition(),
0
)
var resolvedPath = entity.navigation.createPath(target, 0)
if (resolvedPath != null) {
// A CobblemonWalkTarget may have a destinationNodeTypeFilter, which is used to filter the end node of the path.
// Mainly useful for when the thing setting the target did it a bit crudely or didn't know how far the navigator
@@ -10,9 +10,15 @@ package com.cobblemon.mod.common.api.ai.biggerbrain
import com.cobblemon.mod.common.api.ai.CobblemonWanderControl.WanderCenter
import com.cobblemon.mod.common.api.ai.config.task.WanderTaskConfig.Companion.MAX_LOOK_DOWN_DISTANCE
import com.cobblemon.mod.common.api.ai.movement.FluidKind
import com.cobblemon.mod.common.api.ai.movement.MovementCapabilities
import com.cobblemon.mod.common.api.ai.movement.MovementIntent
import com.cobblemon.mod.common.api.ai.movement.Occupancy
import com.cobblemon.mod.common.api.ai.movement.TraversalPolicy
import com.cobblemon.mod.common.entity.OmniPathingEntity
import com.cobblemon.mod.common.entity.ai.CobblemonRandomSurfacePos
import com.cobblemon.mod.common.entity.ai.CobblemonWalkTarget
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import kotlin.math.ceil
import kotlin.math.sqrt
import net.minecraft.core.BlockPos
@@ -22,6 +28,7 @@ import net.minecraft.world.entity.ai.behavior.BehaviorUtils
import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.entity.ai.util.HoverRandomPos
import net.minecraft.world.entity.ai.util.LandRandomPos
import net.minecraft.world.entity.ai.util.RandomPos
import net.minecraft.world.level.material.Fluids
import net.minecraft.world.level.pathfinder.PathType
@@ -68,12 +75,16 @@ enum class WanderType(
return@choosePosition if (depth < requiredDepth) null else pos
},
createWalkTarget = { _, pos, _, speedMultiplier ->
createWalkTarget = { entity, pos, _, speedMultiplier ->
CobblemonWalkTarget(
pos = pos,
nodeTypeFilter = { it == PathType.WATER || it == PathType.WATER_BORDER },
speedModifier = speedMultiplier,
completionRange = 0
completionRange = 0,
// Stays swimming in water, wherever in the water it's comfortable or not.
policy = TraversalPolicy.forIntent(entity, MovementIntent.IDLE) { occupancy, fluid ->
fluid == FluidKind.WATER && (occupancy == Occupancy.SWIM || occupancy == Occupancy.SWIM_SUBMERGED)
}
)
}
),
@@ -134,7 +145,27 @@ enum class WanderType(
!avoidsTargetingAir || nodeType !in listOf(
PathType.OPEN
)
}
},
// Only goes through what it's comfortable with while idling, e.g. no swimming for non-breathers.
policy = TraversalPolicy.forIntent(entity, MovementIntent.IDLE)
)
}
),
/**
* Seafloor walkers in water: they can't swim, but can breathe, so they wander along the bottom (or wade, or step
* out onto land) instead. Neither [LAND] nor [WATER] applies to them while they're in water.
*/
SEABED(
isValid = { entity -> entity.isInWater && isSeafloorWalker(MovementCapabilities.of(entity)) },
choosePosition = { entity, _, _, horizontalRange, verticalRange ->
chooseBottomPosition(entity, horizontalRange, verticalRange)
},
createWalkTarget = { entity, pos, _, speedMultiplier ->
CobblemonWalkTarget(
pos = pos,
speedModifier = speedMultiplier,
completionRange = 0,
policy = TraversalPolicy.forIntent(entity, MovementIntent.IDLE)
)
}
)
@@ -220,4 +251,43 @@ enum class WanderType(
return pos
}
}
}
}
/** Bodies that walk along the bottom of water rather than swimming in it, and can breathe down there. */
private fun isSeafloorWalker(caps: MovementCapabilities) = caps.canWalk && !caps.canSwimInWater && caps.canBreatheUnderwater
private const val BOTTOM_POSITION_SAMPLES = 10
private const val MAX_BOTTOM_DROP = 24
/**
* Picks a random position the entity could stand at, settling sampled points onto the bottom the way a non-swimmer
* would sink onto it. Only positions an idle policy allows are kept; reachability is the pathfinder's job.
*/
private fun chooseBottomPosition(entity: PathfinderMob, horizontalRange: Int, verticalRange: Int): BlockPos? {
val level = entity.level()
val policy = TraversalPolicy.forIntent(entity, MovementIntent.IDLE)
val origin = entity.blockPosition()
val tethering = (entity as? PokemonEntity)?.tethering
repeat(BOTTOM_POSITION_SAMPLES) {
var pos = origin.offset(RandomPos.generateRandomDirection(entity.random, horizontalRange, verticalRange))
var climbed = 0
while (level.getBlockState(pos).isSolid && climbed++ < verticalRange) {
pos = pos.above()
}
var occupancy = Occupancy.at(policy.capabilities, entity.eyeHeight, pos, level::getBlockState)
var dropped = 0
while (occupancy?.isStandable == false && dropped++ < MAX_BOTTOM_DROP && pos.y > level.minBuildHeight) {
pos = pos.below()
occupancy = Occupancy.at(policy.capabilities, entity.eyeHeight, pos, level::getBlockState)
}
if (occupancy == null || pos == origin) {
return@repeat
} else if (policy.cost(occupancy, FluidKind.of(level.getFluidState(pos))) == null) {
return@repeat
} else if (tethering?.canRoamTo(pos) == false) {
return@repeat
}
return pos
}
return null
}
@@ -16,6 +16,8 @@ import com.cobblemon.mod.common.api.ai.biggerbrain.MovementTracker
import com.cobblemon.mod.common.api.ai.biggerbrain.RegularMovementTracker
import com.cobblemon.mod.common.api.ai.biggerbrain.distraction.Distraction
import com.cobblemon.mod.common.api.ai.biggerbrain.distraction.DistractionType
import com.cobblemon.mod.common.api.ai.movement.MovementIntent
import com.cobblemon.mod.common.api.ai.movement.TraversalPolicy
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangValue
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.ai.moves.RealTimeMove
@@ -43,6 +45,8 @@ class AttackFixation(
var movementTracker: MovementTracker? = null
var lookingTracker: LookingTracker? = null
var activeMoveInstance: RealTimeMoveInstance? = null
/** Ticks until it may plan again after a failed or impossible path, so it holds instead of re-pathing every tick. */
var remainingRepathTicks = 0
override fun getDescription(entity: LivingEntity): String {
return "Attacking Target"
}
@@ -126,6 +130,14 @@ class AttackFixation(
if (movementStatus != MovementStatus.PENDING) {
movementTracker!!.stop()
movementTracker = null
if (movementStatus == MovementStatus.FAILED) {
remainingRepathTicks = REPATH_COOLDOWN_TICKS
}
}
} else if (remainingRepathTicks > 0) {
remainingRepathTicks--
if (lookingTracker == null) {
lookingTracker = LookingTracker.forTargetEntity(entity, targetedEntity, trackEyeHeight = true)
}
} else {
// Not moving right now and we need to figure out to what distance to the enemy we want to move to
@@ -145,7 +157,11 @@ class AttackFixation(
}
if (idealDistanceToTarget < distanceToTarget) {
val walkTarget = WalkTarget(EntityTracker(targetedEntity, true), speed, idealDistanceToTarget.toInt())
movementTracker = RegularMovementTracker.attempt(entity, walkTarget)
val policy = TraversalPolicy.forIntent(entity, MovementIntent.URGENT)
movementTracker = RegularMovementTracker.attempt(entity, walkTarget, policy)
if (movementTracker == null) {
remainingRepathTicks = REPATH_COOLDOWN_TICKS
}
lookingTracker = LookingTracker.forTargetEntity(entity, targetedEntity, trackEyeHeight = true)
} else if (lookingTracker == null) {
// Fixate at looking at the target so we don't wander off
@@ -156,6 +172,7 @@ class AttackFixation(
return true
}
companion object {
const val REPATH_COOLDOWN_TICKS = 20
}
}
@@ -27,9 +27,9 @@ open class LookAndMoveFixation(
val distractionTypes: Set<DistractionType>,
/** How to describe the fixation in the debug overlay. */
val description: String,
override val lookingTracker: LookingTracker,
override val lookingTracker: LookingTracker?,
override val movementTracker: MovementTracker,
val onMovementComplete: ((entity: LivingEntity, brain: BiggerBrain) -> Unit)? = null
val onMovementComplete: ((entity: LivingEntity, brain: BiggerBrain, status: MovementStatus?) -> Unit)? = null
): EntityFixation, LookingTrackerFixation, MovementTrackerFixation {
override fun getDescription(entity: LivingEntity) = description
@@ -42,10 +42,11 @@ open class LookAndMoveFixation(
}
override fun tick(entity: LivingEntity, brain: BiggerBrain): Boolean {
lookingTracker.tick()
val continueFixation = tickMovementFixation() == MovementStatus.PENDING
lookingTracker?.tick()
val status = tickMovementFixation()
val continueFixation = status == MovementStatus.PENDING
if (!continueFixation && onMovementComplete != null) {
onMovementComplete.invoke(entity, brain)
onMovementComplete.invoke(entity, brain, status)
}
return continueFixation
}
@@ -52,7 +52,6 @@ class AggressiveBehaviour(
private var remainingCooldownTicks = 0
private var attackFixation : EntityFixation? = null
override val observedMemories = listOf(MemoryModuleType.HURT_BY_ENTITY, CobblemonMemories.ATTACK_TARGET_DATA)
override fun perform(
@@ -23,6 +23,10 @@ import com.cobblemon.mod.common.api.ai.biggerbrain.distraction.Distraction
import com.cobblemon.mod.common.api.ai.biggerbrain.distraction.DistractionType
import com.cobblemon.mod.common.api.ai.biggerbrain.fixation.EntityFixation
import com.cobblemon.mod.common.api.ai.biggerbrain.fixation.LookAndMoveFixation
import com.cobblemon.mod.common.api.ai.biggerbrain.MovementStatus
import com.cobblemon.mod.common.api.ai.biggerbrain.MovementTracker
import com.cobblemon.mod.common.api.ai.movement.MovementIntent
import com.cobblemon.mod.common.api.ai.movement.TraversalPolicy
import com.cobblemon.mod.common.api.ai.config.objective.ObjectiveConfig
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangValue
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
@@ -46,9 +50,10 @@ class AttackTargetMelee(
private val attackCooldownTicks: Int = 20
) : Objective {
private var movementTask: EntityFixation? = null
private var movementTracker: MovementTracker? = null
private var remainingCooldownTicks = 0
override val observedMemories = listOf(MemoryModuleType.ATTACK_TARGET)
/** Ticks until the chase may plan again after failing, so an unreachable target doesn't get re-pathed every tick. */
private var remainingRepathTicks = 0
override fun getDescription(entity: LivingEntity) = "Attack target melee"
@@ -116,9 +121,26 @@ class AttackTargetMelee(
return ObjectiveStatus.FIXATED
}
if (movementTracker?.lastStatus == MovementStatus.FAILED) {
// Got as close as it's willing to (e.g. the bank of a lake it won't enter). Hold there for a bit.
movementTracker = null
remainingRepathTicks = REPATH_COOLDOWN_TICKS
}
if (remainingRepathTicks > 0) {
remainingRepathTicks--
entity.lookControl.setLookAt(target)
return ObjectiveStatus.ACTIVE
}
if (!brain.distract(Distraction.FORCE)) return ObjectiveStatus.BLOCKED
val walkTarget = WalkTarget(EntityTracker(target, true), speed, 1)
val movementTracker = RegularMovementTracker.attempt(entity, walkTarget) ?: return ObjectiveStatus.ACTIVE
val policy = TraversalPolicy.forIntent(entity, MovementIntent.URGENT)
val movementTracker = RegularMovementTracker.attempt(entity, walkTarget, policy)
this.movementTracker = movementTracker
if (movementTracker == null) {
remainingRepathTicks = REPATH_COOLDOWN_TICKS
return ObjectiveStatus.ACTIVE
}
movementTask = brain.fixate(LookAndMoveFixation(
distractionTypes = setOf(DistractionType.FORCE),
lookingTracker = LookingTracker.forTargetEntity(entity, target, trackEyeHeight = true),
@@ -142,6 +164,10 @@ class AttackTargetMelee(
}
}
companion object {
const val REPATH_COOLDOWN_TICKS = 20
}
class Config : ObjectiveConfig {
val speedMultiplier: ExpressionOrEntityVariable = Either.left("1.0".asExpression())
val attackRange: ExpressionOrEntityVariable = Either.left("2.0".asExpression())
@@ -43,8 +43,6 @@ class AvoidTarget(
private var movementTask: EntityFixation? = null
private var remainingCooldownTicks = 0
override val observedMemories = listOf(MemoryModuleType.AVOID_TARGET)
override fun getDescription(entity: LivingEntity) = "Avoiding target"
override fun perform(
@@ -44,7 +44,7 @@ import net.minecraft.world.entity.ai.memory.WalkTarget
* Handles transition of nearest visible attacker to a valid attack target when wanting to defend owner.
* Could possibly be more configurable in the future
*/
class DefendOwner() : Objective {
class DefendOwner : Objective {
var cooldownTicks = 0
override fun getDescription(entity: LivingEntity) = "Following parent species"
@@ -43,7 +43,6 @@ class Defensive(
) : Objective {
private var remainingCooldownTicks = 0
private var trackedEntity: LivingEntity? = null
override val observedMemories = listOf(MemoryModuleType.HURT_BY_ENTITY, CobblemonMemories.ATTACK_TARGET_DATA)
override fun getDescription(entity: LivingEntity) = "Defensive Behavior"
@@ -13,6 +13,7 @@ import com.cobblemon.mod.common.api.ai.ExpressionOrEntityVariable
import com.cobblemon.mod.common.api.ai.asVariables
import com.cobblemon.mod.common.api.ai.biggerbrain.BiggerBrain
import com.cobblemon.mod.common.api.ai.biggerbrain.distraction.Distraction
import com.cobblemon.mod.common.api.ai.biggerbrain.distraction.DistractionType
import com.cobblemon.mod.common.api.ai.biggerbrain.fixation.EntityFixation
import com.cobblemon.mod.common.api.ai.biggerbrain.objective.PickupItem.Companion.dropItem
import com.cobblemon.mod.common.api.ai.biggerbrain.sense.PokemonItemSense.Companion.PICKUP_ITEMS
@@ -67,8 +68,8 @@ class EatHeldItem(
remainingCooldownTicks--
return ObjectiveStatus.INACTIVE
}
val itemStack: ItemStack = entity.pokemon.heldItem()
if (canEat(itemStack, entity)) {
val itemStack = entity.pokemon.heldItem()
if (canEat(itemStack, entity) && brain.distract(distraction)) {
fixation = brain.fixate(EatHeldItemFixation(this))
remainingCooldownTicks = 120
return ObjectiveStatus.FIXATED
@@ -90,7 +91,7 @@ class EatHeldItem(
val itemStack: ItemStack = entity.pokemon.heldItem()
val itemConfig = objective.pickupItems.findMatchingEntry(entity.registryAccess(), itemStack)
if (objective.canEat(itemStack, entity)) {
if (ticksRemaining < 0) {
if (ticksRemaining-- < 0) {
var resultItemStack = itemStack.finishUsingItem(world, entity)
val configuredReturnItem = itemConfig?.returnItem
resultItemStack = if (configuredReturnItem != ItemStack.EMPTY) ItemStack(BuiltInRegistries.ITEM.get(configuredReturnItem))
@@ -117,14 +118,15 @@ class EatHeldItem(
}
}
return false
} else if ((itemConfig?.fullnessValue ?: 0) > 0 && entity.random.nextFloat() < 0.4f) {
ticksRemaining--
}
if ((itemConfig?.fullnessValue ?: 0) > 0 && entity.random.nextFloat() < 0.4f) {
entity.playSound(entity.getEatingSound(itemStack), 1.0f, 1.0f)
world.broadcastEntityEvent(entity, 45.toByte())
spawnFoodParticles(entity, itemStack)
}
return true
}
return true
}
return false
}
@@ -155,16 +157,20 @@ class EatHeldItem(
override fun create(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): Objective? {
): Objective {
var pickupItems = emptyList<ObtainableItem>()
if (entity is PokemonEntity) {
pickupItems = entity.config.getObjectList<ObtainableItem>(PICKUP_ITEMS)
pickupItems = entity.config.getObjectList(PICKUP_ITEMS)
}
val cooldownTicks = cooldownTicks.resolveInt(behaviourConfigurationContext.runtime)
return EatHeldItem(
cooldownTicks = cooldownTicks,
pickupItems = pickupItems
)
cooldownTicks = cooldownTicks,
pickupItems = pickupItems
)
}
}
companion object {
val distraction = Distraction(DistractionType.IDLE_BOREDOM)
}
}
@@ -0,0 +1,146 @@
/*
* 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.biggerbrain.objective
import com.bedrockk.molang.Expression
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
import com.cobblemon.mod.common.api.ai.ExpressionOrEntityVariable
import com.cobblemon.mod.common.api.ai.asSimplifiedExpression
import com.cobblemon.mod.common.api.ai.asVariables
import com.cobblemon.mod.common.api.ai.biggerbrain.BiggerBrain
import com.cobblemon.mod.common.api.ai.biggerbrain.LookingTracker
import com.cobblemon.mod.common.api.ai.biggerbrain.RegularMovementTracker
import com.cobblemon.mod.common.api.ai.biggerbrain.distraction.Distraction
import com.cobblemon.mod.common.api.ai.biggerbrain.distraction.DistractionType
import com.cobblemon.mod.common.api.ai.biggerbrain.fixation.EntityFixation
import com.cobblemon.mod.common.api.ai.biggerbrain.fixation.LookAndMoveFixation
import com.cobblemon.mod.common.api.ai.config.objective.ObjectiveConfig
import com.cobblemon.mod.common.api.ai.movement.MovementIntent
import com.cobblemon.mod.common.api.ai.movement.TraversalPolicy
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangValue
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.closestPosition
import com.cobblemon.mod.common.util.getIsSubmerged
import com.cobblemon.mod.common.util.mainThreadRuntime
import com.cobblemon.mod.common.util.resolveFloat
import com.cobblemon.mod.common.util.withQueryValue
import com.mojang.datafixers.util.Either
import net.minecraft.core.BlockPos
import net.minecraft.util.Mth
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.PathfinderMob
import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.level.LevelReader
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.pathfinder.PathComputationType
class FindAir(
val speedMultiplier: Expression,
) : Objective {
var fixation: EntityFixation? = null
var cooldownTicks = 20
override fun getDescription(entity: LivingEntity) = "Find air when submerged"
override fun perform(
entity: LivingEntity,
brain: BiggerBrain,
activeObjectiveTags: MutableSet<String>
): ObjectiveStatus {
if (entity !is PathfinderMob) {
return ObjectiveStatus.INVALID
} else if (brain.isFixation(fixation)) {
return ObjectiveStatus.FIXATED
}
fixation = null
if (!entity.getIsSubmerged() || entity.isInLava || cooldownTicks-- >= 0) {
return ObjectiveStatus.INACTIVE
}
cooldownTicks = 20
mainThreadRuntime.withQueryValue("entity", entity.asMostSpecificMoLangValue())
val speedMultiplier = mainThreadRuntime.resolveFloat(speedMultiplier)
if (!brain.canDistract(distraction)) {
return ObjectiveStatus.BLOCKED
}
val iterable = BlockPos.betweenClosed(
Mth.floor(entity.x - 4.0),
entity.blockY - 1,
Mth.floor(entity.z - 4.0),
Mth.floor(entity.x + 4.0),
Mth.floor(entity.y + 8.0),
Mth.floor(entity.z + 4.0)
)
val blockPos = entity.closestPosition(iterable) { isAirPos(entity.level(), it) }
?: return ObjectiveStatus.INACTIVE
val walkTarget = WalkTarget(blockPos, speedMultiplier, 0)
val tracker = RegularMovementTracker.attempt(
entity = entity,
walkTarget = walkTarget,
policy = TraversalPolicy.forIntent(
entity = entity,
intent = MovementIntent.URGENT,
),
)
if (tracker != null && brain.distract(distraction)) {
brain.fixate(
LookAndMoveFixation(
distractionTypes = setOf(DistractionType.FORCE),
description = "Finding air",
lookingTracker = LookingTracker.fromNavigationPathOrWalkTarget(entity, walkTarget),
movementTracker = tracker
)
)
}
return ObjectiveStatus.ACTIVE
}
private fun isAirPos(world: LevelReader, pos: BlockPos): Boolean {
val blockState = world.getBlockState(pos)
val aboveState = world.getBlockState(pos.above())
val notFluid = world.getFluidState(pos.above()).isEmpty || blockState.`is`(Blocks.BUBBLE_COLUMN)
val canPathfindThroughAbove = aboveState.isPathfindable(PathComputationType.LAND)
val solidBelow = !blockState.isPathfindable(PathComputationType.LAND)
return notFluid && canPathfindThroughAbove && solidBelow
}
companion object {
val distraction = Distraction(DistractionType.DAMAGE)
}
class Config : ObjectiveConfig {
val speedMultiplier: ExpressionOrEntityVariable = Either.left("0.5".asExpression())
override fun getVariables(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): List<MoLangConfigVariable> {
return listOf(speedMultiplier).asVariables()
}
override fun create(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): Objective {
return FindAir(
speedMultiplier = speedMultiplier.asSimplifiedExpression(entity),
)
}
}
}
@@ -55,7 +55,6 @@ class FriendlyBehaviour(
) {
private var remainingCooldownTicks = 0
private var fixation: EntityFixation? = null
override val observedMemories = listOf(MemoryModuleType.HURT_BY_ENTITY)
private val closeEnoughDistance = 1.5
private var remainingTicks = 0
@@ -17,6 +17,7 @@ import com.cobblemon.mod.common.api.ai.asSimplifiedExpression
import com.cobblemon.mod.common.api.ai.asVariables
import com.cobblemon.mod.common.api.ai.biggerbrain.BiggerBrain
import com.cobblemon.mod.common.api.ai.biggerbrain.LookingTracker
import com.cobblemon.mod.common.api.ai.biggerbrain.MovementStatus
import com.cobblemon.mod.common.api.ai.biggerbrain.RegularMovementTracker
import com.cobblemon.mod.common.api.ai.biggerbrain.distraction.Distraction
import com.cobblemon.mod.common.api.ai.biggerbrain.distraction.DistractionType
@@ -270,7 +271,7 @@ class GatherNectar(
}
}
class DepositNectarInSaccharineLeavesFixation() : EntityFixation {
class DepositNectarInSaccharineLeavesFixation : EntityFixation {
var successfulPollinationTicks = 0
var lastSoundPlayedTick = 0
var hoverPos: Vec3? = null
@@ -367,7 +368,7 @@ class GatherNectar(
}
}
fun onHiveMovementComplete(entity: LivingEntity, brain: BiggerBrain) {
fun onHiveMovementComplete(entity: LivingEntity, brain: BiggerBrain, status: MovementStatus?) {
if (entity !is PokemonEntity) return
if(!tryEnterHive(entity)) {
dropAndBlackListHive(entity)
@@ -378,7 +379,7 @@ class GatherNectar(
entity: PokemonEntity,
blockPos: BlockPos,
description : String,
onMovementComplete : ((entity: LivingEntity, brain: BiggerBrain) -> Unit)? = null,
onMovementComplete : ((entity: LivingEntity, brain: BiggerBrain, status: MovementStatus?) -> Unit)? = null,
) : EntityFixation? {
val speedMultiplier = mainThreadRuntime.resolveFloat(speedMultiplier)
@@ -72,10 +72,7 @@ class GoToSleep(
}
entity.pokemon.status = PersistentStatusContainer(Statuses.SLEEP)
}
entity.brain.eraseMemory(MemoryModuleType.WALK_TARGET)
entity.navigation.stop()
entity.brain.setActiveActivityToFirstValid(listOf(CobblemonActivities.POKEMON_SLEEPING_ACTIVITY))
entity.brain.setMemory(CobblemonMemories.POKEMON_SLEEPING, true)
brain.setMemory(CobblemonMemories.POKEMON_SLEEPING, true)
// Become fixated
fixation = brain.fixate(
@@ -89,20 +86,13 @@ class GoToSleep(
return ObjectiveStatus.BLOCKED
}
class SleepFixation(
val objective: GoToSleep,
override val movementTracker: MovementTracker?,
) : EntityFixation, MovementTrackerFixation {
override fun getDescription(entity: LivingEntity): String {
return "Sleeping"
}
override fun tickMovementFixation(): MovementStatus? {
return MovementStatus.PENDING
}
override fun getDescription(entity: LivingEntity) = "Sleeping"
override fun tickMovementFixation() = MovementStatus.PENDING
override fun canDistract(distraction: Distraction, entity: LivingEntity): Boolean {
if (distraction.isIdleBoredom) {
return false
@@ -112,12 +102,13 @@ class GoToSleep(
}
override fun tick(entity: LivingEntity, brain: BiggerBrain): Boolean {
if (!entity.brain.getMemorySafely(CobblemonMemories.POKEMON_DROWSY).orElse(false)) {
if (brain.getMemory(CobblemonMemories.POKEMON_DROWSY) != true) {
wakeUp(entity)
return false
}
return true
}
fun wakeUp(entity: LivingEntity) {
if (entity !is PokemonEntity) return
val hasSleepStatus = entity.pokemon.status?.status == Statuses.SLEEP
@@ -128,7 +119,6 @@ class GoToSleep(
entity.pokemon.status = null
}
entity.brain.eraseMemory(CobblemonMemories.POKEMON_SLEEPING)
entity.brain.useDefaultActivity()
}
}
@@ -15,30 +15,22 @@ import com.cobblemon.mod.common.api.ai.ExpressionOrEntityVariable
import com.cobblemon.mod.common.api.ai.asSimplifiedExpression
import com.cobblemon.mod.common.api.ai.asVariables
import com.cobblemon.mod.common.api.ai.biggerbrain.BiggerBrain
import com.cobblemon.mod.common.api.ai.biggerbrain.LookingTracker
import com.cobblemon.mod.common.api.ai.biggerbrain.MovementStatus
import com.cobblemon.mod.common.api.ai.biggerbrain.MovementTracker
import com.cobblemon.mod.common.api.ai.biggerbrain.RegularMovementTracker
import com.cobblemon.mod.common.api.ai.biggerbrain.distraction.Distraction
import com.cobblemon.mod.common.api.ai.biggerbrain.distraction.DistractionType
import com.cobblemon.mod.common.api.ai.biggerbrain.distraction.idleBoredomDistraction
import com.cobblemon.mod.common.api.ai.biggerbrain.fixation.EntityFixation
import com.cobblemon.mod.common.api.ai.biggerbrain.fixation.LookingTrackerFixation
import com.cobblemon.mod.common.api.ai.biggerbrain.fixation.MovementTrackerFixation
import com.cobblemon.mod.common.api.ai.config.objective.ObjectiveConfig
import com.cobblemon.mod.common.api.ai.resolveInt
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangValue
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.distanceTo
import com.cobblemon.mod.common.util.getMemorySafely
import com.cobblemon.mod.common.util.mainThreadRuntime
import com.cobblemon.mod.common.util.resolveFloat
import com.cobblemon.mod.common.util.withQueryValue
import com.mojang.datafixers.util.Either
import net.minecraft.sounds.SoundEvents
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.PathfinderMob
import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import net.minecraft.world.level.GameRules
@@ -79,6 +71,11 @@ class HarvestSweetBerryBush(
return ObjectiveStatus.FIXATED
}
if (!entity.level().gameRules.getRule(GameRules.RULE_MOBGRIEFING).get()) {
// Not allowed to alter the world
return ObjectiveStatus.INVALID
}
val berryPosition = entity.biggerBrain.getMemory(CobblemonMemories.NEARBY_SWEET_BERRY_BUSH) ?: return ObjectiveStatus.INACTIVE
if (remainingCooldownTicks > 0) {
@@ -88,93 +85,58 @@ class HarvestSweetBerryBush(
mainThreadRuntime.withQueryValue("entity", entity.asMostSpecificMoLangValue())
val maxDistance = mainThreadRuntime.resolveFloat(maxDistance)
val speedMultiplier = mainThreadRuntime.resolveFloat(speedMultiplier)
val distance = entity.distanceTo(berryPosition)
if (distance < maxDistance) {
if (remainingCooldownTicks <= 0 && brain.distract(idleBoredomDistraction)) {
if (remainingCooldownTicks <= 0 && brain.canDistract(idleBoredomDistraction)) {
remainingCooldownTicks = ticksBetweenAttempts
val movementTracker = RegularMovementTracker.attempt(
entity = entity,
walkTarget = WalkTarget(
berryPosition,
speedMultiplier,
1
)
val (fixation, status) = brain.tryMovementFixation(
blockPos = { berryPosition },
closeEnoughDistance = 1,
speedMultiplier = speedMultiplier,
distraction = idleBoredomDistraction,
distractionTypes = setOf(DistractionType.FORCE, DistractionType.DAMAGE, DistractionType.COMFORT),
description = "Harvesting sweet/glow berry",
onMovementStopped = ::onPathingStopped
)
if (movementTracker != null) {
harvestTask = brain.fixate(
fixation = HarvestSweetBerryBushFixation(
entity = entity,
movementTracker = movementTracker
)
)
}
this.harvestTask = fixation
return status
}
}
return ObjectiveStatus.ACTIVE
}
class HarvestSweetBerryBushFixation(
entity: PathfinderMob,
override val movementTracker: MovementTracker,
): EntityFixation, LookingTrackerFixation, MovementTrackerFixation {
override val lookingTracker: LookingTracker = LookingTracker(entity = entity, lookTarget = movementTracker.walkTarget.target)
override fun getDescription(entity: LivingEntity) = "Harvesting sweet/glow berry"
override fun canDistract(distraction: Distraction, entity: LivingEntity): Boolean {
return distraction.isDamage || distraction.isForce || (movementTracker.lastStatus == MovementStatus.COMPLETED && entity.brain.getMemorySafely(
CobblemonMemories.NEARBY_SWEET_BERRY_BUSH).orElse(null) != null)
private fun onPathingStopped(entity: LivingEntity, brain: BiggerBrain, status: MovementStatus?) {
if (entity !is PokemonEntity) {
return
}
override fun onDistract(distraction: Distraction, entity: LivingEntity) {
movementTracker.stop()
}
override fun tick(entity: LivingEntity, brain: BiggerBrain): Boolean {
lookingTracker.tick()
movementTracker.tick()
if (movementTracker.lastStatus == MovementStatus.COMPLETED) {
val world = entity.level()
if (entity !is PokemonEntity) return false
if (!world.gameRules.getRule(GameRules.RULE_MOBGRIEFING).get()) {
// Not allowed to alter the world
return false
if (status == MovementStatus.COMPLETED) {
val world = entity.level()
val blockPos = brain.getMemory(CobblemonMemories.NEARBY_SWEET_BERRY_BUSH) ?: return
val blockState = world.getBlockState(blockPos)
if (CaveVines.hasGlowBerries(blockState)) { // Glow Berries Case
CaveVines.use(entity, blockState, world, blockPos)
} else if (blockState.`is`(Blocks.SWEET_BERRY_BUSH)) { // Sweet Berries Case
blockState.setValue(SweetBerryBushBlock.AGE, 1)
var j: Int = 1 + entity.level().random.nextInt(2) + if (blockState.getValue<Int>(SweetBerryBushBlock.AGE) == 3) 1 else 0
val itemStack: ItemStack = entity.pokemon.heldItem
if (itemStack.isEmpty) {
entity.pokemon.swapHeldItem(ItemStack(Items.SWEET_BERRIES))
--j
}
val blockPos = entity.brain.getMemory(CobblemonMemories.NEARBY_SWEET_BERRY_BUSH).orElse(null) ?: return false
val blockState = world.getBlockState(blockPos)
if (CaveVines.hasGlowBerries(blockState)) { // Glow Berries Case
CaveVines.use(entity, blockState, world, blockPos)
} else if (blockState.`is`(Blocks.SWEET_BERRY_BUSH)) { // Sweet Berries Case
blockState.setValue(SweetBerryBushBlock.AGE, 1)
var j: Int = 1 + entity.level().random.nextInt(2) + if (blockState.getValue<Int>(SweetBerryBushBlock.AGE) == 3) 1 else 0
val itemStack: ItemStack = entity.pokemon.heldItem
if (itemStack.isEmpty) {
entity.pokemon.swapHeldItem(ItemStack(Items.SWEET_BERRIES))
--j
}
if (j > 0) {
Block.popResource(entity.level(), blockPos, ItemStack(Items.SWEET_BERRIES, j))
}
entity.playSound(SoundEvents.SWEET_BERRY_BUSH_PICK_BERRIES, 1.0f, 1.0f)
entity.level().setBlock(blockPos, blockState.setValue(SweetBerryBushBlock.AGE, 1) as BlockState, 2)
entity.level().gameEvent(GameEvent.BLOCK_CHANGE, blockPos, GameEvent.Context.of(entity))
if (j > 0) {
Block.popResource(entity.level(), blockPos, ItemStack(Items.SWEET_BERRIES, j))
}
entity.brain.eraseMemory(CobblemonMemories.NEARBY_SWEET_BERRY_BUSH)
return false
} else if (movementTracker.lastStatus == MovementStatus.FAILED) {
// Cannot reach, give up for a awhile
entity.brain.setMemoryWithExpiry(CobblemonMemories.DISABLE_WALK_TO_BERRY_BUSH, true, 200)
entity.brain.eraseMemory(CobblemonMemories.NEARBY_SWEET_BERRY_BUSH)
return false
entity.playSound(SoundEvents.SWEET_BERRY_BUSH_PICK_BERRIES, 1.0f, 1.0f)
entity.level().setBlock(blockPos, blockState.setValue(SweetBerryBushBlock.AGE, 1) as BlockState, 2)
entity.level().gameEvent(GameEvent.BLOCK_CHANGE, blockPos, GameEvent.Context.of(entity))
}
return true
brain.eraseMemory(CobblemonMemories.NEARBY_SWEET_BERRY_BUSH)
} else if (status == MovementStatus.FAILED) {
// Cannot reach, give up for a while
brain.setMemory(CobblemonMemories.DISABLE_WALK_TO_BERRY_BUSH, true, 200)
brain.eraseMemory(CobblemonMemories.NEARBY_SWEET_BERRY_BUSH)
}
}
@@ -49,7 +49,7 @@ class MoveIntoWater(
var isWanderTurnDirectionClockwise = true
var wanderTurnChance = BASE_TURN_CHANCE
var failCount = 0
override fun getDescription(entity: LivingEntity) = "Move into water if currently out of water"
override fun getDescription(entity: LivingEntity) = "Return to water"
override fun perform(
entity: LivingEntity,
brain: BiggerBrain,
@@ -84,12 +84,12 @@ class MoveIntoWater(
val runtime = mainThreadRuntime
val wanderControl = entity.brain.getMemorySafely(CobblemonMemories.WANDER_CONTROL).orElse(null)
val allowLand = wanderControl?.allowLand
?: runtime.resolveBoolean("(q.entity.behaviour.moving.walk.can_walk && q.entity.behaviour.moving.walk.avoids_land) || q.entity.behaviour.moving.fly.can_fly".asExpression())
?: runtime.resolveBoolean("(q.entity.movement.can_walk && q.entity.movement.avoids_land) || q.entity.movement.can_fly".asExpression())
val allowAir = wanderControl?.allowAir
?: runtime.resolveBoolean("q.entity.behaviour.moving.fly.can_fly && !q.entity.behaviour.moving.walk.can_walk".asExpression())
?: runtime.resolveBoolean("q.entity.movement.can_fly && !q.entity.movement.can_walk".asExpression())
val types = mutableSetOf<WanderType>()
if ((allowLand || allowAir)) {
if (!(allowLand || allowAir)) {
types.add(WanderType.WATER)
}
@@ -215,7 +215,8 @@ class MoveIntoWater(
override fun tick(entity: LivingEntity, brain: BiggerBrain): Boolean {
val movementStatus = tickMovementFixation()
if (movementStatus == MovementStatus.COMPLETED) return false
if (!entity.isInWater) {
// We're done, yay!
if (entity.isInWater) {
return false
}
return movementStatus != MovementStatus.FAILED
@@ -55,27 +55,21 @@ class MoveOutOfWater(
brain: BiggerBrain,
activeObjectiveTags: MutableSet<String>
): ObjectiveStatus {
if (entity !is TamableAnimal) {
return ObjectiveStatus.INVALID
}
brain.checkObjectivePrerequisites<TamableAnimal>(
cooldownTicks = remainingCooldownTicks--,
fixation = movementTask,
ifIneligible = { remainingCooldownTicks = ticksBetweenAttempts }
)?.let { return it }
entity as TamableAnimal
if (brain.isFixation(movementTask)) {
remainingCooldownTicks = ticksBetweenAttempts
return ObjectiveStatus.FIXATED
}
if (!entity.isInWater || entity.isUnderWater) {
// Focus on not drowning first
// TODO: 100% boned if in an underwater cave
return ObjectiveStatus.INACTIVE
}
if (remainingCooldownTicks > 0) {
remainingCooldownTicks--
return ObjectiveStatus.INACTIVE
} else {
remainingCooldownTicks = ticksBetweenAttempts
}
remainingCooldownTicks = ticksBetweenAttempts
val maxDistance = mainThreadRuntime.resolveInt(maxDistance)
val speedMultiplier = mainThreadRuntime.resolveFloat(speedMultiplier)
@@ -83,16 +77,15 @@ class MoveOutOfWater(
val runtime = mainThreadRuntime
val wanderControl = entity.brain.getMemorySafely(CobblemonMemories.WANDER_CONTROL).orElse(null)
val allowLand = wanderControl?.allowLand
?: runtime.resolveBoolean("(q.entity.behaviour.moving.walk.can_walk && !q.entity.behaviour.moving.walk.avoids_land) || q.entity.behaviour.moving.fly.can_fly".asExpression())
?: runtime.resolveBoolean("(q.entity.movement.can_walk && !q.entity.movement.avoids_land) || q.entity.movement.can_fly".asExpression())
val allowAir = wanderControl?.allowAir
?: runtime.resolveBoolean("q.entity.behaviour.moving.fly.can_fly && !q.entity.behaviour.moving.walk.can_walk".asExpression())
?: runtime.resolveBoolean("q.entity.movement.can_fly && !q.entity.movement.can_walk".asExpression())
val types = mutableSetOf<WanderType>()
if ((allowLand || allowAir)) {
types.add(WanderType.LAND)
}
if (types.isEmpty()) {
return ObjectiveStatus.INACTIVE
}
@@ -104,10 +97,7 @@ class MoveOutOfWater(
walkTarget = CobblemonWalkTarget(
BlockPos.containing(targetVec3.x, targetVec3.y, targetVec3.z),
speedMultiplier,
0,
nodeTypeFilter = { nodeType ->
true
}
0
)
}
@@ -10,19 +10,27 @@ package com.cobblemon.mod.common.api.ai.biggerbrain.objective
import com.bedrockk.molang.Expression
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.api.ai.*
import com.cobblemon.mod.common.api.ai.biggerbrain.*
import com.cobblemon.mod.common.api.ai.biggerbrain.distraction.Distraction
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
import com.cobblemon.mod.common.api.ai.ExpressionOrEntityVariable
import com.cobblemon.mod.common.api.ai.asSimplifiedExpression
import com.cobblemon.mod.common.api.ai.asVariables
import com.cobblemon.mod.common.api.ai.biggerbrain.BiggerBrain
import com.cobblemon.mod.common.api.ai.biggerbrain.distraction.comfortDistraction
import com.cobblemon.mod.common.api.ai.biggerbrain.fixation.EntityFixation
import com.cobblemon.mod.common.api.ai.biggerbrain.fixation.MovementTrackerFixation
import com.cobblemon.mod.common.api.ai.config.objective.ObjectiveConfig
import com.cobblemon.mod.common.api.ai.movement.MovementIntent
import com.cobblemon.mod.common.api.ai.movement.TraversalPolicy
import com.cobblemon.mod.common.api.ai.resolveInt
import com.cobblemon.mod.common.api.pokemon.status.Statuses
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.*
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.getBlockPositions
import com.cobblemon.mod.common.util.mainThreadRuntime
import com.cobblemon.mod.common.util.random
import com.cobblemon.mod.common.util.resolveBoolean
import com.cobblemon.mod.common.util.resolveDouble
import com.mojang.datafixers.util.Either
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.phys.AABB
@@ -79,58 +87,26 @@ class MoveToRestingPlace(
val hSearchDistance = mainThreadRuntime.resolveDouble(horizontalSearchDistance)
val vSearchDistance = mainThreadRuntime.resolveDouble(verticalSearchDistance)
val positionList = entity.level()
.getBlockPositions(AABB.ofSize(entity.position(), hSearchDistance, vSearchDistance, hSearchDistance))
.filter(entity::canSleepAt)
val position = if (positionList.isEmpty()) null else positionList.random(1).firstOrNull()
val (movementTask, status) = brain.tryMovementFixation(
blockPos = {
val positionList = entity.level()
.getBlockPositions(AABB.ofSize(entity.position(), hSearchDistance, vSearchDistance, hSearchDistance))
.filter(entity::canSleepAt)
if (positionList.isEmpty()) null else positionList.random(1).firstOrNull()
},
speedMultiplier = speedMultiplier,
closeEnoughDistance = 0,
traversalPolicy = { TraversalPolicy.forIntent(entity, MovementIntent.IDLE) },
description = "Moving to a resting place",
distraction = comfortDistraction
)
if (position != null && brain.distract(comfortDistraction)) {
val speedMultiplier = mainThreadRuntime.resolveFloat(speedMultiplier)
val movementTracker = RegularMovementTracker.attempt(
entity = entity,
walkTarget = WalkTarget(
position,
speedMultiplier,
0
)
)
if (movementTracker != null) {
// Become fixated
movementTask = brain.fixate(
fixation = MoveToRestingPlaceFixation(
movementTracker = movementTracker,
)
)
}
} else {
return ObjectiveStatus.INACTIVE
}
return ObjectiveStatus.ACTIVE
this.movementTask = movementTask
return status
}
return ObjectiveStatus.INACTIVE
}
class MoveToRestingPlaceFixation(
override val movementTracker: MovementTracker,
) : EntityFixation, MovementTrackerFixation {
override fun getDescription(entity: LivingEntity) = "Moving to a resting place"
override fun canDistract(distraction: Distraction, entity: LivingEntity): Boolean {
return distraction.isDamage || distraction.isForce || movementTracker.lastStatus == MovementStatus.COMPLETED
}
override fun onDistract(distraction: Distraction, entity: LivingEntity) {
movementTracker.stop()
}
override fun tick(entity: LivingEntity, brain: BiggerBrain): Boolean {
val movementStatus = tickMovementFixation()
if (movementStatus == MovementStatus.COMPLETED) return false
return movementStatus != MovementStatus.FAILED
}
}
class Config : ObjectiveConfig {
val ticksBetweenAttempts: ExpressionOrEntityVariable = Either.left("60".asExpression())
val horizontalSearchDistance: ExpressionOrEntityVariable = Either.left("10.0".asExpression())
@@ -51,7 +51,6 @@ class NeutralBehaviour(
targetCondition = targetCondition, // target condition (will be checked on entity interaction)
description = description,
) {
override val observedMemories = listOf(MemoryModuleType.HURT_BY_ENTITY, CobblemonMemories.ATTACK_TARGET_DATA)
private var fixation: EntityFixation? = null
override fun perform(
@@ -164,7 +163,7 @@ class NeutralBehaviour(
): Objective {
return NeutralBehaviour(
condition = condition.asSimplifiedExpression(entity),
targetCondition = condition.asSimplifiedExpression(entity),
targetCondition = targetCondition.asSimplifiedExpression(entity),
speedMultiplier = speedMultiplier.asSimplifiedExpression(entity),
engageRange = engageRange.asSimplifiedExpression(entity),
disengageRange = disengageRange.asSimplifiedExpression(entity),
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.api.ai.biggerbrain.objective
import com.cobblemon.mod.common.api.ai.biggerbrain.BiggerBrain
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.memory.MemoryModuleType
/**
* An ongoing priority for an entity. Objectives are managed by the [BiggerBrain] system. Objectives can act out
@@ -29,10 +28,6 @@ interface Objective {
val OBJECTIVE_TAG_WANDERING = "WANDERING"
}
/** The memory types that, upon being changed, should trigger [onMemoryChanged]. */
val observedMemories: List<MemoryModuleType<*>>
get() = emptyList()
/** Gets a current description for this objective. This is purely used for the debug overlay. */
fun getDescription(entity: LivingEntity): String
/** Performs this objective's logic for a tick. */
@@ -110,6 +110,7 @@ class PickupItem(
val lookingTracker = LookingTracker(entity = entity, lookTarget = movementTracker.walkTarget.target)
collectItemTask = brain.fixate(
fixation = PickupItemFixation(
brain = brain,
objective = this,
itemEntity = itemEntity,
movementTracker = movementTracker,
@@ -140,6 +141,7 @@ class PickupItem(
}
class PickupItemFixation(
val brain: BiggerBrain,
val objective: PickupItem,
val itemEntity: ItemEntity,
override val movementTracker: MovementTracker,
@@ -148,8 +150,8 @@ class PickupItem(
override fun getDescription(entity: LivingEntity) = "Moving to desired item"
override fun canDistract(distraction: Distraction, entity: LivingEntity): Boolean {
return distraction.isDamage || distraction.isForce || (movementTracker.lastStatus == MovementStatus.COMPLETED && entity.brain.getMemorySafely(
CobblemonMemories.NEARBY_SWEET_BERRY_BUSH).orElse(null) != null)
return distraction.isDamage || distraction.isForce
|| (movementTracker.lastStatus == MovementStatus.COMPLETED && brain.getMemory(MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM) != null)
}
override fun onDistract(distraction: Distraction, entity: LivingEntity) {
@@ -38,7 +38,6 @@ class SkittishBehaviour(
description = description,
) {
private var remainingCooldownTicks = 0
override val observedMemories = listOf(MemoryModuleType.HURT_BY_ENTITY, CobblemonMemories.ATTACK_TARGET_DATA)
override fun perform(
entity: LivingEntity,
@@ -24,6 +24,8 @@ import com.cobblemon.mod.common.api.ai.biggerbrain.fixation.EntityFixation
import com.cobblemon.mod.common.api.ai.biggerbrain.fixation.LookingTrackerFixation
import com.cobblemon.mod.common.api.ai.biggerbrain.fixation.MovementTrackerFixation
import com.cobblemon.mod.common.api.ai.config.objective.ObjectiveConfig
import com.cobblemon.mod.common.api.ai.movement.MovementIntent
import com.cobblemon.mod.common.api.ai.movement.TraversalPolicy
import com.cobblemon.mod.common.api.ai.resolveInt
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangValue
import com.cobblemon.mod.common.util.asExpression
@@ -111,7 +113,9 @@ class StayNearOwner(
owner,
speedMultiplier,
maxOf(completionRange, ceil(horizontalHitboxRadius).toInt()) - 1
)
),
// Will put up with some discomfort to keep up, but not as much as when chasing something.
policy = TraversalPolicy.forIntent(entity, MovementIntent.COMFORT)
)
if (movementTracker != null) {
followTask = brain.fixate(
@@ -58,7 +58,6 @@ class TerritorialBehaviour(
private var remainingCooldownTicks = 0
private val objectiveDescription: String = mainThreadRuntime.resolveString(description)
override val observedMemories = listOf(MemoryModuleType.HURT_BY_ENTITY, CobblemonMemories.ATTACK_TARGET_DATA)
private var cryCooldownTicks = 0
private var proximityAttackCooldown = 0
private var satiationHitCount = 999
@@ -0,0 +1,224 @@
/*
* 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.biggerbrain.objective
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
import com.cobblemon.mod.common.api.ai.ExpressionOrEntityVariable
import com.cobblemon.mod.common.api.ai.asVariables
import com.cobblemon.mod.common.api.ai.biggerbrain.BiggerBrain
import com.cobblemon.mod.common.api.ai.biggerbrain.MovementStatus
import com.cobblemon.mod.common.api.ai.biggerbrain.MovementTracker
import com.cobblemon.mod.common.api.ai.biggerbrain.RegularMovementTracker
import com.cobblemon.mod.common.api.ai.biggerbrain.distraction.Distraction
import com.cobblemon.mod.common.api.ai.biggerbrain.fixation.EntityFixation
import com.cobblemon.mod.common.api.ai.biggerbrain.fixation.MovementTrackerFixation
import com.cobblemon.mod.common.api.ai.config.objective.ObjectiveConfig
import com.cobblemon.mod.common.api.ai.movement.FluidKind
import com.cobblemon.mod.common.api.ai.movement.MovementCapabilities
import com.cobblemon.mod.common.api.ai.movement.Occupancy
import com.cobblemon.mod.common.api.ai.movement.TraversalPolicy
import com.cobblemon.mod.common.api.ai.resolveFloat
import com.cobblemon.mod.common.api.ai.resolveInt
import com.cobblemon.mod.common.entity.ai.OmniPathNavigation
import com.cobblemon.mod.common.util.asExpression
import com.mojang.datafixers.util.Either
import net.minecraft.core.BlockPos
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.PathfinderMob
import net.minecraft.world.entity.ai.memory.WalkTarget
/**
* Gets the entity out of a fluid that's killing it:
* - A fluid it can neither swim nor breathe in, once its head is under. It sinks, and this looks for a route along the
* bottom to anywhere its head would be above the surface, preferring the route that spends the least time under.
* - Lava, when it isn't fire immune. Everything can swim in lava, so this looks for the quickest way to dry land or,
* failing that, water.
*
* If there's no route, nothing is done about it and the entity drowns or burns. Once a route is found, the entity is
* committed to it: damage and forceful distractions don't interrupt it while it's still in danger.
*
* @author Hiroku
* @since September 30th, 2026
*/
class WalkOutOfWater(
/** How far horizontally to look for somewhere to surface. */
val horizontalRange: Int = 12,
/** How far above the entity to look for somewhere to surface. */
val verticalRange: Int = 12,
/** How many of the closest exits to hand to the pathfinder. More exits make the search slower. */
val maxExits: Int = 48,
val speedMultiplier: Float = 0.4F,
/**
* How many times a route may be searched for while submerged. A search that finds nothing ends the attempt
* outright; extra searches are only for when a found route gets blocked along the way.
*/
val maxSearches: Int = 3,
) : Objective {
var fixation: EntityFixation? = null
/** Searches done since the entity's head last went under. */
var searches = 0
/** Set when a search found no way out. Reset once the entity's head is out of the fluid. */
var gaveUp = false
override fun getDescription(entity: LivingEntity) = when {
gaveUp -> "Escape deadly fluid (no way out)"
else -> "Escape deadly fluid"
}
override fun perform(
entity: LivingEntity,
brain: BiggerBrain,
activeObjectiveTags: MutableSet<String>
): ObjectiveStatus {
if (entity !is PathfinderMob || entity.navigation !is OmniPathNavigation) {
return ObjectiveStatus.INVALID
}
// Cheap check first since this runs every tick for every Pokémon.
val capabilities = if (entity.isInWater || entity.isInLava) MovementCapabilities.of(entity) else null
val danger = capabilities?.let { deadlyFluid(entity, it) } ?: FluidKind.NONE
if (capabilities == null || danger == FluidKind.NONE) {
searches = 0
gaveUp = false
return if (brain.isFixation(fixation)) ObjectiveStatus.FIXATED else ObjectiveStatus.INACTIVE
}
if (brain.isFixation(fixation)) {
return ObjectiveStatus.FIXATED
} else if (gaveUp || searches >= maxSearches) {
return ObjectiveStatus.INACTIVE
} else if (!brain.canDistract(Distraction.FORCE)) {
return ObjectiveStatus.BLOCKED
}
searches++
val navigation = entity.navigation as OmniPathNavigation
val policy = TraversalPolicy.escapeFluid(entity, danger)
val searchRange = horizontalRange * 1.5F + verticalRange
// Dry land first so the escape doesn't end knee-deep, then anywhere else that's safe.
val isDryLand: (Occupancy, FluidKind) -> Boolean = { occupancy, fluid -> occupancy == Occupancy.WALK && fluid == FluidKind.NONE }
val isSafe: (Occupancy, FluidKind) -> Boolean = when (danger) {
// Wading in it is fine since the head's out. Lava is never a way out of water.
FluidKind.LAVA -> { occupancy, fluid -> fluid != FluidKind.LAVA && policy.allows(occupancy, fluid) }
else -> { occupancy, fluid -> occupancy == Occupancy.WADE && fluid == danger }
}
val path = sequenceOf(isDryLand, isSafe)
.map { findExits(entity, capabilities, it) }
.filter { it.isNotEmpty() }
.mapNotNull { navigation.planPath(it, 0, policy, searchRange) }
.firstOrNull { it.canReach() }
if (path == null) {
gaveUp = true
return ObjectiveStatus.INACTIVE
}
val walkTarget = WalkTarget(path.target, speedMultiplier, 0)
val tracker = RegularMovementTracker.following(entity, walkTarget, path, policy)
if (tracker == null) {
gaveUp = true
return ObjectiveStatus.INACTIVE
}
// Only interrupt whatever else was going on once there's actually somewhere to go.
brain.distract(Distraction.FORCE)
fixation = brain.fixate(WalkOutOfWaterFixation(capabilities, tracker))
return ObjectiveStatus.FIXATED
}
/**
* Nearby positions matching [accepts], closest first. Only positions the entity could be in are considered;
* whether they can be reached is left to the pathfinder.
*/
private fun findExits(
entity: PathfinderMob,
capabilities: MovementCapabilities,
accepts: (Occupancy, FluidKind) -> Boolean
): Set<BlockPos> {
val level = entity.level()
val origin = entity.blockPosition()
val exits = mutableListOf<BlockPos>()
// Hardly anything below is useful: the way out of water is up the bank, not into a cave under the bed.
for (pos in BlockPos.betweenClosed(
origin.offset(-horizontalRange, -MAX_EXIT_DEPTH_BELOW, -horizontalRange),
origin.offset(horizontalRange, verticalRange, horizontalRange)
)) {
val occupancy = Occupancy.at(capabilities, entity.eyeHeight, pos, level::getBlockState) ?: continue
if (occupancy.isStandable && accepts(occupancy, FluidKind.of(level.getFluidState(pos)))) {
exits.add(pos.immutable())
}
}
// Closest horizontally so that exits straight overhead (unreachable cliff tops) don't crowd out the shore.
return exits
.sortedBy { (it.x - origin.x) * (it.x - origin.x) + (it.z - origin.z) * (it.z - origin.z) }
.take(maxExits)
.toSet()
}
class WalkOutOfWaterFixation(
private val capabilities: MovementCapabilities,
override val movementTracker: MovementTracker
) : EntityFixation, MovementTrackerFixation {
override fun getDescription(entity: LivingEntity) = "Escaping deadly fluid"
override fun canDistract(distraction: Distraction, entity: LivingEntity): Boolean {
// Nothing is more important than surviving, but once out of danger it's fair game.
return deadlyFluid(entity, capabilities) == FluidKind.NONE
}
override fun onDistract(distraction: Distraction, entity: LivingEntity) {
movementTracker.stop()
}
override fun tick(entity: LivingEntity, brain: BiggerBrain): Boolean {
// Keeps going after surfacing, but by then canDistract lets anything more interesting take over.
return tickMovementFixation() == MovementStatus.PENDING
}
}
class Config : ObjectiveConfig {
val horizontalRange: ExpressionOrEntityVariable = Either.left("12".asExpression())
val verticalRange: ExpressionOrEntityVariable = Either.left("12".asExpression())
val maxExits: ExpressionOrEntityVariable = Either.left("48".asExpression())
val speedMultiplier: ExpressionOrEntityVariable = Either.left("0.4".asExpression())
val maxSearches: ExpressionOrEntityVariable = Either.left("3".asExpression())
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) =
listOf(horizontalRange, verticalRange, maxExits, speedMultiplier, maxSearches).asVariables()
override fun create(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext): Objective {
val runtime = behaviourConfigurationContext.runtime
return WalkOutOfWater(
horizontalRange = horizontalRange.resolveInt(runtime),
verticalRange = verticalRange.resolveInt(runtime),
maxExits = maxExits.resolveInt(runtime),
speedMultiplier = speedMultiplier.resolveFloat(runtime),
maxSearches = maxSearches.resolveInt(runtime)
)
}
}
companion object {
const val MAX_EXIT_DEPTH_BELOW = 2
/** Whether the entity's head is in a fluid it can neither swim nor breathe in. */
fun isDrowningIn(capabilities: MovementCapabilities, headFluid: FluidKind): Boolean {
return headFluid != FluidKind.NONE && !capabilities.canSwimIn(headFluid) && !capabilities.canBreatheIn(headFluid)
}
/** The fluid that's currently killing the entity, or [FluidKind.NONE]. */
fun deadlyFluid(entity: LivingEntity, capabilities: MovementCapabilities): FluidKind {
val headFluid = FluidKind.atEyes(entity)
return when {
isDrowningIn(capabilities, headFluid) -> headFluid
entity.isInLava && capabilities.isHarmedBy(FluidKind.LAVA) -> FluidKind.LAVA
else -> FluidKind.NONE
}
}
}
}
@@ -48,9 +48,9 @@ import net.minecraft.world.entity.ai.memory.WalkTarget
class WanderAround(
val condition: Expression = "true".asExpression(),
val cooldownTicks: Int = 80,
val allowLand: Expression = "(q.entity.behaviour.moving.walk.can_walk && !q.entity.behaviour.moving.walk.avoids_land) || q.entity.behaviour.moving.fly.can_fly".asExpression(),
val allowWater: Expression = "q.entity.behaviour.moving.swim.can_swim_in_water && !q.entity.behaviour.moving.swim.avoids_water".asExpression(),
val allowAir: Expression = "q.entity.behaviour.moving.fly.can_fly && !q.entity.behaviour.moving.walk.can_walk".asExpression(),
val allowLand: Expression = "(q.entity.movement.can_walk && !q.entity.movement.avoids_land) || q.entity.movement.can_fly".asExpression(),
val allowWater: Expression = "q.entity.movement.can_swim_in_water && !q.entity.movement.avoids_water".asExpression(),
val allowAir: Expression = "q.entity.movement.can_fly && !q.entity.movement.can_walk".asExpression(),
val wanderChance: Expression = "1/(20*6)".asExpression(),
val horizontalRange: Expression = "10".asExpression(),
val verticalRange: Expression = "5".asExpression(),
@@ -113,6 +113,10 @@ class WanderAround(
if (allowWater && WanderType.WATER.isValid(entity)) {
types.add(WanderType.WATER)
}
// Seafloor walkers can't swim, so allowWater's default never covers them; only an explicit opt-out stops them.
if (wanderControl?.allowWater != false && WanderType.SEABED.isValid(entity)) {
types.add(WanderType.SEABED)
}
if (types.isEmpty()) {
return ObjectiveStatus.INVALID
@@ -157,9 +161,9 @@ class WanderAround(
class Config : ObjectiveConfig {
val condition: ExpressionOrEntityVariable = Either.left("true".asExpression())
val cooldownTicks: ExpressionOrEntityVariable = Either.left("80".asExpression())
val allowLand: ExpressionOrEntityVariable = Either.left("(q.entity.behaviour.moving.walk.can_walk && !q.entity.behaviour.moving.walk.avoids_land) || q.entity.behaviour.moving.fly.can_fly".asExpression())
val allowWater: ExpressionOrEntityVariable = Either.left("q.entity.behaviour.moving.swim.can_swim_in_water && !q.entity.behaviour.moving.swim.avoids_water".asExpression())
val allowAir: ExpressionOrEntityVariable = Either.left("q.entity.behaviour.moving.fly.can_fly && !q.entity.behaviour.moving.walk.can_walk".asExpression())
val allowLand: ExpressionOrEntityVariable = Either.left("(q.entity.movement.can_walk && !q.entity.movement.avoids_land) || q.entity.movement.can_fly".asExpression())
val allowWater: ExpressionOrEntityVariable = Either.left("q.entity.movement.can_swim_in_water && !q.entity.movement.avoids_water".asExpression())
val allowAir: ExpressionOrEntityVariable = Either.left("q.entity.movement.can_fly && !q.entity.movement.can_walk".asExpression())
val wanderChance: ExpressionOrEntityVariable = Either.left("1/(20*6)".asExpression())
val horizontalRange: ExpressionOrEntityVariable = Either.left("10".asExpression())
val verticalRange: ExpressionOrEntityVariable = Either.left("5".asExpression())
@@ -217,9 +221,9 @@ class WanderAround(
val wanderControl = entity.brain.getMemorySafely(CobblemonMemories.WANDER_CONTROL).orElse(null)
runtime.withQueryValue("entity", entity.asMostSpecificMoLangValue())
val allowLand = wanderControl?.allowLand ?: runtime.resolveBoolean("(q.entity.behaviour.moving.walk.can_walk && !q.entity.behaviour.moving.walk.avoids_land) || q.entity.behaviour.moving.fly.can_fly".asExpression())
val allowWater = wanderControl?.allowWater ?: runtime.resolveBoolean("q.entity.behaviour.moving.swim.can_swim_in_water && !q.entity.behaviour.moving.swim.avoids_water".asExpression())
val allowAir = wanderControl?.allowAir ?: runtime.resolveBoolean("q.entity.behaviour.moving.fly.can_fly && !q.entity.behaviour.moving.walk.can_walk".asExpression())
val allowLand = wanderControl?.allowLand ?: runtime.resolveBoolean("(q.entity.movement.can_walk && !q.entity.movement.avoids_land) || q.entity.movement.can_fly".asExpression())
val allowWater = wanderControl?.allowWater ?: runtime.resolveBoolean("q.entity.movement.can_swim_in_water && !q.entity.movement.avoids_water".asExpression())
val allowAir = wanderControl?.allowAir ?: runtime.resolveBoolean("q.entity.movement.can_fly && !q.entity.movement.can_walk".asExpression())
val types = mutableSetOf<WanderType>()
if ((allowLand || allowAir) && WanderType.LAND.isValid(entity)) {
@@ -228,6 +232,9 @@ class WanderAround(
if (allowWater && WanderType.WATER.isValid(entity)) {
types.add(WanderType.WATER)
}
if (wanderControl?.allowWater != false && WanderType.SEABED.isValid(entity)) {
types.add(WanderType.SEABED)
}
if (types.isEmpty()) {
return null
@@ -9,7 +9,6 @@
package com.cobblemon.mod.common.api.ai.biggerbrain.sense
import com.cobblemon.mod.common.api.ai.biggerbrain.BiggerBrain
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.memory.MemoryModuleType
@@ -23,30 +22,6 @@ import net.minecraft.world.entity.ai.memory.MemoryModuleType
* @since December 29th, 2025
*/
interface Sense {
companion object {
val types = mutableMapOf(
cobblemonResource("battling_pokemon") to BattlingPokemonSense::class.java,
cobblemonResource("drowsy") to DrowsySense::class.java,
cobblemonResource("nearby_entities") to NearbyEntitiesSense::class.java,
cobblemonResource("npc_battling") to NPCBattlingSense::class.java,
cobblemonResource("pokemon_adult") to PokemonAdultSense::class.java,
cobblemonResource("hurt_by") to HurtBySense::class.java,
cobblemonResource("mate_sensor") to MateSense::class.java,
cobblemonResource("nest_sensor") to NestSense::class.java,
cobblemonResource("egg_sensor") to EggSense::class.java,
cobblemonResource("ideal_nest_location_sensor") to IdealNestPlacementSense::class.java,
cobblemonResource("hurt_by") to HurtBySense::class.java,
cobblemonResource("nearby_sweet_berry_bush") to SweetBerryBushSense::class.java,
cobblemonResource("jukebox_playing") to JukeboxSense::class.java,
cobblemonResource("nearby_sweet_berry_bush") to SweetBerryBushSense::class.java,
cobblemonResource("pokemon_nearby_wanted_item") to PokemonItemSense.Config::class.java,
cobblemonResource("flower_sense") to FlowerSense.Config::class.java,
cobblemonResource("beehive_sense") to BeeHiveSense.Config::class.java,
cobblemonResource("sacc_leaf_sense") to SaccLeafSense.Config::class.java,
)
}
/** The ticks until it will next be ticked. This is also displayed in the debug overlay. */
var ticksUntilNextExecution: Int
@@ -20,6 +20,7 @@ import com.cobblemon.mod.common.api.ai.biggerbrain.objective.EggRecovery
import com.cobblemon.mod.common.api.ai.biggerbrain.objective.Defensive
import com.cobblemon.mod.common.api.ai.biggerbrain.objective.EatGrass
import com.cobblemon.mod.common.api.ai.biggerbrain.objective.EatHeldItem
import com.cobblemon.mod.common.api.ai.biggerbrain.objective.FindAir
import com.cobblemon.mod.common.api.ai.biggerbrain.objective.FocusDuringBattle
import com.cobblemon.mod.common.api.ai.biggerbrain.objective.FriendlyBehaviour
import com.cobblemon.mod.common.api.ai.biggerbrain.objective.GatherNectar
@@ -42,6 +43,7 @@ import com.cobblemon.mod.common.api.ai.biggerbrain.objective.PointToSpawn
import com.cobblemon.mod.common.api.ai.biggerbrain.objective.SkittishBehaviour
import com.cobblemon.mod.common.api.ai.biggerbrain.objective.TerritorialBehaviour
import com.cobblemon.mod.common.api.ai.biggerbrain.objective.WalkTowardsParent
import com.cobblemon.mod.common.api.ai.biggerbrain.objective.WalkOutOfWater
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.resources.ResourceLocation
@@ -60,6 +62,7 @@ interface ObjectiveConfig {
cobblemonResource("maintain_fluid_y") to MaintainFluidY.Config::class.java,
cobblemonResource("move_out_of_water") to MoveOutOfWater.Config::class.java,
cobblemonResource("move_into_water") to MoveIntoWater.Config::class.java,
cobblemonResource("walk_out_of_water") to WalkOutOfWater.Config::class.java,
cobblemonResource("jump_out_of_water") to JumpOutOfWater.Config::class.java,
cobblemonResource("move_to_resting_place") to MoveToRestingPlace.Config::class.java,
cobblemonResource("go_to_sleep") to GoToSleep.Config::class.java,
@@ -70,6 +73,7 @@ interface ObjectiveConfig {
cobblemonResource("pickup_item") to PickupItem.Config::class.java,
cobblemonResource("eat_held_item") to EatHeldItem.Config::class.java,
cobblemonResource("gather_nectar") to GatherNectar.Config::class.java,
cobblemonResource("find_air") to FindAir.Config::class.java,
cobblemonResource("dance") to Dance.Config::class.java,
cobblemonResource("defend_owner") to DefendOwner.Config::class.java,
cobblemonResource("point_to_spawn") to PointToSpawn.Config::class.java,
@@ -52,13 +52,12 @@ interface SenseConfig {
cobblemonResource("nest_sensor") to NestSense.Config::class.java,
cobblemonResource("egg_sensor") to EggSense.Config::class.java,
cobblemonResource("ideal_nest_location_sensor") to IdealNestPlacementSense.Config::class.java,
cobblemonResource("pokemon_adult") to PokemonAdultSense.Config::class.java,
cobblemonResource("nearby_sweet_berry_bush") to SweetBerryBushSense.Config::class.java,
cobblemonResource("pokemon_nearby_wanted_item") to PokemonItemSense.Config::class.java,
cobblemonResource("flower_sense") to FlowerSense.Config::class.java,
cobblemonResource("beehive_sense") to BeeHiveSense.Config::class.java,
cobblemonResource("sacc_leaf_sense") to SaccLeafSense.Config::class.java,
)
)
}
/** The variables that this sense config uses. These are used to declare variables on the entity cleanly. */
@@ -0,0 +1,105 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai.movement
import com.cobblemon.mod.common.entity.MoLangScriptingEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import net.minecraft.world.entity.LivingEntity
/**
* What an entity would rather avoid, as opposed to what it can't do at all ([MovementCapabilities]). These turn into
* a discomfort for each occupancy, which [MovementIntent] tolerates to differing degrees.
*
* @author Hiroku
* @since October 1st, 2026
*/
data class MediumPreferences(
val avoidsWater: Boolean = false,
val avoidsLand: Boolean = false,
/** Whether the entity is comfortable in lava, assuming it's fire immune. The species' canSwimInLava. */
val prefersLava: Boolean = false,
) {
/**
* How uncomfortable the entity is entering the occupancy, from 0 (natural) to 1 (only when desperate), or null if
* it will never do so voluntarily. Hazards like burning or holding its breath are covered here too, since a
* [MovementIntent] never tolerates them. Escaping a fluid uses its own rules.
*/
fun discomfort(occupancy: Occupancy, fluid: FluidKind, capabilities: MovementCapabilities): Float? {
if (fluid == FluidKind.LAVA) {
return when {
capabilities.isHarmedBy(FluidKind.LAVA) -> null
prefersLava -> 0F
else -> LAVA_DISCOMFORT
}
}
val breathes = capabilities.canBreatheIn(fluid)
return when (occupancy) {
Occupancy.WALK -> if (avoidsLand) 0.8F else 0F
Occupancy.FLY -> 0F
Occupancy.WADE -> if (avoidsWater) 0.7F else 0.3F
Occupancy.SEABED -> if (breathes) 0F else null
// Non-breathers swimming is a bit more than idle wandering will do, which matches the old wander filter.
Occupancy.SWIM -> if (breathes) 0F else if (avoidsWater) 0.8F else 0.4F
Occupancy.SWIM_SUBMERGED -> if (breathes) 0F else if (avoidsWater) 0.9F else 0.5F
Occupancy.SINK, Occupancy.FALL -> null
}
}
companion object {
const val LAVA_DISCOMFORT = 0.5F
// Config variable names, shared with NPCs which have no species behaviour to read these from.
const val AVOIDS_WATER = "avoids_water"
const val AVOIDS_LAND = "avoids_land"
const val PREFERS_LAVA = "prefers_lava"
fun of(entity: LivingEntity): MediumPreferences {
if (entity is PokemonEntity) {
// The exposed form, like MovementCapabilities, so transformations and illusions agree.
val moving = entity.exposedForm.behaviour.moving
return MediumPreferences(
avoidsWater = moving.swim.avoidsWater,
avoidsLand = moving.walk.avoidsLand,
prefersLava = moving.swim.canSwimInLava
)
} else if (entity is MoLangScriptingEntity) {
return MediumPreferences(
avoidsWater = entity.configFlag(AVOIDS_WATER, false),
avoidsLand = entity.configFlag(AVOIDS_LAND, false),
prefersLava = entity.configFlag(PREFERS_LAVA, false)
)
}
return MediumPreferences()
}
}
}
/** Reads a boolean config variable, falling back if it isn't declared. */
fun MoLangScriptingEntity.configFlag(name: String, default: Boolean): Boolean {
// Vanilla may ask things like fireImmune() while the entity is still being constructed, before config exists.
@Suppress("SENSELESS_COMPARISON")
if (config == null) return default
val value = config.map[name] ?: return default
return value.asDouble() != 0.0
}
/**
* Why an entity is moving, which decides how much discomfort it will put up with along the way.
*
* @author Hiroku
* @since October 1st, 2026
*/
enum class MovementIntent(val tolerance: Float) {
/** Wandering and other things it's doing out of boredom. */
IDLE(0.3F),
/** Getting somewhere it would be more comfortable, like next to its owner. */
COMFORT(0.6F),
/** Chasing, fleeing, defending. Anything it can survive is on the table. */
URGENT(1F)
}
@@ -0,0 +1,108 @@
/*
* 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.movement
import com.cobblemon.mod.common.entity.OmniPathingEntity
import net.minecraft.tags.FluidTags
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.level.material.FluidState
import net.minecraft.world.level.material.Fluids
/** The fluids that matter for movement decisions. */
enum class FluidKind {
NONE,
WATER,
LAVA;
companion object {
fun of(fluidState: FluidState): FluidKind = when {
fluidState.isEmpty -> NONE
fluidState.`is`(FluidTags.WATER) -> WATER
fluidState.`is`(FluidTags.LAVA) -> LAVA
else -> NONE
}
/** The fluid the entity's eyes are currently in, if any. */
fun atEyes(entity: LivingEntity): FluidKind = when {
entity.isEyeInFluid(FluidTags.WATER) -> WATER
entity.isEyeInFluid(FluidTags.LAVA) -> LAVA
else -> NONE
}
}
}
/**
* A snapshot of what an entity's body is physically able to do. These are hard limits: no amount of
* urgency lets an entity swim if it can't swim. Preferences and urgency live in [TraversalPolicy].
*
* Lava is viscous enough that every body can move through it freely; what differs is whether it burns.
* Whether an entity actually likes being in lava (the species' canSwimInLava) is a preference, see [MediumPreferences].
*
* @author Hiroku
* @since September 30th, 2026
*/
data class MovementCapabilities(
val canWalk: Boolean = true,
val canFly: Boolean = false,
val canSwimInWater: Boolean = false,
val fireImmune: Boolean = false,
val canBreatheUnderwater: Boolean = false,
val canBreatheUnderLava: Boolean = false,
val canWalkOnWater: Boolean = false,
val canWalkOnLava: Boolean = false,
) {
fun canSwimIn(fluid: FluidKind) = when (fluid) {
FluidKind.WATER -> canSwimInWater
FluidKind.LAVA -> true
FluidKind.NONE -> false
}
/** Whether being in the fluid hurts, regardless of breathing. */
fun isHarmedBy(fluid: FluidKind) = fluid == FluidKind.LAVA && !fireImmune
fun canBreatheIn(fluid: FluidKind) = when (fluid) {
FluidKind.WATER -> canBreatheUnderwater
FluidKind.LAVA -> canBreatheUnderLava
FluidKind.NONE -> true
}
fun canWalkOn(fluid: FluidKind) = when (fluid) {
FluidKind.WATER -> canWalkOnWater
FluidKind.LAVA -> canWalkOnLava
FluidKind.NONE -> false
}
/** Whether this body could ever end up in the given occupancy through its own movement. */
fun canOccupy(occupancy: Occupancy) = when (occupancy) {
Occupancy.WALK -> canWalk || canFly
Occupancy.FLY -> canFly
Occupancy.WADE, Occupancy.SEABED -> canWalk
// Lava can always be swum in, and classification only produces swimming in water for water swimmers.
Occupancy.SWIM, Occupancy.SWIM_SUBMERGED -> true
Occupancy.SINK, Occupancy.FALL -> false
}
companion object {
fun of(entity: LivingEntity): MovementCapabilities {
if (entity !is OmniPathingEntity) {
return MovementCapabilities(canBreatheUnderwater = entity.canBreatheUnderwater(), fireImmune = entity.fireImmune())
}
return MovementCapabilities(
canWalk = entity.canWalk(),
canFly = entity.canFly(),
canSwimInWater = entity.canSwimInWater(),
fireImmune = entity.fireImmune(),
canBreatheUnderwater = entity.canSwimUnderFluid(Fluids.WATER.defaultFluidState()),
canBreatheUnderLava = entity.canSwimUnderFluid(Fluids.LAVA.defaultFluidState()),
canWalkOnWater = entity.canWalkOnWater(),
canWalkOnLava = entity.canWalkOnLava(),
)
}
}
}
@@ -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.movement
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.value.DoubleValue
import net.minecraft.world.entity.LivingEntity
/**
* Creates the q.entity.movement struct: the entity's movement capabilities and preferences, the same for Pokémon and
* NPCs. Values are read when queried, so they follow form changes and config edits.
*
* Unlike q.entity.behaviour.moving, which is the species data, this is what the movement system actually uses.
* Lava has no swimming entry because every body can move through it; fire_immune decides whether it burns.
*
* @author Hiroku
* @since October 1st, 2026
*/
fun createMovementStruct(entity: LivingEntity): QueryStruct {
val capabilities = { MovementCapabilities.of(entity) }
val preferences = { MediumPreferences.of(entity) }
return QueryStruct(hashMapOf())
.addFunction("can_walk") { DoubleValue(capabilities().canWalk) }
.addFunction("can_fly") { DoubleValue(capabilities().canFly) }
.addFunction("can_swim_in_water") { DoubleValue(capabilities().canSwimInWater) }
.addFunction("can_breathe_underwater") { DoubleValue(capabilities().canBreatheUnderwater) }
.addFunction("can_breathe_under_lava") { DoubleValue(capabilities().canBreatheUnderLava) }
.addFunction("can_walk_on_water") { DoubleValue(capabilities().canWalkOnWater) }
.addFunction("can_walk_on_lava") { DoubleValue(capabilities().canWalkOnLava) }
.addFunction("fire_immune") { DoubleValue(capabilities().fireImmune) }
.addFunction("avoids_water") { DoubleValue(preferences().avoidsWater) }
.addFunction("avoids_land") { DoubleValue(preferences().avoidsLand) }
.addFunction("prefers_lava") { DoubleValue(preferences().prefersLava) }
.addFunction("is_seafloor_walker") {
val caps = capabilities()
DoubleValue(caps.canWalk && !caps.canSwimInWater && caps.canBreatheUnderwater)
}
}
@@ -0,0 +1,104 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai.movement
import net.minecraft.core.BlockPos
import net.minecraft.util.Mth
import net.minecraft.world.level.block.state.BlockState
/**
* How a specific entity would be occupying a position. The same block of water is [SWIM] for a swimmer,
* [SEABED] for a non-swimmer standing on the bottom, and [SINK] for a non-swimmer with nothing below it.
*
* @author Hiroku
* @since September 30th, 2026
*/
enum class Occupancy {
/** Standing on something with no fluid around the feet. */
WALK,
/** Airborne, with the ability to fly. */
FLY,
/** Standing on the bottom of a fluid it can't swim in, with its head above the surface. */
WADE,
/** Standing on the bottom of a fluid it can't swim in, with its head under the surface. */
SEABED,
/** In a fluid it can't swim in with nothing to stand on. It can only go down from here. */
SINK,
/** Swimming with its head above the surface. */
SWIM,
/** Swimming with its head under the surface. */
SWIM_SUBMERGED,
/** In the air without the ability to fly, with nothing to stand on. */
FALL;
/** Whether an entity can come to rest here. [SINK] and [FALL] are only ever passed through on the way down. */
val isStandable: Boolean
get() = this != SINK && this != FALL
/** Whether the head is under a fluid in this occupancy, which is where breath gets spent. */
val isSubmerged: Boolean
get() = this == SEABED || this == SWIM_SUBMERGED
companion object {
/**
* Classifies an occupancy from already-resolved facts about a position.
*
* @param fluid The fluid at the entity's feet.
* @param hasFloor Whether there is something to stand on: a solid block below, or a fluid surface the entity can walk on.
* @param headFluid The fluid at the entity's eye level.
*/
fun classify(caps: MovementCapabilities, fluid: FluidKind, hasFloor: Boolean, headFluid: FluidKind): Occupancy {
val headUnder = headFluid != FluidKind.NONE
return when {
fluid == FluidKind.NONE -> when {
hasFloor -> WALK
caps.canFly -> FLY
else -> FALL
}
caps.canSwimIn(fluid) -> if (headUnder) SWIM_SUBMERGED else SWIM
!hasFloor -> SINK
headUnder -> SEABED
else -> WADE
}
}
/**
* Classifies the occupancy at a position using the given block lookup, or returns null if the position
* is obstructed by a solid block. This only looks at the feet column; hitbox clearance is the pathfinder's job.
*/
fun at(caps: MovementCapabilities, eyeHeight: Float, pos: BlockPos, getState: (BlockPos) -> BlockState): Occupancy? {
val state = getState(pos)
if (state.isSolid) {
return null
}
val fluid = FluidKind.of(state.fluidState)
val below = getState(pos.below())
val belowFluid = FluidKind.of(below.fluidState)
val hasFloor = below.isSolid || (fluid == FluidKind.NONE && caps.canWalkOn(belowFluid))
return classify(caps, fluid, hasFloor, fluidAtEyes(pos, eyeHeight, getState))
}
/**
* The fluid the eyes would be in when standing at the position. Compares the eye height against the fluid's
* surface rather than just the block, so a small entity can wade through a thin layer of flowing water.
*/
private fun fluidAtEyes(pos: BlockPos, eyeHeight: Float, getState: (BlockPos) -> BlockState): FluidKind {
val eyeOffset = Mth.floor(eyeHeight)
val eyePos = pos.above(eyeOffset)
val eyeFluidState = getState(eyePos).fluidState
val eyeFluid = FluidKind.of(eyeFluidState)
if (eyeFluid == FluidKind.NONE) {
return FluidKind.NONE
}
// Fluid stacked on top fills the block, otherwise the surface is however high this fluid sits.
val surface = if (FluidKind.of(getState(eyePos.above()).fluidState) == eyeFluid) 1F else eyeFluidState.ownHeight
return if (eyeHeight - eyeOffset < surface) eyeFluid else FluidKind.NONE
}
}
}
@@ -0,0 +1,114 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai.movement
import com.cobblemon.mod.common.entity.ai.OmniPathNavigation
import com.cobblemon.mod.common.pokemon.ai.OmniPathNodeMaker
import net.minecraft.core.BlockPos
import net.minecraft.util.Mth
import net.minecraft.world.entity.LivingEntity
/**
* The rules for a single movement request: which occupancies the entity is willing to path through for this
* particular reason, and what each one costs. A policy is attached to the path it produced by
* [OmniPathNavigation], so recomputations of that path keep following the same rules.
*
* Positions in the open are only governed by a policy for their medium (walkable, open). Hazards such as fire, fences
* and damaging blocks keep using the entity's regular pathfinding malus in [OmniPathNodeMaker]. Positions in a fluid
* are always priced by the policy.
*
* Capabilities always win: an occupancy the body can't be in is never allowed, whatever the pricing says.
*
* @author Hiroku
* @since September 30th, 2026
*/
class TraversalPolicy(
val capabilities: MovementCapabilities,
/** Shown in debugging. */
val description: String,
/** The cost of entering an occupancy in a fluid, or null if it's forbidden. */
private val pricing: (Occupancy, FluidKind) -> Float?,
) {
/** The cost of entering a node in the given occupancy, or null if this request won't go there. */
fun cost(occupancy: Occupancy, fluid: FluidKind): Float? {
if (!occupancy.isStandable || !capabilities.canOccupy(occupancy)) {
return null
}
return pricing(occupancy, fluid)
}
fun allows(occupancy: Occupancy, fluid: FluidKind) = cost(occupancy, fluid) != null
override fun toString() = "TraversalPolicy($description)"
companion object {
/** Path cost of something at the limit of what the entity will put up with. Comparable to the old fluid malus. */
const val DISCOMFORT_COST = 16F
/** Cost per node where the head is under a fluid the entity can't breathe in. Roughly "time spent drowning". */
const val HELD_BREATH_COST = 4F
/** Cost per node of being in lava without fire immunity. */
const val BURNING_COST = 4F
/**
* The usual policy: occupancies cost their discomfort, and anything more uncomfortable than the intent tolerates
* is forbidden. Whatever the entity is currently standing in is tolerated so that it can make its way out,
* unless it's something it would never enter voluntarily (burning, holding its breath); that's for escapes.
*
* @param within If given, the movement is confined to occupancies matching it, e.g. a swimmer wandering within
* water. Choosing that medium was already the decision, so the intent's tolerance doesn't apply within it.
* Hazards (burning, sinking) stay forbidden.
*/
fun forIntent(
entity: LivingEntity,
intent: MovementIntent,
within: ((Occupancy, FluidKind) -> Boolean)? = null
): TraversalPolicy {
val capabilities = MovementCapabilities.of(entity)
val preferences = MediumPreferences.of(entity)
val level = entity.level()
// The same position the pathfinder starts from, so the exemption below covers the actual start node.
val box = entity.boundingBox
val pos = BlockPos(Mth.floor(box.minX), Mth.floor(box.minY + 0.5), Mth.floor(box.minZ))
val currentOccupancy = Occupancy.at(capabilities, entity.eyeHeight, pos, level::getBlockState)
val currentFluid = FluidKind.of(level.getFluidState(pos))
return TraversalPolicy(capabilities, intent.name.lowercase()) { occupancy, fluid ->
if (within != null && !within(occupancy, fluid)) {
return@TraversalPolicy null
}
val discomfort = preferences.discomfort(occupancy, fluid, capabilities) ?: return@TraversalPolicy null
val tolerated = within != null || discomfort <= intent.tolerance ||
(occupancy == currentOccupancy && fluid == currentFluid)
if (tolerated) discomfort * DISCOMFORT_COST else null
}
}
/**
* A policy for an entity trying to get out of a fluid that's killing it: one it can neither swim nor breathe in,
* or lava without fire immunity. Being in that fluid is allowed but expensive, so the shortest way out wins.
* Anything else is held to [MovementIntent.URGENT] so it doesn't escape into something worse.
*/
fun escapeFluid(entity: LivingEntity, fluid: FluidKind): TraversalPolicy {
val capabilities = MovementCapabilities.of(entity)
val preferences = MediumPreferences.of(entity)
return TraversalPolicy(capabilities, "escape ${fluid.name.lowercase()}") { occupancy, nodeFluid ->
if (nodeFluid != fluid || fluid == FluidKind.NONE) {
val discomfort = preferences.discomfort(occupancy, nodeFluid, capabilities) ?: return@TraversalPolicy null
return@TraversalPolicy if (discomfort > MovementIntent.URGENT.tolerance) null else discomfort * DISCOMFORT_COST
}
when {
capabilities.isHarmedBy(fluid) -> BURNING_COST
occupancy == Occupancy.WADE -> 1F
occupancy == Occupancy.SWIM -> 2F
occupancy.isSubmerged -> HELD_BREATH_COST
else -> 0F
}
}
}
}
}
@@ -16,6 +16,7 @@ import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.api.ai.CobblemonBlockPosTracker
import com.cobblemon.mod.common.api.ai.CobblemonWanderControl
import com.cobblemon.mod.common.api.ai.movement.createMovementStruct
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
@@ -55,6 +56,8 @@ object LivingEntityMoLangFunctions : AbstractMoLangFunctionHolder<LivingEntity>(
val entity = this
val functions = hashMapOf<String, (MoParams) -> Any>()
val movementStruct = createMovementStruct(entity)
functions["movement"] = { movementStruct }
functions["is_player"] = { DoubleValue(entity is Player) }
functions["is_npc"] = { DoubleValue(entity is NPCEntity) }
functions["is_mob"] = { DoubleValue(entity is Mob) }
@@ -8,6 +8,7 @@
package com.cobblemon.mod.common.entity.ai
import com.cobblemon.mod.common.api.ai.movement.TraversalPolicy
import net.minecraft.core.BlockPos
import net.minecraft.world.entity.ai.behavior.BlockPosTracker
import net.minecraft.world.entity.ai.memory.WalkTarget
@@ -25,7 +26,12 @@ class CobblemonWalkTarget(
speedModifier: Float,
completionRange: Int,
val nodeTypeFilter: (PathType) -> Boolean = { true },
val destinationNodeTypeFilter: (PathType) -> Boolean = { true }
val destinationNodeTypeFilter: (PathType) -> Boolean = { true },
/**
* The traversal rules for reaching this target. Where supported (the bigger brain's movement trackers) this is
* used instead of [nodeTypeFilter].
*/
val policy: TraversalPolicy? = null
) : WalkTarget(
BlockPosTracker(pos),
speedModifier,
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.entity.ai
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.api.ai.movement.TraversalPolicy
import com.cobblemon.mod.common.entity.OmniPathingEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.ai.OmniPathNodeMaker
@@ -35,6 +36,7 @@ import net.minecraft.world.phys.Vec3
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.acos
import java.util.WeakHashMap
/**
* A navigator designed to work with the [OmniPathNodeMaker], allowing a path that can cross land, water, and air
@@ -62,6 +64,97 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
var navigationContext = NavigationContext()
/**
* The policy of the path currently being followed. Recomputations of that path (after nearby block changes or
* drifting off course) are evaluated under the same policy. Null means the legacy rules.
*/
var activePolicy: TraversalPolicy? = null
private set
/** Where a planned path was headed and under which policy, adopted when that path is followed. */
private class Plan(val targetPos: BlockPos?, val reachRange: Int, val policy: TraversalPolicy?)
/** Weak so that plans which are never followed don't accumulate. */
private val plans = WeakHashMap<Path, Plan>()
private var planning = false
private var planningPolicy: TraversalPolicy? = null
private var recomputing = false
/**
* Plans a path under the given policy without disturbing the path currently being followed. Unlike [createPath]
* this never hands back the in-progress path, since that path may have been made under different rules. Following
* the result with [moveTo] makes its policy the [activePolicy].
*/
fun planPath(target: BlockPos, accuracy: Int, policy: TraversalPolicy?): Path? {
return plan(policy) { createPath(target, accuracy) }
}
/**
* Plans a path to whichever of the targets is best to reach under the given policy. If none can be reached, the
* result is a partial path to the closest point, so check [Path.canReach].
*
* @param searchRange How far from the entity the search may go, in blocks.
*/
fun planPath(targets: Set<BlockPos>, accuracy: Int, policy: TraversalPolicy?, searchRange: Float): Path? {
return plan(policy) { createPath(targets, 8, false, accuracy, searchRange) }
}
private fun plan(policy: TraversalPolicy?, block: () -> Path?): Path? {
val previousPath = path
val previousTargetPos = targetPos
val previousReachRange = reachRange
val wasPlanning = planning
val previousPlanningPolicy = planningPolicy
// Vanilla returns the current path if it already heads to one of the targets, so hide it while planning.
path = null
planning = true
planningPolicy = policy
try {
val planned = block() ?: return null
plans[planned] = Plan(targetPos, reachRange, policy)
return planned
} finally {
path = previousPath
// Vanilla overwrites these on success, which would redirect recomputation of the path being followed.
targetPos = previousTargetPos
reachRange = previousReachRange
planning = wasPlanning
planningPolicy = previousPlanningPolicy
}
}
override fun recomputePath() {
recomputing = true
try {
super.recomputePath()
} finally {
recomputing = false
}
}
override fun createPath(
targets: Set<BlockPos>,
regionOffset: Int,
offsetUpward: Boolean,
accuracy: Int,
followRange: Float
): Path? {
val nodeMaker = nodeEvaluator as? OmniPathNodeMaker
?: return super.createPath(targets, regionOffset, offsetUpward, accuracy, followRange)
val previousPolicy = nodeMaker.policy
nodeMaker.policy = when {
planning -> planningPolicy
recomputing -> activePolicy
else -> null // Ad-hoc requests (vanilla tasks, MoLang walk_to) use the legacy rules.
}
try {
return super.createPath(targets, regionOffset, offsetUpward, accuracy, followRange)
} finally {
nodeMaker.policy = previousPolicy
}
}
companion object {
val verticallyPreciseNodeTypes = setOf(PathType.WATER, PathType.OPEN, PathType.LAVA)
}
@@ -74,8 +167,9 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
override fun canUpdatePath(): Boolean {
val (isInLiquid, isTouchingLava) = mob.level().getWaterAndLavaIn(mob.boundingBox)
// Lava is viscous enough for anything to move through it, so being in it never stops pathing.
val isAtValidPosition = (!mob.isInLava && !mob.isEyeInFluid(FluidTags.LAVA)) ||
(isTouchingLava && pather.canSwimInLava()) ||
isTouchingLava ||
this.mob.isPassenger
return if (pather.canSwimInWater()) {
isInLiquid || ((this.mob.onGround() || pather.isFlying()) || this.mob.isInLiquid || this.mob.isPassenger)
@@ -287,7 +381,21 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
}
}
return super.moveTo(path, speed)
val plan = path?.let(plans::remove)
val alreadyFollowing = path != null && path === this.path
val moving = super.moveTo(path, speed)
when {
!moving -> activePolicy = null
plan != null -> {
activePolicy = plan.policy
// Vanilla's moveTo doesn't record the destination, so recomputation would head for a stale target.
targetPos = plan.targetPos
reachRange = plan.reachRange
}
// A recomputed path is already assigned before whoever follows it calls moveTo, so keep its policy.
!alreadyFollowing -> activePolicy = null
}
return moving
}
override fun trimPath() {
@@ -395,6 +503,7 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
override fun stop() {
super.stop()
activePolicy = null
this.currentNodeDistance = -1F
this.cachedCurrentNode = null
path = null
@@ -18,6 +18,10 @@ import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.ai.biggerbrain.BiggerBrain
import com.cobblemon.mod.common.api.ai.booleanVariable
import com.cobblemon.mod.common.api.ai.config.task.SharedEntityVariables
import com.cobblemon.mod.common.api.ai.movement.MediumPreferences
import com.cobblemon.mod.common.api.ai.movement.configFlag
import com.cobblemon.mod.common.api.entity.PokemonSender
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
@@ -96,6 +100,7 @@ import net.minecraft.world.entity.ai.sensing.SensorType
import net.minecraft.world.entity.npc.Npc
import net.minecraft.world.entity.player.Player
import net.minecraft.world.level.Level
import net.minecraft.tags.FluidTags
import net.minecraft.world.level.material.FluidState
class NPCEntity(world: Level) : AgeableMob(CobblemonEntities.NPC, world), PosableEntity, PokemonSender, Schedulable, MoLangScriptingEntity, OmniPathingEntity {
@@ -289,6 +294,27 @@ class NPCEntity(world: Level) : AgeableMob(CobblemonEntities.NPC, world), Posabl
// This has to be below constructor and entity tracker fields otherwise initialization order is weird and breaks them syncing
companion object {
const val CAN_WALK = "can_walk"
const val CAN_SWIM_IN_WATER = "can_swim_in_water"
const val CAN_BREATHE_UNDERWATER = "can_breathe_underwater"
const val CAN_WALK_ON_WATER = "can_walk_on_water"
const val CAN_WALK_ON_LAVA = "can_walk_on_lava"
const val FIRE_IMMUNE = "fire_immune"
private val MOVEMENT_VARIABLES = SharedEntityVariables.MOVEMENT_CATEGORY.let { category ->
listOf(
booleanVariable(category, CAN_WALK, true),
booleanVariable(category, CAN_SWIM_IN_WATER, true),
booleanVariable(category, CAN_BREATHE_UNDERWATER, false),
booleanVariable(category, CAN_WALK_ON_WATER, false),
booleanVariable(category, CAN_WALK_ON_LAVA, false),
booleanVariable(category, FIRE_IMMUNE, false),
booleanVariable(category, MediumPreferences.AVOIDS_WATER, false),
booleanVariable(category, MediumPreferences.AVOIDS_LAND, false),
booleanVariable(category, MediumPreferences.PREFERS_LAVA, false),
)
}
fun createAttributes(): AttributeSupplier.Builder = createMobAttributes()
.add(Attributes.ATTACK_DAMAGE, 1.0)
.add(Attributes.ATTACK_KNOCKBACK)
@@ -692,29 +718,26 @@ class NPCEntity(world: Level) : AgeableMob(CobblemonEntities.NPC, world), Posabl
override fun getNavigation() = navigation as OmniPathNavigation
override fun createNavigation(level: Level) = OmniPathNavigation(level, this)
// At some point these need to be changeable from MoLang or something.
override fun canWalk(): Boolean {
return true
}
/*
* Movement capabilities and preferences come from config variables so they can be set per NPC in the editor or by
* presets. The defaults match what NPCs could always do. Flight isn't configurable because NPCs can't fly yet.
*/
override fun getExtraVariables(): List<MoLangConfigVariable> = MOVEMENT_VARIABLES
override fun canSwimInWater(): Boolean {
return true
}
override fun canSwimInLava(): Boolean {
return false
}
override fun canWalkOnLava(): Boolean {
return false
}
override fun canWalk() = configFlag(CAN_WALK, true)
override fun canSwimInWater() = configFlag(CAN_SWIM_IN_WATER, true)
// Lava is movable by anything; this is whether the NPC treats it like home, same as a Pokémon species' canSwimInLava.
override fun canSwimInLava() = configFlag(MediumPreferences.PREFERS_LAVA, false)
override fun canWalkOnLava() = configFlag(CAN_WALK_ON_LAVA, false)
override fun canWalkOnWater() = configFlag(CAN_WALK_ON_WATER, false)
override fun canBreatheUnderwater() = configFlag(CAN_BREATHE_UNDERWATER, false)
override fun fireImmune() = configFlag(FIRE_IMMUNE, false) || super.fireImmune()
override fun canSwimUnderFluid(fluidState: FluidState): Boolean {
return false
}
override fun canWalkOnWater(): Boolean {
return false
return when {
fluidState.`is`(FluidTags.WATER) -> canBreatheUnderwater()
else -> false
}
}
override fun canPathThroughSaccLeaves(): Boolean {
@@ -9,6 +9,9 @@
package com.cobblemon.mod.common.pokemon.ai
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.api.ai.movement.FluidKind
import com.cobblemon.mod.common.api.ai.movement.Occupancy
import com.cobblemon.mod.common.api.ai.movement.TraversalPolicy
import com.cobblemon.mod.common.entity.OmniPathingEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.util.canFit
@@ -59,16 +62,83 @@ class OmniPathNodeMaker : NodeEvaluator() {
var nodeFilter: Predicate<PathType> = Predicate { true }
/**
* The traversal policy for the path being computed, set by the navigation. When null, the legacy rules apply:
* the entity's pathfinding malus alone decides costs and its location decides between walking and 3D rules.
*/
var policy: TraversalPolicy? = null
private val nodePosToOccupancy: Long2ObjectMap<Occupancy?> = Long2ObjectOpenHashMap()
override fun prepare(cachedWorld: PathNavigationRegion, entity: Mob) {
super.prepare(cachedWorld, entity)
nodePosToType.clear()
nodePosToState.clear()
nodePosToOccupancy.clear()
}
override fun done() {
super.done()
nodePosToType.clear()
nodePosToState.clear()
nodePosToOccupancy.clear()
}
/** How the mob would occupy the given position, or null if it's obstructed. Only meaningful while pathing. */
fun occupancyAt(x: Int, y: Int, z: Int): Occupancy? {
val policy = policy ?: return null
return nodePosToOccupancy.computeIfAbsent(BlockPos.asLong(x, y, z), Long2ObjectFunction {
Occupancy.at(policy.capabilities, mob.eyeHeight, BlockPos(x, y, z)) { currentContext.getCachedBlockState(it) }
})
}
/**
* The cost of entering the position as the given path type, negative if it can't be entered. Medium types are
* priced by the [policy] from how the mob would occupy the position; hazards always use the mob's malus.
*/
fun malusAt(type: PathType, x: Int, y: Int, z: Int): Float {
val policy = policy ?: return mob.getPathfindingMalus(type)
// Obstructed positions (fences, doors and so on) are left to the mob's malus.
val occupancy = occupancyAt(x, y, z) ?: return mob.getPathfindingMalus(type)
val fluid = FluidKind.of(currentContext.getCachedBlockState(BlockPos(x, y, z)).fluidState)
val cost = policy.cost(occupancy, fluid) ?: return -1F
// Fluid positions are entirely the policy's call: lava in particular is typed as a fire hazard.
if (type in MEDIUM_PATH_TYPES || fluid != FluidKind.NONE) {
return cost
}
// Hazards in the open (berry bushes, fire nearby) still cost what the mob thinks they cost.
val hazard = mob.getPathfindingMalus(type)
return if (hazard < 0) hazard else max(hazard, cost)
}
/** Whether the path should be expanded with walking rules rather than moving freely in 3D. */
private fun usesWalkingRules(): Boolean {
val caps = policy?.capabilities ?: return !canFly() && !mob.isInWater
// Lava is swimmable by everyone.
return !caps.canFly && !(caps.canSwimInWater && mob.isInWater) && !mob.isInLava
}
/**
* For a position the mob would sink or fall through, finds where it would come to rest. Sinking goes all the way
* to the bottom; falling is limited by the mob's max fall distance. Returns null if it can't land anywhere usable.
*/
private fun findLandingBelow(x: Int, y: Int, z: Int): Node? {
var fallen = 1 // Leaving the position at y is already one block of falling.
for (i in y - 1 downTo mob.level().minBuildHeight) {
val occupancy = occupancyAt(x, i, z) ?: return null
if (occupancy == Occupancy.FALL) {
if (++fallen > mob.maxFallDistance) {
return null
}
continue
} else if (occupancy == Occupancy.SINK) {
continue
}
val type = getNodeType(mob, x, i, z)
val malus = malusAt(type, x, i, z)
return if (malus >= 0) getNodeAndUpdateCostToMax(x, i, z, type, malus) else null
}
return null
}
fun PathfindingContext.getCachedBlockState(pos: BlockPos): BlockState {
@@ -83,9 +153,25 @@ class OmniPathNodeMaker : NodeEvaluator() {
val x = Mth.floor(mob.boundingBox.minX)
val y = Mth.floor(mob.boundingBox.minY + 0.5)
val z = Mth.floor(mob.boundingBox.minZ)
val node = super.getNode(x, y, z)
var startY = y
if (policy != null && !mob.onGround() && occupancyAt(x, y, z)?.isStandable == false) {
// A sinking or falling mob will have landed by the time it can follow a path, so plan from there.
// Falls are capped so a mob that's merely clipping a ledge isn't planned from the bottom of a cliff.
var fallen = 0
while (startY > mob.level().minBuildHeight && occupancyAt(x, startY, z)?.isStandable == false) {
if (occupancyAt(x, startY, z) == Occupancy.FALL && ++fallen > mob.maxFallDistance) {
startY = y
break
}
startY--
}
if (occupancyAt(x, startY, z)?.isStandable != true) {
startY = y
}
}
val node = super.getNode(x, startY, z)
node.type = this.getNodeType(mob, node.asBlockPos())
node.costMalus = mob.getPathfindingMalus(node.type)
node.costMalus = malusAt(node.type, x, startY, z)
return node
}
@@ -123,13 +209,18 @@ class OmniPathNodeMaker : NodeEvaluator() {
return if (!canFly() && d - nodeFloorLevel > this.getMobJumpHeight()) {
null
} else {
if (policy != null && occupancyAt(x, y, z)?.isStandable == false) {
// Walking off an edge or into a fluid the mob can't swim in: it ends up wherever it lands.
return findLandingBelow(x, y, z)
}
var pathType2: PathType = this.getNodeType(this.mob, x, y, z)
if (pathType2 == PathType.WALKABLE && direction == Direction.UP) {
pathType2 = PathType.OPEN
}
val f = mob.getPathfindingMalus(pathType2)
val f = malusAt(pathType2, x, y, z)
if (f >= 0.0f) {
node = this.getNodeAndUpdateCostToMax(x, y, z, pathType2, f)
}
@@ -150,9 +241,10 @@ class OmniPathNodeMaker : NodeEvaluator() {
pathType!!,
mutableBlockPos
)
} else if (pathType2 == PathType.WATER && !canFloat()) {
} else if (policy == null && pathType2 == PathType.WATER && !canFloat()) {
// With a policy, sinking is handled up front through findLandingBelow.
node = this.tryFindFirstNonWaterBelow(x, y, z, node)
} else if (pathType2 == PathType.OPEN) {
} else if (policy == null && pathType2 == PathType.OPEN) {
node = this.tryFindFirstGroundNodeBelow(x, y, z)
} else if (doesBlockHavePartialCollision(pathType2) && node == null) {
node = this.getClosedNode(x, y, z, pathType2)
@@ -318,8 +410,8 @@ class OmniPathNodeMaker : NodeEvaluator() {
// Hitbox thing looks confusing but if the hitbox volume is more than like, 5, it starts getting pretty
// fucking slow to use the findAcceptedNodeWalk function
val strictlyWalkPathing = !canFly() && !mob.isInWater
val doVerticalNeighbourChecks = (canFly() || mob.isInWater) && mob.boundingBox.size < MAX_HITBOX_FOR_VERTICAL_NEIGHBOURS
val strictlyWalkPathing = usesWalkingRules()
val doVerticalNeighbourChecks = !strictlyWalkPathing && mob.boundingBox.size < MAX_HITBOX_FOR_VERTICAL_NEIGHBOURS
// Non-diagonal surroundings in 3d space
for (direction in Direction.entries) {
@@ -430,7 +522,7 @@ class OmniPathNodeMaker : NodeEvaluator() {
// If they're in a blocked node and there are multiple successors, choose whichever is closest to get out of the blocked position.
// This addresses instances where they're next to a fence and should move away from the fence in the nearest open
// direction before regular pathing.
if (mob.getPathfindingMalus(node.type) < 0 && i > 1) {
if (malusAt(node.type, node.x, node.y, node.z) < 0 && i > 1) {
val x = mob.boundingBox.minX
val y = mob.boundingBox.minY + 0.5
val z = mob.boundingBox.minZ
@@ -476,6 +568,11 @@ class OmniPathNodeMaker : NodeEvaluator() {
return false
}
if (policy != null) {
// The policy decides per position through malusAt, using the capabilities it was made with.
return true
}
return when {
(type == PathType.BREACH || type == PathType.WATER || type == PathType.WATER_BORDER) && canSwimInWater() -> true
type == PathType.LAVA && canSwimInLava() -> true
@@ -491,7 +588,7 @@ class OmniPathNodeMaker : NodeEvaluator() {
val type = addNodePos(x, y, z)
if (isValidPathType(type) &&
mob.getPathfindingMalus(type).also { nodePenalty = it } >= 0.0f &&
malusAt(type, x, y, z).also { nodePenalty = it } >= 0.0f &&
super.getNode(x, y, z).also { pathNode = it } != null
) {
pathNode!!.type = type
@@ -767,6 +864,15 @@ class OmniPathNodeMaker : NodeEvaluator() {
companion object {
const val MAX_HITBOX_SIZE_FOR_WALKING = 1.6F
const val MAX_HITBOX_FOR_VERTICAL_NEIGHBOURS = 2F
/** Path types that describe the medium a position is in, as opposed to a hazard or obstacle. */
val MEDIUM_PATH_TYPES: Set<PathType> = EnumSet.of(
PathType.WALKABLE,
PathType.OPEN,
PathType.WATER,
PathType.WATER_BORDER,
PathType.LAVA,
PathType.BREACH
)
}
fun canWalk(): Boolean {
@@ -3660,6 +3660,24 @@
"cobblemon.entity.variable.walk_speed.desc": "The speed the entity walks at.",
"cobblemon.entity.variable.can_float.name": "Floating In Fluid",
"cobblemon.entity.variable.can_float.desc": "Whether the entity will swim to the surface in fluids.",
"cobblemon.entity.variable.can_walk.name": "Can Walk",
"cobblemon.entity.variable.can_walk.desc": "Whether the entity can walk on land.",
"cobblemon.entity.variable.can_swim_in_water.name": "Can Swim In Water",
"cobblemon.entity.variable.can_swim_in_water.desc": "Whether the entity can swim. Without this it sinks and walks along the bottom.",
"cobblemon.entity.variable.can_breathe_underwater.name": "Can Breathe Underwater",
"cobblemon.entity.variable.can_breathe_underwater.desc": "Whether the entity can stay underwater without drowning.",
"cobblemon.entity.variable.can_walk_on_water.name": "Can Walk On Water",
"cobblemon.entity.variable.can_walk_on_water.desc": "Whether the entity can walk on the surface of water.",
"cobblemon.entity.variable.can_walk_on_lava.name": "Can Walk On Lava",
"cobblemon.entity.variable.can_walk_on_lava.desc": "Whether the entity can walk on the surface of lava.",
"cobblemon.entity.variable.fire_immune.name": "Fire Immune",
"cobblemon.entity.variable.fire_immune.desc": "Whether the entity is unharmed by fire and lava.",
"cobblemon.entity.variable.avoids_water.name": "Avoids Water",
"cobblemon.entity.variable.avoids_water.desc": "Whether the entity prefers to stay out of water unless it has good reason.",
"cobblemon.entity.variable.avoids_land.name": "Avoids Land",
"cobblemon.entity.variable.avoids_land.desc": "Whether the entity prefers to stay off land unless it has good reason.",
"cobblemon.entity.variable.prefers_lava.name": "Prefers Lava",
"cobblemon.entity.variable.prefers_lava.desc": "Whether the entity is at home in lava. Only matters if it is fire immune.",
"cobblemon.entity.variable.float_chance.name": "Float Chance",
"cobblemon.entity.variable.float_chance.desc": "Chance of the entity choosing to jump towards the surface each tick. Higher values swim upwards faster.",
"cobblemon.entity.variable.look_at_entities.name": "Looks At Entities",
@@ -3764,6 +3782,8 @@
"cobblemon.behaviour.pokemon_sleeps_on_trainer_bed.desc": "Pokémon that can sleep will rest on the trainer if the trainer gets into bed.",
"cobblemon.behaviour.avoids_water.name": "Avoids Water",
"cobblemon.behaviour.avoids_water.desc": "Tries to find air and then gets away from water.",
"cobblemon.behaviour.pokemon_walk_out_of_water.name": "Escapes Deadly Fluids",
"cobblemon.behaviour.pokemon_walk_out_of_water.desc": "Walks along the bottom to air when drowning, and swims out of lava when not fire immune. Drowns or burns if there is no way out.",
"cobblemon.behaviour.pokemon_eats_grass.name": "Eats Grass",
"cobblemon.behaviour.pokemon_eats_grass.desc": "The Pokémon occasionally eats grass underfoot.",
"cobblemon.behaviour.player_textured.name": "Player Textured",
@@ -41,6 +41,8 @@ transitive-accessible field net/minecraft/world/entity/player/Inventory compartm
#transitive-accessible method net/minecraft/world/gen/feature/TreeFeature createStraightBlobTree (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;IIII)Lnet/minecraft/world/level/levelgen/feature/configurations/TreeConfiguration$TreeConfigurationBuilder;
transitive-accessible method net/minecraft/world/level/block/LeavesBlock updateDistance (Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/LevelAccessor;Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;
transitive-accessible method net/minecraft/world/entity/ai/navigation/PathNavigation shouldTargetNextNodeInDirection (Lnet/minecraft/world/phys/Vec3;)Z
transitive-accessible field net/minecraft/world/entity/ai/navigation/PathNavigation targetPos Lnet/minecraft/core/BlockPos;
transitive-accessible field net/minecraft/world/entity/ai/navigation/PathNavigation reachRange I
transitive-accessible field net/minecraft/world/effect/MobEffectInstance duration I
transitive-accessible field net/minecraft/world/effect/MobEffectInstance ambient Z
transitive-accessible method net/minecraft/client/renderer/LevelRenderer calculateParticleLevel (Z)Lnet/minecraft/client/ParticleStatus;
@@ -2,6 +2,13 @@
"name": "cobblemon.behaviour.avoids_water.name",
"description": "cobblemon.behaviour.avoids_water.desc",
"configurations": [
{
"type": "cobblemon:add_objectives",
"objectivesByPriority": {
"4": ["cobblemon:move_out_of_water"],
"5": ["cobblemon:find_air"]
}
},
{
"type": "add_tasks_to_activity",
"activity": "idle",
@@ -45,9 +45,8 @@
},
{
"type": "apply_behaviours",
"condition": "q.entity.behaviour.moving.swim.avoids_water",
"behaviours": [
"cobblemon:pokemon_move_out_of_water"
"cobblemon:pokemon_walk_out_of_water"
]
},
{
@@ -72,4 +71,4 @@
]
}
]
}
}
@@ -3,18 +3,17 @@
"configurations": [
{
"type": "apply_behaviours",
"condition": "!q.entity.behaviour.moving.swim.can_breathe_underwater && !q.entity.behaviour.moving.swim.can_walk_on_water",
"condition": "!q.entity.behaviour.moving.swim.can_breathe_underwater && q.entity.behaviour.moving.swim.can_swim_in_water",
"behaviours": [
"cobblemon:avoids_water",
"cobblemon:floats"
]
},
{
"type": "apply_behaviours",
"condition": "!q.entity.behaviour.moving.swim.can_breathe_underwater && q.entity.behaviour.moving.swim.can_walk_on_water",
"condition": "q.entity.behaviour.moving.swim.avoids_water",
"behaviours": [
"cobblemon:floats"
"cobblemon:pokemon_move_out_of_water"
]
}
]
}
}
@@ -17,6 +17,9 @@
"objectivesByPriority": {
"3": [
"cobblemon:move_out_of_water"
],
"4": [
"cobblemon:find_air"
]
}
}
@@ -0,0 +1,14 @@
{
"name": "cobblemon.behaviour.pokemon_walk_out_of_water.name",
"description": "cobblemon.behaviour.pokemon_walk_out_of_water.desc",
"configurations": [
{
"type": "cobblemon:add_objectives",
"objectivesByPriority": {
"0": [
"cobblemon:walk_out_of_water"
]
}
}
]
}
@@ -5,9 +5,6 @@
{
"type": "cobblemon:add_objectives",
"objectivesByPriority": {
"7": [
"cobblemon:wander_around"
],
"9": [
{
"type": "cobblemon:circle_around",