Compare commits

...
Author SHA1 Message Date
Nadja Reitzenstein c56f8b098f Merge branch 'main' into fickle-behaviour
* main: (359 commits)
  Stop using Architectury Runtime Transfomers implicitly
  Re-enabled aprijuice and made creative menu aprijuice 10 in all flavours because w/e
  Release the held pokemon before sorting, closes #1663
  Fixed persistence issues for pokemon item pickup behaviours
  Fixed pokemon having the NPC dialogues memory
  Fixed pokemon in the party following and looking around while asleep, closes #1668
  Fixed crash when adding stationary Behaviour without a movement providing behaviour. Closes #1678
  Fixed terralith:shrubland not counting as plains for pokemon spawning, closes #1685
  tweak default stat ranges for some flying behaviours
  Added math.sign and math.copy_sigh molang dep update
  Grain Improvements
  update fabric-permissions-api to 0.3.1
  Behaviour tweaks
  Update mochis.json
  IV Drip
  Ride settings editor
  Fix in air dismount
  Fix riding waterwalk
  swap out textures
  Remove unnecessary check on resource
  ...

# Conflicts:
#	common/src/main/kotlin/com/cobblemon/mod/common/api/molang/MoLangFunctions.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/pokemon/ai/FormPokemonBehaviour.kt
#	common/src/main/resources/assets/cobblemon/lang/en_us.json
#	common/src/main/resources/data/cobblemon/behaviours/looks_around.json
#	common/src/main/resources/data/cobblemon/behaviours/pokemon/auto/pokemon_core.json
#	common/src/main/resources/data/cobblemon/behaviours/pokemon/auto/pokemon_non_party.json
2025-09-21 18:53:26 +02:00
Paul e7197a7d3a Outlining some behaviours for responsiveness to triggers 2025-08-23 16:25:18 +10:00
Paul d2febef6cf Merge remote-tracking branch 'origin/main' into fickle-behaviour 2025-08-22 17:23:24 +10:00
Paul d98145617f Fixes from merge 2025-08-16 10:31:32 +10:00
Paul c7b7fd3236 Merge remote-tracking branch 'origin/LionsFoxesAndWolvesOhMy' into fickle-behaviour
# Conflicts:
#	common/src/main/kotlin/com/cobblemon/mod/common/api/molang/MoLangFunctions.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/entity/npc/ai/MeleeAttackTask.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/pokemon/ai/FormPokemonBehaviour.kt
2025-08-16 10:19:34 +10:00
Paul 0a18c67e34 Merge remote-tracking branch 'origin/main' into fickle-behaviour 2025-08-16 10:16:44 +10:00
Paul 6341ca417f More entity callbacks incl damage and entity visibility (untested), working on avoid target data impl for panicking and move away 2025-08-16 10:14:19 +10:00
Paul fe6841cac4 Merge remote-tracking branch 'origin/main' into fickle-behaviour
# Conflicts:
#	common/src/main/kotlin/com/cobblemon/mod/common/api/molang/MoLangFunctions.kt
2025-08-09 16:44:48 +10:00
Paul 0536c98b6a Some more molang for memories 2025-08-06 18:54:03 +10:00
Paul fc73384ea2 Respect attack target data disengaging in melee combat 2025-08-02 10:28:30 +10:00
40 changed files with 627 additions and 89 deletions
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.ai.CobblemonAttackTargetData
import com.cobblemon.mod.common.api.ai.CobblemonAvoidTargetData
import com.cobblemon.mod.common.api.ai.CobblemonWanderControl
import com.cobblemon.mod.common.api.dialogue.ActiveDialogue
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
@@ -57,6 +58,7 @@ object CobblemonMemories {
/** how many are following me rn? */
val HERD_SIZE = register<Int>("herd_size") // Don't bother saving it, we'll try to keep count roughly.
val ATTACK_TARGET_DATA = register<CobblemonAttackTargetData>("attack_target_data") // This is used to store additional information about the attack target, such as when to give up pursuit.
val AVOID_TARGET_DATA = register<CobblemonAvoidTargetData>("avoid_target_data") // This is used to store additional information about the avoid target, such as how fast to move.
val WANDER_CONTROL = register<CobblemonWanderControl>("wander_control")
val DISABLE_WALK_TO_WANTED_ITEM = register<Boolean>("disable_walk_to_wanted_item")
val TIME_TRYING_TO_REACH_WANTED_ITEM = register<Int>("time_trying_to_wanted_item")
@@ -20,6 +20,8 @@ import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.resolve
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.memory.MemoryModuleType
/**
* This is used in the AI system to store optional additional information about the attack target,
@@ -41,15 +43,21 @@ import net.minecraft.world.entity.Entity
class CobblemonAttackTargetData(
/** An expression that returns false when the target is no longer worth attacking. */
var shouldContinue: Expression = "true".asExpression(),
var hitsUntilDisengage: Int = -1, // -1 means no limit
var disengageAction: (entity: Entity) -> Unit = {}
var dealtHitsUntilDisengage: Int = -1, // -1 means no limit
var disengageAction: (entity: LivingEntity) -> Unit = ::defaultDisengage
) {
companion object {
fun defaultDisengage(entity: LivingEntity) {
entity.brain.eraseMemory(MemoryModuleType.ATTACK_TARGET)
entity.brain.updateActivityFromSchedule(entity.level().dayTime, entity.level().gameTime)
}
}
val struct = ObjectValue<CobblemonAttackTargetData>(this).also {
it.addFunction("hits_until_disengage") { DoubleValue(hitsUntilDisengage.toDouble()) }
it.addFunction("set_hits_until_disengage") { params ->
it.addFunction("dealt_hits_until_disengage") { DoubleValue(dealtHitsUntilDisengage.toDouble()) }
it.addFunction("set_dealt_hits_until_disengage") { params ->
val value = params.getDouble(0)
hitsUntilDisengage = value.toInt()
DoubleValue(hitsUntilDisengage.toDouble())
dealtHitsUntilDisengage = value.toInt()
DoubleValue(dealtHitsUntilDisengage.toDouble())
}
it.addFunction("set_disengage_action") { params ->
val script = params.getString(0).asIdentifierDefaultingNamespace()
@@ -64,8 +72,15 @@ class CobblemonAttackTargetData(
}
DoubleValue.ONE
}
it.addFunction("reset") {
shouldContinue = "true".asExpression()
dealtHitsUntilDisengage = -1
disengageAction = ::defaultDisengage
DoubleValue.ONE
}
it.addFunction("unset_disengage_action") { params ->
disengageAction = {}
disengageAction = ::defaultDisengage
}
}
}
@@ -0,0 +1,23 @@
/*
* 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
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.util.getDoubleOrNull
class CobblemonAvoidTargetData {
var walkSpeed: Float? = null
@Transient
val struct = ObjectValue<CobblemonAvoidTargetData>(this).also {
it.addFunction("walk_speed") { DoubleValue(walkSpeed) }
it.addFunction("set_walk_speed") { walkSpeed = it.getDoubleOrNull(0)?.toFloat() }
}
}
@@ -13,7 +13,6 @@ import com.cobblemon.mod.common.util.getBooleanOrNull
import com.cobblemon.mod.common.util.getDoubleOrNull
import kotlin.math.sqrt
import net.minecraft.core.BlockPos
import net.minecraft.world.entity.PathfinderMob
/**
* Used to maintain some settings around how the entity should wander this tick. The idea is you
@@ -30,7 +29,8 @@ class CobblemonWanderControl(
var allowWater: Boolean = false,
var allowAir: Boolean = false,
var pathCooldownTicks: Int = 20 * 5, // Used to mitigate the cost of pathfinding.
var wanderSpeed: Float = 0.35F,
var speedMultiplier: Float = 1F,
var wanderChance: Float? = null
) {
class WanderCenter(val x: Number, val y: Number, val z: Number, val minRange: Float, val maxRange: Float)
@@ -46,7 +46,7 @@ class CobblemonWanderControl(
.addFunction("allow_water") { if (allowWater) 1.0 else 0.0 }
.addFunction("allow_air") { if (allowAir) 1.0 else 0.0 }
.addFunction("path_cooldown_ticks") { pathCooldownTicks.toDouble() }
.addFunction("wander_speed") { wanderSpeed }
.addFunction("speed_multiplier") { speedMultiplier }
.addFunction("set_center") { params ->
if (params.params.size < 3) {
throw IllegalArgumentException("set_center requires at least 3 parameters: x, y, z")
@@ -69,15 +69,19 @@ class CobblemonWanderControl(
.addFunction("set_allow_water") { params -> allowWater = params.getBooleanOrNull(0) ?: false }
.addFunction("set_allow_air") { params -> allowAir = params.getBooleanOrNull(0) ?: false }
.addFunction("set_path_cooldown_ticks") { params -> pathCooldownTicks = params.getDoubleOrNull(0)?.toInt() ?: (20 * 5) }
.addFunction("set_wander_speed") { params -> wanderSpeed = params.getDoubleOrNull(0)?.toFloat() ?: 0.35F }
.addFunction("set_speed_multiplier") { params -> speedMultiplier = params.getDoubleOrNull(0)?.toFloat() ?: 0.35F }
.addFunction("set_wander_chance") { params ->
wanderChance = params.getDoubleOrNull(0)?.toFloat()
}
.addFunction("reset") {
center = null
allowLand = false
allowWater = false
allowAir = false
pathCooldownTicks = 20 * 5
wanderSpeed = 0.35F
speedMultiplier = 1F
maxAttempts = 4
wanderChance = null
}
fun isSuitable(position: BlockPos): Boolean {
@@ -0,0 +1,87 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai.config.task
import com.cobblemon.mod.common.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.molang.MoLangFunctions.asMostSpecificMoLangValue
import com.cobblemon.mod.common.entity.EntityCallbacks
import com.cobblemon.mod.common.entity.MoLangScriptingEntity
import com.cobblemon.mod.common.util.asExpression
import com.mojang.datafixers.util.Either
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.Behavior
import net.minecraft.world.entity.ai.behavior.BehaviorControl
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.MemoryStatus
import net.minecraft.world.entity.ai.sensing.SensorType
/**
* Frequently checks a diff on the visibility of entities around the given entity so EntityCallbacks can be triggered.
*
* @author Hiroku
* @since August 7th, 2025
*/
class NotifyAboutEntityVisibilityTaskConfig : SingleTaskConfig {
val ticksBetweenChecks: ExpressionOrEntityVariable = Either.left("20".asExpression())
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) = listOf(ticksBetweenChecks).asVariables()
override fun createTask(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): BehaviorControl<in LivingEntity>? {
if (entity !is MoLangScriptingEntity) {
return null
}
val ticksBetweenChecks = ticksBetweenChecks.resolveInt(behaviourConfigurationContext.runtime)
behaviourConfigurationContext.addMemories(MemoryModuleType.NEAREST_LIVING_ENTITIES)
behaviourConfigurationContext.addSensors(SensorType.NEAREST_LIVING_ENTITIES)
return object : Behavior<LivingEntity>(mapOf(MemoryModuleType.NEAREST_LIVING_ENTITIES to MemoryStatus.VALUE_PRESENT), Int.MAX_VALUE) {
override fun canStillUse(level: ServerLevel, entity: LivingEntity, gameTime: Long) = true
override fun checkExtraStartConditions(level: ServerLevel, owner: LivingEntity) = true
// Buffer of known entities
val knownEntities = mutableListOf<LivingEntity>()
override fun tick(level: ServerLevel, owner: LivingEntity, gameTime: Long) {
if (gameTime % ticksBetweenChecks != 0L) {
return
}
val visibleEntities = owner.getBrain().getMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES)
.orElse(emptyList<LivingEntity>())
.filter { it.isAlive }
val newEntities = visibleEntities.filterNot { it in knownEntities }
val removedEntities = knownEntities.filterNot { it in visibleEntities }
newEntities.forEach {
entity.callbacks.process(
type = EntityCallbacks.SAW_ENTITY,
functions = mapOf("other_entity" to { _ -> it.asMostSpecificMoLangValue() })
)
}
removedEntities.forEach {
entity.callbacks.process(
type = EntityCallbacks.LOST_ENTITY,
functions = mapOf("other_entity" to { _ -> it.asMostSpecificMoLangValue() })
)
}
knownEntities.clear()
knownEntities.addAll(visibleEntities)
}
}
}
}
@@ -120,6 +120,7 @@ interface TaskConfig {
cobblemonResource("target_entity") to TargetEntityTaskConfig::class.java,
cobblemonResource("memory_aspect") to MemoryAspectTaskConfig::class.java,
cobblemonResource("activity_change") to ActivityChangeTaskConfig::class.java,
cobblemonResource("notify_about_entity_visibility") to NotifyAboutEntityVisibilityTaskConfig::class.java,
)
}
@@ -133,7 +133,7 @@ class WanderTaskConfig : SingleTaskConfig {
val wanderControl = it.tryGet(wanderControl).orElse(null) ?: CobblemonWanderControl()
mainThreadRuntime.withQueryValue("entity", entity.asMostSpecificMoLangValue())
val avoidsTargetingAir = avoidTargetingAir.resolveBoolean(mainThreadRuntime)
val wanderChance = mainThreadRuntime.resolveFloat(wanderChanceExpression)
val wanderChance = wanderControl.wanderChance ?: mainThreadRuntime.resolveFloat(wanderChanceExpression)
if (wanderChance <= 0 || world.random.nextFloat() > wanderChance || !wanderControl.allowLand) {
return@Trigger false
}
@@ -180,7 +180,7 @@ class WanderTaskConfig : SingleTaskConfig {
walkTarget.set(
CobblemonWalkTarget(
pos = pos,
speedModifier = speedMultiplier.resolveFloat(mainThreadRuntime),
speedModifier = speedMultiplier.resolveFloat(mainThreadRuntime) * wanderControl.speedMultiplier,
completionRange = 0,
nodeTypeFilter = { nodeType -> nodeType !in listOf(PathType.WATER, PathType.WATER_BORDER) },
destinationNodeTypeFilter = { nodeType -> !avoidsTargetingAir || nodeType !in listOf(PathType.OPEN) }
@@ -78,7 +78,7 @@ class WaterWanderTaskConfig : SingleTaskConfig {
}
mainThreadRuntime.withQueryValue("entity", entity.asMostSpecificMoLangValue())
val wanderChance = mainThreadRuntime.resolveFloat(wanderChanceExpression)
val wanderChance = wanderControl.wanderChance ?: mainThreadRuntime.resolveFloat(wanderChanceExpression)
if (wanderChance <= 0 || world.random.nextFloat() > wanderChance) {
return@Trigger false
}
@@ -104,7 +104,7 @@ class WaterWanderTaskConfig : SingleTaskConfig {
CobblemonWalkTarget(
pos = pos,
nodeTypeFilter = { it == PathType.WATER || it == PathType.WATER_BORDER },
speedModifier = speedMultiplier.resolveFloat(mainThreadRuntime),
speedModifier = speedMultiplier.resolveFloat(mainThreadRuntime) * wanderControl.speedMultiplier,
completionRange = 0
)
)
@@ -12,7 +12,6 @@ import com.bedrockk.molang.runtime.MoLangEnvironment
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.MoParams
import com.bedrockk.molang.runtime.struct.ArrayStruct
import com.bedrockk.molang.runtime.struct.MoStruct
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.struct.VariableStruct
import com.bedrockk.molang.runtime.value.DoubleValue
@@ -25,6 +24,8 @@ import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.CobblemonUnlockableWallpapers
import com.cobblemon.mod.common.Environment
import com.cobblemon.mod.common.api.ai.CobblemonBlockPosTracker
import com.cobblemon.mod.common.api.ai.CobblemonAttackTargetData
import com.cobblemon.mod.common.api.ai.CobblemonAvoidTargetData
import com.cobblemon.mod.common.api.ai.CobblemonWanderControl
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
@@ -348,10 +349,7 @@ object MoLangFunctions {
val b = format.parse(dateB)
DoubleValue(a.after(b))
},
"create_simple_party_provider" to java.util.function.Function { params ->
val partyProvider = SimplePartyProvider()
return@Function partyProvider.struct
},
"create_simple_party_provider" to java.util.function.Function { SimplePartyProvider().struct },
"create_pickup_item" to java.util.function.Function { params ->
val item = params.getOrNull<MoValue>(0)?.asString()?.let(ObtainableItemCondition::parseFromString)
val pickupPriority = params.getIntOrNull(1) ?: 0
@@ -364,6 +362,7 @@ object MoLangFunctions {
{ worldHolder ->
val world = worldHolder.value()
val map = hashMapOf<String, java.util.function.Function<MoParams, Any>>()
map.put("id") { StringValue(world.dimension().location().toString()) }
map.put("is_time_of_day") { params ->
val timeOfDay = TimeRange.timeRanges[params.getString(0).lowercase()]
?: return@put DoubleValue.ZERO
@@ -807,6 +806,13 @@ object MoLangFunctions {
val source = DamageSource(entity.level().registryAccess().registryOrThrow(Registries.DAMAGE_TYPE).getHolder(DamageTypes.GENERIC).get())
entity.hurt(source, amount.toFloat())
}
map.put("damage_source") { params ->
val registry = entity.level().registryAccess().registryOrThrow(Registries.DAMAGE_TYPE)
val type = params.getStringOrNull(0)?.asIdentifierDefaultingNamespace()?.let(registry::getHolder)
?: registry.getHolder(DamageTypes.GENERIC)
val damageSource = DamageSource(type.get(), entity)
return@put damageSource.asMoLangValue()
}
map.put("distance_to_owner") {
if (entity !is TamableAnimal) {
return@put DoubleValue.ZERO
@@ -930,6 +936,10 @@ object MoLangFunctions {
map.put("is_on_fire") { _ -> DoubleValue(entity.isOnFire) }
map.put("is_invisible") { _ -> DoubleValue(entity.isInvisible) }
map.put("is_riding") { _ -> DoubleValue(entity.isPassenger) }
map.put("distance_to_entity") { params ->
val otherEntity = params.get<MoValue>(0) as? ObjectValue<Entity> ?: return@put DoubleValue.ZERO
return@put DoubleValue(entity.distanceTo(otherEntity.obj))
}
map.put("distance_to_pos") { params ->
val x = params.getDouble(0)
val y = params.getDouble(1)
@@ -1063,7 +1073,19 @@ object MoLangFunctions {
}
)
val livingEntityFunctions: MutableList<(LivingEntity) -> HashMap<String, java.util.function.Function<MoParams, Any>>> = mutableListOf<(LivingEntity) -> HashMap<String, java.util.function.Function<MoParams, Any>>>(
val damageSourceFunctions: MutableList<(DamageSource) -> java.util.HashMap<String, java.util.function.Function<MoParams, Any>>> = mutableListOf(
{ source ->
val map = hashMapOf<String, java.util.function.Function<MoParams, Any>>()
map.put("direct_entity") { source.directEntity?.asMostSpecificMoLangValue() ?: DoubleValue.ZERO }
map.put("entity") { source.entity?.asMostSpecificMoLangValue() ?: DoubleValue.ZERO }
map.put("is_food_exhaustion") { DoubleValue(source.foodExhaustion) }
map.put("is_direct") { DoubleValue(source.isDirect) }
map.put("type") { StringValue(source.typeHolder().unwrapKey().get().location().toString()) }
return@mutableListOf map
}
)
val livingEntityFunctions: MutableList<(LivingEntity) -> HashMap<String, java.util.function.Function<MoParams, Any>>> = mutableListOf(
{ entity ->
val map = hashMapOf<String, java.util.function.Function<MoParams, Any>>()
map.put("is_player") { _ -> DoubleValue(entity is Player) }
@@ -1105,6 +1127,23 @@ object MoLangFunctions {
val amount = params.getDouble(0)
entity.heal(amount.toFloat())
}
map.put("reset_activity") { _ ->
entity.brain.updateActivityFromSchedule(entity.level().dayTime, entity.level().gameTime)
DoubleValue.ONE
}
map.put("activity") { _ ->
return@put entity.brain.activeNonCoreActivity.map { StringValue(it.name) }.orElse(null) ?: DoubleValue.ZERO
}
map.put("set_activity") { params ->
val activityId = params.getString(0).asIdentifierDefaultingNamespace()
val activity = BuiltInRegistries.ACTIVITY.get(activityId)
if (activity != null) {
entity.brain.setActiveActivityIfPossible(activity)
return@put DoubleValue.ONE
} else {
return@put DoubleValue.ZERO
}
}
map.put("is_looking_at") { params ->
val targetEntity = params.get<MoValue>(0)
val maxDistance = params.getDoubleOrNull(1)?.toFloat() ?: 2.0F
@@ -1131,6 +1170,26 @@ object MoLangFunctions {
val brain = entity.brain
brain.setMemoryWithExpiry(MemoryModuleType.LOOK_TARGET, CobblemonBlockPosTracker(Vec3(x, y, z), flags.toSet()), duration.toLong())
}
map.put("is_doing_activity") { params ->
val identifiers = params.params.map { it.asString().asIdentifierDefaultingNamespace(namespace = "minecraft") }
val activities = identifiers.mapNotNull(BuiltInRegistries.ACTIVITY::get)
if (activities.isNotEmpty()) {
return@put DoubleValue(activities.any(entity.brain::isActive))
} else {
return@put DoubleValue.ZERO
}
}
map.put("get_attack_target_data_memory") {
val value = entity.brain.getMemorySafely(CobblemonMemories.ATTACK_TARGET_DATA).orElse(null)
?: CobblemonAttackTargetData()
if (
!entity.brain.checkMemory(CobblemonMemories.ATTACK_TARGET_DATA, MemoryStatus.VALUE_PRESENT)
&& entity.brain.checkMemory(CobblemonMemories.ATTACK_TARGET_DATA, MemoryStatus.REGISTERED)
) {
entity.brain.setMemory(CobblemonMemories.ATTACK_TARGET_DATA, value)
}
return@put value.struct
}
map.put("get_wander_control_memory") {
val value = entity.brain.getMemorySafely(CobblemonMemories.WANDER_CONTROL).orElse(null)
?: CobblemonWanderControl()
@@ -1142,6 +1201,17 @@ object MoLangFunctions {
}
return@put value.struct
}
map.put("get_avoid_target_data_memory") {
val value = entity.brain.getMemorySafely(CobblemonMemories.AVOID_TARGET_DATA).orElse(null)
?: CobblemonAvoidTargetData()
if (
!entity.brain.checkMemory(CobblemonMemories.AVOID_TARGET_DATA, MemoryStatus.VALUE_PRESENT)
&& entity.brain.checkMemory(CobblemonMemories.AVOID_TARGET_DATA, MemoryStatus.REGISTERED)
) {
entity.brain.setMemory(CobblemonMemories.AVOID_TARGET_DATA, value)
}
return@put value.struct
}
map.put("get_position_memory") { params ->
val id = params.getString(0).asIdentifierDefaultingNamespace()
val memoryType = BuiltInRegistries.MEMORY_MODULE_TYPE.get(id)
@@ -1168,6 +1238,30 @@ object MoLangFunctions {
return@put DoubleValue.ZERO
}
}
map.put("get_entity_memory") { params ->
val id = params.getString(0).asIdentifierDefaultingNamespace()
val memoryType = BuiltInRegistries.MEMORY_MODULE_TYPE.get(id)
if (entity.brain.checkMemory(memoryType, MemoryStatus.VALUE_PRESENT)) {
val memory = entity.brain.getMemory(memoryType).get()
return@put when (memory) {
is Entity -> memory.asMostSpecificMoLangValue()
else -> DoubleValue.ZERO
}
} else {
return@put DoubleValue.ZERO
}
}
map.put("set_entity_memory") { params ->
val memory = params.getString(0).asIdentifierDefaultingNamespace().let(BuiltInRegistries.MEMORY_MODULE_TYPE::get) as MemoryModuleType<LivingEntity>
val entityValue = (params.get<MoValue>(1) as? ObjectValue<*>)?.obj as? LivingEntity ?: return@put DoubleValue.ZERO
val expiry = params.getIntOrNull(2) ?: -1
if (expiry != -1) {
entity.brain.setMemoryWithExpiry(memory, entityValue, expiry.toLong())
} else {
entity.brain.setMemory(memory, entityValue)
}
return@put DoubleValue.ONE
}
map.put("set_uuid_memory") { params ->
val memory = params.getString(0).asIdentifierDefaultingNamespace().let(BuiltInRegistries.MEMORY_MODULE_TYPE::get) as MemoryModuleType<UUID>
val uuid = params.getString(1).asUUID ?: return@put DoubleValue.ZERO
@@ -1214,6 +1308,18 @@ object MoLangFunctions {
return@put DoubleValue.ZERO
}
}
map.put("is_visible_entity") { params ->
val otherEntity = params.get<ObjectValue<*>>(0)?.obj as? LivingEntity ?: return@put DoubleValue.ZERO
val visibleMobs = entity.brain.getMemorySafely(MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES).orElse(null)
?: return@put DoubleValue.ZERO
return@put DoubleValue(visibleMobs.contains(otherEntity))
}
map.put("is_nearby_entity") { params ->
val otherEntity = params.get<ObjectValue<*>>(0)?.obj as? LivingEntity ?: return@put DoubleValue.ZERO
val nearbyMobs = entity.brain.getMemorySafely(MemoryModuleType.NEAREST_LIVING_ENTITIES).orElse(null)
?: return@put DoubleValue.ZERO
return@put DoubleValue(nearbyMobs.contains(otherEntity))
}
if (entity is PathfinderMob) {
map.put("walk_to") { params ->
val x = params.getDouble(0)
@@ -1252,15 +1358,6 @@ object MoLangFunctions {
map.put("in_battle") { DoubleValue(npc.isInBattle()) }
map.put("battles") { ArrayStruct(npc.battleIds.mapNotNull { BattleRegistry.getBattle(it)?.struct }.mapIndexed { index, value -> "$index" to value }.toMap()) }
map.put("stop_battles") { _ -> npc.battleIds.forEach { BattleRegistry.getBattle(it)?.stop() } }
map.put("is_doing_activity") { params ->
val identifiers = params.params.map { it.asString().asIdentifierDefaultingNamespace(namespace = "minecraft") }
val activities = identifiers.mapNotNull { identifier -> BuiltInRegistries.ACTIVITY.get(identifier) }
if (activities.isNotEmpty()) {
return@put DoubleValue(activities.any { activity -> npc.brain.isActive(activity) })
} else {
return@put DoubleValue.ZERO
}
}
map.put("run_script_on_client") { params ->
val world = npc.level()
if (world is ServerLevel) {
@@ -2471,6 +2568,15 @@ object MoLangFunctions {
return value
}
fun DamageSource.asMoLangValue(): ObjectValue<DamageSource> {
val value = ObjectValue(
obj = this,
stringify = { it.toString() }
)
value.addFunctions(damageSourceFunctions.flatMap { it(this).entries.map { it.key to it.value } }.toMap())
return value
}
/**
* Different functions exist depending on the entity type, this tries to make the struct that's most specific to the type.
*/
@@ -38,6 +38,7 @@ class EntityCallbacks(val entity: Entity) : HashMap<ResourceLocation, MutableLis
companion object {
val HIT_BY_POKEBALL = cobblemonResource("hit_by_pokeball")
val SAW_ENTITY = cobblemonResource("saw_entity")
val LOST_ENTITY = cobblemonResource("lost_sight_of_entity")
val HURT = cobblemonResource("hurt")
val DIED = cobblemonResource("died")
}
@@ -644,6 +644,20 @@ class NPCEntity(world: Level) : AgeableMob(CobblemonEntities.NPC, world), Npc, P
return InteractionResult.SUCCESS
}
override fun hurt(source: DamageSource, amount: Float): Boolean {
if (super.hurt(source, amount)) {
callbacks.process(
type = EntityCallbacks.HURT,
functions = mapOf(
"source" to { source.asMoLangValue() },
"amount" to { DoubleValue(amount.toDouble()) },
)
)
return true
}
return false
}
override fun recalling(pokemonEntity: PokemonEntity): CompletableFuture<Unit> {
playAnimation(RECALL_ANIMATION, pokemonExpressions(pokemonEntity.pokemon))
return delayedFuture(seconds = 1.6F)
@@ -9,7 +9,9 @@
package com.cobblemon.mod.common.entity.npc.ai
import com.bedrockk.molang.Expression
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangValue
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.resolveInt
@@ -24,9 +26,11 @@ object MeleeAttackTask {
fun create(range: Expression, cooldownTicks: Expression): OneShot<LivingEntity> = BehaviorBuilder.create {
it.group(
it.present(MemoryModuleType.ATTACK_TARGET),
it.absent(MemoryModuleType.ATTACK_COOLING_DOWN)
it.absent(MemoryModuleType.ATTACK_COOLING_DOWN),
).apply(it) { attackTarget, cooldown ->
Trigger { world, entity, _ ->
val attackTargetData = entity.brain.getMemorySafely(CobblemonMemories.ATTACK_TARGET_DATA).orElse(null)
mainThreadRuntime.withQueryValue("entity", entity.asMostSpecificMoLangValue())
val range = mainThreadRuntime.resolveFloat(range)
val cooldownTicks = mainThreadRuntime.resolveInt(cooldownTicks)
@@ -35,6 +39,14 @@ object MeleeAttackTask {
if (entity.distanceTo(attackTarget) <= range) {
entity.doHurtTarget(attackTarget)
cooldown.setWithExpiry(true, cooldownTicks.toLong())
if (attackTargetData != null) {
if (attackTargetData.dealtHitsUntilDisengage == 1) {
attackTargetData.dealtHitsUntilDisengage = 0
attackTargetData.disengageAction(entity)
} else if (attackTargetData.dealtHitsUntilDisengage > 0){
attackTargetData.dealtHitsUntilDisengage--
}
}
true
} else {
false
@@ -36,6 +36,7 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.addLivingEntityFuncti
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addPokemonEntityFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addPokemonFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addStandardFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.net.serializers.PlatformTypeDataSerializer
@@ -1220,6 +1221,14 @@ open class PokemonEntity(
// Only touch battle HP for non-wild Pokémon so that quick ball et. al. aren't owerpowered.
pokemon.currentHealth = (pokemon.maxHealth * (this.health / this.maxHealth)).toInt()
}
callbacks.process(
type = EntityCallbacks.HURT,
functions = mapOf(
"source" to { source.asMoLangValue() },
"amount" to { DoubleValue(amount.toDouble()) },
)
)
true
} else false
}
@@ -67,12 +67,12 @@ class FormPokemonBehaviour {
val entityInteract: EntityBehaviour
get() = _entityInteract ?: parent.entityInteract
val dampensVibrations: Boolean
get() = _dampensVibrations ?: parent.dampensVibrations
val blockInteract: BlockBehavior
get() = _blockInteract ?: parent.blockInteract
val dampensVibrations: Boolean
get() = _dampensVibrations ?: parent.dampensVibrations
val combat: CombatBehaviour
get() = _combat ?: parent.combat
@@ -35,5 +35,6 @@ class MoveBehaviour {
it.addFunction("wander_chance") { DoubleValue(wanderChance) }
it.addFunction("wander_speed") { DoubleValue(wanderSpeed) }
it.addFunction("can_look") { DoubleValue(canLook) }
it.addFunction("can_move") { DoubleValue(walk.canWalk || swim.canSwimInLava || swim.canSwimInWater || fly.canFly) }
}
}
@@ -3754,6 +3754,15 @@
"cobblemon.behaviour.pokemon_bee.name": "Pollinates Flowers",
"cobblemon.behaviour.pokemon_bee.desc": "Pollinates flowers and adds honey to hives and Saccharine tree leaves.",
"cobblemon.behaviour.pokemon_neutral_response.name": "Pokémon Response - Neutral",
"cobblemon.behaviour.pokemon_neutral_response.desc": "Neutral-style Pokémon responses to triggers. Attacks once and moves away when aggravated. Attacks persistently when attacked while moving away.",
"cobblemon.behaviour.pokemon_friendly_response.name": "Pokémon Response - Friendly",
"cobblemon.behaviour.pokemon_friendly_response.desc": "Friendly-style Pokémon responses to triggers. Flees when attacked. Observes players.",
"cobblemon.behaviour.callback_hooks.name": "Callback Hooks",
"cobblemon.behaviour.callback_hooks.desc": "Runs MoLang scripts when certain events happen, such as when the entity is attacked or when it sees a new entity. Needed for some behaviours to function.",
"cobblemon.behaviour.pokemon_picks_up_items.name": "Picks Up Items",
"cobblemon.behaviour.pokemon_picks_up_items.desc": "The Pokémon will pick up items that it cares about.",
@@ -0,0 +1,15 @@
{
"name": "cobblemon.behaviour.callback_hooks.name",
"description": "cobblemon.behaviour.callback_hooks.desc",
"configurations": [
{
"type": "cobblemon:add_tasks_to_activity",
"activity": "minecraft:core",
"tasksByPriority": {
"0": [
"cobblemon:notify_about_entity_visibility"
]
}
}
]
}
@@ -19,6 +19,19 @@
"look_at_target"
]
}
},
{
"type": "add_tasks_to_activity",
"activity": "minecraft:idle",
"condition": "q.entity.is_pokemon",
"tasksByPriority": {
"0": [
{
"type": "cobblemon:run_script",
"script": "cobblemon:pokemon_wander_control"
}
]
}
}
]
}
@@ -4,7 +4,7 @@
"configurations": [
{
"type": "cobblemon:add_tasks_to_activity",
"activity": "minecraft:idle",
"activities": ["minecraft:idle", "minecraft:avoid", "minecraft:panic"],
"tasksByPriority": {
"0": [
{
@@ -1,6 +1,12 @@
{
"visible": false,
"configurations": [
{
"type": "cobblemon:apply_behaviours",
"behaviours": [
"cobblemon:callback_hooks"
]
},
{
"type": "add_tasks_to_activity",
"activity": "minecraft:idle",
@@ -10,7 +10,8 @@
"type": "apply_behaviours",
"behaviours": [
"cobblemon:core",
"cobblemon:pokemon_battles"
"cobblemon:pokemon_battles",
"cobblemon:callback_hooks"
]
},
{
@@ -26,18 +27,6 @@
"behaviours": [
"cobblemon:pokemon_sleeps"
]
},
{
"type": "add_tasks_to_activity",
"activity": "minecraft:idle",
"tasksByPriority": {
"0": [
{
"type": "cobblemon:run_script",
"script": "cobblemon:pokemon_wander_control"
}
]
}
}
]
}
@@ -3,37 +3,9 @@
"configurations": [
{
"type": "apply_behaviours",
"condition": "!q.entity.is_in_party && q.entity.behaviour.moving.walk.can_walk && !q.entity.behaviour.moving.walk.avoids_land",
"condition": "!q.entity.is_in_party",
"behaviours": [
"cobblemon:wanders"
]
},
{
"type": "apply_behaviours",
"condition": "!q.entity.is_in_party && q.entity.behaviour.moving.fly.can_fly",
"behaviours": [
"cobblemon:wanders_hover"
]
},
{
"type": "apply_behaviours",
"condition": "!q.entity.is_in_party && q.entity.behaviour.moving.swim.can_swim_in_water && !q.entity.behaviour.moving.swim.avoids_water",
"behaviours": [
"cobblemon:wanders_water"
]
},
{
"type": "apply_behaviours",
"condition": "!q.entity.is_in_party && (q.entity.behaviour.combat.will_flee && !q.entity.behaviour.combat.will_defend_self)",
"behaviours": [
"cobblemon:panics"
]
},
{
"type": "apply_behaviours",
"condition": "!q.entity.is_in_party && q.entity.behaviour.combat.will_defend_self",
"behaviours": [
"cobblemon:retaliates"
"cobblemon:pokemon_sleeps"
]
},
{
@@ -0,0 +1,26 @@
{
"visible": false,
"configurations": [
{
"type": "apply_behaviours",
"condition": "!q.entity.is_in_party && q.entity.behaviour.moving.walk.can_walk && !q.entity.behaviour.moving.walk.avoids_land",
"behaviours": [
"cobblemon:wanders"
]
},
{
"type": "apply_behaviours",
"condition": "!q.entity.is_in_party && !q.entity.behaviour.moving.walk.can_walk && q.entity.behaviour.moving.fly.can_fly",
"behaviours": [
"cobblemon:wanders_hover"
]
},
{
"type": "apply_behaviours",
"condition": "!q.entity.is_in_party && q.entity.behaviour.moving.walk.avoids_land && q.entity.behaviour.moving.swim.can_swim_in_water && !q.entity.behaviour.moving.swim.avoids_water",
"behaviours": [
"cobblemon:wanders_water"
]
}
]
}
@@ -0,0 +1,28 @@
{
"_comment": "Used to give the basics of avoidance behaviour for Pokémon.",
"visible": false,
"configurations": [
{
"type": "cobblemon:add_memories_and_sensors",
"memories": [
"minecraft:avoid_target",
"cobblemon:avoid_target_data"
]
},
{
"type": "cobblemon:add_tasks_to_activity",
"activity": "minecraft:avoid",
"tasksByPriority": {
"0": [
"cobblemon:follow_walk_target"
],
"1": [
{
"type": "cobblemon:run_script",
"script": "cobblemon:wander_avoid"
}
]
}
}
]
}
@@ -0,0 +1,35 @@
{
"visible": false,
"configurations": [
{
"type": "cobblemon:add_memories_and_sensors",
"memories": [
"minecraft:visible_mobs"
],
"sensors": [
"minecraft:nearest_living_entities"
]
},
{
"type": "cobblemon:add_tasks_to_activity",
"activity": "minecraft:panic",
"tasksByPriority": {
"0": [
"cobblemon:follow_walk_target"
],
"1": [
{
"type": "cobblemon:run_script",
"script": "cobblemon:wander_avoid"
}
],
"2": [
{
"type": "cobblemon:run_script",
"script": "cobblemon:wander_panic"
}
]
}
}
]
}
@@ -0,0 +1,41 @@
{
"name": "cobblemon.behaviour.pokemon_friendly_response.name",
"description": "cobblemon.behaviour.pokemon_friendly_response.desc",
"onAdd": [
"q.entity.add_callback('cobblemon:hurt', 'cobblemon:flee_aggressor');",
"q.entity.add_callback('cobblemon:saw_entity', 'cobblemon:begin_to_observe_from_distance');",
"q.entity.add_callback('cobblemon:lost_sight_of_entity', 'cobblemon:stop_observing_from_distance');"
],
"onRemove": [
"q.entity.remove_callback('cobblemon:hurt', 'cobblemon:flee_aggressor');",
"q.entity.remove_callback('cobblemon:saw_entity', 'cobblemon:begin_to_observe_from_distance');",
"q.entity.remove_callback('cobblemon:lost_sight_of_entity', 'cobblemon:stop_observing_from_distance');"
],
"configurations": [
{
"type": "cobblemon:apply_behaviours",
"behaviours": [
"cobblemon:pokemon_panics"
]
},
{
"type": "cobblemon:add_memories_and_sensors",
"memories": [
"minecraft:avoid_target",
"cobblemon:avoid_target_data"
]
},
{
"type": "cobblemon:add_tasks_to_activity",
"activity": "minecraft:panic",
"tasksByPriority": {
"2": [
{
"type": "cobblemon:run_script",
"script": "cobblemon:stop_avoiding_when_target_gone"
}
]
}
}
]
}
@@ -0,0 +1,39 @@
{
"name": "cobblemon.behaviour.pokemon_neutral_response.name",
"description": "cobblemon.behaviour.pokemon_neutral_response.desc",
"onAdd": [
"q.entity.add_callback('cobblemon:hurt', 'cobblemon:attack_aggressor_once_then_avoid');",
"q.entity.add_callback('cobblemon:hit_by_pokeball', 'cobblemon:attack_aggressor_once_then_avoid');"
],
"onRemove": [
"q.entity.remove_callback('cobblemon:hurt', 'cobblemon:attack_aggressor_once_then_avoid');",
"q.entity.remove_callback('cobblemon:hit_by_pokeball', 'cobblemon:attack_aggressor_once_then_avoid');"
],
"configurations": [
{
"type": "cobblemon:apply_behaviours",
"behaviours": [
"cobblemon:pokemon_avoids"
]
},
{
"type": "cobblemon:add_memories_and_sensors",
"memories": [
"minecraft:attack_target",
"cobblemon:attack_target_data"
]
},
{
"type": "cobblemon:add_tasks_to_activity",
"activity": "minecraft:avoid",
"tasksByPriority": {
"2": [
{
"type": "cobblemon:run_script",
"script": "cobblemon:stop_avoiding_when_target_gone"
}
]
}
}
]
}
@@ -3,11 +3,11 @@
"description": "cobblemon.behaviour.wanders.desc",
"configurations": [
{
"type": "add_tasks_to_activity",
"activity": "idle",
"type": "cobblemon:add_tasks_to_activity",
"activities": ["minecraft:idle", "minecraft:avoid", "minecraft:panic"],
"tasksByPriority": {
"7": [
"wander"
"cobblemon:wander"
]
}
}
@@ -3,8 +3,8 @@
"description": "cobblemon.behaviour.wanders_air.desc",
"configurations": [
{
"type": "add_tasks_to_activity",
"activity": "idle",
"type": "cobblemon:add_tasks_to_activity",
"activities": ["minecraft:idle", "minecraft:avoid", "minecraft:panic"],
"tasksByPriority": {
"7": [
"cobblemon:air_wander"
@@ -3,8 +3,8 @@
"description": "cobblemon.behaviour.wanders_hover.desc",
"configurations": [
{
"type": "add_tasks_to_activity",
"activity": "idle",
"type": "cobblemon:add_tasks_to_activity",
"activities": ["minecraft:idle", "minecraft:avoid", "minecraft:panic"],
"tasksByPriority": {
"7": [
{
@@ -3,11 +3,11 @@
"description": "cobblemon.behaviour.wanders_water.desc",
"configurations": [
{
"type": "add_tasks_to_activity",
"activity": "idle",
"type": "cobblemon:add_tasks_to_activity",
"activities": ["minecraft:idle", "minecraft:avoid", "minecraft:panic"],
"tasksByPriority": {
"7": [
"water_wander"
"cobblemon:water_wander"
],
"9": [
{
@@ -0,0 +1,22 @@
t.aggressor = q.run_script('cobblemon:get_aggressor');
t.aggressor == 0 ? {
return 0;
};
t.activity = q.entity.activity;
q.entity.is_doing_activity('cobblemon:battling', 'cobblemon:fight', 'cobblemon:sleep') ? {
return 0;
};
q.entity.is_doing_activity('cobblemon:avoid') ? {
'Oh we are very angry now';
q.entity.set_entity_memory('minecraft:attack_target', t.aggressor);
q.entity.erase_memory('cobblemon:path_cooldown', 'cobblemon:attack_target_data');
q.entity.set_activity('minecraft:fight');
} : {
t.attack_target_data = q.entity.get_attack_target_data_memory();
t.attack_target_data.set_disengage_action('cobblemon:move_away_from_attack_target');
t.attack_target_data.set_dealt_hits_until_disengage(1);
q.entity.set_entity_memory('minecraft:attack_target', t.aggressor);
q.entity.erase_memory('cobblemon:path_cooldown');
q.entity.set_activity('minecraft:fight');
};
@@ -0,0 +1,2 @@
t.seen_entity = q.other_entity;
'Do some stuff if we are in the right activity or whatever';
@@ -0,0 +1,7 @@
t.aggressor = q.run_script('cobblemon:get_aggressor');
t.aggressor == 0 ? {
return 0;
};
q.entity.set_entity_memory('minecraft:avoid_target', t.aggressor);
q.entity.erase_memory('cobblemon:path_cooldown');
q.entity.set_activity('minecraft:panic');
@@ -0,0 +1,7 @@
t.attack_target = q.entity.get_entity_memory('minecraft:attack_target');
t.attack_target == 0 ? {
return 0;
};
q.entity.erase_memory('minecraft:attack_target');
q.entity.set_entity_memory('minecraft:avoid_target', t.attack_target);
q.entity.set_activity('minecraft:avoid');
@@ -0,0 +1,2 @@
t.lost_entity = q.other_entity;
'Some logic';
@@ -0,0 +1,5 @@
t.avoid_target = q.entity.get_entity_memory('minecraft:avoid_target');
t.avoid_target == 0 || !q.entity.is_nearby_entity(t.avoid_target) ? {
q.entity.reset_activity();
return 0;
};
@@ -0,0 +1,10 @@
q.pokeball != 0 ? {
return q.pokeball.thrower;
};
q.source != 0 ? {
return q.source.entity;
};
q.other_entity != 0 ? {
return q.other_entity;
};
return 0;
@@ -0,0 +1,23 @@
'Takes the avoid target and uses their position as a place to avoid being near. This goes for move away and for panic fleeing.';
q.entity.has_memory_value('minecraft:avoid_target') == 0 ? {
return 0;
};
t.wander_control = q.entity.get_wander_control_memory();
t.avoid_target_data = q.entity.get_avoid_target_data_memory();
t.avoid_entity = q.entity.get_entity_memory('minecraft:avoid_target');
t.avoid_entity == 0 || t.avoid_entity.world.id != q.entity.world.id ? {
return 0;
};
t.distance_to_entity = t.avoid_entity.distance_to_entity(q.entity);
t.distance_to_entity - 8 > 24 ? {
t.min_distance = 24;
} : {
t.min_distance = t.distance_to_entity + 8;
};
t.wander_control.set_center(t.avoid_entity.x, t.avoid_entity.y, t.avoid_entity.z, t.min_distance, 9999);
t.wander_control.set_path_cooldown_ticks(20);
t.wander_control.set_wander_chance(1/20);
@@ -0,0 +1,12 @@
'Panicking means moving very fast if the avoid target is nearby.';
t.wander_control = q.entity.get_wander_control_memory();
t.avoid_target_data = q.entity.get_avoid_target_data_memory();
t.avoid_entity = q.entity.get_entity_memory('minecraft:avoid_target');
t.avoid_entity != 0 && q.entity.distance_to_entity(t.avoid_entity) > 20 ? {
return 0;
};
t.wander_control.set_speed_multiplier(1.4);
t.wander_control.set_wander_chance(1);
t.wander_control.set_path_cooldown_ticks(10);