mirror of
https://gitlab.com/cable-mc/cobblemon.git
synced 2026-10-31 22:38:52 -04:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
406ee40d98 | ||
|
|
aff32241f3 | ||
|
|
428f8aea13 | ||
|
|
8cbb87e11e | ||
|
|
c3c87df066 | ||
|
|
f8ee62386f | ||
|
|
52261ea533 |
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)
|
||||
}
|
||||
}
|
||||
+40
-9
@@ -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
|
||||
}
|
||||
+20
-3
@@ -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
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -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
|
||||
}
|
||||
|
||||
-1
@@ -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(
|
||||
|
||||
+29
-3
@@ -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())
|
||||
|
||||
-2
@@ -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(
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
-1
@@ -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"
|
||||
|
||||
|
||||
+17
-11
@@ -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)
|
||||
}
|
||||
}
|
||||
+146
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
-1
@@ -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
|
||||
|
||||
|
||||
+4
-3
@@ -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)
|
||||
|
||||
|
||||
+5
-15
@@ -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()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+43
-81
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-5
@@ -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
|
||||
|
||||
+11
-21
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+29
-53
@@ -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())
|
||||
|
||||
+1
-2
@@ -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),
|
||||
|
||||
-5
@@ -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. */
|
||||
|
||||
+4
-2
@@ -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) {
|
||||
|
||||
-1
@@ -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,
|
||||
|
||||
+5
-1
@@ -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(
|
||||
|
||||
-1
@@ -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
|
||||
|
||||
+224
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-9
@@ -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
|
||||
|
||||
|
||||
+4
@@ -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)
|
||||
}
|
||||
+108
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -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 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+4
-5
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+3
@@ -17,6 +17,9 @@
|
||||
"objectivesByPriority": {
|
||||
"3": [
|
||||
"cobblemon:move_out_of_water"
|
||||
],
|
||||
"4": [
|
||||
"cobblemon:find_air"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user