Compare commits

...
15 Commits
Author SHA1 Message Date
jackowes 3d54e51fb9 properly switch pose to walking when walk speed is super slow. 2026-10-05 15:38:25 -05:00
jackowes e3c1fff02a Update speedtuner with the ability to slow animations, +- buttons on the slider. and a working pause. 2026-10-05 15:19:48 -05:00
jackowes c672a4d690 Merge remote-tracking branch 'origin/main' into mon-smoothening 2026-10-04 18:44:12 -05:00
jackowes e64df48441 allow for the speed tuner to set walk speeds during the session 2026-10-04 18:34:03 -05:00
jackowes acd375f533 - fix jitter in dynamic animations.
- Lookahead scales with turn distance
- Walk instead of sprint through narrow path sections
- Eased head look
- No looking around while travelling
- Pitch tilt applies to the whole model
- Slower floatier turns in water and air
- Large Pokémon slide onto close targets instead of spinning
2026-10-03 17:24:59 -05:00
jackowes 9076c659de run script to ungate many riding overlay animations 2026-10-02 20:19:16 -05:00
jackowes 9292504f8c Move away from non minecrafty speed holding and redo how rotations are handled. move away from reynolds model. 2026-10-02 20:00:56 -05:00
jackowes ad484abf56 Merge remote-tracking branch 'origin/main' into mon-smoothening 2026-10-01 23:14:46 -05:00
jackowes fc1c7cf6ca allow for dynamic animations outside of rides. 2026-10-01 23:13:01 -05:00
jackowes 33abca9b43 First draft of sprint and walk speed tuning tool 2026-09-29 21:40:04 -05:00
jackowes ea3c051772 Sprint now in blocks per second and default sprint speed is player sprint speed. 2026-09-29 20:44:00 -05:00
jackowes 4cde730259 these are not the correct file 2026-09-27 21:53:02 -05:00
jackowes 2bcce06318 - fix sliding after being hit while walking on a path.
- make it so running starts when they mon is farther
- smaller mons get closer before stopping when traveling to the owner.
- make it so mons immediately sprint when the player is sprinting.
2026-09-27 21:45:35 -05:00
jackowes 930c3568bb add temp sprint speeds to all sprinters. add sprint to attacktargettask as well 2026-09-27 21:03:42 -05:00
jackowes ffe480b12f Smoother path following, steering move control, body rotation and AI sprinting 2026-09-27 20:20:54 -05:00
209 changed files with 2726 additions and 1004 deletions
@@ -22,6 +22,7 @@ import com.cobblemon.mod.common.client.net.data.CobblemonMechanicsSyncHandler
import com.cobblemon.mod.common.client.net.data.DataRegistrySyncPacketHandler
import com.cobblemon.mod.common.client.net.data.RideSettingsSyncHandler
import com.cobblemon.mod.common.client.net.debug.OpenRidingStatsDebugGUIHandler
import com.cobblemon.mod.common.client.net.debug.OpenSpeedTunerHandler
import com.cobblemon.mod.common.client.net.dialogue.DialogueClosedHandler
import com.cobblemon.mod.common.client.net.dialogue.DialogueOpenedHandler
import com.cobblemon.mod.common.client.net.effect.PokeSnackBlockParticlesHandler
@@ -83,6 +84,7 @@ import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyMoveCallba
import com.cobblemon.mod.common.net.messages.client.cooking.SeasoningRegistrySyncPacket
import com.cobblemon.mod.common.net.messages.client.cooking.ToggleCookingPotLidPacket
import com.cobblemon.mod.common.net.messages.client.data.*
import com.cobblemon.mod.common.net.messages.client.debug.OpenSpeedTunerPacket
import com.cobblemon.mod.common.net.messages.client.debug.RequestOpenRidingStatsDebugGUIPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueClosedPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueOpenedPacket
@@ -173,6 +175,9 @@ import com.cobblemon.mod.common.net.messages.server.callback.party.PartySelectCa
import com.cobblemon.mod.common.net.messages.server.callback.partymove.PartyMoveSelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.partymove.PartyPokemonMoveSelectedPacket
import com.cobblemon.mod.common.net.messages.server.debug.OpenRidingStatsDebugGUIPacket
import com.cobblemon.mod.common.net.messages.server.debug.RequestOpenSpeedTunerPacket
import com.cobblemon.mod.common.net.messages.server.debug.SetSpeedTunerSprintSpeedPacket
import com.cobblemon.mod.common.net.messages.server.debug.SetSpeedTunerWalkSpeedPacket
import com.cobblemon.mod.common.net.messages.server.debug.ServerboundUpdateRidingSettingsPacket
import com.cobblemon.mod.common.net.messages.server.debug.ServerboundUpdateRidingStatRangePacket
import com.cobblemon.mod.common.net.messages.server.debug.ServerboundUpdateRidingStatsPacket
@@ -231,6 +236,9 @@ import com.cobblemon.mod.common.net.serverhandling.callback.party.PartySelectCan
import com.cobblemon.mod.common.net.serverhandling.callback.partymove.PartyMoveSelectCancelledHandler
import com.cobblemon.mod.common.net.serverhandling.callback.partymove.PartyPokemonMoveSelectedHandler
import com.cobblemon.mod.common.net.serverhandling.debug.RequestOpenRidingStatsDebugGUIHandler
import com.cobblemon.mod.common.net.serverhandling.debug.RequestOpenSpeedTunerHandler
import com.cobblemon.mod.common.net.serverhandling.debug.SetSpeedTunerSprintSpeedHandler
import com.cobblemon.mod.common.net.serverhandling.debug.SetSpeedTunerWalkSpeedHandler
import com.cobblemon.mod.common.net.serverhandling.debug.ServerboundUpdateRidingSettingsHandler
import com.cobblemon.mod.common.net.serverhandling.debug.ServerboundUpdateRidingStatRangeHandler
import com.cobblemon.mod.common.net.serverhandling.debug.ServerboundUpdateRidingStatsHandler
@@ -517,6 +525,7 @@ object CobblemonNetwork {
// Debug
list.add(PacketRegisterInfo(OpenRidingStatsDebugGUIPacket.ID, OpenRidingStatsDebugGUIPacket::decode, OpenRidingStatsDebugGUIHandler))
list.add(PacketRegisterInfo(OpenSpeedTunerPacket.ID, OpenSpeedTunerPacket::decode, OpenSpeedTunerHandler))
return list
@@ -649,6 +658,9 @@ object CobblemonNetwork {
// Debug / cheats
list.add(PacketRegisterInfo(RequestOpenRidingStatsDebugGUIPacket.ID, RequestOpenRidingStatsDebugGUIPacket::decode, RequestOpenRidingStatsDebugGUIHandler))
list.add(PacketRegisterInfo(RequestOpenSpeedTunerPacket.ID, RequestOpenSpeedTunerPacket::decode, RequestOpenSpeedTunerHandler))
list.add(PacketRegisterInfo(SetSpeedTunerSprintSpeedPacket.ID, SetSpeedTunerSprintSpeedPacket::decode, SetSpeedTunerSprintSpeedHandler))
list.add(PacketRegisterInfo(SetSpeedTunerWalkSpeedPacket.ID, SetSpeedTunerWalkSpeedPacket::decode, SetSpeedTunerWalkSpeedHandler))
return list
@@ -22,17 +22,22 @@ import net.minecraft.world.entity.ai.memory.MemoryModuleType
class MoveToOwnerTaskConfig : SingleTaskConfig {
val condition: ExpressionOrEntityVariable = Either.left("true".asExpression())
var completionRange: ExpressionOrEntityVariable = Either.left("4.0".asExpression())
// Gap between the pokemon's hitbox and the owner's where it stops
var completionRange: ExpressionOrEntityVariable = Either.left("2.0".asExpression())
var speedMultiplier: ExpressionOrEntityVariable = Either.left("0.4".asExpression())
var teleportDistance: ExpressionOrEntityVariable = Either.left("24.0".asExpression())
var maxDistance: ExpressionOrEntityVariable = Either.left("14.0".asExpression())
// Gap between the pokemon's hitbox and the owner's before it sprints to catch up. Past maxDistance so short
// catch ups are walked
var sprintDistance: ExpressionOrEntityVariable = Either.left("14.0".asExpression())
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) = listOf(
condition,
completionRange,
speedMultiplier,
teleportDistance,
maxDistance
maxDistance,
sprintDistance
).asVariables()
override fun createTask(
@@ -49,7 +54,8 @@ class MoveToOwnerTaskConfig : SingleTaskConfig {
completionRange = completionRange.asExpression(),
speedMultiplier = speedMultiplier.asExpression(),
teleportDistance = teleportDistance.asExpression(),
maxDistance = maxDistance.asExpression()
maxDistance = maxDistance.asExpression(),
sprintDistance = sprintDistance.asExpression()
),
PokemonEntity::class.java
)
@@ -54,10 +54,15 @@ object PokemonEntityMoLangFunctions : AbstractMoLangFunctionHolder<PokemonEntity
}
}
functions["is_sprinting"] = {
pokemonEntity.ifRidingAvailableSupply(DoubleValue.ZERO) { behaviour, settings, state ->
val moValue = behaviour.asMoLangValue(settings, state, pokemonEntity)
return@ifRidingAvailableSupply moValue.functions["sprinting"]?.apply(MoParams.EMPTY) as? MoValue
?: DoubleValue.ZERO
// Ridden pokemon sprint from the ride state, otherwise from the AI
if (!pokemonEntity.hasControllingPassenger()) {
DoubleValue(pokemonEntity.isPokemonSprinting)
} else {
pokemonEntity.ifRidingAvailableSupply(DoubleValue.ZERO) { behaviour, settings, state ->
val moValue = behaviour.asMoLangValue(settings, state, pokemonEntity)
return@ifRidingAvailableSupply moValue.functions["sprinting"]?.apply(MoParams.EMPTY) as? MoValue
?: DoubleValue.ZERO
}
}
}
functions["is_drifting"] = {
@@ -151,6 +151,7 @@ object CobblemonPermissions {
@JvmStatic
val USE_RIDING_STATS_DEBUG = this.create("useridingstatsdebug", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
val USE_SPEED_TUNER = this.create("usespeedtuner", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
fun all(): Iterable<Permission> = this.permissions
@@ -18,50 +18,49 @@ import org.joml.Quaternionf
import org.joml.Vector3f
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.sign
import kotlin.math.sin
/**
* Class to handle the updating and containment of ride data used in animation.
* Class to handle the updating and holding of motion data used in animation. Motion values accumulate for every
* pokemon on the client, some only while ridden.
*
* @author Jackowes
* @since April 7th, 2025
*/
class RidingAnimationData(val ride: PokemonEntity)
class DynamicAnimationData(val ride: PokemonEntity)
{
companion object {
// Blend time for motion values, they catch up within 1.5x this. Longer is smoother but lags more
const val MOTION_BLEND_SECONDS = 0.8F
// Rider input comes straight from key presses so it catches up quicker
const val INPUT_BLEND_SECONDS = 0.3F
}
var prevOrientation: Matrix3f = Matrix3f()
var prevRot: Vec3 = Vec3.ZERO
var velocitySpring: Vec3Spring = Vec3Spring()
var rotSpring: Vec3Spring = Vec3Spring()
var rotDeltaSpring: Vec3Spring = Vec3Spring()
var localVelocitySpring: Vec3Spring = Vec3Spring()
var driverInputSpring: Vec3Spring = Vec3Spring()
var diveSpring: Vec3Spring = Vec3Spring()
var velocitySpring = Vec3Ease(MOTION_BLEND_SECONDS)
var rotSpring = Vec3Ease(MOTION_BLEND_SECONDS, isAngle = true)
var rotDeltaSpring = Vec3Ease(MOTION_BLEND_SECONDS)
var localVelocitySpring = Vec3Ease(MOTION_BLEND_SECONDS)
var driverInputSpring = Vec3Ease(INPUT_BLEND_SECONDS)
var diveSpring = Vec3Ease(MOTION_BLEND_SECONDS)
private var initialized = false
private var dismounted = false
/** Called on dismount. Motion keeps easing on its own, only the ride orientation needs to start fresh. */
fun clear() {
prevOrientation = Matrix3f()
prevRot = Vec3.ZERO
velocitySpring = Vec3Spring()
rotSpring = Vec3Spring()
rotDeltaSpring = Vec3Spring()
localVelocitySpring = Vec3Spring()
driverInputSpring = Vec3Spring()
diveSpring = Vec3Spring()
dismounted = true
}
fun update() {
val activeRide = ride.hasControllingPassenger()
if (!activeRide) return
var currRot = Vec3.ZERO
var angDelta = Vec3.ZERO
val stiffness = 90.0
val damping = 18.0
// Update velocity first
val currentVelocity = Vec3(
ride.x - ride.xOld,
@@ -69,7 +68,7 @@ class RidingAnimationData(val ride: PokemonEntity)
ride.z - ride.zOld
)
val r = ride.ifRidingAvailableSupply(false) { behaviour, settings, state ->
val r = activeRide && ride.ifRidingAvailableSupply(false) { behaviour, settings, state ->
behaviour.shouldRoll(settings, state, ride);
}
if( r ) {
@@ -118,22 +117,39 @@ class RidingAnimationData(val ride: PokemonEntity)
val localVelocityF = Matrix3f(orientation).transpose().transform(velocityWorldF)
val localVelocity = Vec3(localVelocityF.x.toDouble(), localVelocityF.y.toDouble(), localVelocityF.z.toDouble())
localVelocitySpring.update(localVelocity, stiffness, damping)
localVelocitySpring.update(localVelocity)
// Update previous values
prevOrientation.set(currOrientation)
} else {
val currPitch = ride.xRot
// Pitch only means something while ridden
val currPitch = if (activeRide) ride.xRot else 0.0f
val currYaw = ride.yRot
val currRoll = 0.0f
currRot = Vec3(currPitch.toDouble(), currYaw.toDouble(), currRoll.toDouble())
// Start from where the pokemon already is instead of turning in from zero
if (!initialized) {
prevRot = currRot
rotSpring.snap(currRot)
initialized = true
}
// measure turning from here rather than as one big turn
if (dismounted) {
prevRot = currRot
dismounted = false
}
//Scale rotation delta by 20 to represent degree per second instead of tick
angDelta = Vec3(
(currRot.x - prevRot.x),
Mth.degreesDifference(currRot.y.toFloat(), prevRot.y.toFloat()).toDouble(),
Mth.degreesDifference(currRot.z.toFloat(), prevRot.z.toFloat()).toDouble()
).scale(20.0)
// Unridden turning comes straight from the heading ease, it's already smooth
if (!activeRide) {
angDelta = Vec3(angDelta.x, ride.heading.yaw.velocity().toDouble() * -0.5, angDelta.z)
}
/******************************************************
* Calculate ride local velocity
@@ -147,7 +163,7 @@ class RidingAnimationData(val ride: PokemonEntity)
val localVelocityF = Matrix3f(orientationNonRollable).transpose().transform(velocityWorldF)
val localVelocity = Vec3(localVelocityF.x.toDouble(), localVelocityF.y.toDouble(), localVelocityF.z.toDouble())
localVelocitySpring.update(localVelocity, stiffness, damping)
localVelocitySpring.update(localVelocity)
}
/******************************************************
@@ -159,14 +175,14 @@ class RidingAnimationData(val ride: PokemonEntity)
driver != null && driver.isShiftKeyDown -> -1.0
else -> 0.0
}
val input = if(driver != null) Vec3(driver.xxa.toDouble() * -1, vertInput, driver.zza.toDouble()) else Vec3.ZERO
driverInputSpring.update(input, stiffness * 2, damping)
rotDeltaSpring.update(angDelta, stiffness, damping)
velocitySpring.update(currentVelocity, stiffness, damping)
rotSpring.update(currRot, stiffness, damping)
val input = if(driver != null && activeRide) Vec3(driver.xxa.toDouble() * -1, vertInput, driver.zza.toDouble()) else Vec3.ZERO
driverInputSpring.update(input)
if (activeRide) rotDeltaSpring.update(angDelta) else rotDeltaSpring.follow(angDelta)
velocitySpring.update(currentVelocity)
rotSpring.update(currRot)
val diving = abs(max(sin(currRot.x.toRadians()),0f))
diveSpring.update(Vec3(0.0,diving.toDouble(),0.0), stiffness, damping)
diveSpring.update(Vec3(0.0,diving.toDouble(),0.0))
prevRot = currRot
}
}
@@ -0,0 +1,109 @@
/*
* 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.riding.util
import com.cobblemon.mod.common.util.math.InertialVals
import com.cobblemon.mod.common.util.math.smoothstep
import com.cobblemon.mod.common.util.math.smoothstepDt
import com.cobblemon.mod.common.util.math.smoothstepSolve
import net.minecraft.util.Mth
import net.minecraft.world.phys.Vec3
/**
* Eases a Vec3 towards a target with inertial easing, see SpringUtils and https://theorangeduck.com/page/inertial-easing
* When the target changes each component keeps its current velocity
*
* With [isAngle] the components are treated as degrees and always ease the short way round.
*/
class Vec3Ease(
private val blendTime: Float,
private val isAngle: Boolean = false
) {
companion object {
const val SECONDS_PER_TICK = 0.05F
}
var value: Vec3 = Vec3.ZERO
private set
private val axes = Array(3) { InertialVals() }
// Last tick's value while following a value that's already smooth, null while easing
private var followedFrom: Vec3? = null
// How far along the curve one tick is
private val step = SECONDS_PER_TICK / blendTime
private var target = Vec3.ZERO
/** Moves one tick towards [newTarget] */
fun update(newTarget: Vec3) {
followedFrom = null
val current = doubleArrayOf(value.x, value.y, value.z)
val oldGoal = doubleArrayOf(target.x, target.y, target.z)
val goal = doubleArrayOf(newTarget.x, newTarget.y, newTarget.z)
for (i in 0..2) {
val vals = axes[i]
if (goal[i] != oldGoal[i]) {
val difference = if (isAngle) Mth.wrapDegrees(goal[i] - current[i]) else goal[i] - current[i]
// Velocity along the current curve, carried into the new one so there's no sudden change
smoothstepSolve(vals, difference.toFloat(), smoothstepDt(vals.t, vals.s))
vals.offset = current[i].toFloat() - smoothstep(vals.t, vals.s)
}
vals.t += step
current[i] = (vals.offset + smoothstep(vals.t, vals.s)).toDouble()
if (isAngle) {
// Keep turning angles in range so they don't grow forever
val turns = current[i] - Mth.wrapDegrees(current[i])
if (turns != 0.0) {
vals.offset -= turns.toFloat()
current[i] -= turns
}
}
}
target = newTarget
value = Vec3(current[0], current[1], current[2])
}
/** Takes [to] as is for something already smooth. Still blends from last tick's value between ticks */
fun follow(to: Vec3) {
val from = value
snap(to)
followedFrom = from
}
/** Jumps straight to [to] with no easing */
fun snap(to: Vec3) {
followedFrom = null
val components = floatArrayOf(to.x.toFloat(), to.y.toFloat(), to.z.toFloat())
for (i in 0..2) {
axes[i].t = 1F
axes[i].s = 0F
axes[i].offset = components[i]
}
target = to
value = to
}
/** Value between last tick and this one read straight off the curve. [lookBackTick] isn't used anymore */
fun getInterpolated(partialTick: Double, @Suppress("UNUSED_PARAMETER") lookBackTick: Int = 0): Vec3 {
followedFrom?.let { from ->
if (!isAngle) return from.add(value.subtract(from).scale(partialTick))
// Short way around for angles
return Vec3(
Mth.wrapDegrees(from.x + Mth.wrapDegrees(value.x - from.x) * partialTick),
Mth.wrapDegrees(from.y + Mth.wrapDegrees(value.y - from.y) * partialTick),
Mth.wrapDegrees(from.z + Mth.wrapDegrees(value.z - from.z) * partialTick)
)
}
val back = step * (1 - partialTick.toFloat())
val x = axes[0].let { it.offset + smoothstep(it.t - back, it.s) }.toDouble()
val y = axes[1].let { it.offset + smoothstep(it.t - back, it.s) }.toDouble()
val z = axes[2].let { it.offset + smoothstep(it.t - back, it.s) }.toDouble()
// Can land just past ±180 partway through a tick
return if (isAngle) Vec3(Mth.wrapDegrees(x), Mth.wrapDegrees(y), Mth.wrapDegrees(z)) else Vec3(x, y, z)
}
}
@@ -1,68 +0,0 @@
/*
* 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.riding.util
import net.minecraft.world.phys.Vec3
import kotlin.math.min
/**
* A spring-based smoother for Vec3 values, designed for applying damped spring motion to riding data.
* Stores the previous and current values, along with a history buffer for lookback-based interpolation.
*
* @author Jackowes
* @since March 27th, 2025
*/
class Vec3Spring(
var value: Vec3 = Vec3.ZERO,
var velocity: Vec3 = Vec3.ZERO
) {
val history = ArrayDeque<Vec3>()
//Hold up to a seconds worth of values
val lookBackSize = 20
fun update(target: Vec3, stiffness: Double, damping: Double) {
//Hooke's Law f = kx where:
//f is force
//k is the spring constant or stiffness of this spring
//x is the distance from rest and in this case the difference between the most recent
//rotation delta ( the target ) and the springs current value for delta rot
val force = target.subtract(value).scale(stiffness)
//Now calculate acceleration and apply damping
val accel = force.subtract(velocity.scale(damping))
velocity = velocity.add(accel.scale(1.0/20.0))
value = value.add(velocity.scale(1.0/20.0))
history.addFirst(value)
//Adding one since if we want to check max look back point it needs something to interp with.
if( history.size > lookBackSize + 1) {
history.removeLast()
}
}
fun getInterpolated(partialTick: Double, lookBackTick: Int): Vec3 {
if (history.size < 2) {
return value // or Vec3.ZERO if you prefer
}
val tick = min(lookBackTick, history.size - 2)
var ret = Vec3.ZERO
for( i in 0..tick) {
val older = history.get(i + 1).scale(1.0)
val newer = history.get(i).scale(1.0)
ret = ret.add(older.add(newer.subtract(older).scale(partialTick)))
}
return ret.scale(1.0/(tick+1))
}
}
@@ -0,0 +1,466 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.debug.speed
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.net.messages.client.debug.SpeedTunerEntry
import com.cobblemon.mod.common.net.messages.server.debug.SetSpeedTunerSprintSpeedPacket
import com.cobblemon.mod.common.net.messages.server.debug.SetSpeedTunerWalkSpeedPacket
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.platform.events.RenderEvent
import com.cobblemon.mod.common.pokemon.Pokemon
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import java.nio.file.Files
import java.nio.file.Path
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.floor
import kotlin.math.sin
import kotlin.math.sqrt
import net.minecraft.client.Minecraft
import net.minecraft.client.multiplayer.ClientLevel
import net.minecraft.client.renderer.LevelRenderer
import net.minecraft.core.particles.ParticleTypes
import net.minecraft.ChatFormatting
import net.minecraft.network.chat.ClickEvent
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.HoverEvent
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.Mth
import net.minecraft.world.entity.ai.attributes.Attributes
import net.minecraft.world.phys.Vec3
/**
* Client side session for tuning species walk and sprint speeds. A display only pokemon runs a circle or a line at
* exactly the speed being tuned, and edits are kept in blocks per second and saved to a file for applying to the
* species data later.
*/
object SpeedTuner {
enum class Gait(val jsonKey: String) { WALK("walkSpeed"), SPRINT("sprintSpeed") }
enum class Track { CIRCLE, LINE }
// Vanilla player top speeds in blocks per second, shown and run alongside for comparison
const val PLAYER_WALK_SPEED = 4.317
const val PLAYER_SPRINT_SPEED = 5.612
// Sprint speed shown for species without one yet, matches the data script default
const val DEFAULT_SPRINT_SPEED = 5.6
// Walk speed values are multiplied by this in PokemonMoveControl
const val WALK_SPEED_SCALE = 2.5
// Friction of ordinary blocks, walk estimates assume flat normal ground
const val VANILLA_BLOCK_FRICTION = 0.6
const val TICKS_PER_SECOND = 20.0
const val DEFAULT_CIRCLE_RADIUS = 6.0
const val DEFAULT_LINE_LENGTH = 16.0
// How often the track gets outlined, and the gap between outline markers in blocks
const val TRACK_MARKER_INTERVAL_TICKS = 10
const val TRACK_MARKER_SPACING = 1.0
// Markers sit this far above the start height so they aren't inside the ground
const val MARKER_HEIGHT = 0.1
// Edits made while dragging a slider are written out at most this often
const val AUTOSAVE_INTERVAL_TICKS = 40
private const val UNITS = "blocks per second"
private val gson = GsonBuilder().setPrettyPrinting().create()
private val sessionFile: Path
get() = Minecraft.getInstance().gameDirectory.toPath().resolve("cobblemon-dev").resolve("speed_tuning_session.json")
var active = false
private set
var entries: List<SpeedTunerEntry> = emptyList()
private set
var selected: SpeedTunerEntry? = null
private set
var gait = Gait.SPRINT
set(value) {
field = value
ensureSelectionVisible()
restartRun()
}
var track = Track.CIRCLE
set(value) {
field = value
restartRun()
}
var radius = DEFAULT_CIRCLE_RADIUS
var lineLength = DEFAULT_LINE_LENGTH
var paused = false
// How fast the display pokemon plays, movement and animation together. 1 is real time
var playbackSpeed = 1.0
// Species id to edited speeds by gait json key, all in blocks per second
private val edits = mutableMapOf<ResourceLocation, MutableMap<String, Double>>()
private val reviewed = mutableSetOf<ResourceLocation>()
private var dirty = false
private var subscribed = false
private var origin = Vec3.ZERO
private var lineDirection = Vec3(0.0, 0.0, 1.0)
private var distance = 0.0
private var referenceDistance = 0.0
private var ticks = 0L
// The display pokemon's own clock in ticks, so it can run slower than the game or stand still
private var displayTime = 0.0
// Game ticks since the display pokemon last stepped, for how far between its steps it is
private var gameTicksSinceStep = 0
// Partial tick it was drawn at last, held while paused so it freezes exactly where it was
private var lastPartialTick = 0F
private var puppet: PokemonEntity? = null
fun open(entries: List<SpeedTunerEntry>) {
val player = Minecraft.getInstance().player ?: return
this.entries = entries.sortedBy { it.dex }
if (!active) {
origin = player.position()
val look = Vec3(player.lookAngle.x, 0.0, player.lookAngle.z)
lineDirection = if (look.lengthSqr() > 0) look.normalize() else Vec3(0.0, 0.0, 1.0)
load()
active = true
// The server starts from the species data, so bring it up to date with the loaded session
edits.forEach { (id, speeds) ->
speeds[Gait.SPRINT.jsonKey]?.let { sendSprintSpeed(id, it.toFloat()) }
speeds[Gait.WALK.jsonKey]?.let { sendWalkSpeed(id, walkSpeedFor(it)) }
}
}
subscribe()
ensureSelectionVisible()
Minecraft.getInstance().setScreen(SpeedTunerScreen())
}
fun stop() {
save()
active = false
puppet = null
selected = null
}
/** Species that make sense for the current gait. Only species with a sprint speed can sprint. */
fun visibleEntries(): List<SpeedTunerEntry> {
return if (gait == Gait.SPRINT) entries.filter { it.sprintSpeed != null } else entries
}
fun select(entry: SpeedTunerEntry) {
selected = entry
puppet = null
restartRun()
}
fun step(offset: Int) {
val visible = visibleEntries().ifEmpty { return }
val index = visible.indexOf(selected).coerceAtLeast(0)
select(visible[Math.floorMod(index + offset, visible.size)])
}
/** Finds a species by dex number or by name, preferring names that start with the search. */
fun search(text: String): Boolean {
val query = text.trim().lowercase().ifEmpty { return false }
val visible = visibleEntries()
val match = query.toIntOrNull()?.let { dex -> visible.firstOrNull { it.dex == dex } }
?: visible.firstOrNull { it.speciesId.path.startsWith(query) }
?: visible.firstOrNull { it.speciesId.path.contains(query) }
?: return false
select(match)
return true
}
fun speed(entry: SpeedTunerEntry, gait: Gait = this.gait): Double {
return edits[entry.speciesId]?.get(gait.jsonKey) ?: originalSpeed(entry, gait)
}
fun originalSpeed(entry: SpeedTunerEntry, gait: Gait = this.gait): Double {
return when (gait) {
Gait.SPRINT -> entry.sprintSpeed?.toDouble() ?: DEFAULT_SPRINT_SPEED
Gait.WALK -> estimatedWalkSpeed(entry)
}
}
/**
* Walk speed is still relative in the species data, so this works out what it comes to in blocks per second on
* normal ground. Walkers always use the same speed modifier so this is exact.
*/
fun estimatedWalkSpeed(entry: SpeedTunerEntry): Double {
val input = walkInputPerWalkSpeed() * entry.walkSpeed
return PokemonMoveControl.vanillaGroundTopSpeed(input, VANILLA_BLOCK_FRICTION) * TICKS_PER_SECOND
}
/** The other way round, walkSpeed that comes out at [blocksPerSecond]. Top speed grows with the square of the input */
fun walkSpeedFor(blocksPerSecond: Double): Float {
val topSpeedPerInputSquared = PokemonMoveControl.vanillaGroundTopSpeed(1.0, VANILLA_BLOCK_FRICTION)
val input = sqrt(blocksPerSecond / TICKS_PER_SECOND / topSpeedPerInputSquared)
return (input / walkInputPerWalkSpeed()).toFloat()
}
// Move control input for each unit of walkSpeed
private fun walkInputPerWalkSpeed(): Double {
return Attributes.MOVEMENT_SPEED.value().defaultValue * PokemonMoveControl.GROUND_SPEED_MODIFIER * WALK_SPEED_SCALE
}
fun referenceSpeed() = if (gait == Gait.SPRINT) PLAYER_SPRINT_SPEED else PLAYER_WALK_SPEED
fun setSpeed(value: Double) {
val entry = selected ?: return
edits.getOrPut(entry.speciesId) { mutableMapOf() }[gait.jsonKey] = value
dirty = true
when (gait) {
Gait.SPRINT -> sendSprintSpeed(entry.speciesId, value.toFloat())
Gait.WALK -> sendWalkSpeed(entry.speciesId, walkSpeedFor(value))
}
}
fun resetSpeed() {
val entry = selected ?: return
edits[entry.speciesId]?.let {
it.remove(gait.jsonKey)
if (it.isEmpty()) edits.remove(entry.speciesId)
}
dirty = true
when (gait) {
Gait.SPRINT -> sendSprintSpeed(entry.speciesId, entry.sprintSpeed)
Gait.WALK -> sendWalkSpeed(entry.speciesId, entry.walkSpeed)
}
}
private fun sendSprintSpeed(speciesId: ResourceLocation, sprintSpeed: Float?) {
SetSpeedTunerSprintSpeedPacket(speciesId, sprintSpeed).sendToServer()
}
/** Walk speeds go over in the species data's own units, converted from the tuner's blocks per second */
private fun sendWalkSpeed(speciesId: ResourceLocation, walkSpeed: Float) {
SetSpeedTunerWalkSpeedPacket(speciesId, walkSpeed).sendToServer()
}
fun isEdited(entry: SpeedTunerEntry) = edits[entry.speciesId]?.containsKey(gait.jsonKey) == true
fun isReviewed(entry: SpeedTunerEntry) = entry.speciesId in reviewed
fun toggleReviewed() {
val entry = selected ?: return
if (!reviewed.remove(entry.speciesId)) reviewed.add(entry.speciesId)
dirty = true
}
fun editedCount() = edits.size
fun reviewedCount() = reviewed.size
fun restartRun() {
distance = 0.0
referenceDistance = 0.0
}
private fun ensureSelectionVisible() {
val visible = visibleEntries()
if (selected !in visible) {
visible.firstOrNull()?.let { select(it) } ?: run { selected = null }
}
}
private fun subscribe() {
if (subscribed) return
subscribed = true
PlatformEvents.CLIENT_TICK_POST.subscribe { tick() }
PlatformEvents.RENDER.subscribe { render(it) }
PlatformEvents.CLIENT_PLAYER_LOGOUT.subscribe { if (active) stop() }
}
private fun tick() {
if (!active) return
ticks++
if (dirty && ticks % AUTOSAVE_INTERVAL_TICKS == 0L) save()
val level = Minecraft.getInstance().level ?: return
val entry = selected ?: return
val display = puppet?.takeIf { it.level() === level } ?: createPuppet(level, entry)?.also { puppet = it } ?: return
val rate = if (paused) 0.0 else playbackSpeed
val before = displayTime
displayTime += rate
// One display tick each time its clock passes a whole tick, so slow motion moves and animates less often
val steps = (floor(displayTime) - floor(before)).toInt()
repeat(steps) { stepDisplay(display, entry) }
// Hold while paused so it picks up from the same spot when resumed
if (steps > 0) gameTicksSinceStep = 0 else if (!paused) gameTicksSinceStep++
display.isPokemonSprinting = gait == Gait.SPRINT
display.entityData.set(PokemonEntity.POSE_TYPE, PoseType.WALK)
display.entityData.set(PokemonEntity.MOVING, !paused)
val reference = trackPosition(referenceDistance)
level.addParticle(ParticleTypes.ELECTRIC_SPARK, reference.x, reference.y + MARKER_HEIGHT, reference.z, 0.0, 0.0, 0.0)
if (ticks % TRACK_MARKER_INTERVAL_TICKS == 0L) outlineTrack(level)
}
/** One tick of the display pokemon's own clock, moving it along the track and ticking its animations */
private fun stepDisplay(display: PokemonEntity, entry: SpeedTunerEntry) {
distance += speed(entry) / TICKS_PER_SECOND
referenceDistance += referenceSpeed() / TICKS_PER_SECOND
display.tickCount++
val lapLength = lapLength()
// Line runs start over from the beginning, so jump without smoothing between the two ends
val wrapped = distance >= lapLength
distance %= lapLength
referenceDistance %= lapLength
display.setOldPosAndRot()
display.yBodyRotO = display.yBodyRot
display.yHeadRotO = display.yHeadRot
val position = trackPosition(distance)
// Keep yaw continuous with last tick so nothing lerping it spins round at the wrap
val yaw = display.yBodyRot + Mth.wrapDegrees(trackYaw(distance) - display.yBodyRot)
display.setPos(position.x, position.y, position.z)
display.yRot = yaw
display.yBodyRot = yaw
display.yHeadRot = yaw
if (wrapped && track == Track.LINE) {
display.setOldPosAndRot()
display.yBodyRotO = yaw
display.yHeadRotO = yaw
}
// Animation age and pose changes advance in the client delegate's tick, which normally runs from the
// entity's own tick. The display pokemon is never ticked so do it here
display.delegate.tick(display)
}
private fun render(event: RenderEvent) {
if (!active) return
val puppet = puppet ?: return
val level = Minecraft.getInstance().level ?: return
if (puppet.level() !== level) return
val camera = event.camera.position
// How far between its own steps the display pokemon is, so slow motion stays smooth and paused really stops
val partialTick = if (paused) {
lastPartialTick
} else {
val gamePartialTick = event.tickCounter.getGameTimeDeltaPartialTick(false)
((gameTicksSinceStep + gamePartialTick) * playbackSpeed).coerceAtMost(1.0).toFloat()
}
lastPartialTick = partialTick
val x = Mth.lerp(partialTick.toDouble(), puppet.xo, puppet.x) - camera.x
val y = Mth.lerp(partialTick.toDouble(), puppet.yo, puppet.y) - camera.y
val z = Mth.lerp(partialTick.toDouble(), puppet.zo, puppet.z) - camera.z
val light = LevelRenderer.getLightColor(level, puppet.blockPosition())
val buffers = Minecraft.getInstance().renderBuffers().bufferSource()
Minecraft.getInstance().entityRenderDispatcher.render(puppet, x, y, z, puppet.yRot, partialTick, event.poseStack, buffers, light)
buffers.endBatch()
}
/** Same setup as HabitatBlockRenderer, a pokemon that only exists to be drawn. */
private fun createPuppet(level: ClientLevel, entry: SpeedTunerEntry): PokemonEntity? {
val species = PokemonSpecies.getByIdentifier(entry.speciesId) ?: return null
val pokemon = Pokemon().apply {
isClient = true
this.species = species
initialize()
}
return PokemonEntity(level, pokemon).apply {
noPhysics = true
setNoGravity(true)
val start = trackPosition(distance)
setPos(start.x, start.y, start.z)
setOldPosAndRot()
}
}
private fun lapLength() = if (track == Track.CIRCLE) 2 * PI * radius else lineLength
private fun trackPosition(along: Double): Vec3 {
return when (track) {
Track.CIRCLE -> {
val angle = along / radius
origin.add(cos(angle) * radius, 0.0, sin(angle) * radius)
}
Track.LINE -> origin.add(lineDirection.scale(along))
}
}
private fun trackYaw(along: Double): Float {
val direction = when (track) {
// Tangent of the circle in the direction of travel
Track.CIRCLE -> (along / radius).let { Vec3(-sin(it), 0.0, cos(it)) }
Track.LINE -> lineDirection
}
return (Mth.atan2(direction.z, direction.x) * Mth.RAD_TO_DEG).toFloat() - 90F
}
private fun outlineTrack(level: ClientLevel) {
val markers = (lapLength() / TRACK_MARKER_SPACING).toInt()
for (i in 0..markers) {
val marker = trackPosition(i * TRACK_MARKER_SPACING)
level.addParticle(ParticleTypes.COMPOSTER, marker.x, marker.y + MARKER_HEIGHT, marker.z, 0.0, 0.0, 0.0)
}
}
private fun toJson(): JsonObject {
val species = JsonObject()
edits.toSortedMap(compareBy { it.toString() }).forEach { (id, speeds) ->
species.add(id.toString(), JsonObject().also { speedsJson -> speeds.forEach { (key, value) -> speedsJson.addProperty(key, value) } })
}
return JsonObject().apply {
addProperty("units", UNITS)
add("species", species)
add("reviewed", JsonArray().also { array -> reviewed.map { it.toString() }.sorted().forEach { array.add(it) } })
}
}
fun save() {
runCatching {
Files.createDirectories(sessionFile.parent)
Files.writeString(sessionFile, gson.toJson(toJson()))
dirty = false
}.onFailure { Cobblemon.LOGGER.error("Couldn't save the speed tuning session", it) }
}
/** Writes a timestamped copy of the session next to it and posts links to it in chat. */
fun export() {
save()
val stamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"))
val file = sessionFile.resolveSibling("speed_tuning_$stamp.json").toAbsolutePath()
runCatching {
Files.writeString(file, gson.toJson(toJson()))
val message = Component.literal("Speed tuning exported: ")
.append(chatLink("[open file]", ClickEvent.Action.OPEN_FILE, file.toString()))
.append(" ")
.append(chatLink("[open folder]", ClickEvent.Action.OPEN_FILE, file.parent.toString()))
.append(" ")
.append(chatLink("[copy path]", ClickEvent.Action.COPY_TO_CLIPBOARD, file.toString()))
Minecraft.getInstance().player?.displayClientMessage(message, false)
}.onFailure { Cobblemon.LOGGER.error("Couldn't export the speed tuning session", it) }
}
private fun chatLink(label: String, action: ClickEvent.Action, value: String): Component {
return Component.literal(label).withStyle { style ->
style.withColor(ChatFormatting.AQUA)
.withUnderlined(true)
.withClickEvent(ClickEvent(action, value))
.withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal(value)))
}
}
private fun load() {
edits.clear()
reviewed.clear()
if (!Files.exists(sessionFile)) return
runCatching {
val json = JsonParser.parseString(Files.readString(sessionFile)).asJsonObject
json.getAsJsonObject("species")?.entrySet()?.forEach { (id, speeds) ->
val map = edits.getOrPut(ResourceLocation.parse(id)) { mutableMapOf() }
speeds.asJsonObject.entrySet().forEach { (key, value) -> map[key] = value.asDouble }
}
json.getAsJsonArray("reviewed")?.forEach { reviewed.add(ResourceLocation.parse(it.asString)) }
}.onFailure { Cobblemon.LOGGER.error("Couldn't load the speed tuning session", it) }
}
}
@@ -0,0 +1,240 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.debug.speed
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.text.text
import java.awt.Color
import java.util.Locale
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractSliderButton
import net.minecraft.client.gui.components.Button
import net.minecraft.client.gui.components.EditBox
import net.minecraft.client.gui.screens.Screen
import net.minecraft.network.chat.Component
import net.minecraft.util.Mth
import org.lwjgl.glfw.GLFW
/**
* Panel for [SpeedTuner] down the left side of the screen. Doesn't pause and leaves the world visible so the display
* pokemon can be watched from anywhere. Closing it leaves the session running, the debug keybind brings it back.
*/
class SpeedTunerScreen : Screen("Speed Tuner".text()) {
companion object {
const val PANEL_WIDTH = 180
const val MARGIN = 6
const val ROW_HEIGHT = 20
const val ROW_GAP = 4
const val TEXT_LINE_HEIGHT = 11
// Typed speeds can go up to this, the slider only covers the usual range
const val MAX_SPEED = 20.0
const val SLIDER_MAX_SPEED = 8.0
// Blocks per second the - and + buttons change the speed by
const val SPEED_STEP = 0.05
val PLAYBACK_SPEEDS = listOf(1.0, 0.5, 0.25, 0.1)
const val MIN_RADIUS = 2.0
const val MAX_RADIUS = 24.0
const val MIN_LINE_LENGTH = 4.0
const val MAX_LINE_LENGTH = 48.0
val PANEL_COLOUR = Color(0, 0, 0, 150).rgb
val EDITED_COLOUR = Color(255, 200, 80).rgb
val TEXT_COLOUR = Color(255, 255, 255).rgb
}
private lateinit var searchBox: EditBox
private lateinit var speedBox: EditBox
private lateinit var speedSlider: DecimalSlider
private var infoTop = 0
override fun isPauseScreen() = false
override fun init() {
super.init()
val fullWidth = PANEL_WIDTH - MARGIN * 2
val halfWidth = (fullWidth - ROW_GAP) / 2
var y = MARGIN
fun nextRow(): Int = y.also { y += ROW_HEIGHT + ROW_GAP }
nextRow().let { row ->
addRenderableWidget(Button.builder("<".text()) { step(-1) }.bounds(MARGIN, row, ROW_HEIGHT, ROW_HEIGHT).build())
addRenderableWidget(Button.builder(">".text()) { step(1) }.bounds(PANEL_WIDTH - MARGIN - ROW_HEIGHT, row, ROW_HEIGHT, ROW_HEIGHT).build())
}
searchBox = addRenderableWidget(EditBox(font, MARGIN, nextRow(), fullWidth, ROW_HEIGHT, "Search".text())).apply {
setHint("Dex number or name, Enter".text())
}
nextRow().let { row ->
addRenderableWidget(Button.builder("Gait: ${SpeedTuner.gait.name.lowercase()}".text()) {
SpeedTuner.gait = if (SpeedTuner.gait == SpeedTuner.Gait.SPRINT) SpeedTuner.Gait.WALK else SpeedTuner.Gait.SPRINT
rebuildWidgets()
}.bounds(MARGIN, row, halfWidth, ROW_HEIGHT).build())
addRenderableWidget(Button.builder("Track: ${SpeedTuner.track.name.lowercase()}".text()) {
SpeedTuner.track = if (SpeedTuner.track == SpeedTuner.Track.CIRCLE) SpeedTuner.Track.LINE else SpeedTuner.Track.CIRCLE
rebuildWidgets()
}.bounds(MARGIN + halfWidth + ROW_GAP, row, halfWidth, ROW_HEIGHT).build())
}
val entry = SpeedTuner.selected
nextRow().let { row ->
addRenderableWidget(Button.builder("-".text()) { nudgeSpeed(-SPEED_STEP) }.bounds(MARGIN, row, ROW_HEIGHT, ROW_HEIGHT).build())
speedSlider = addRenderableWidget(
DecimalSlider(MARGIN + ROW_HEIGHT + ROW_GAP, row, fullWidth - 2 * (ROW_HEIGHT + ROW_GAP), "Speed", 0.0, SLIDER_MAX_SPEED, entry?.let { SpeedTuner.speed(it) } ?: 0.0) {
SpeedTuner.setSpeed(it)
speedBox.value = format(it)
}
)
addRenderableWidget(Button.builder("+".text()) { nudgeSpeed(SPEED_STEP) }.bounds(PANEL_WIDTH - MARGIN - ROW_HEIGHT, row, ROW_HEIGHT, ROW_HEIGHT).build())
}
nextRow().let { row ->
speedBox = addRenderableWidget(EditBox(font, MARGIN, row, halfWidth, ROW_HEIGHT, "Speed".text())).apply {
value = entry?.let { format(SpeedTuner.speed(it)) } ?: ""
}
addRenderableWidget(Button.builder("Set".text()) { applyTypedSpeed() }.bounds(MARGIN + halfWidth + ROW_GAP, row, halfWidth, ROW_HEIGHT).build())
}
if (SpeedTuner.track == SpeedTuner.Track.CIRCLE) {
addRenderableWidget(DecimalSlider(MARGIN, nextRow(), fullWidth, "Radius", MIN_RADIUS, MAX_RADIUS, SpeedTuner.radius) {
SpeedTuner.radius = it
})
} else {
addRenderableWidget(DecimalSlider(MARGIN, nextRow(), fullWidth, "Length", MIN_LINE_LENGTH, MAX_LINE_LENGTH, SpeedTuner.lineLength) {
SpeedTuner.lineLength = it
})
}
nextRow().let { row ->
addRenderableWidget(Button.builder((if (SpeedTuner.paused) "Resume" else "Pause").text()) {
SpeedTuner.paused = !SpeedTuner.paused
rebuildWidgets()
}.bounds(MARGIN, row, halfWidth, ROW_HEIGHT).build())
addRenderableWidget(Button.builder("Reset speed".text()) {
SpeedTuner.resetSpeed()
rebuildWidgets()
}.bounds(MARGIN + halfWidth + ROW_GAP, row, halfWidth, ROW_HEIGHT).build())
}
addRenderableWidget(Button.builder("Playback: ${format(SpeedTuner.playbackSpeed)}x".text()) {
val next = (PLAYBACK_SPEEDS.indexOf(SpeedTuner.playbackSpeed) + 1) % PLAYBACK_SPEEDS.size
SpeedTuner.playbackSpeed = PLAYBACK_SPEEDS[next]
rebuildWidgets()
}.bounds(MARGIN, nextRow(), fullWidth, ROW_HEIGHT).build())
nextRow().let { row ->
val reviewed = entry?.let { SpeedTuner.isReviewed(it) } == true
addRenderableWidget(Button.builder((if (reviewed) "Reviewed ✔" else "Mark reviewed").text()) {
SpeedTuner.toggleReviewed()
rebuildWidgets()
}.bounds(MARGIN, row, halfWidth, ROW_HEIGHT).build())
addRenderableWidget(Button.builder("Export".text()) { SpeedTuner.export() }
.bounds(MARGIN + halfWidth + ROW_GAP, row, halfWidth, ROW_HEIGHT).build())
}
addRenderableWidget(Button.builder("End session".text()) {
SpeedTuner.stop()
onClose()
}.bounds(MARGIN, nextRow(), fullWidth, ROW_HEIGHT).build())
infoTop = y
}
private fun step(offset: Int) {
SpeedTuner.step(offset)
rebuildWidgets()
}
private fun nudgeSpeed(change: Double) {
val entry = SpeedTuner.selected ?: return
SpeedTuner.setSpeed((SpeedTuner.speed(entry) + change).coerceIn(0.0, MAX_SPEED))
rebuildWidgets()
}
private fun applyTypedSpeed() {
val typed = speedBox.value.toDoubleOrNull() ?: return
SpeedTuner.setSpeed(typed.coerceIn(0.0, MAX_SPEED))
rebuildWidgets()
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) {
if (searchBox.isFocused) {
if (SpeedTuner.search(searchBox.value)) rebuildWidgets()
return true
}
if (speedBox.isFocused) {
applyTypedSpeed()
return true
}
}
return super.keyPressed(keyCode, scanCode, modifiers)
}
override fun onClose() {
SpeedTuner.save()
super.onClose()
}
// Only darken the panel so the world stays visible
override fun renderBackground(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
guiGraphics.fill(0, 0, PANEL_WIDTH, height, PANEL_COLOUR)
}
override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
super.render(guiGraphics, mouseX, mouseY, partialTick)
val entry = SpeedTuner.selected
val title = entry?.let { selected ->
val name = PokemonSpecies.getByIdentifier(selected.speciesId)?.translatedName ?: selected.speciesId.path.text()
Component.literal("#${selected.dex} ").append(name)
} ?: "No species".text()
guiGraphics.drawCenteredString(font, title, PANEL_WIDTH / 2, MARGIN + (ROW_HEIGHT - font.lineHeight) / 2, TEXT_COLOUR)
var y = infoTop
fun line(text: String, colour: Int = TEXT_COLOUR) {
guiGraphics.drawString(font, text, MARGIN, y, colour)
y += TEXT_LINE_HEIGHT
}
if (entry != null) {
val speed = SpeedTuner.speed(entry)
line("Speed: ${format(speed)} blocks/s", if (SpeedTuner.isEdited(entry)) EDITED_COLOUR else TEXT_COLOUR)
line("Original: ${format(SpeedTuner.originalSpeed(entry))} blocks/s")
if (SpeedTuner.gait == SpeedTuner.Gait.WALK) {
line("(estimated, raw walkSpeed ${format(entry.walkSpeed.toDouble())})")
}
}
line("Sparks: player ${SpeedTuner.gait.name.lowercase()} ${format(SpeedTuner.referenceSpeed())} blocks/s")
line("Edited: ${SpeedTuner.editedCount()} Reviewed: ${SpeedTuner.reviewedCount()}")
line("Species: ${SpeedTuner.visibleEntries().size}")
}
private fun format(value: Double) = String.format(Locale.ROOT, "%.2f", value)
/** Slider over a decimal range that reports every change. */
class DecimalSlider(
x: Int,
y: Int,
width: Int,
private val label: String,
private val min: Double,
private val max: Double,
initial: Double,
private val onChange: (Double) -> Unit
) : AbstractSliderButton(x, y, width, ROW_HEIGHT, Component.empty(), 0.0) {
private val current: Double
get() = Mth.lerp(value, min, max)
init {
value = ((initial - min) / (max - min)).coerceIn(0.0, 1.0)
updateMessage()
}
override fun updateMessage() {
message = "$label: ${String.format(Locale.ROOT, "%.2f", current)}".text()
}
override fun applyValue() {
onChange(current)
}
}
}
@@ -11,11 +11,14 @@ package com.cobblemon.mod.common.client.keybind.keybinds
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.keybind.CobblemonKeyBinding
import com.cobblemon.mod.common.client.gui.debug.speed.SpeedTuner
import com.cobblemon.mod.common.client.gui.debug.speed.SpeedTunerScreen
import com.cobblemon.mod.common.client.keybind.KeybindCategories
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.debug.RequestOpenRidingStatsDebugGUIPacket
import com.cobblemon.mod.common.net.messages.server.debug.RequestOpenSpeedTunerPacket
import com.mojang.blaze3d.platform.InputConstants
import net.minecraft.client.Minecraft
import net.minecraft.network.chat.Component
@@ -30,6 +33,7 @@ object DebugKeybindings {
TranslateDownKeybinding(),
PrintModelSettingsKeybinding(),
ToggleRidingStatsDebugGUIKeybinding(),
ToggleSpeedTunerKeybinding(),
)
class ScaleUpKeybinding: CobblemonKeyBinding(
@@ -163,4 +167,20 @@ object DebugKeybindings {
}
}
}
class ToggleSpeedTunerKeybinding : CobblemonKeyBinding(
"key.cobblemon.togglespeedtuner",
InputConstants.Type.KEYSYM,
InputConstants.KEY_APOSTROPHE,
KeybindCategories.COBBLEMON_DEBUG_CATEGORY
) {
override fun onPress() {
// A running session just needs its panel back, otherwise the server checks permission and sends speeds
if (SpeedTuner.active) {
Minecraft.getInstance().setScreen(SpeedTunerScreen())
} else {
RequestOpenSpeedTunerPacket().sendToServer()
}
}
}
}
@@ -0,0 +1,20 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.debug
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.gui.debug.speed.SpeedTuner
import com.cobblemon.mod.common.net.messages.client.debug.OpenSpeedTunerPacket
import net.minecraft.client.Minecraft
object OpenSpeedTunerHandler : ClientNetworkPacketHandler<OpenSpeedTunerPacket> {
override fun handle(packet: OpenSpeedTunerPacket, client: Minecraft) {
SpeedTuner.open(packet.entries)
}
}
@@ -48,6 +48,7 @@ import com.mojang.blaze3d.vertex.DefaultVertexFormat
import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.blaze3d.vertex.VertexConsumer
import com.mojang.blaze3d.vertex.VertexFormat
import com.cobblemon.mod.common.client.render.pokemon.ModelPitch
import com.mojang.math.Axis
import net.minecraft.Util
import net.minecraft.client.model.geom.ModelPart
@@ -776,7 +777,14 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
matrixStack.mulPose(transformationMatrix)
} else {
yRot = Mth.lerp(state.getPartialTicks(), entity.yBodyRotO, entity.yBodyRot)
yRot = 180f - Mth.wrapDegrees(yRot)
val tilt = ModelPitch.tiltDegrees(entity, state.getPartialTicks())
if (tilt != 0F) {
// Same as the model tilt in PokemonRenderer
matrixStack.mulPose(ModelPitch.aroundCentre(entity, ModelPitch.orientation(yRot, tilt)))
yRot = 0F
} else {
yRot = 180f - Mth.wrapDegrees(yRot)
}
}
} else if (entity is EmptyPokeBallEntity) {
scale = 0.7F
@@ -19,7 +19,7 @@ import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.riding.util.RidingAnimationData
import com.cobblemon.mod.common.api.riding.util.DynamicAnimationData
import com.cobblemon.mod.common.api.scheduling.Schedulable
import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.client.particle.BedrockParticleOptionsRepository
@@ -141,7 +141,7 @@ abstract class PosableState : Schedulable {
*/
val renderMarkers = mutableSetOf<String>()
/** The MoLang functions that expose current [RidingAnimationData] data.
/** The MoLang functions that expose current [DynamicAnimationData] data.
* They are all divided by a reference value and coerece into a float range useful in animation
*/
val ridingFunctions = QueryStruct(hashMapOf())
@@ -10,23 +10,20 @@ package com.cobblemon.mod.common.client.render.models.blockbench.animation
import com.cobblemon.mod.common.client.render.models.blockbench.PosableModel
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
import com.cobblemon.mod.common.client.render.models.blockbench.addRotation
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Bone
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
import com.cobblemon.mod.common.util.math.geometry.toDegrees
import com.cobblemon.mod.common.util.math.geometry.toRadians
import net.minecraft.util.Mth
/**
* Animation used to tilt the targeted bone of the entity by a pitch that is proportionate
* to the direction the entity is moving. This is used to make birds and fish tilt upwards
* or downwards based on their motion.
* or downwards based on their motion. The pitch comes from the eased heading locomotion travels on.
*
* @author Hiroku
* @since August 9th, 2025
*/
class PitchTiltAnimation(
val bone: Bone,
// Not used anymore since the heading is already eased, kept so existing posers still load
val maxChangePerTick: Float = 1F,
val minPitch: Float = -45F,
val maxPitch: Float = 45F,
@@ -35,6 +32,8 @@ class PitchTiltAnimation(
const val PREVIOUS_ANGLE = "previous_pitch_tilt_angle"
const val CORRECTED_ANGLE = "corrected_pitch_tilt_angle"
const val PITCHED_TILT = "has_pitched_tilt"
// How much of the tilt has been applied this render, so poses blending together add up to one full tilt
const val APPLIED_WEIGHT = "applied_pitch_tilt_weight"
}
override fun setupAnim(
@@ -48,36 +47,7 @@ class PitchTiltAnimation(
headPitch: Float,
intensity: Float
) {
val entity = context.entity ?: return
if (entity.passengers.isNotEmpty()) {
return
}
val delta = entity.deltaMovement
val vertical = delta.y
val horizontal = delta.horizontalDistance()
// It should be the angle in degrees that becomes higher as the entity's motion becomes more upwards, and negative as the entity's motion becomes more downwards
val angleOfMotion = if (horizontal == 0.0) {
when {
vertical > 0 -> maxPitch
vertical < 0 -> minPitch
else -> 0F
}
} else {
Mth.atan2(vertical, horizontal).toDegrees()
}.coerceIn(minPitch, maxPitch) * -1
val lastAngle = state.numbers[PREVIOUS_ANGLE] ?: 0F
val change = (angleOfMotion - lastAngle).coerceIn(-maxChangePerTick, maxChangePerTick)
if (PITCHED_TILT !in state.renderMarkers) {
state.numbers.remove(CORRECTED_ANGLE)
val correctedAngle = (lastAngle + change) * intensity
bone.addRotation(0, correctedAngle.toRadians())
state.numbers[PREVIOUS_ANGLE] = correctedAngle
} else {
// It was already pitched, which means the body bone already has a rotation, so only rotate it a pinch more.
bone.addRotation(0, change.toRadians() * intensity)
state.numbers[PREVIOUS_ANGLE] = lastAngle + change * intensity
}
state.renderMarkers += PITCHED_TILT
// The whole model is tilted by ModelPitch now so this does nothing\
//TODO: remove posers with this anim if we like ModelPitch
}
}
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.pokemon
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.math.geometry.toRadians
import net.minecraft.util.Mth
import org.joml.Matrix4f
import org.joml.Quaternionf
import org.joml.Vector3f
/**
* Tilts an unridden pokemon's whole model with how it's climbing or diving, the same way ridden flyers are turned
* by their orientation. Walkers always travel level so they never tilt.
*/
object ModelPitch {
// Most the model tilts either way
const val MAX_TILT_DEGREES = 45F
/** Minecraft pitch to tilt by. Only as much as travel is horizontal */
fun tiltDegrees(entity: PokemonEntity, partialTick: Float): Float {
if (entity.passengers.isNotEmpty()) return 0F
val pitch = entity.heading.pitch.valueAt(partialTick)
return (pitch * Mth.cos(pitch.toRadians())).coerceIn(-MAX_TILT_DEGREES, MAX_TILT_DEGREES)
}
/** Built the same way as OrientationController.rotate so it lines up with ridden orientation */
fun orientation(yaw: Float, tilt: Float): Quaternionf {
return Quaternionf().rotateY(-(yaw - 180F).toRadians()).rotateX(-tilt.toRadians())
}
/** Pivots orientation around the middle of the hitbox */
fun aroundCentre(entity: PokemonEntity, orientation: Quaternionf): Matrix4f {
val centre = Vector3f(0F, entity.bbHeight / 2, 0F)
return Matrix4f().translate(centre).rotate(orientation).translate(centre.negate(Vector3f()))
}
}
@@ -149,7 +149,19 @@ class PokemonRenderer(
if (entity.passengers.isNotEmpty()) {
renderRiding(entity, entityYaw, partialTicks, poseMatrix, buffer, packedLight)
} else {
super.render(entity, entityYaw, partialTicks, poseMatrix, buffer, packedLight)
val tilt = ModelPitch.tiltDegrees(entity, partialTicks)
if (tilt != 0F) {
poseMatrix.pushPose()
val yaw = Mth.rotLerp(partialTicks, entity.yBodyRotO, entity.yBodyRot)
val transformationMatrix = ModelPitch.aroundCentre(entity, ModelPitch.orientation(yaw, tilt))
// Pre-undo yaw rotation, the orientation already has it
transformationMatrix.rotate(Axis.YP.rotationDegrees(yaw + 180F))
poseMatrix.last().pose().mul(transformationMatrix)
super.render(entity, entityYaw, partialTicks, poseMatrix, buffer, packedLight)
poseMatrix.popPose()
} else {
super.render(entity, entityYaw, partialTicks, poseMatrix, buffer, packedLight)
}
}
// Call rendering for alpha eye trail
@@ -10,24 +10,41 @@ package com.cobblemon.mod.common.entity.ai
import net.minecraft.core.BlockPos
import net.minecraft.world.entity.ai.behavior.BlockPosTracker
import net.minecraft.world.entity.ai.behavior.PositionTracker
import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.level.pathfinder.PathType
/**
* This is a trick to allow us to instruct the node-maker to only consider specific node types,
* even if the entity can, in a pinch, use more.
* even if the entity can, in a pinch, use more. Can also have the entity sprint while it is further
* than [sprintDistance] from the target, and count as arrived once within [stopDistance]. Both are measured
* from the edge of its hitbox so big mons keep the same gap as small ones. With [sprintWithTarget] it also sprints
* while the entity it's following is sprinting.
*
* @author Hiroku
* @since March 24th, 2025
*/
class CobblemonWalkTarget(
pos: BlockPos,
target: PositionTracker,
speedModifier: Float,
completionRange: Int,
val nodeTypeFilter: (PathType) -> Boolean = { true },
val destinationNodeTypeFilter: (PathType) -> Boolean = { true }
val destinationNodeTypeFilter: (PathType) -> Boolean = { true },
val sprintDistance: Float? = null,
val stopDistance: Float? = null,
val sprintWithTarget: Boolean = false
) : WalkTarget(
BlockPosTracker(pos),
target,
speedModifier,
completionRange
)
) {
constructor(
pos: BlockPos,
speedModifier: Float,
completionRange: Int,
nodeTypeFilter: (PathType) -> Boolean = { true },
destinationNodeTypeFilter: (PathType) -> Boolean = { true },
sprintDistance: Float? = null,
stopDistance: Float? = null
) : this(BlockPosTracker(pos), speedModifier, completionRange, nodeTypeFilter, destinationNodeTypeFilter, sprintDistance, stopDistance)
}
@@ -38,6 +38,7 @@ class FollowWalkTargetTask(
private var path: Path? = null
private var lookTargetPos: BlockPos? = null
private var speed = 0f
private var sprinting = false
override fun checkExtraStartConditions(level: ServerLevel, entity: PathfinderMob): Boolean {
if (this.pathUpdateCountdownTicks > 0) {
@@ -87,14 +88,17 @@ class FollowWalkTargetTask(
entity.brain.eraseMemory(MemoryModuleType.WALK_TARGET)
entity.brain.eraseMemory(MemoryModuleType.PATH)
this.path = null
this.sprinting = false
}
override fun start(arg: ServerLevel, entity: PathfinderMob, time: Long) {
entity.brain.setMemory(MemoryModuleType.PATH, this.path)
entity.navigation.moveTo(this.path, speed.toDouble())
updateSprinting(entity)
}
override fun tick(world: ServerLevel, entity: PathfinderMob, l: Long) {
updateSprinting(entity)
val path = entity.navigation.path
val brain = entity.brain
if (this.path !== path) {
@@ -196,11 +200,42 @@ class FollowWalkTargetTask(
return path
}
/**
* Sprints while far from the walk target, stopping a bit closer than it started so it doesn't flicker.
* Can also sprint while the entity being followed is sprinting.
*/
private fun updateSprinting(entity: PathfinderMob) {
val navigation = entity.navigation as? OmniPathNavigation ?: return
val walkTarget = entity.brain.getMemory(MemoryModuleType.WALK_TARGET).orElse(null) as? CobblemonWalkTarget
val sprintDistance = walkTarget?.sprintDistance
sprinting = if (walkTarget == null || sprintDistance == null) {
false
} else {
val gap = edgeGap(entity, walkTarget)
val targetSprinting = walkTarget.sprintWithTarget && (walkTarget.target as? EntityTracker)?.entity?.isSprinting == true
targetSprinting || gap > if (sprinting) sprintDistance * SPRINT_STOP_FRACTION else sprintDistance
}
navigation.sprinting = sprinting
}
private fun hasReached(entity: PathfinderMob, walkTarget: WalkTarget): Boolean {
val stopDistance = (walkTarget as? CobblemonWalkTarget)?.stopDistance
if (stopDistance != null) {
return edgeGap(entity, walkTarget) <= stopDistance
}
return walkTarget.target.currentBlockPosition().distManhattan(entity.blockPosition()) <= walkTarget.closeEnoughDist
}
/** Straight line distance between hitbox edges, or to the target position when it isn't an entity. */
private fun edgeGap(entity: PathfinderMob, walkTarget: WalkTarget): Double {
val targetRadius = (walkTarget.target as? EntityTracker)?.entity?.let { it.bbWidth / 2.0 } ?: 0.0
return walkTarget.target.currentPosition().distanceTo(entity.position()) - entity.bbWidth / 2.0 - targetRadius
}
companion object {
// Once sprinting, keep going until this fraction of the sprint distance
const val SPRINT_STOP_FRACTION = 0.5F
private fun isTargetSpectator(target: WalkTarget): Boolean {
val lookTarget = target.target
return if (lookTarget is EntityTracker) {
@@ -16,16 +16,18 @@ import com.cobblemon.mod.common.util.resolveFloat
import com.cobblemon.mod.common.util.resolveInt
import com.cobblemon.mod.common.util.withQueryValue
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.EntityTracker
import net.minecraft.world.entity.ai.behavior.OneShot
import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder
import net.minecraft.world.entity.ai.behavior.declarative.Trigger
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.WalkTarget
object MoveToAttackTargetTask {
fun create(
speedMultiplier: Expression = "0.5".asExpression(),
closeEnoughDistance: Expression = "0".asExpression()
closeEnoughDistance: Expression = "0".asExpression(),
// Gap between the pokemon's hitbox and the target's before it sprints. At 0 it sprints until they touch
sprintDistance: Expression = "0.0".asExpression()
): OneShot<LivingEntity> = BehaviorBuilder.create {
it.group(
it.present(MemoryModuleType.ATTACK_TARGET),
@@ -35,12 +37,22 @@ object MoveToAttackTargetTask {
mainThreadRuntime.withQueryValue("entity", entity.asMostSpecificMoLangValue())
val speedMultiplier = mainThreadRuntime.resolveFloat(speedMultiplier)
val closeEnoughDistance = mainThreadRuntime.resolveInt(closeEnoughDistance)
val sprintDistance = mainThreadRuntime.resolveFloat(sprintDistance)
val attackTarget = it.get(attackTarget)
val position = attackTarget.position()
val walkTarget = it.tryGet(walkTarget).orElse(null)
if (walkTarget == null || walkTarget.target.currentPosition().distanceToSqr(position) > closeEnoughDistance) {
entity.brain.setMemory(MemoryModuleType.WALK_TARGET, WalkTarget(attackTarget, speedMultiplier, closeEnoughDistance))
entity.brain.setMemory(
MemoryModuleType.WALK_TARGET,
CobblemonWalkTarget(
target = EntityTracker(attackTarget, false),
speedModifier = speedMultiplier,
completionRange = closeEnoughDistance,
sprintDistance = sprintDistance,
stopDistance = 0F
)
)
true
} else {
false
@@ -31,10 +31,16 @@ import net.minecraft.world.level.pathfinder.Path
import net.minecraft.world.level.pathfinder.PathComputationType
import net.minecraft.world.level.pathfinder.PathFinder
import net.minecraft.world.level.pathfinder.PathType
import net.minecraft.world.level.pathfinder.WalkNodeEvaluator
import net.minecraft.world.phys.Vec3
import com.cobblemon.mod.common.util.math.arriveSpeed
import com.cobblemon.mod.common.util.math.slowDownSpeed
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.acos
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
/**
* A navigator designed to work with the [OmniPathNodeMaker], allowing a path that can cross land, water, and air
@@ -48,6 +54,12 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
val pather = entity as OmniPathingEntity
var cachedCurrentNode: Node? = null
// Per node steering results for the path they were worked out for, by node index
private var cachedSteeringPath: Path? = null
private val tightCorners = mutableMapOf<Int, Boolean>()
private val narrowNodes = mutableMapOf<Int, Boolean>()
private val stepNodes = mutableMapOf<Int, Boolean>()
private val steerPositions = mutableMapOf<Int, Vec3>()
var currentNodeDistance = 0F
data class NavigationContext(
@@ -61,9 +73,35 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
)
var navigationContext = NavigationContext()
// How far a steered mover travels while finishing a turn, given by its move control each tick
private var turnDistance = 0.0
// Whether whatever started the current path wants it sprinted. Cleared on every new path and on stop
var sprinting = false
/**
* Where a steered mover should head this tick. [isFinal] means it should slow down and stop there.
* [maxSpeed] is how fast it can go right now while still slowing in time for the corners ahead.
*/
data class SteerTarget(
val position: Vec3,
val isFinal: Boolean,
val maxSpeed: Double,
val narrowAhead: Boolean = false,
// Which way the path carries on from a node the target is waiting on, null when it isn't waiting on one
val afterDirection: Vec3? = null
)
companion object {
val verticallyPreciseNodeTypes = setOf(PathType.WATER, PathType.OPEN, PathType.LAVA)
// Corners sharper than this start slowing the mob down
const val CORNER_FREE_ANGLE_DEGREES = 45.0
// Fraction of top speed kept going round a full reversal
const val CORNER_MIN_SPEED_FRACTION = 0.5
// Fraction of top speed kept going round a corner that can't be cut
const val TIGHT_CORNER_SPEED_FRACTION = 0.3
// Largest distance from a tight corner that still counts as reaching it, so big mobs can't turn early
const val TIGHT_CORNER_REACH_DISTANCE = 0.5
}
override fun createPathFinder(range: Int): PathFinder {
@@ -137,8 +175,11 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
&& f < this.maxDistanceToWaypoint.toDouble()
&& e < (if (currentNode.type in verticallyPreciseNodeTypes && (mob.isUnderWater || pather.isFlying())) maxDistanceToWaypoint else 1.0).toDouble()
val reachedTightCorner = !mustTouch(path!!, path!!.nextNodeIndex) ||
horizontalDistance(mob.position(), steerPosition(path!!, path!!.nextNodeIndex)) <= min(maxDistanceToWaypoint.toDouble(), TIGHT_CORNER_REACH_DISTANCE)
// Corner cutting is commented out because it makes pokemon and NPCs 'cut' the corner and fall into water or lava
if (closeEnough) {// || mob.navigation.canCutCorner(path!!.nextNode.type) && shouldTargetNextNodeInDirection(vec3d)) {
if ((closeEnough && reachedTightCorner) || hasPassedNextNode(path!!)) {// || mob.navigation.canCutCorner(path!!.nextNode.type) && shouldTargetNextNodeInDirection(vec3d)) {
path!!.advance()
if (path!!.isDone) {
path = null
@@ -162,6 +203,212 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
doStuckDetection(vec3d)
}
/**
* Pure pursuit path following. The target is a point a lookahead distance further along the path than the
* mob's closest point on it, so the mob eases back onto the path and rounds corners instead of zigzagging.
* Also scans ahead for corners and the path end to work out how fast the mob can go and still slow in time.
*/
fun getSteerTarget(maxSpeed: Double, speedBlendTicks: Double, turnDistance: Double): SteerTarget? {
this.turnDistance = turnDistance
val currentPath = path?.takeUnless { it.isDone } ?: return null
val index = currentPath.nextNodeIndex
val lastIndex = currentPath.nodeCount - 1
if (lastIndex == 0) return SteerTarget(steerPosition(currentPath, 0), true, maxSpeed)
// A fresh path starts next to the mob, so follow its first segment rather than seeking node 0 itself
val segmentEnd = max(index, 1)
val lookAhead = lookAheadDistance()
// Far enough to see anything we'd need to start slowing for, stopping from top speed covers this much
val scanDistance = max(lookAhead, maxSpeed * speedBlendTicks / 2)
var point = closestPointOnSegment(mob.position(), steerPosition(currentPath, segmentEnd - 1), steerPosition(currentPath, segmentEnd))
var distance = 0.0
var carrot: Vec3? = null
var speedLimit = maxSpeed
var narrowAhead = isNarrow(currentPath, segmentEnd - 1)
var afterDirection: Vec3? = null
for (i in segmentEnd..lastIndex) {
narrowAhead = narrowAhead || isNarrow(currentPath, i)
val node = steerPosition(currentPath, i)
val segmentLength = node.distanceTo(point)
if (carrot == null && distance + segmentLength >= lookAhead) {
carrot = point.add(node.subtract(point).normalize().scale(lookAhead - distance))
}
distance += segmentLength
if (i == lastIndex) {
// Brake to a stop on the end
speedLimit = min(speedLimit, arriveSpeed(distance, maxSpeed, speedBlendTicks))
break
}
if (distance > scanDistance) break
val tight = mustTouch(currentPath, i)
val cornerSpeed = maxSpeed * if (tight) TIGHT_CORNER_SPEED_FRACTION else cornerSpeedFraction(currentPath, i)
speedLimit = min(speedLimit, slowDownSpeed(distance, cornerSpeed, speedBlendTicks))
// Rounding this corner would leave the path, so the carrot waits on it until it's reached
if (tight && carrot == null) {
carrot = node
afterDirection = steerPosition(currentPath, i + 1).subtract(node)
}
point = node
}
// Lookahead ran off the end so arrive on the last node
return if (carrot == null) {
SteerTarget(steerPosition(currentPath, lastIndex), true, speedLimit, narrowAhead)
} else {
SteerTarget(carrot, false, speedLimit, narrowAhead, afterDirection)
}
}
/**
* Aim at least as far ahead as the mob travels while finishing a turn otherwise it reaches the path before the
* correction is done and crosses over it.
*/
private fun lookAheadDistance() = max(turnDistance, mob.bbWidth.toDouble()) + mob.bbWidth / 2.0
private fun closestPointOnSegment(pos: Vec3, start: Vec3, end: Vec3): Vec3 {
val segment = end.subtract(start)
val lengthSqr = segment.lengthSqr()
if (lengthSqr == 0.0) return start
val t = (pos.subtract(start).dot(segment) / lengthSqr).coerceIn(0.0, 1.0)
return start.add(segment.scale(t))
}
/**
* A walking corner where cutting the inside would cross a cell that isn't walkable, like a turn on a
* narrow ledge or around a hole.
*/
/** Nodes the mob has to properly reach rather than round off, tight corners and steps */
private fun mustTouch(currentPath: Path, i: Int) = isTightCorner(currentPath, i) || isStep(currentPath, i)
/**
* A walking node next to a change in height
*/
private fun isStep(currentPath: Path, i: Int): Boolean {
refreshSteeringCache(currentPath)
return stepNodes.getOrPut(i) {
val node = currentPath.getNode(i)
if (node.type != PathType.WALKABLE) return@getOrPut false
val previous = if (i > 0) currentPath.getNode(i - 1) else null
val next = if (i < currentPath.nodeCount - 1) currentPath.getNode(i + 1) else null
(previous != null && previous.y != node.y) || (next != null && next.y != node.y)
}
}
private fun isTightCorner(currentPath: Path, i: Int): Boolean {
if (i <= 0 || i >= currentPath.nodeCount - 1) return false
refreshSteeringCache(currentPath)
return tightCorners.getOrPut(i) {
val node = currentPath.getNode(i)
if (node.type != PathType.WALKABLE) return@getOrPut false
val position = steerPosition(currentPath, i)
// The cell diagonally inside the turn. For a straight line this is the node's own cell
val insidePosition = steerPosition(currentPath, i - 1).add(steerPosition(currentPath, i + 1)).subtract(position)
val inside = BlockPos.containing(insidePosition.x, node.y.toDouble(), insidePosition.z)
val cell = BlockPos.containing(position.x, node.y.toDouble(), position.z)
inside != cell && WalkNodeEvaluator.getPathTypeStatic(mob, inside.mutable()) != PathType.WALKABLE
}
}
/** A walking node with nothing walkable on either side of the path, like a 1 wide ledge */
private fun isNarrow(currentPath: Path, i: Int): Boolean {
refreshSteeringCache(currentPath)
return narrowNodes.getOrPut(i) {
val node = currentPath.getNode(i)
if (node.type != PathType.WALKABLE || currentPath.nodeCount < 2) return@getOrPut false
val position = steerPosition(currentPath, i)
// Direction of the path through this node, from whichever neighbours it has
val from = steerPosition(currentPath, max(i - 1, 0))
val to = steerPosition(currentPath, min(i + 1, currentPath.nodeCount - 1))
val along = Vec3(to.x - from.x, 0.0, to.z - from.z)
if (along.lengthSqr() == 0.0) return@getOrPut false
val side = Vec3(-along.z, 0.0, along.x).normalize()
val left = BlockPos.containing(position.x + side.x, node.y.toDouble(), position.z + side.z)
val right = BlockPos.containing(position.x - side.x, node.y.toDouble(), position.z - side.z)
WalkNodeEvaluator.getPathTypeStatic(mob, left.mutable()) != PathType.WALKABLE &&
WalkNodeEvaluator.getPathTypeStatic(mob, right.mutable()) != PathType.WALKABLE
}
}
/**
* Where the mob's centre should be at node [i]. Big mobs are checked over a footprint of blocks and a node counts
* as walkable with only part of that footprint over ground, so they're centred over the walkable part instead.
*/
private fun steerPosition(currentPath: Path, i: Int): Vec3 {
refreshSteeringCache(currentPath)
return steerPositions.getOrPut(i) {
val default = currentPath.getEntityPosAtNode(mob, i)
val node = currentPath.getNode(i)
// Same footprint as OmniPathNodeMaker.getPathTypeOfMob
val sizeX = (mob.boundingBox.xsize.toInt() + 1).coerceAtMost(2)
val sizeZ = (mob.boundingBox.zsize.toInt() + 1).coerceAtMost(2)
if (node.type != PathType.WALKABLE || (sizeX == 1 && sizeZ == 1)) return@getOrPut default
val walkable = mutableListOf<BlockPos>()
for (dx in 0 until sizeX) {
for (dz in 0 until sizeZ) {
val pos = BlockPos(node.x + dx, node.y, node.z + dz)
if (WalkNodeEvaluator.getPathTypeStatic(mob, pos.mutable()) == PathType.WALKABLE) {
walkable.add(pos)
}
}
}
if (walkable.isEmpty() || walkable.size == sizeX * sizeZ) {
default
} else {
Vec3(walkable.map { it.x }.average() + 0.5, default.y, walkable.map { it.z }.average() + 0.5)
}
}
}
private fun refreshSteeringCache(currentPath: Path) {
if (cachedSteeringPath !== currentPath) {
cachedSteeringPath = currentPath
tightCorners.clear()
narrowNodes.clear()
stepNodes.clear()
steerPositions.clear()
}
}
private fun horizontalDistance(a: Vec3, b: Vec3) = sqrt((a.x - b.x) * (a.x - b.x) + (a.z - b.z) * (a.z - b.z))
/** 1 for gentle turns, falling to [CORNER_MIN_SPEED_FRACTION] for a full reversal at node [i]. */
private fun cornerSpeedFraction(currentPath: Path, i: Int): Double {
val node = steerPosition(currentPath, i)
val incoming = node.subtract(steerPosition(currentPath, i - 1))
val outgoing = steerPosition(currentPath, i + 1).subtract(node)
if (incoming.lengthSqr() == 0.0 || outgoing.lengthSqr() == 0.0) return 1.0
val cosAngle = incoming.normalize().dot(outgoing.normalize()).coerceIn(-1.0, 1.0)
val angle = Math.toDegrees(acos(cosAngle))
if (angle <= CORNER_FREE_ANGLE_DEGREES) return 1.0
val sharpness = (angle - CORNER_FREE_ANGLE_DEGREES) / (180.0 - CORNER_FREE_ANGLE_DEGREES)
return Mth.lerp(sharpness, 1.0, CORNER_MIN_SPEED_FRACTION)
}
/**
* Steering rounds corners instead of touching each node, so a node counts as reached once the mob is past it
* along its segment. Node type changes and the final node still need a real touch.
*/
private fun hasPassedNextNode(currentPath: Path): Boolean {
val index = currentPath.nextNodeIndex
if (index >= currentPath.nodeCount - 1) return false
if (currentPath.getNode(index).type != currentPath.getNode(index + 1).type) return false
if (mustTouch(currentPath, index)) return false
if (index == 0) {
// Nothing behind the first node, so it's passed once the mob is on the far side of it
val first = steerPosition(currentPath, 0)
val outgoing = steerPosition(currentPath, 1).subtract(first)
return mob.position().subtract(first).dot(outgoing) >= 0 && mob.position().distanceTo(first) < maxDistanceToWaypoint + lookAheadDistance()
}
val start = steerPosition(currentPath, index - 1)
val end = steerPosition(currentPath, index)
val along = mob.position().subtract(start).dot(end.subtract(start).normalize())
return along >= end.distanceTo(start) && mob.position().distanceTo(end) < maxDistanceToWaypoint + lookAheadDistance()
}
fun isAirborne(world: Level, pos: BlockPos) =
world.getBlockState(pos).isPathfindable(PathComputationType.AIR)
&& world.getBlockState(pos.below(1)).isPathfindable(PathComputationType.AIR)
@@ -224,6 +471,7 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
fun moveTo(x: Double, y: Double, z: Double, speed: Double = 1.0, navigationContext: NavigationContext) {
this.navigationContext = navigationContext
this.moveTo(x, y, z, speed)
this.sprinting = navigationContext.sprinting
}
override fun isStableDestination(pos: BlockPos): Boolean {
@@ -277,6 +525,7 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
}
override fun moveTo(path: Path?, speed: Double): Boolean {
sprinting = false
if (path != null && path.nodeCount > 0) {
val node = path.getNode(0)!!
// pokemonEntity.discard()
@@ -395,6 +644,7 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
override fun stop() {
super.stop()
sprinting = false
this.currentNodeDistance = -1F
this.cachedCurrentNode = null
path = null
@@ -56,7 +56,7 @@ import com.cobblemon.mod.common.api.riding.behaviour.RidingController
import com.cobblemon.mod.common.api.riding.events.SelectDriverEvent
import com.cobblemon.mod.common.api.riding.sound.RideSoundManager
import com.cobblemon.mod.common.api.riding.stats.RidingStat
import com.cobblemon.mod.common.api.riding.util.RidingAnimationData
import com.cobblemon.mod.common.api.riding.util.DynamicAnimationData
import com.cobblemon.mod.common.api.scheduling.Schedulable
import com.cobblemon.mod.common.api.scheduling.SchedulingTracker
import com.cobblemon.mod.common.api.scheduling.afterOnServer
@@ -77,6 +77,9 @@ import com.cobblemon.mod.common.entity.ai.OmniPathNavigation
import com.cobblemon.mod.common.entity.generic.GenericBedrockEntity
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.entity.pokeball.EmptyPokeBallEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonBodyRotationControl
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonLookControl
import com.cobblemon.mod.common.entity.pokemon.ai.HeadingEase
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.entity.pokemon.ai.sensors.PokemonItemSensor
import com.cobblemon.mod.common.entity.pokemon.effects.EffectTracker
@@ -150,6 +153,7 @@ import net.minecraft.world.entity.*
import net.minecraft.world.entity.ai.Brain
import net.minecraft.world.entity.ai.attributes.AttributeSupplier
import net.minecraft.world.entity.ai.attributes.Attributes
import net.minecraft.world.entity.ai.control.BodyRotationControl
import net.minecraft.world.entity.ai.control.MoveControl
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.MemoryStatus
@@ -175,6 +179,7 @@ import net.minecraft.world.level.material.FluidState
import net.minecraft.world.level.pathfinder.PathType
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import org.joml.Vector3f
import java.util.*
import java.util.concurrent.CompletableFuture
import kotlin.math.PI
@@ -217,6 +222,12 @@ open class PokemonEntity(
@JvmStatic var RIDE_STAMINA = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.FLOAT)
@JvmStatic var SCALE_MODIFIER = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.FLOAT)
@JvmStatic var IS_ALPHA = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.BOOLEAN)
@JvmStatic val IS_SPRINTING = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.BOOLEAN)
// Heading the server is easing towards as yaw, pitch and 0 when not steering, 1 when steering, 2 for a floating look turn
// and 3 for a standing look turn
@JvmStatic val HEADING_TARGET = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.VECTOR3)
// Blend time in seconds travel turns use for how the pokemon is moving
@JvmStatic val HEADING_TRAVEL_BLEND = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.FLOAT)
const val BATTLE_LOCK = "battle"
const val EVOLUTION_LOCK = "evolving"
@@ -298,6 +309,9 @@ open class PokemonEntity(
var shownItem: ItemStack
get() = entityData.get(SHOWN_HELD_ITEM)
set(value) = entityData.set(SHOWN_HELD_ITEM, value)
var isPokemonSprinting: Boolean
get() = entityData.get(IS_SPRINTING)
set(value) { entityData.set(IS_SPRINTING, value) }
var lastLightningBoltUUID: UUID? = null
@@ -310,7 +324,7 @@ open class PokemonEntity(
var enablePoseTypeRecalculation = true
val ridingAnimationData: RidingAnimationData = RidingAnimationData(this)
val ridingAnimationData: DynamicAnimationData = DynamicAnimationData(this)
var rideSoundManager: RideSoundManager = RideSoundManager(this)
private val rideStatOverrides = mutableMapOf<RidingStyle, MutableMap<RidingStat, Double>>()
@@ -320,6 +334,15 @@ open class PokemonEntity(
var previousRidingState: RidingBehaviourState? = null
private set
/** Push from PokemonMoveControl, added on the next travel right before vanilla moves the mon and applies friction */
var pendingThrust: Vec3 = Vec3.ZERO
/** Eased travel direction. Moved by PokemonMoveControl on the server and PokemonBodyRotationControl on the client */
val heading = HeadingEase()
/** Yaw a standing pokemon is turning its body to, set by PokemonBodyRotationControl and eased by PokemonMoveControl */
var stationaryTurnTarget: Float? = null
/** Whether that turn is a floating one, which uses the slower floating turn blend */
var stationaryTurnFloating = false
val runtime: MoLangRuntime by lazy {
MoLangRuntime()
.setup()
@@ -430,6 +453,7 @@ open class PokemonEntity(
delegate.changePokemon(pokemon)
addPosableFunctions(struct)
moveControl = PokemonMoveControl(this)
lookControl = PokemonLookControl(this)
if (!level().isClientSide) {
remakeBrain()
}
@@ -467,6 +491,9 @@ open class PokemonEntity(
builder.define(RIDE_STAMINA, 1F)
builder.define(SCALE_MODIFIER, 1F)
builder.define(IS_ALPHA, false)
builder.define(IS_SPRINTING, false)
builder.define(HEADING_TARGET, Vector3f())
builder.define(HEADING_TRAVEL_BLEND, PokemonMoveControl.GROUND_HEADING_BLEND_SECONDS)
}
override fun onSyncedDataUpdated(data: EntityDataAccessor<*>) {
@@ -653,11 +680,13 @@ open class PokemonEntity(
isPokemonFlying = flyDist - flyDistO > 0.005F
isPokemonWalking = walkDist - walkDistO > 0.005F
if (passengers.isNotEmpty() && level().isClientSide) {
rideSoundManager.tick()
if (level().isClientSide) {
ridingAnimationData.update()
} else if (!passengers.isNotEmpty() && level().isClientSide) {
rideSoundManager.stop()
if (passengers.isNotEmpty()) {
rideSoundManager.tick()
} else {
rideSoundManager.stop()
}
}
flyDistO = flyDist
@@ -708,6 +737,10 @@ open class PokemonEntity(
clearRestriction()
val spawnDirection = entityData.get(SPAWN_DIRECTION).takeIf { it.isFinite() } ?: 0F
yBodyRot = (spawnDirection * 1000F).toInt() / 1000F
yRot = yBodyRot
// hold the heading here too so turning starts from the spawn direction once this ends
heading.settle(yRot, snap = true)
stationaryTurnTarget = null
}
jumping = false
@@ -784,6 +817,10 @@ open class PokemonEntity(
this.moveControl = moveControl
}
override fun createBodyControl(): BodyRotationControl {
return PokemonBodyRotationControl(this)
}
/**
* Prevents water type Pokémon from taking drowning damage.
*/
@@ -1915,7 +1952,16 @@ open class PokemonEntity(
//Prevent current travel logic when riding a pokemon.
if (!this.hasRider() || this.controllingPassenger == null) {
// Same place vanilla adds walk input. Used up here so a second travel in a tick can't push twice
deltaMovement = deltaMovement.add(pendingThrust)
pendingThrust = Vec3.ZERO
// Powered flight uses its own drag instead of vanilla air friction
val poweredFlight = getBehaviourFlag(PokemonBehaviourFlag.FLYING) && !isInLiquid
setDiscardFriction(poweredFlight)
super.travel(movementInput)
if (poweredFlight) {
deltaMovement = deltaMovement.scale(PokemonMoveControl.FLYER_DRAG)
}
} else {
val inp = ifRidingAvailableSupply(fallback = Vec3.ZERO) { behaviour, settings, state ->
behaviour.velocity(settings, state, this, this.controllingPassenger as Player, deltaMovement)
@@ -26,6 +26,7 @@ import com.cobblemon.mod.common.api.spawning.fishing.FishingSpawnCause.Companion
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.activestate.ActivePokemonState
import com.cobblemon.mod.common.pokemon.activestate.SentOutState
@@ -244,11 +245,13 @@ class PokemonServerDelegate : PokemonSideDelegate {
entity.entityData.set(PokemonEntity.ASPECTS, trackedAspects)
entity.entityData.set(PokemonEntity.LABEL_LEVEL, entity.pokemon.level)
// Slow mons can travel less than the distance check each tick so steering somewhere counts too.
val travelling = entity.controllingPassenger == null && (entity.moveControl as? PokemonMoveControl)?.isTravelling == true
if (entity.getCurrentPoseType() in PoseType.FLYING_POSES) {
entity.entityData.set(PokemonEntity.MOVING, entity.isPokemonFlying)
entity.entityData.set(PokemonEntity.MOVING, entity.isPokemonFlying || travelling)
}
else {
entity.entityData.set(PokemonEntity.MOVING, entity.isPokemonWalking)
entity.entityData.set(PokemonEntity.MOVING, entity.isPokemonWalking || travelling)
}
entity.entityData.set(PokemonEntity.FRIENDSHIP, entity.pokemon.friendship)
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.entity.pokemon.ai
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl.Companion.STANDING_TURN_BLEND_SECONDS
import com.cobblemon.mod.common.util.math.InertialEase
import net.minecraft.world.phys.Vec3
/**
* The direction a pokemon is travelling, eased so it turns smoothly. The server moves with it and the client runs
* the same easing from the synced target so the body and animations see the same turn.
*/
class HeadingEase {
val yaw = InertialEase(STANDING_TURN_BLEND_SECONDS, isAngle = true)
// Minecraft pitch, negative is up
val pitch = InertialEase(STANDING_TURN_BLEND_SECONDS, isAngle = true)
/**
* Eases towards a direction taking [blendTime] for the turn. The blend only gets re-fit when it actually changes,
* so passing the same blend every tick keeps each turn's S curve.
*/
fun steer(targetYaw: Float, targetPitch: Float, blendTime: Float) {
yaw.setBlendTime(blendTime)
pitch.setBlendTime(blendTime)
yaw.update(targetYaw)
pitch.update(targetPitch)
}
/** Not steering so follow whatever else is turning the pokemon and level out */
fun settle(currentYaw: Float, snap: Boolean) {
if (snap) yaw.snap(currentYaw) else yaw.update(currentYaw)
// Level out at the standing turn pace rather than whatever travel was using
pitch.setBlendTime(STANDING_TURN_BLEND_SECONDS)
pitch.update(0F)
}
fun forward(): Vec3 = Vec3.directionFromRotation(pitch.value, yaw.value)
}
@@ -0,0 +1,97 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.entity.pokemon.ai
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import kotlin.math.abs
import net.minecraft.util.Mth
import net.minecraft.world.entity.ai.control.BodyRotationControl
/**
* Controls body movement of pokemon. Runs on both sides.
*
* Server: the body always faces yRot. When standing still it turns towards what it's looking at through the heading
* ease. Ground mons turn once, just far enough for the head to reach. Floating ones keep following the look.
* Client: the body follows the same heading ease as the server, run from the synced heading target.
*/
class PokemonBodyRotationControl(private val pokemon: PokemonEntity) : BodyRotationControl(pokemon) {
private var headingStarted = false
companion object {
// Share of the head's yaw limit a standing ground mon is happy to look across before turning its body.
// Posers usually limit the head well below the full limit so the body turns to keep things in that range
const val STANDING_TURN_HEAD_REACH_FRACTION = 0.25F
}
override fun clientTick() {
// Riding sets rotations itself
if (pokemon.controllingPassenger != null) {
// Keep the heading with the ride so it doesn't swing round on dismount
pokemon.heading.settle(pokemon.yRot, snap = true)
super.clientTick()
return
}
if (mob.level().isClientSide) {
smoothTowardsHeading()
} else {
followHeading()
}
}
private fun followHeading() {
if (!isMoving()) {
val lookYaw = (pokemon.lookControl as? PokemonLookControl)?.lookTargetYaw
val pose = pokemon.getCurrentPoseType()
val floating = pose in PoseType.FLYING_POSES || pose in PoseType.SWIMMING_POSES
if (floating && lookYaw != null) {
// Nothing to plant so keep easing towards whatever it's looking at
pokemon.stationaryTurnTarget = Mth.wrapDegrees(lookYaw)
pokemon.stationaryTurnFloating = true
} else if (lookYaw != null && pokemon.stationaryTurnTarget == null) {
val offset = Mth.wrapDegrees(lookYaw - pokemon.yRot)
val comfortableReach = pokemon.maxHeadYRot * STANDING_TURN_HEAD_REACH_FRACTION
if (abs(offset) > comfortableReach) {
// Turn the body in one go so the head is comfortable looking at it
val turn = offset - Math.copySign(comfortableReach, offset)
pokemon.stationaryTurnTarget = Mth.wrapDegrees(pokemon.yRot + turn)
pokemon.stationaryTurnFloating = false
}
}
}
pokemon.yBodyRot = pokemon.yRot
if (isMoving()) {
rotateHeadIfNecessary()
lastStableYHeadRot = mob.yHeadRot
headStableTime = 0
}
}
private fun smoothTowardsHeading() {
// Start facing where the pokemon already is instead of turning in from zero
if (!headingStarted) {
pokemon.heading.settle(pokemon.yRot, snap = true)
headingStarted = true
}
val target = pokemon.entityData.get(PokemonEntity.HEADING_TARGET)
if (target.z > 2.5F) {
pokemon.heading.steer(target.x, target.y, PokemonMoveControl.STANDING_TURN_BLEND_SECONDS)
} else if (target.z > 1.5F) {
pokemon.heading.steer(target.x, target.y, PokemonMoveControl.FLOATING_TURN_BLEND_SECONDS)
} else if (target.z > 0F) {
pokemon.heading.steer(target.x, target.y, pokemon.entityData.get(PokemonEntity.HEADING_TRAVEL_BLEND))
} else {
// Ease rather than snap so stationary turns from the server don't jump
pokemon.heading.settle(pokemon.yRot, snap = false)
}
// Keep continuous with last tick so plain lerps between yBodyRotO and yBodyRot don't spin at the wrap
pokemon.yBodyRot = pokemon.yBodyRotO + Mth.wrapDegrees(pokemon.heading.yaw.value - pokemon.yBodyRotO)
}
}
@@ -0,0 +1,66 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.entity.pokemon.ai
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.math.InertialEase
import net.minecraft.util.Mth
import net.minecraft.world.entity.ai.control.LookControl
/**
* Controls head movement of pokemon. The head is clamped to the body within the look itself, and
* [PokemonBodyRotationControl] turns the body using [lookTargetYaw].
*/
class PokemonLookControl(private val pokemon: PokemonEntity) : LookControl(pokemon) {
companion object {
const val HEAD_BLEND_SECONDS = 0.5F
}
private val headYaw = InertialEase(HEAD_BLEND_SECONDS, isAngle = true)
private val headPitch = InertialEase(HEAD_BLEND_SECONDS, isAngle = true)
private var headStarted = false
/** Yaw towards what the pokemon is looking at, null when it isn't looking at anything */
var lookTargetYaw: Float? = null
private set
override fun tick() {
lookTargetYaw = null
// Start from where the head already is instead of turning in from zero
if (!headStarted) {
headYaw.snap(mob.yHeadRot)
headPitch.snap(mob.xRot)
headStarted = true
}
// Focus on where it's going while travelling looking starts back up once it stops
if (!pokemon.navigation.isDone || pokemon.moveControl.hasWanted()) {
this.lookAtCooldown = 0
}
// nothing to look at so face forward and level out
var targetYaw = mob.yBodyRot
var targetPitch = 0F
val maxHeadTurn = mob.maxHeadYRot.toFloat()
if (this.lookAtCooldown > 0) {
--this.lookAtCooldown
getYRotD().ifPresent { lookYaw ->
lookTargetYaw = lookYaw
val offset = Mth.wrapDegrees(lookYaw - mob.yBodyRot)
targetYaw = mob.yBodyRot + Mth.clamp(offset, -maxHeadTurn, maxHeadTurn)
}
getXRotD().ifPresent { lookPitch -> targetPitch = lookPitch }
}
headYaw.update(Mth.wrapDegrees(targetYaw))
headPitch.update(targetPitch)
mob.yHeadRot = mob.yBodyRot + Mth.clamp(Mth.wrapDegrees(headYaw.value - mob.yBodyRot), -maxHeadTurn, maxHeadTurn)
mob.setXRot(headPitch.value)
}
}
@@ -16,7 +16,8 @@ import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.pokemon.PokemonBehaviourFlag
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.getWaterAndLavaIn
import com.cobblemon.mod.common.util.math.geometry.toDegrees
import com.cobblemon.mod.common.util.math.InertialEase
import com.cobblemon.mod.common.util.math.arriveSpeed
import com.cobblemon.mod.common.util.math.geometry.toRadians
import com.cobblemon.mod.common.util.resolveFloat
import kotlin.math.abs
@@ -33,11 +34,41 @@ import net.minecraft.world.level.material.FluidState
import net.minecraft.world.level.pathfinder.PathType
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import org.joml.Vector3f
import kotlin.math.sqrt
class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemonEntity) {
companion object {
const val VERY_CLOSE = 2.500000277905201E-3
const val TICKS_PER_SECOND = 20.0
// Travel turns get to a new direction within these times
const val GROUND_HEADING_BLEND_SECONDS = 0.5F
const val WATER_HEADING_BLEND_SECONDS = 0.8F
const val AIR_HEADING_BLEND_SECONDS = 1.0F
const val FLOATING_TURN_BLEND_SECONDS = 1.0F
const val STANDING_TURN_BLEND_SECONDS = 0.8F
// Speed gets to a new target
const val SPEED_BLEND_SECONDS = 0.3F
// Velocity kept each tick in powered flight
const val FLYER_DRAG = 0.7
// walkers ignore the task's speed so walk animations always match
const val GROUND_SPEED_MODIFIER = 0.35F
const val ARRIVAL_TOLERANCE = 0.05
const val FLUID_EXIT_Y_OFFSET = 0.05
// Vanilla values from LivingEntity.travel and aiStep
const val VANILLA_INPUT_SCALE = 0.98
const val VANILLA_GROUND_SPEED_FACTOR = 0.21600002
const val VANILLA_AIR_FRICTION = 0.91
const val VANILLA_FLYING_SPEED = 0.02
/** Speed where vanilla ground friction on a block with [blockFriction] balances a walk input of [speed]. */
fun vanillaGroundTopSpeed(speed: Double, blockFriction: Double): Double {
val frictionInfluencedSpeed = speed * (VANILLA_GROUND_SPEED_FACTOR / (blockFriction * blockFriction * blockFriction))
val inputForce = speed * VANILLA_INPUT_SCALE * frictionInfluencedSpeed
return inputForce / (1 - blockFriction * VANILLA_AIR_FRICTION)
}
}
val runtime = MoLangRuntime().also {
@@ -46,7 +77,32 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
private var waterLevel : Double = 0.0
var banking = false
// Where path following is steering this tick, null when not following a path
private var currentSteerTarget: Vec3? = null
var bankDurationTicks = -1
// Speed along the heading in blocks per tick of travel, eased so speed changes are smooth. Heading lives on the entity
private val speedEase = InertialEase(SPEED_BLEND_SECONDS)
// What steering asked for this tick, null when it isn't steering
private var steerGoal: SteerGoal? = null
/** True while steering somewhere this tick, however slowly. Used for the walk and fly poses */
var isTravelling = false
private set
private class SteerGoal(
val yaw: Float,
val pitch: Float,
val direction: Vec3,
val speed: Double,
val topSpeed: Double,
val distance: Double,
val horizontalOnly: Boolean,
val headingBlend: Float,
// Close enough to slide straight onto the target instead of turning to it. Attempting to prevent spinning
val slide: Boolean
)
// Travel turn blend for the way the mon is moving this tick
private var headingBlend = GROUND_HEADING_BLEND_SECONDS
var bankForwardBlocksPerTick = 0.1F
var bankUpwardsBlocksPerTick = 0F
var bankRightDegreesPerTick = 1F
@@ -74,6 +130,14 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
}
override fun tick() {
steerGoal = null
tickOperation()
updateMotion()
}
private fun tickOperation() {
val followedPath = currentSteerTarget != null
currentSteerTarget = null
if (banking && operation != Operation.WAIT) {
banking = false
}
@@ -92,17 +156,29 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
}
val behaviour = pokemonEntity.behaviour
val mediumSpeed = runtime.resolveFloat(if (pokemonEntity.getCurrentPoseType() in setOf(PoseType.FLY, PoseType.HOVER)) {
val flying = pokemonEntity.getCurrentPoseType() in setOf(PoseType.FLY, PoseType.HOVER)
val swimming = pokemonEntity.isEyeInFluid(FluidTags.WATER) || pokemonEntity.isEyeInFluid(FluidTags.LAVA)
// Sprinting is ground only and has to be asked for by whatever started the path
val sprintSpeed = behaviour.moving.walk.sprintSpeed
?.takeIf { operation == Operation.MOVE_TO && !flying && !swimming && pokemonEntity.navigation.sprinting }
pokemonEntity.isPokemonSprinting = sprintSpeed != null
val mediumSpeed = runtime.resolveFloat(if (flying) {
behaviour.moving.fly.flySpeedHorizontal
} else if (pokemonEntity.isEyeInFluid(FluidTags.WATER) || pokemonEntity.isEyeInFluid(FluidTags.LAVA)) {
} else if (swimming) {
behaviour.moving.swim.swimSpeed
} else {
behaviour.moving.walk.walkSpeed
}) * 2.5F
val baseSpeed = mob.getAttributeValue(Attributes.MOVEMENT_SPEED).toFloat() * this.speedModifier.toFloat()
val taskSpeed = if (flying || swimming) this.speedModifier.toFloat() else GROUND_SPEED_MODIFIER
val baseSpeed = mob.getAttributeValue(Attributes.MOVEMENT_SPEED).toFloat() * taskSpeed
// Mult the baseSpeed by the square root of the effectivescale to prevent foot slide (alpha/baby scaling)
val adjustedSpeed = baseSpeed * mediumSpeed * sqrt(pokemonEntity.pokemon.effectiveScale)
// Sprint speed is an exact top speed in blocks per second, so it skips attributes, task speed and friction
val sprintTopSpeed = sprintSpeed?.let {
runtime.resolveFloat(it).toDouble() / TICKS_PER_SECOND * sqrt(pokemonEntity.pokemon.effectiveScale.toDouble())
}
if (banking) {
pokemonEntity.yRot += bankRightDegreesPerTick
@@ -144,9 +220,46 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
if (!pokemonEntity.isFlying() && !pokemonEntity.isInLiquid) {
operation = Operation.WAIT
}
var xDist = wantedX - mob.x
var zDist = wantedZ - mob.z
var yDist = wantedY - mob.y
val (inWater, inLava) = mob.level().getWaterAndLavaIn(mob.boundingBox)
val inFluid = inWater || inLava
val fluidMover = pokemonEntity.getBehaviourFlag(PokemonBehaviourFlag.FLYING) || inFluid
val walkTopSpeed = if (fluidMover) adjustedSpeed.toDouble() else groundTopSpeed(adjustedSpeed.toDouble())
var topSpeed = if (fluidMover) walkTopSpeed else sprintTopSpeed ?: walkTopSpeed
val speedBlendTicks = SPEED_BLEND_SECONDS * TICKS_PER_SECOND
headingBlend = when {
inFluid -> WATER_HEADING_BLEND_SECONDS
pokemonEntity.getBehaviourFlag(PokemonBehaviourFlag.FLYING) -> AIR_HEADING_BLEND_SECONDS
else -> GROUND_HEADING_BLEND_SECONDS
}
// The speed ease is the step size this is how far we go while a turn finishes
val turnDistance = (speedEase.value * headingBlend * TICKS_PER_SECOND).toDouble()
// Follow the path when there is one otherwise go straight for the wanted position
var steerTarget = pokemonEntity.navigation.getSteerTarget(topSpeed, speedBlendTicks, turnDistance)
if (steerTarget != null && steerTarget.narrowAhead && topSpeed > walkTopSpeed) {
// No room to sprint on a narrow path, walk through it instead
topSpeed = walkTopSpeed
pokemonEntity.isPokemonSprinting = false
steerTarget = pokemonEntity.navigation.getSteerTarget(topSpeed, speedBlendTicks, turnDistance)
}
currentSteerTarget = steerTarget?.position
if (steerTarget == null && followedPath && fluidMover) {
// The path just ended or was cancelled, so glide to a stop instead of carrying on to the last waypoint.
// walkers stop on friction already
val velocity = mob.deltaMovement
// The speed ease stops in its blend time at half the speed on average
val brakingDistance = velocity.length() * speedBlendTicks / 2
val stop = mob.position().add(velocity.normalize().scale(brakingDistance))
setWantedPosition(stop.x, stop.y, stop.z, speedModifier)
}
val target = steerTarget?.position ?: Vec3(wantedX, wantedY, wantedZ)
val arrive = steerTarget?.isFinal ?: true
val maxSpeed = steerTarget?.maxSpeed ?: topSpeed
val xDist = target.x - mob.x
val zDist = target.z - mob.z
var yDist = target.y - mob.y
if (xDist * xDist + yDist * yDist + zDist * zDist < VERY_CLOSE) {
// If we're close enough, pull up stumps here.
@@ -156,52 +269,34 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
if ((pokemonEntity.isFlying() || pokemonEntity.isInLiquid)) {
operation = Operation.WAIT
mob.deltaMovement = Vec3.ZERO
speedEase.snap(0F)
}
return
}
val horizontalDistanceFromTarget = xDist * xDist + zDist * zDist
val closeHorizontally = horizontalDistanceFromTarget < VERY_CLOSE
if (!closeHorizontally) {
val angleToTarget = Mth.atan2(zDist, xDist).toDegrees() - 90.0f
val currentMovingAngle = mob.yRot
val steppedAngle = Mth.approachDegrees(currentMovingAngle, angleToTarget, 100 * mediumSpeed)
mob.yRot = steppedAngle
}
val (inWater, inLava) = mob.level().getWaterAndLavaIn(mob.boundingBox)
val inFluid = inWater || inLava
var verticalHandled = false
val blockPos = mob.blockPosition()
val blockState = mob.level().getBlockState(blockPos)
val voxelShape = blockState.getCollisionShape(mob.level(), blockPos)
if (pokemonEntity.getBehaviourFlag(PokemonBehaviourFlag.FLYING) || inFluid) {
if (fluidMover) {
verticalHandled = true
mob.yya = 0F
mob.speed = 0F
// Refinement is to prevent the entity from spinning around trying to get to a super precise location.
val refine: (Double) -> Double = { if (abs(it) < 0.05) 0.0 else it }
val fluidTarget = if (inFluid) target.add(0.0, FLUID_EXIT_Y_OFFSET, 0.0) else target
yDist = fluidTarget.y - mob.y
val fullDistance = Vec3(
xDist,
refine(yDist + 0.05), // + 0.05 for dealing with swimming out of water, they otherwise get stuck on the lip
zDist
)
val direction = fullDistance.normalize()
val scale = min(adjustedSpeed.toDouble(), fullDistance.length())
mob.deltaMovement = direction.scale(scale)
xDist = fullDistance.x
zDist = fullDistance.z
yDist = fullDistance.y
if (arrive && fluidTarget.distanceTo(mob.position()) < ARRIVAL_TOLERANCE) {
operation = Operation.WAIT
mob.deltaMovement = Vec3.ZERO
speedEase.snap(0F)
stopInputs()
return
}
steerTowards(fluidTarget, maxSpeed, topSpeed, speedBlendTicks, arrive, steerTarget?.afterDirection, horizontalOnly = false)
} else {
// division is to slow the speed down a bit so they don't overshoot when they get there.
val forwardSpeed = min(adjustedSpeed, max(horizontalDistanceFromTarget.toFloat() / 2, 0.15F))
mob.speed = forwardSpeed
steerTowards(target, maxSpeed, topSpeed, speedBlendTicks, arrive, steerTarget?.afterDirection, horizontalOnly = true)
}
if (!verticalHandled) {
@@ -210,10 +305,17 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
val zComponent = Mth.cos(mob.yRot.toRadians()).toDouble()
val motion = Vec3(xComponent, 0.0, zComponent).normalize()
val offset = motion.scale(mob.speed.toDouble())
val offset = motion.scale(adjustedSpeed.toDouble())
val closeEnoughToJump = !mob.isFree(offset.x, 0.0, offset.z)// sqrt(xDist * xDist + zDist * zDist) - 0.5 < entity.width / 2 + entity.movementSpeed * 4
// Vanilla's jump check against the next node itself, doesn't depend on the wanted position or where we're facing
val nextNodeNeedsJump = pokemonEntity.navigation.path?.takeUnless { it.isDone }?.getNextEntityPos(mob)?.let { node ->
val dx = node.x - mob.x
val dz = node.z - mob.z
node.y - mob.y > pokemonEntity.behaviour.moving.stepHeight && dx * dx + dz * dz < max(1F, mob.bbWidth)
} ?: false
if (tooBigToStep &&
if (nextNodeNeedsJump ||
tooBigToStep &&
closeEnoughToJump ||
!voxelShape.isEmpty && mob.y < voxelShape.max(Direction.Axis.Y) + blockPos.y.toDouble() &&
!blockState.`is`(BlockTags.DOORS) &&
@@ -282,6 +384,122 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
}
}
/** Works out the heading and speed steering wants this tick. Locomotion turns them into thrust in [updateMotion] */
private fun steerTowards(
target: Vec3,
maxSpeed: Double,
topSpeed: Double,
speedBlendTicks: Double,
arrive: Boolean,
afterDirection: Vec3?,
horizontalOnly: Boolean
) {
stopInputs()
val offset = target.subtract(mob.position()).let { if (horizontalOnly) Vec3(it.x, 0.0, it.z) else it }
val distance = offset.length()
if (distance < VERY_CLOSE) return
val direction = offset.scale(1 / distance)
// Target is inside our own footprint so turning to it would just spin. Slide onto it and face where the
// path goes next instead, or keep facing the same way if there's nowhere next
val slide = distance < mob.bbWidth / 2.0
val onwards = afterDirection?.let { if (horizontalOnly) Vec3(it.x, 0.0, it.z) else it }?.takeIf { it.lengthSqr() > 0 }
val faceDirection = if (slide) onwards?.normalize() else direction
steerGoal = SteerGoal(
yaw = faceDirection?.let { (Mth.atan2(it.z, it.x) * Mth.RAD_TO_DEG).toFloat() - 90F } ?: mob.yRot,
// Minecraft pitch is negative looking up
pitch = faceDirection?.let { -(Mth.atan2(it.y, it.horizontalDistance()) * Mth.RAD_TO_DEG).toFloat() } ?: pokemonEntity.heading.pitch.value,
direction = direction,
speed = if (arrive || slide) arriveSpeed(distance, maxSpeed, speedBlendTicks) else maxSpeed,
topSpeed = topSpeed,
distance = distance,
horizontalOnly = horizontalOnly,
headingBlend = headingBlend,
slide = slide
)
}
/**
* Eases heading and speed towards this tick's goal and works out the push that makes this tick's step match.
* Vanilla moves by velocity plus push and then applies friction, so knockback, ice and falling all work like vanilla.
*/
private fun updateMotion() {
val goal = steerGoal
isTravelling = goal != null && !pokemonEntity.isDeadOrDying
val heading = pokemonEntity.heading
val turnTarget = pokemonEntity.stationaryTurnTarget
if (goal == null && turnTarget != null) {
// Standing still and turning to look at something
val floating = pokemonEntity.stationaryTurnFloating
heading.steer(turnTarget, 0F, if (floating) FLOATING_TURN_BLEND_SECONDS else STANDING_TURN_BLEND_SECONDS)
mob.yRot = heading.yaw.value
// 2 and 3 tell the client which look turn blend to use
pokemonEntity.entityData.set(PokemonEntity.HEADING_TARGET, Vector3f(turnTarget, 0F, if (floating) 2F else 3F))
if (heading.yaw.isSettled()) {
pokemonEntity.stationaryTurnTarget = null
}
} else if (goal == null) {
heading.settle(mob.yRot, snap = true)
pokemonEntity.entityData.set(PokemonEntity.HEADING_TARGET, Vector3f(mob.yRot, 0F, 0F))
} else {
pokemonEntity.stationaryTurnTarget = null
heading.steer(goal.yaw, goal.pitch, goal.headingBlend)
pokemonEntity.entityData.set(PokemonEntity.HEADING_TRAVEL_BLEND, goal.headingBlend)
mob.yRot = heading.yaw.value
pokemonEntity.entityData.set(PokemonEntity.HEADING_TARGET, Vector3f(goal.yaw, goal.pitch, 1F))
}
val forward = heading.forward()
val currentSpeed = mob.deltaMovement.dot(forward)
// Walkers can't push off the air so a jump or fall just coasts
val airborneWalker = goal != null && goal.horizontalOnly && !mob.onGround()
if (goal == null || airborneWalker || pokemonEntity.isDeadOrDying) {
// Not pushing, follow the real speed so steering picks up from it without a jump
speedEase.snap(currentSpeed.toFloat())
pokemonEntity.pendingThrust = Vec3.ZERO
return
}
if (goal.slide) {
// Push straight at the target whichever way we're facinge
val velocity = mob.deltaMovement.let { if (goal.horizontalOnly) Vec3(it.x, 0.0, it.z) else it }
val push = goal.direction.scale(goal.speed).subtract(velocity)
pokemonEntity.pendingThrust = if (push.length() > goal.topSpeed) push.normalize().scale(goal.topSpeed) else push
speedEase.snap(currentSpeed.toFloat())
return
}
// Only go as fast as we're facing the right way
val facing = forward.dot(goal.direction).coerceAtLeast(0.0)
speedEase.update((goal.speed * facing).toFloat())
// Vanilla steps velocity plus push
val push = (speedEase.value - currentSpeed).coerceIn(-goal.topSpeed, goal.topSpeed)
pokemonEntity.pendingThrust = forward.scale(push)
}
private fun blockFriction(): Double {
return mob.level().getBlockState(mob.getBlockPosBelowThatAffectsMyMovement()).block.getFriction().toDouble()
}
private fun stopInputs() {
mob.speed = 0F
mob.yya = 0F
mob.setZza(0F)
mob.setXxa(0F)
}
/**
* Speed where vanilla ground friction balances the walk input force
*/
private fun groundTopSpeed(speed: Double): Double {
val blockFriction = blockFriction()
if (mob.onGround()) {
return vanillaGroundTopSpeed(speed, blockFriction)
}
val inputForce = speed * VANILLA_INPUT_SCALE * VANILLA_FLYING_SPEED
return inputForce / (1 - blockFriction * VANILLA_AIR_FRICTION)
}
/*
* Returns the ideal water level y position of a given pokemon during battle.
* This is separate for the eye height check and is dependent on if the pokemon is rafted or swimming.
@@ -11,6 +11,7 @@ package com.cobblemon.mod.common.entity.pokemon.ai.tasks
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.entity.ai.CobblemonWalkTarget
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.mainThreadRuntime
import com.cobblemon.mod.common.util.resolveBoolean
@@ -18,7 +19,7 @@ import com.cobblemon.mod.common.util.resolveFloat
import com.cobblemon.mod.common.util.resolveInt
import com.cobblemon.mod.common.util.withQueryValue
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.floor
import net.minecraft.core.BlockPos
import net.minecraft.util.RandomSource
import net.minecraft.world.entity.Entity
@@ -27,12 +28,18 @@ import net.minecraft.world.entity.ai.behavior.OneShot
import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder
import net.minecraft.world.entity.ai.behavior.declarative.Trigger
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.level.pathfinder.PathType
import net.minecraft.world.level.pathfinder.WalkNodeEvaluator
object MoveToOwnerTask {
fun create(condition: Expression, completionRange: Expression, maxDistance: Expression, teleportDistance: Expression, speedMultiplier: Expression): OneShot<PokemonEntity> = BehaviorBuilder.create {
fun create(
condition: Expression,
completionRange: Expression,
maxDistance: Expression,
teleportDistance: Expression,
speedMultiplier: Expression,
sprintDistance: Expression
): OneShot<PokemonEntity> = BehaviorBuilder.create {
it.group(
it.registered(MemoryModuleType.WALK_TARGET)
).apply(it) { walkTarget ->
@@ -53,17 +60,30 @@ object MoveToOwnerTask {
val completionRange = mainThreadRuntime.resolveInt(completionRange)
val horizontalHitboxRadius = maxOf(entity.boundingBox.xsize, entity.boundingBox.zsize) / 2
val distanceToOwner = entity.distanceTo(owner) - horizontalHitboxRadius
if (entity.distanceTo(owner) - horizontalHitboxRadius > teleportDistance) {
if (distanceToOwner > teleportDistance) {
if (tryTeleport(entity, owner)) {
entity.brain.eraseMemory(MemoryModuleType.LOOK_TARGET)
entity.brain.eraseMemory(MemoryModuleType.WALK_TARGET)
}
return@Trigger true
} else if (entity.distanceTo(owner) - horizontalHitboxRadius > maxDistance && it.tryGet(walkTarget).isEmpty && !entity.brain.hasMemoryValue(CobblemonMemories.PATH_COOLDOWN)) {
} else if (distanceToOwner > maxDistance && it.tryGet(walkTarget).isEmpty && !entity.brain.hasMemoryValue(CobblemonMemories.PATH_COOLDOWN)) {
val sprintDistance = mainThreadRuntime.resolveFloat(sprintDistance)
entity.brain.setMemoryWithExpiry(CobblemonMemories.PATH_COOLDOWN, true, 60L)
entity.brain.setMemory(MemoryModuleType.LOOK_TARGET, EntityTracker(owner, true))
entity.brain.setMemory(MemoryModuleType.WALK_TARGET, WalkTarget(owner, speedMultiplier, maxOf(completionRange, ceil(horizontalHitboxRadius).toInt()) - 1))
entity.brain.setMemory(
MemoryModuleType.WALK_TARGET,
CobblemonWalkTarget(
target = EntityTracker(owner, false),
speedModifier = speedMultiplier,
// Path has to end within the stop gap or it finishes before the mon gets close enough
completionRange = floor(completionRange + horizontalHitboxRadius + owner.bbWidth / 2.0).toInt(),
sprintDistance = sprintDistance,
stopDistance = completionRange.toFloat(),
sprintWithTarget = true
)
)
return@Trigger true
}
return@Trigger false
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.debug
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
/**
* Opens the speed tuner with every species' current speeds. Species behaviour lives in server data so the client
* can't read it itself.
*/
class OpenSpeedTunerPacket(val entries: List<SpeedTunerEntry>) : NetworkPacket<OpenSpeedTunerPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeCollection(entries) { buf, entry -> entry.encode(buf) }
}
companion object {
val ID = cobblemonResource("open_speed_tuner")
fun decode(buffer: RegistryFriendlyByteBuf): OpenSpeedTunerPacket {
return OpenSpeedTunerPacket(buffer.readList { buf -> SpeedTunerEntry.decode(buf) })
}
}
}
/** A species' raw walk speed and its sprint speed in blocks per second, null if it can't sprint. */
data class SpeedTunerEntry(
val speciesId: ResourceLocation,
val dex: Int,
val walkSpeed: Float,
val sprintSpeed: Float?
) {
fun encode(buffer: FriendlyByteBuf) {
buffer.writeResourceLocation(speciesId)
buffer.writeVarInt(dex)
buffer.writeFloat(walkSpeed)
buffer.writeBoolean(sprintSpeed != null)
sprintSpeed?.let { buffer.writeFloat(it) }
}
companion object {
fun decode(buffer: FriendlyByteBuf): SpeedTunerEntry {
val speciesId = buffer.readResourceLocation()
val dex = buffer.readVarInt()
val walkSpeed = buffer.readFloat()
val sprintSpeed = if (buffer.readBoolean()) buffer.readFloat() else null
return SpeedTunerEntry(speciesId, dex, walkSpeed, sprintSpeed)
}
}
}
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.server.debug
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
/** Asks the server to open the speed tuner, which checks debug keys and permission first. */
class RequestOpenSpeedTunerPacket : NetworkPacket<RequestOpenSpeedTunerPacket> {
override val id = ID
companion object {
val ID = cobblemonResource("request_open_speed_tuner")
fun decode(buffer: RegistryFriendlyByteBuf): RequestOpenSpeedTunerPacket {
return RequestOpenSpeedTunerPacket()
}
}
override fun encode(buffer: RegistryFriendlyByteBuf) {}
}
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.server.debug
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
/** Sets a species' sprint speed in blocks per second on the running server. Null takes sprinting away. */
class SetSpeedTunerSprintSpeedPacket(val speciesId: ResourceLocation, val sprintSpeed: Float?) : NetworkPacket<SetSpeedTunerSprintSpeedPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeResourceLocation(speciesId)
buffer.writeBoolean(sprintSpeed != null)
sprintSpeed?.let { buffer.writeFloat(it) }
}
companion object {
val ID = cobblemonResource("set_speed_tuner_sprint_speed")
fun decode(buffer: RegistryFriendlyByteBuf): SetSpeedTunerSprintSpeedPacket {
val speciesId = buffer.readResourceLocation()
val sprintSpeed = if (buffer.readBoolean()) buffer.readFloat() else null
return SetSpeedTunerSprintSpeedPacket(speciesId, sprintSpeed)
}
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.server.debug
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
/** Sets a species' walkSpeed, in the species data's own units, on the running server. */
class SetSpeedTunerWalkSpeedPacket(val speciesId: ResourceLocation, val walkSpeed: Float) : NetworkPacket<SetSpeedTunerWalkSpeedPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeResourceLocation(speciesId)
buffer.writeFloat(walkSpeed)
}
companion object {
val ID = cobblemonResource("set_speed_tuner_walk_speed")
fun decode(buffer: RegistryFriendlyByteBuf): SetSpeedTunerWalkSpeedPacket {
return SetSpeedTunerWalkSpeedPacket(buffer.readResourceLocation(), buffer.readFloat())
}
}
}
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.serverhandling.debug
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.api.permission.CobblemonPermissions.USE_SPEED_TUNER
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.net.messages.client.debug.OpenSpeedTunerPacket
import com.cobblemon.mod.common.net.messages.client.debug.SpeedTunerEntry
import com.cobblemon.mod.common.net.messages.server.debug.RequestOpenSpeedTunerPacket
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.mainThreadRuntime
import com.cobblemon.mod.common.util.resolveFloat
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
object RequestOpenSpeedTunerHandler : ServerNetworkPacketHandler<RequestOpenSpeedTunerPacket> {
override fun handle(
packet: RequestOpenSpeedTunerPacket,
server: MinecraftServer,
player: ServerPlayer
) {
if (!Cobblemon.config.enableDebugKeys) {
player.sendSystemMessage(lang("requires_debug_keys").red())
return
}
if (!Cobblemon.permissionValidator.hasPermission(player, USE_SPEED_TUNER)) {
player.sendSystemMessage(lang("requires_permission").red())
return
}
val entries = PokemonSpecies.implemented.map { species ->
val walk = species.standardForm.behaviour.moving.walk
SpeedTunerEntry(
speciesId = species.resourceIdentifier,
dex = species.nationalPokedexNumber,
// Speeds that are expressions needing a pokemon context can't be resolved here, show them as 0
walkSpeed = runCatching { mainThreadRuntime.resolveFloat(walk.walkSpeed) }.getOrDefault(0F),
sprintSpeed = walk.sprintSpeed?.let { runCatching { mainThreadRuntime.resolveFloat(it) }.getOrDefault(0F) }
)
}
OpenSpeedTunerPacket(entries).sendToPlayer(player)
}
}
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.serverhandling.debug
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.api.permission.CobblemonPermissions.USE_SPEED_TUNER
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.net.messages.server.debug.SetSpeedTunerSprintSpeedPacket
import com.cobblemon.mod.common.util.asExpression
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
/**
* Changes the loaded species data so every pokemon of that species sprints at the new speed straight away.
* Only lasts until the species data is reloaded, the tuner export is what makes it permanent.
*/
object SetSpeedTunerSprintSpeedHandler : ServerNetworkPacketHandler<SetSpeedTunerSprintSpeedPacket> {
override fun handle(packet: SetSpeedTunerSprintSpeedPacket, server: MinecraftServer, player: ServerPlayer) {
if (!Cobblemon.config.enableDebugKeys) return
if (!Cobblemon.permissionValidator.hasPermission(player, USE_SPEED_TUNER)) return
val species = PokemonSpecies.getByIdentifier(packet.speciesId) ?: return
species.standardForm.behaviour.moving.walk.sprintSpeed = packet.sprintSpeed?.toString()?.asExpression()
}
}
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.serverhandling.debug
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.api.permission.CobblemonPermissions.USE_SPEED_TUNER
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.net.messages.server.debug.SetSpeedTunerWalkSpeedPacket
import com.cobblemon.mod.common.util.asExpression
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
/**
* Changes the loaded species data so every pokemon of that species walks at the new speed straight away.
* Only lasts until the species data is reloaded, the tuner export is what makes it permanent.
*/
object SetSpeedTunerWalkSpeedHandler : ServerNetworkPacketHandler<SetSpeedTunerWalkSpeedPacket> {
override fun handle(packet: SetSpeedTunerWalkSpeedPacket, server: MinecraftServer, player: ServerPlayer) {
if (!Cobblemon.config.enableDebugKeys) return
if (!Cobblemon.permissionValidator.hasPermission(player, USE_SPEED_TUNER)) return
val species = PokemonSpecies.getByIdentifier(packet.speciesId) ?: return
species.standardForm.behaviour.moving.walk.walkSpeed = packet.walkSpeed.toString().asExpression()
}
}
@@ -19,11 +19,15 @@ class WalkBehaviour {
val canWalk = true
val avoidsLand = false
var walkSpeed: Expression = "0.35".asExpression()
// Top speed in blocks per second while sprinting, player sprint is about 5.6. Only species with one can sprint
var sprintSpeed: Expression? = null
@Transient
val struct = ObjectValue(this).also {
it.addFunction("can_walk") { DoubleValue(canWalk) }
it.addFunction("avoids_land") { DoubleValue(avoidsLand) }
it.addFunction("walk_speed") { it.environment.createDuplicateRuntime().resolveFloat(walkSpeed) }
it.addFunction("can_sprint") { DoubleValue(sprintSpeed != null) }
it.addFunction("sprint_speed") { params -> sprintSpeed?.let { params.environment.createDuplicateRuntime().resolveFloat(it) } ?: 0F }
}
}
@@ -0,0 +1,90 @@
/*
* 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.util.math
import net.minecraft.util.Mth
/**
* Eases one value towards a target with inertial easing, see SpringUtils and https://theorangeduck.com/page/inertial-easing
* Keeps its velocity when the target changes and gets there within 1.5x [blendTime]
*
* With [isAngle] the value is in degrees and always eases the short way round.
*/
class InertialEase(
private var blendTime: Float,
private val isAngle: Boolean = false
) {
companion object {
const val SECONDS_PER_TICK = 0.05F
}
var value = 0F
private set
private val vals = InertialVals()
// How far along the curve one tick is
private var step = SECONDS_PER_TICK / blendTime
private var target = 0F
/** Moves one tick towards [newTarget] */
fun update(newTarget: Float) {
if (newTarget != target) {
val difference = if (isAngle) Mth.wrapDegrees(newTarget - value) else newTarget - value
// Velocity along the current curve, carried into the new one so there's no sudden change
smoothstepSolve(vals, difference, smoothstepDt(vals.t, vals.s))
vals.offset = value - smoothstep(vals.t, vals.s)
target = newTarget
}
vals.t += step
value = vals.offset + smoothstep(vals.t, vals.s)
if (isAngle) {
// Keep turning angles in range so they don't grow forever
val turns = value - Mth.wrapDegrees(value)
if (turns != 0F) {
vals.offset -= turns
value -= turns
}
}
}
/** Changes how long easing takes. A curve in progress is re-fit so it keeps the same speed and target */
fun setBlendTime(newBlendTime: Float) {
if (newBlendTime == blendTime) return
val currentVelocity = velocity()
blendTime = newBlendTime
step = SECONDS_PER_TICK / blendTime
if (!isSettled()) {
val difference = if (isAngle) Mth.wrapDegrees(target - value) else target - value
// Velocity along the curve is per unit of curve time, which is blendTime seconds long
smoothstepSolve(vals, difference, currentVelocity * blendTime)
vals.offset = value - smoothstep(vals.t, vals.s)
}
}
/** True once the curve has reached its target */
fun isSettled() = vals.t >= 1F
/** How fast the value is changing in units per second */
fun velocity() = smoothstepDt(vals.t, vals.s) / blendTime
/** Value between last tick and this one read straight off the curve */
fun valueAt(partialTick: Float): Float {
val eased = vals.offset + smoothstep(vals.t - step * (1 - partialTick), vals.s)
return if (isAngle) Mth.wrapDegrees(eased) else eased
}
/** Jumps straight to [to] with no easing */
fun snap(to: Float) {
vals.t = 1F
vals.s = 0F
vals.offset = to
target = to
value = to
}
}
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.util.math
/**
* Fastest speed that the speed ease can still stop from within [distance]. The ease always takes [blendTicks]
* to stop and averages half the speed on the way, so it covers speed × blendTicks / 2.
* Capped at [distance] so the last tick lands on the target instead of stepping past it.
*/
fun arriveSpeed(distance: Double, maxSpeed: Double, blendTicks: Double): Double {
return minOf(maxSpeed, 2 * distance / blendTicks, distance)
}
/**
* Fastest speed at [distance] from somewhere that has to be passed at [slowSpeed].
*/
fun slowDownSpeed(distance: Double, slowSpeed: Double, blendTicks: Double): Double {
return maxOf(slowSpeed, 2 * distance / blendTicks - slowSpeed)
}
@@ -33,10 +33,7 @@
"condition": "q.is_holding_item",
"animation": "q.bedrock('venusaur', 'hold_item')"
},
{
"condition": "q.is_ridden",
"animation": "q.bedrock('venusaur', 'ride_ground_run')"
},
"q.bedrock('venusaur', 'ride_ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('venusaur', 'rider_overlay')"
@@ -96,10 +93,7 @@
"condition": "q.is_holding_item",
"animation": "q.bedrock('venusaur', 'hold_item')"
},
{
"condition": "q.is_ridden",
"animation": "q.bedrock('venusaur', 'ride_ground_walk')"
},
"q.bedrock('venusaur', 'ride_ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('venusaur', 'rider_overlay')"
@@ -120,10 +114,7 @@
"condition": "q.is_holding_item",
"animation": "q.bedrock('venusaur', 'hold_item')"
},
{
"condition": "q.is_ridden",
"animation": "q.bedrock('venusaur', 'ride_ground_run')"
},
"q.bedrock('venusaur', 'ride_ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('venusaur', 'rider_overlay')"
@@ -121,10 +121,7 @@
"q.look('neck4', 0.35, 0.35, 5, -5, 15, -15)",
"q.look('head', 0.4, 0.25, 5, -5, 10, -10)",
"q.bedrock('charizard', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('charizard', 'ride_ground_run')"
},
"q.bedrock('charizard', 'ride_ground_run')",
{
"condition": "q.is_holding_item",
"animation": "q.bedrock('charizard', 'hold_item')"
@@ -167,10 +164,7 @@
"q.look('neck4', 0.35, 0.35, 5, -5, 15, -15)",
"q.look('head', 0.4, 0.25, 5, -5, 10, -10)",
"q.bedrock('charizard', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('charizard', 'ride_ground_run')"
},
"q.bedrock('charizard', 'ride_ground_run')",
{
"condition": "q.is_holding_item",
"animation": "q.bedrock('charizard', 'hold_item')"
@@ -208,10 +202,7 @@
"q.look('neck4', 0.35, 0.35, 5, -5, 15, -15)",
"q.look('head', 0.4, 0.25, 5, -5, 10, -10)",
"q.bedrock('charizard', 'air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('charizard', 'ride_air_fly')"
},
"q.bedrock('charizard', 'ride_air_fly')",
{
"condition": "q.is_holding_item",
"animation": "q.bedrock('charizard', 'hold_item')"
@@ -233,10 +224,7 @@
"q.look('head', 0.4, 0.25, 5, -5, 10, -10)",
"q.bedrock('charizard', 'air_glide')",
"q.bedrock('charizard', 'ride_air_dive')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('charizard', 'ride_air_fly')"
},
"q.bedrock('charizard', 'ride_air_fly')",
{
"condition": "q.is_holding_item",
"animation": "q.bedrock('charizard', 'hold_item')"
@@ -88,10 +88,7 @@
"animations": [
"q.look('head_ai')",
"q.bedrock('blastoise', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('blastoise', 'ride_ground_walk')"
}
"q.bedrock('blastoise', 'ride_ground_walk')"
],
"transformTicks": 5,
"transformToTicks": 5
@@ -104,10 +101,7 @@
"animations": [
"q.look('head_ai')",
"q.bedrock('blastoise', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('blastoise', 'ride_ground_run')"
}
"q.bedrock('blastoise', 'ride_ground_run')"
],
"transformTicks": 5,
"transformToTicks": 5
@@ -209,10 +203,7 @@
"animations": [
"q.look('head_ai')",
"q.bedrock('blastoise', 'surfacewater_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('blastoise', 'ride_water_swim')"
}
"q.bedrock('blastoise', 'ride_water_swim')"
],
"quirks": [
"q.bedrock_quirk('blastoise', 'blink')"
@@ -383,10 +383,7 @@
"condition": "q.is_ridden",
"animation": "q.bedrock('pidgeot', 'ride_air_glide')"
},
{
"condition": "q.is_ridden",
"animation": "q.bedrock('pidgeot', 'ride_dive')"
}
"q.bedrock('pidgeot', 'ride_dive')"
],
"quirks": [
"q.bedrock_quirk('pidgeot', 'blink')"
@@ -45,7 +45,7 @@
"animations": [
"q.bedrock('parasect', 'ground_walk')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('parasect', 'ride_ground_run')"
}
]
@@ -57,7 +57,7 @@
"animations": [
"q.bedrock('parasect', 'ground_run')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('parasect', 'ride_ground_run')"
}
],
@@ -51,10 +51,7 @@
"animations": [
"q.look('head')",
"q.bedrock('arcanine', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('arcanine', 'ride_ground_walk')"
}
"q.bedrock('arcanine', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('arcanine', 'blink')"]
},
@@ -65,10 +62,7 @@
"animations": [
"q.look('head')",
"q.bedrock('arcanine', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('arcanine', 'ride_ground_run')"
}
"q.bedrock('arcanine', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('arcanine', 'blink')"]
},
@@ -78,10 +72,7 @@
"animations": [
"q.look('head')",
"q.bedrock('arcanine', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('arcanine', 'ride_ground_walk')"
}
"q.bedrock('arcanine', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('arcanine', 'blink')"]
},
@@ -63,7 +63,7 @@
"q.look('head_ai')",
"q.bedrock('dewgong', 'water_swim')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('dewgong', 'ride_water_swim')"
}
],
@@ -77,7 +77,7 @@
"q.look('head_ai')",
"q.bedrock('dewgong', 'water_swim')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('dewgong', 'ride_water_swim')"
}
],
@@ -91,7 +91,7 @@
"q.look('head_ai')",
"q.bedrock('dewgong', 'water_idle')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('dewgong', 'ride_water_swim')"
}
],
@@ -106,7 +106,7 @@
"q.look('head_ai')",
"q.bedrock('dewgong', 'water_idle')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('dewgong', 'ride_water_swim')"
}
],
@@ -43,10 +43,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhyhorn', 'ride_jump')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhyhorn', 'ride_ground_run')"
}
"q.bedrock('rhyhorn', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('rhyhorn', 'blink')"
@@ -61,10 +58,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhyhorn', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhyhorn', 'ride_ground_walk')"
}
"q.bedrock('rhyhorn', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('rhyhorn', 'blink')",
@@ -77,10 +71,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhyhorn', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhyhorn', 'ride_ground_walk')"
}
"q.bedrock('rhyhorn', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('rhyhorn', 'blink')"]
},
@@ -90,10 +81,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhyhorn', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhyhorn', 'ride_ground_run')"
}
"q.bedrock('rhyhorn', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('rhyhorn', 'blink')"]
},
@@ -44,10 +44,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhydon', 'ride_jump')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhydon', 'ride_ground_run')"
}
"q.bedrock('rhydon', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('rhydon', 'blink')"
@@ -61,10 +58,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhydon', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhydon', 'ride_ground_run')"
}
"q.bedrock('rhydon', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('rhydon', 'blink')",
@@ -78,10 +72,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhydon', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhydon', 'ride_ground_walk')"
}
"q.bedrock('rhydon', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('rhydon', 'blink')"
@@ -93,10 +84,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhydon', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhydon', 'ride_ground_walk')"
}
"q.bedrock('rhydon', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('rhydon', 'blink')"]
},
@@ -26,10 +26,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros', 'ride_jump')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros', 'ride_ground_run')"
}
"q.bedrock('tauros', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('tauros', 'blink')"
@@ -43,10 +40,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros', 'ride_ground_run')"
}
"q.bedrock('tauros', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('tauros', 'blink')"]
},
@@ -56,10 +50,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros', 'ride_ground_walk')"
}
"q.bedrock('tauros', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('tauros', 'blink')"]
},
@@ -69,10 +60,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros', 'ride_ground_walk')"
}
"q.bedrock('tauros', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('tauros', 'blink')"]
},
@@ -26,10 +26,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_aqua', 'ride_jump_liquid')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_aqua', 'ride_surfacewater_swim')"
}
"q.bedrock('tauros_paldean_aqua', 'ride_surfacewater_swim')"
],
"quirks": [
"q.bedrock_quirk('tauros_paldean_aqua', 'blink')"
@@ -43,10 +40,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_aqua', 'ride_jump_land')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_aqua', 'ride_ground_run')"
}
"q.bedrock('tauros_paldean_aqua', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('tauros_paldean_aqua', 'blink')"
@@ -61,10 +55,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_aqua', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_aqua', 'ride_ground_run')"
}
"q.bedrock('tauros_paldean_aqua', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('tauros_paldean_aqua', 'blink')"]
},
@@ -75,10 +66,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_aqua', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_aqua', 'ride_ground_walk')"
}
"q.bedrock('tauros_paldean_aqua', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('tauros_paldean_aqua', 'blink')"]
},
@@ -89,10 +77,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_aqua', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_aqua', 'ride_ground_walk')"
}
"q.bedrock('tauros_paldean_aqua', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('tauros_paldean_aqua', 'blink')"]
},
@@ -101,10 +86,7 @@
"poseTypes": ["WALK", "SWIM"],
"animations": [
"q.bedrock('tauros_paldean_aqua', 'surfacewater_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_aqua', 'ride_surfacewater_swim')"
}
"q.bedrock('tauros_paldean_aqua', 'ride_surfacewater_swim')"
],
"quirks": ["q.bedrock_quirk('tauros_paldean_aqua', 'blink')"]
},
@@ -114,10 +96,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_aqua', 'surfacewater_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_aqua', 'ride_surfacewater_swim')"
}
"q.bedrock('tauros_paldean_aqua', 'ride_surfacewater_swim')"
],
"quirks": ["q.bedrock_quirk('tauros_paldean_aqua', 'blink')"]
},
@@ -25,10 +25,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_blaze', 'ride_jump')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_blaze', 'ride_ground_run')"
}
"q.bedrock('tauros_paldean_blaze', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('tauros_paldean_blaze', 'blink')"
@@ -42,10 +39,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_blaze', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_blaze', 'ride_ground_run')"
}
"q.bedrock('tauros_paldean_blaze', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('tauros_paldean_blaze', 'blink')"]
},
@@ -55,10 +49,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_blaze', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_blaze', 'ride_ground_walk')"
}
"q.bedrock('tauros_paldean_blaze', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('tauros_paldean_blaze', 'blink')"]
},
@@ -68,10 +59,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tauros_paldean_blaze', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tauros_paldean_blaze', 'ride_ground_walk')"
}
"q.bedrock('tauros_paldean_blaze', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('tauros_paldean_blaze', 'blink')"]
},
@@ -134,10 +134,7 @@
"animations": [
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('gyarados', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gyarados', 'ride_ground_walk')"
}
"q.bedrock('gyarados', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('gyarados', 'blink')",
@@ -153,10 +150,7 @@
"animations": [
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('gyarados', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gyarados', 'ride_ground_walk')"
}
"q.bedrock('gyarados', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('gyarados', 'blink')",
@@ -170,10 +164,7 @@
"animations": [
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('gyarados', 'water_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gyarados', 'ride_water_swim')"
}
"q.bedrock('gyarados', 'ride_water_swim')"
],
"quirks": [
"q.bedrock_quirk('gyarados', 'blink')",
@@ -191,10 +182,7 @@
"animations": [
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('gyarados', 'water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gyarados', 'ride_water_swim')"
}
"q.bedrock('gyarados', 'ride_water_swim')"
],
"quirks": [
"q.bedrock_quirk('gyarados', 'blink')",
@@ -258,10 +246,7 @@
"animations": [
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('gyarados', 'surface_water_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gyarados', 'ride_surface_water')"
}
"q.bedrock('gyarados', 'ride_surface_water')"
],
"quirks": [
"q.bedrock_quirk('gyarados', 'blink')",
@@ -284,10 +269,7 @@
"animations": [
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('gyarados', 'surface_water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gyarados', 'ride_surface_water')"
}
"q.bedrock('gyarados', 'ride_surface_water')"
],
"quirks": [
"q.bedrock_quirk('gyarados', 'blink')",
@@ -306,10 +288,7 @@
"animations": [
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('gyarados', 'air_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gyarados', 'ride_air_fly')"
}
"q.bedrock('gyarados', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('gyarados', 'blink')",
@@ -322,10 +301,7 @@
"animations": [
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('gyarados', 'air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gyarados', 'ride_air_fly')"
}
"q.bedrock('gyarados', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('gyarados', 'blink')",
@@ -73,10 +73,7 @@
"animations": [
"q.look('head')",
"q.bedrock('lapras', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('lapras', 'ride_ground_walk')"
}
"q.bedrock('lapras', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('lapras', 'blink')",
@@ -89,10 +86,7 @@
"animations": [
"q.look('head')",
"q.bedrock('lapras', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('lapras', 'ride_ground_walk')"
}
"q.bedrock('lapras', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('lapras', 'blink')"]
},
@@ -124,10 +118,7 @@
"animations": [
"q.look('head')",
"q.bedrock('lapras', 'surfacewater_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('lapras', 'ride_surfacewater_swim')"
}
"q.bedrock('lapras', 'ride_surfacewater_swim')"
],
"quirks": [
"q.bedrock_quirk('lapras', 'blink')",
@@ -160,10 +151,7 @@
"animations": [
"q.look('head')",
"q.bedrock('lapras', 'surfacewater_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('lapras', 'ride_surfacewater_swim')"
}
"q.bedrock('lapras', 'ride_surfacewater_swim')"
],
"quirks": [
"q.bedrock_quirk('lapras', 'blink')",
@@ -58,10 +58,7 @@
"q.look('neck', 1, 1, 25, -20, 20, -20)",
"q.look('head', 1, 1, 22.5, -10, 15, -15)",
"q.bedrock('aerodactyl', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('aerodactyl', 'ride_ground_walk')"
}
"q.bedrock('aerodactyl', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('aerodactyl', 'blink')"
@@ -83,10 +80,7 @@
"q.look('neck', 1, 1, 25, -20, 20, -20)",
"q.look('head', 1, 1, 22.5, -10, 15, -15)",
"q.bedrock('aerodactyl', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('aerodactyl', 'ride_ground_walk')"
}
"q.bedrock('aerodactyl', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('aerodactyl', 'blink')"]
},
@@ -97,10 +91,7 @@
"q.look('neck', 1, 1, 25, -20, 20, -20)",
"q.look('head', 1, 1, 22.5, -10, 15, -15)",
"q.bedrock('aerodactyl', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('aerodactyl', 'ride_ground_run')"
}
"q.bedrock('aerodactyl', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('aerodactyl', 'blink')"
@@ -170,10 +161,7 @@
"q.look('head', 1, 1, 22.5, -10, 15, -15)",
"q.bedrock('aerodactyl', 'air_glide')",
"q.bedrock('aerodactyl', 'air_dive')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('aerodactyl', 'ride_air_glide')"
}
"q.bedrock('aerodactyl', 'ride_air_glide')"
],
"quirks": [
"q.bedrock_quirk('aerodactyl', 'blink')"
@@ -61,7 +61,7 @@
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('dragonite', 'ground_idle')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('dragonite', 'ride_ground_walk')"
}
],
@@ -76,7 +76,7 @@
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('dragonite', 'ground_walk')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('dragonite', 'ride_ground_walk')"
}
],
@@ -116,7 +116,7 @@
"q.look('head', 1, 0.25, 10, -15, 5, -5)",
"q.bedrock('dragonite', 'air_glide')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('dragonite', 'ride_air_fly')"
}
],
@@ -133,7 +133,7 @@
"q.bedrock('dragonite', 'air_glide')",
"q.bedrock('dragonite', 'ride_air_dive')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('dragonite', 'ride_air_glide')"
}
],
@@ -62,10 +62,7 @@
"animations": [
"q.look('head')",
"q.bedrock('ariados', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('ariados', 'ride_ground_walk')"
}
"q.bedrock('ariados', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('ariados', 'blink')"],
"transformTicks": 5
@@ -77,10 +74,7 @@
"animations": [
"q.look('head')",
"q.bedrock('ariados', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('ariados', 'ride_ground_run')"
}
"q.bedrock('ariados', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('ariados', 'blink')"
@@ -85,7 +85,7 @@
"q.look('head', 1, 1, 35, -15, 15, -15)",
"q.bedrock('girafarig', 'ground_walk')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('girafarig', 'ride_ground_walk')"
}
],
@@ -104,7 +104,7 @@
"q.look('head', 1, 1, 35, -15, 15, -15)",
"q.bedrock('girafarig', 'ground_run')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('girafarig', 'ride_ground_run')"
}
],
@@ -44,10 +44,7 @@
"condition": "!q.in_air && !q.is_sprinting",
"animations": [
"q.bedrock('heracross', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('heracross', 'ride_ground_run')"
}
"q.bedrock('heracross', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('heracross', 'blink')"]
},
@@ -56,10 +53,7 @@
"condition": "!q.in_air && q.is_sprinting",
"animations": [
"q.bedrock('heracross', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('heracross', 'ride_ground_run')"
}
"q.bedrock('heracross', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('heracross', 'blink')"]
},
@@ -78,10 +72,7 @@
"condition": "!q.is_gliding",
"animations": [
"q.bedrock('heracross', 'air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('heracross', 'ride_air_fly')"
}
"q.bedrock('heracross', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('heracross', 'blink')"
@@ -93,10 +84,7 @@
"animations": [
"q.bedrock('heracross', 'air_fly')",
"q.bedrock('heracross', 'ride_dive')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('heracross', 'ride_air_fly')"
}
"q.bedrock('heracross', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('heracross', 'blink')"
@@ -52,10 +52,7 @@
"animations": [
"q.look('head')",
"q.bedrock('ursaring', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('ursaring', 'ride_ground_walk')"
}
"q.bedrock('ursaring', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('ursaring', 'blink')"
@@ -69,10 +66,7 @@
"animations": [
"q.look('head')",
"q.bedrock('ursaring', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('ursaring', 'ride_ground_run')"
}
"q.bedrock('ursaring', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('ursaring', 'blink')"],
"transformTicks": 5
@@ -63,10 +63,7 @@
"animations": [
"q.look('head')",
"q.bedrock('piloswine', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('piloswine', 'ride_ground_walk')"
}
"q.bedrock('piloswine', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('piloswine', 'blink')",
@@ -79,10 +76,7 @@
"animations": [
"q.look('head')",
"q.bedrock('piloswine', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('piloswine', 'ride_ground_run')"
}
"q.bedrock('piloswine', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('piloswine', 'blink')"
@@ -59,10 +59,7 @@
"animations": [
"q.look('head')",
"q.bedrock('mantine', 'water_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('mantine', 'ride_water_swim')"
}
"q.bedrock('mantine', 'ride_water_swim')"
],
"quirks": [
"q.bedrock_quirk('mantine', 'blink')"
@@ -75,10 +72,7 @@
"animations": [
"q.look('head')",
"q.bedrock('mantine', 'water_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('mantine', 'ride_water_swim')"
}
"q.bedrock('mantine', 'ride_water_swim')"
],
"quirks": [
"q.bedrock_quirk('mantine', 'blink')"
@@ -91,10 +85,7 @@
"animations": [
"q.look('head')",
"q.bedrock('mantine', 'water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('mantine', 'ride_water_swim')"
}
"q.bedrock('mantine', 'ride_water_swim')"
],
"quirks": [
"q.bedrock_quirk('mantine', 'blink')"
@@ -107,10 +98,7 @@
"animations": [
"q.look('head')",
"q.bedrock('mantine', 'water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('mantine', 'ride_water_swim')"
}
"q.bedrock('mantine', 'ride_water_swim')"
],
"quirks": [
"q.bedrock_quirk('mantine', 'blink')"
@@ -121,10 +109,7 @@
"animations": [
"q.look('head')",
"q.bedrock('mantine', 'water_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('mantine', 'ride_water_swim')"
}
"q.bedrock('mantine', 'ride_water_swim')"
],
"quirks": [
"q.bedrock_quirk('mantine', 'blink')"
@@ -54,10 +54,7 @@
"animations": [
"q.look('head', 0.4, 0.25, 5, -5, 10, -10)",
"q.bedrock('skarmory', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('skarmory', 'ride_ground_run')"
}
"q.bedrock('skarmory', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('skarmory', 'blink')"
@@ -79,10 +76,7 @@
"animations": [
"q.look('head')",
"q.bedrock('skarmory', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('skarmory', 'ride_ground_run')"
}
"q.bedrock('skarmory', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('skarmory', 'blink')"]
},
@@ -68,10 +68,7 @@
"animations": [
"q.look('head')",
"q.bedrock('slaking', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('slaking', 'ride_ground')"
}
"q.bedrock('slaking', 'ride_ground')"
],
"quirks": ["q.bedrock_quirk('slaking', 'blink')"]
},
@@ -82,10 +79,7 @@
"animations": [
"q.look('head')",
"q.bedrock('slaking', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('slaking', 'ride_ground')"
}
"q.bedrock('slaking', 'ride_ground')"
],
"quirks": ["q.bedrock_quirk('slaking', 'blink')"]
},
@@ -95,10 +89,7 @@
"animations": [
"q.look('head')",
"q.bedrock('slaking', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('slaking', 'ride_ground')"
}
"q.bedrock('slaking', 'ride_ground')"
],
"quirks": ["q.bedrock_quirk('slaking', 'blink')"]
},
@@ -27,10 +27,7 @@
"isTouchingWater": false,
"animations": [
"q.bedrock('sharpedo', 'water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('sharpedo', 'ride_water_swim')"
}
"q.bedrock('sharpedo', 'ride_water_swim')"
],
"transformTicks": 3,
"transformToTicks": 3
@@ -66,10 +63,7 @@
"isUnderWater": true,
"animations": [
"q.bedrock('sharpedo', 'water_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('sharpedo', 'ride_water_swim')"
}
"q.bedrock('sharpedo', 'ride_water_swim')"
]
},
"swimming": {
@@ -78,10 +72,7 @@
"isUnderWater": true,
"animations": [
"q.bedrock('sharpedo', 'water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('sharpedo', 'ride_water_swim')"
}
"q.bedrock('sharpedo', 'ride_water_swim')"
]
}
}
@@ -63,10 +63,7 @@
"animations": [
"q.look('head')",
"q.bedrock('camerupt', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('camerupt', 'ride_ground_run')"
}
"q.bedrock('camerupt', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('camerupt', 'blink')"]
},
@@ -76,10 +73,7 @@
"animations": [
"q.look('head')",
"q.bedrock('camerupt', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('camerupt', 'ride_ground_run')"
}
"q.bedrock('camerupt', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('camerupt', 'blink')"]
},
@@ -90,10 +84,7 @@
"animations": [
"q.look('head')",
"q.bedrock('camerupt', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('camerupt', 'ride_ground_run')"
}
"q.bedrock('camerupt', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('camerupt', 'blink')"]
}
@@ -61,10 +61,7 @@
"animations": [
"q.look('head_ai')",
"q.bedrock('flygon', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('flygon', 'ride_ground_walk')"
}
"q.bedrock('flygon', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('flygon', 'blink')"]
},
@@ -74,10 +71,7 @@
"animations": [
"q.look('head_ai')",
"q.bedrock('flygon', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('flygon', 'ride_ground_run')"
}
"q.bedrock('flygon', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('flygon', 'blink')"]
},
@@ -112,10 +106,7 @@
"animations": [
"q.look('head_ai')",
"q.bedrock('flygon', 'air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('flygon', 'ride_air_fly')"
}
"q.bedrock('flygon', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('flygon', 'blink')"
@@ -128,10 +119,7 @@
"q.look('head_ai')",
"q.bedrock('flygon', 'air_glide')",
"q.bedrock('flygon', 'ride_dive')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('flygon', 'ride_air_fly')"
}
"q.bedrock('flygon', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('flygon', 'blink')"
@@ -81,10 +81,7 @@
"condition": "q.is_holding_item",
"animation": "q.bedrock('altaria', 'hold_item')"
},
{
"condition": "q.is_ridden",
"animation": "q.bedrock('altaria', 'ride_air_fly')"
}
"q.bedrock('altaria', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('altaria', 'blink')"
@@ -104,10 +101,7 @@
"condition": "q.is_holding_item",
"animation": "q.bedrock('altaria', 'hold_item')"
},
{
"condition": "q.is_ridden",
"animation": "q.bedrock('altaria', 'ride_air_fly')"
}
"q.bedrock('altaria', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('altaria', 'blink')"
@@ -14,10 +14,7 @@
"isTouchingWater": false,
"animations": [
"q.bedrock('milotic', 'water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('milotic', 'ride_water_swim')"
}
"q.bedrock('milotic', 'ride_water_swim')"
],
"transformTicks": 3,
"transformToTicks": 3
@@ -62,10 +59,7 @@
"animations": [
"q.look('head')",
"q.bedrock('milotic', 'water_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('milotic', 'ride_water_swim')"
}
"q.bedrock('milotic', 'ride_water_swim')"
],
"quirks": ["q.bedrock_quirk('milotic', 'blink')"]
},
@@ -75,10 +69,7 @@
"animations": [
"q.look('head')",
"q.bedrock('milotic', 'water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('milotic', 'ride_water_swim')"
}
"q.bedrock('milotic', 'ride_water_swim')"
],
"quirks": ["q.bedrock_quirk('milotic', 'blink')"]
},
@@ -14,10 +14,7 @@
"isTouchingWater": false,
"animations": [
"q.bedrock('milotic', 'water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('milotic', 'ride_water_swim')"
}
"q.bedrock('milotic', 'ride_water_swim')"
],
"transformTicks": 3,
"transformToTicks": 3
@@ -62,10 +59,7 @@
"animations": [
"q.look('head')",
"q.bedrock('milotic', 'water_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('milotic', 'ride_water_swim')"
}
"q.bedrock('milotic', 'ride_water_swim')"
],
"quirks": ["q.bedrock_quirk('milotic', 'blink')"]
},
@@ -75,10 +69,7 @@
"animations": [
"q.look('head')",
"q.bedrock('milotic', 'water_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('milotic', 'ride_water_swim')"
}
"q.bedrock('milotic', 'ride_water_swim')"
],
"quirks": ["q.bedrock_quirk('milotic', 'blink')"]
},
@@ -82,10 +82,7 @@
"q.look('neck4', 0.5, 0.5, 5, -5, 10, -10)",
"q.look('neck5', 0.5, 0.5, 5, -5, 5, -5)",
"q.bedrock('tropius', 'air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tropius', 'ride_air_fly')"
}
"q.bedrock('tropius', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('tropius', 'blink')"
@@ -102,10 +99,7 @@
"q.look('neck5', 0.5, 0.5, 5, -5, 5, -5)",
"q.bedrock('tropius', 'air_glide')",
"q.bedrock('tropius', 'ride_dive')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tropius', 'ride_air_glide')"
}
"q.bedrock('tropius', 'ride_air_glide')"
],
"quirks": [
"q.bedrock_quirk('tropius', 'blink')"
@@ -57,7 +57,7 @@
"q.look('head')",
"q.bedrock('salamence', 'air_idle')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('salamence', 'ride_air_fly')"
}
],
@@ -74,7 +74,7 @@
"q.look('head')",
"q.bedrock('salamence', 'air_fly')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('salamence', 'ride_air_fly')"
}
],
@@ -92,7 +92,7 @@
"q.bedrock('salamence', 'air_glide')",
"q.bedrock('salamence', 'ride_dive')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('salamence', 'ride_air_glide')"
}
],
@@ -109,7 +109,7 @@
"q.look('head')",
"q.bedrock('salamence', 'ride_jump')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('salamence', 'ride_ground_run')"
}
],
@@ -125,7 +125,7 @@
"q.look('head')",
"q.bedrock('salamence', 'ground_walk')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('salamence', 'ride_ground_run')"
}
],
@@ -141,7 +141,7 @@
"q.look('head')",
"q.bedrock('salamence', 'ground_run')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('salamence', 'ride_ground_run')"
}
],
@@ -167,10 +167,7 @@
"condition": "!q.in_air && q.is_sprinting",
"animations": [
"q.bedrock('metagross', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('metagross', 'ride_ground_run')"
}
"q.bedrock('metagross', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('metagross', 'blink')"
@@ -72,10 +72,7 @@
"animations": [
"q.look('head')",
"q.bedrock('staraptor', 'air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('staraptor', 'ride_air_fly')"
}
"q.bedrock('staraptor', 'ride_air_fly')"
],
"quirks": ["q.bedrock_quirk('staraptor', 'blink')"]
},
@@ -86,10 +83,7 @@
"q.look('head')",
"q.bedrock('staraptor', 'air_glide')",
"q.bedrock('staraptor', 'ride_dive')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('staraptor', 'ride_air_glide')"
}
"q.bedrock('staraptor', 'ride_air_glide')"
],
"quirks": ["q.bedrock_quirk('staraptor', 'blink')"
]
@@ -49,7 +49,7 @@
"q.look('head')",
"q.bedrock('bastiodon', 'ground_idle')",
{
"condition": "q.has_entity && q.is_ridden",
"condition": "q.has_entity",
"animation": "q.bedrock('bastiodon', 'ride_ground_walk')"
},
{
@@ -87,7 +87,7 @@
"q.look('head')",
"q.bedrock('bastiodon', 'ground_walk')",
{
"condition": "q.has_entity && q.is_ridden",
"condition": "q.has_entity",
"animation": "q.bedrock('bastiodon', 'ride_ground_walk')"
},
{
@@ -107,7 +107,7 @@
"q.look('head')",
"q.bedrock('bastiodon', 'ground_run')",
{
"condition": "q.has_entity && q.is_ridden",
"condition": "q.has_entity",
"animation": "q.bedrock('bastiodon', 'ride_ground_walk')"
},
{
@@ -52,10 +52,7 @@
"animations": [
"q.look('head')",
"q.bedrock('gastrodon_east', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gastrodon_east', 'ride_ground_run')"
}
"q.bedrock('gastrodon_east', 'ride_ground_run')"
],
"transformToTicks": 2
},
@@ -77,10 +74,7 @@
"animations": [
"q.look('head')",
"q.bedrock('gastrodon_east', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gastrodon_east', 'ride_ground_walk')"
}
"q.bedrock('gastrodon_east', 'ride_ground_walk')"
]
},
"sleep": {
@@ -52,10 +52,7 @@
"animations": [
"q.look('head')",
"q.bedrock('gastrodon_west', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gastrodon_west', 'ride_ground_run')"
}
"q.bedrock('gastrodon_west', 'ride_ground_run')"
],
"transformToTicks": 2
},
@@ -77,10 +74,7 @@
"animations": [
"q.look('head')",
"q.bedrock('gastrodon_west', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('gastrodon_west', 'ride_ground_walk')"
}
"q.bedrock('gastrodon_west', 'ride_ground_walk')"
]
},
"sleep": {
@@ -72,10 +72,7 @@
"animations": [
"q.look('head_ai', 1, 1, 10, -10, 45, -45)",
"q.bedrock('honchkrow', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('honchkrow', 'ride_ground_walk')"
},
"q.bedrock('honchkrow', 'ride_ground_walk')",
{
"condition": "q.is_holding_item",
"animation": "q.bedrock('honchkrow', 'hold_item')"
@@ -100,10 +97,7 @@
"animations": [
"q.look('head_ai', 1, 1, 10, -10, 45, -45)",
"q.bedrock('honchkrow', 'air_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('honchkrow', 'ride_air_fly')"
},
"q.bedrock('honchkrow', 'ride_air_fly')",
{
"condition": "q.is_holding_item",
"animation": "q.bedrock('honchkrow', 'hold_item')"
@@ -129,10 +123,7 @@
"animations": [
"q.look('head_ai', 1, 1, 10, -10, 45, -45)",
"q.bedrock('honchkrow', 'air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('honchkrow', 'ride_air_fly')"
},
"q.bedrock('honchkrow', 'ride_air_fly')",
{
"condition": "q.is_holding_item",
"animation": "q.bedrock('honchkrow', 'hold_item')"
@@ -159,14 +150,8 @@
"animations": [
"q.look('head_ai', 1, 1, 10, -10, 45, -45)",
"q.bedrock('honchkrow', 'air_glide')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('honchkrow', 'ride_air_glide')"
},
{
"condition": "q.is_ridden",
"animation": "q.bedrock('honchkrow', 'ride_dive')"
},
"q.bedrock('honchkrow', 'ride_air_glide')",
"q.bedrock('honchkrow', 'ride_dive')",
{
"condition": "q.is_holding_item",
"animation": "q.bedrock('honchkrow', 'hold_item')"
@@ -31,10 +31,7 @@
"q.look('head', 1, 1, 25, -25, 25, -25)",
"q.look('neck', 1, 1, 25, -25, 25, -25)",
"q.bedrock('garchomp', 'ride_jump_liquid')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('garchomp', 'ride_surfacewater_swim')"
}
"q.bedrock('garchomp', 'ride_surfacewater_swim')"
],
"quirks": [
"q.bedrock_quirk('garchomp', 'blink')"
@@ -49,10 +46,7 @@
"q.look('head', 1, 1, 25, -25, 25, -25)",
"q.look('neck', 1, 1, 25, -25, 25, -25)",
"q.bedrock('garchomp', 'ride_jump_land')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('garchomp', 'ride_ground_run')"
}
"q.bedrock('garchomp', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('garchomp', 'blink')"
@@ -212,10 +206,7 @@
"poseTypes": ["WALK"],
"animations": [
"q.bedrock('garchomp', 'surfacewater_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('garchomp', 'ride_surfacewater_swim')"
}
"q.bedrock('garchomp', 'ride_surfacewater_swim')"
],
"quirks": ["q.bedrock_quirk('garchomp', 'blink')"],
"transformedParts": [
@@ -236,10 +227,7 @@
"q.look('head', 1, 1, 25, -25, 25, -25)",
"q.look('neck', 1, 1, 25, -25, 25, -25)",
"q.bedrock('garchomp', 'surfacewater_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('garchomp', 'ride_surfacewater_swim')"
}
"q.bedrock('garchomp', 'ride_surfacewater_swim')"
],
"quirks": ["q.bedrock_quirk('garchomp', 'blink')"],
"transformedParts": [
@@ -260,10 +248,7 @@
"q.look('head', 1, 1, 25, -25, 25, -25)",
"q.look('neck', 1, 1, 25, -25, 25, -25)",
"q.bedrock('garchomp', 'surfacewater_swim')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('garchomp', 'ride_surfacewater_swim')"
}
"q.bedrock('garchomp', 'ride_surfacewater_swim')"
],
"quirks": ["q.bedrock_quirk('garchomp', 'blink')"],
"transformTicks": 5,
@@ -278,10 +263,7 @@
"q.look('head', 1, 1, 25, -25, 25, -25)",
"q.look('neck', 1, 1, 25, -25, 25, -25)",
"q.bedrock('garchomp', 'surfacewater_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('garchomp', 'ride_surfacewater_swim')"
}
"q.bedrock('garchomp', 'ride_surfacewater_swim')"
],
"quirks": ["q.bedrock_quirk('garchomp', 'blink')"]
},
@@ -67,7 +67,7 @@
"q.look('head', 1, 1, 0, 0, 45, -45)",
"q.bedrock('lickilicky', 'ground_walk')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('lickilicky', 'ride_ground_walk')"
}
],
@@ -83,7 +83,7 @@
"q.look('head', 1, 1, 0, 0, 45, -45)",
"q.bedrock('lickilicky', 'ground_run')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('lickilicky', 'ride_ground_run')"
}
],
@@ -43,10 +43,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhyperior', 'ride_jump')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhyperior', 'ride_ground_run')"
}
"q.bedrock('rhyperior', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('rhyperior', 'blink')"
@@ -60,10 +57,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhyperior', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhyperior', 'ride_ground_run')"
}
"q.bedrock('rhyperior', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('rhyperior', 'blink')"
@@ -87,10 +81,7 @@
"animations": [
"q.look('head')",
"q.bedrock('rhyperior', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('rhyperior', 'ride_ground_walk')"
}
"q.bedrock('rhyperior', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('rhyperior', 'blink')"]
},
@@ -88,10 +88,7 @@
"q.pitch_tilt('body')",
"q.look('neck', 1, 1, 0, 0, 2, -2)",
"q.bedrock('togekiss', 'air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('togekiss', 'ride_air_fly')"
}
"q.bedrock('togekiss', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('togekiss', 'blink')"
@@ -105,10 +102,7 @@
"q.look('head_ai', 1, 1, 15, 0, 0, 0)",
"q.bedrock('togekiss', 'air_glide')",
"q.bedrock('togekiss', 'ride_dive')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('togekiss', 'ride_air_fly')"
}
"q.bedrock('togekiss', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('togekiss', 'blink')"
@@ -65,10 +65,7 @@
"animations": [
"q.look('head')",
"q.bedrock('mamoswine', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('mamoswine', 'ride_ground_walk')"
}
"q.bedrock('mamoswine', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('mamoswine', 'blink')"]
},
@@ -78,10 +75,7 @@
"animations": [
"q.look('head')",
"q.bedrock('mamoswine', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('mamoswine', 'ride_ground_run')"
}
"q.bedrock('mamoswine', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('mamoswine', 'blink')"]
},
@@ -58,7 +58,7 @@
"q.look('head', 1, 1, 17.5, -45, 30, -30)",
"q.bedrock('serperior', 'ride_jump')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('serperior', 'ride_ground_run')"
}
],
@@ -90,7 +90,7 @@
"q.look('head', 1, 1, 17.5, -45, 30, -30)",
"q.bedrock('serperior', 'ground_run')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('serperior', 'ride_ground_run')"
}
],
@@ -71,10 +71,7 @@
"animations": [
"q.look('head_ai')",
"q.bedrock('zebstrika', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('zebstrika', 'ride_ground_walk')"
}
"q.bedrock('zebstrika', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('zebstrika', 'blink')"]
},
@@ -84,10 +81,7 @@
"animations": [
"q.look('head_ai')",
"q.bedrock('zebstrika', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('zebstrika', 'ride_ground_run')"
}
"q.bedrock('zebstrika', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('zebstrika', 'blink')"]
},
@@ -85,7 +85,7 @@
"q.look('head_ai', 1, 1, 30, -5, 25, -25)",
"q.bedrock('scolipede', 'ground_walk')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('scolipede', 'ride_ground_walk')"
}
],
@@ -103,7 +103,7 @@
"q.look('head_ai', 1, 1, 30, -5, 25, -25)",
"q.bedrock('scolipede', 'ground_run')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('scolipede', 'ride_ground_run')"
}
],
@@ -48,10 +48,7 @@
"condition": "!q.in_air && q.is_sprinting",
"animations": [
"q.bedrock('darmanitan', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('darmanitan', 'ride_ground_run')"
}
"q.bedrock('darmanitan', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('darmanitan', 'blink')"]
},
@@ -62,10 +59,7 @@
"animations": [
"q.look('head')",
"q.bedrock('darmanitan', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('darmanitan', 'ride_ground_walk')"
}
"q.bedrock('darmanitan', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('darmanitan', 'blink')",
@@ -78,10 +72,7 @@
"animations": [
"q.look('head')",
"q.bedrock('darmanitan', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('darmanitan', 'ride_ground_walk')"
}
"q.bedrock('darmanitan', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('darmanitan', 'blink')"]
},
@@ -75,10 +75,7 @@
"animations": [
"q.look('head')",
"q.bedrock('crustle', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('crustle', 'ride_ground_walk')"
}
"q.bedrock('crustle', 'ride_ground_walk')"
],
"quirks": ["q.bedrock_quirk('crustle', 'blink')"]
},
@@ -91,10 +88,7 @@
"animations": [
"q.look('head')",
"q.bedrock('crustle', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('crustle', 'ride_ground_run')"
}
"q.bedrock('crustle', 'ride_ground_run')"
],
"quirks": ["q.bedrock_quirk('crustle', 'blink')"]
},
@@ -79,10 +79,7 @@
"animations": [
"q.look('head')",
"q.bedrock('archeops', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('archeops', 'ride_ground_run')"
}
"q.bedrock('archeops', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('archeops', 'blink')"
@@ -110,10 +107,7 @@
"animations": [
"q.look('head')",
"q.bedrock('archeops', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('archeops', 'ride_ground_walk')"
}
"q.bedrock('archeops', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('archeops', 'blink')"
@@ -29,10 +29,7 @@
"isBattle": false,
"animations": [
"q.bedrock('klinklang', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('klinklang', 'ride_mount_run')"
}
"q.bedrock('klinklang', 'ride_mount_run')"
],
"quirks": [
"q.bedrock_quirk('klinklang', 'idle_quirk')"
@@ -76,10 +76,7 @@
"animations": [
"q.look('head', 1, 1, 0, 0, 45, -45)",
"q.bedrock('golurk', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('golurk', 'ride_ground_run')"
}
"q.bedrock('golurk', 'ride_ground_run')"
],
"transformTicks": 5,
"transformToTicks": 5
@@ -92,10 +89,7 @@
"animations": [
"q.look('head', 1, 1, 0, 0, 45, -45)",
"q.bedrock('golurk', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('golurk', 'ride_ground_run')"
}
"q.bedrock('golurk', 'ride_ground_run')"
],
"transformTicks": 5,
"transformToTicks": 5
@@ -39,7 +39,7 @@
"q.look('head_ai', 1, 1, 50, -30, 25, -25)",
"q.bedrock('braviary', 'ground_walk')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('braviary', 'ride_ground_walk')"
}
],
@@ -68,7 +68,7 @@
"q.look('head_ai', 1, 1, 50, -30, 25, -25)",
"q.bedrock('braviary', 'air_fly')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('braviary', 'ride_air_fly')"
}
],
@@ -86,7 +86,7 @@
"q.bedrock('braviary', 'air_glide')",
"q.bedrock('braviary', 'ride_air_dive')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('braviary', 'ride_air_glide')"
}
],
@@ -39,7 +39,7 @@
"q.look('head_ai', 1, 1, 50, -10, 25, -25)",
"q.bedrock('braviary_hisuian', 'ground_walk')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('braviary_hisuian', 'ride_ground_walk')"
}
],
@@ -68,7 +68,7 @@
"q.look('head_ai', 1, 1, 50, -10, 25, -25)",
"q.bedrock('braviary_hisuian', 'air_fly')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('braviary_hisuian', 'ride_air_fly')"
}
],
@@ -86,7 +86,7 @@
"q.bedrock('braviary_hisuian', 'air_glide')",
"q.bedrock('braviary_hisuian', 'ride_air_dive')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('braviary', 'ride_air_glide')"
}
],
@@ -133,7 +133,7 @@
"q.bedrock('hydreigon', 'air_glide')",
"q.bedrock('hydreigon', 'ride_dive')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('hydreigon', 'ride_air_glide')"
}
],
@@ -83,7 +83,7 @@
"q.look('head')",
"q.bedrock('skiddo', 'ground_walk')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('skiddo', 'ride_ground_walk')"
}
],
@@ -101,7 +101,7 @@
"q.look('head')",
"q.bedrock('skiddo', 'ground_run')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('skiddo', 'ride_ground_run')"
}
],
@@ -73,7 +73,7 @@
"q.look('head')",
"q.bedrock('gogoat', 'ground_walk')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('gogoat', 'ride_ground_walk')"
}
],
@@ -91,7 +91,7 @@
"q.look('head')",
"q.bedrock('gogoat', 'ground_run')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('gogoat', 'ride_ground_run')"
}
],
@@ -68,10 +68,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tyrantrum', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tyrantrum', 'ride_ground_run')"
}
"q.bedrock('tyrantrum', 'ride_ground_run')"
]
},
"run": {
@@ -81,10 +78,7 @@
"animations": [
"q.look('head')",
"q.bedrock('tyrantrum', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('tyrantrum', 'ride_ground_run')"
}
"q.bedrock('tyrantrum', 'ride_ground_run')"
]
},
"sleep": {
@@ -25,10 +25,7 @@
"animations": [
"q.look('head_ai')",
"q.bedrock('noivern', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('noivern', 'ride_ground')"
}
"q.bedrock('noivern', 'ride_ground')"
],
"quirks": [
"q.bedrock_quirk('noivern', 'blink')"
@@ -47,10 +44,7 @@
"animations": [
"q.look('head_ai')",
"q.bedrock('noivern', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('noivern', 'ride_ground')"
}
"q.bedrock('noivern', 'ride_ground')"
],
"quirks": [
"q.bedrock_quirk('noivern', 'blink')"
@@ -62,10 +56,7 @@
"animations": [
"q.look('head_ai')",
"q.bedrock('noivern', 'ground_run2')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('noivern', 'ride_ground')"
}
"q.bedrock('noivern', 'ride_ground')"
],
"quirks": [
"q.bedrock_quirk('noivern', 'blink')"
@@ -87,10 +78,7 @@
"animations": [
"q.look('head_ai')",
"q.bedrock('noivern', 'air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('noivern', 'ride_air_fly')"
}
"q.bedrock('noivern', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('noivern', 'blink')"
@@ -103,10 +91,7 @@
"q.look('head_ai')",
"q.bedrock('noivern', 'air_glide')",
"q.bedrock('noivern', 'ride_dive')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('noivern', 'ride_air_glide')"
}
"q.bedrock('noivern', 'ride_air_glide')"
],
"quirks": [
"q.bedrock_quirk('noivern', 'blink')"
@@ -44,10 +44,7 @@
"animations": [
"q.look('head_ai', 1, -1, 70, -45, 45, -45)",
"q.bedrock('drampa', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('drampa', 'ride_ground_walk')"
}
"q.bedrock('drampa', 'ride_ground_walk')"
],
"quirks": []
},
@@ -74,10 +71,7 @@
"animations": [
"q.look('head_ai', 1, -1, 70, -45, 45, -45)",
"q.bedrock('drampa', 'air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('drampa', 'ride_air_fly')"
}
"q.bedrock('drampa', 'ride_air_fly')"
],
"quirks": []
}
@@ -79,7 +79,7 @@
"q.bedrock('corviknight', 'air_fly')",
"q.bedrock('corviknight', 'wangs')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('corviknight', 'ride_air_fly')"
}
],
@@ -99,7 +99,7 @@
"q.bedrock('corviknight', 'ride_dive')",
"q.bedrock('corviknight', 'wangs')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('corviknight', 'ride_air_fly')"
}
],
@@ -118,7 +118,7 @@
"q.bedrock('corviknight', 'ride_jump')",
"q.bedrock('corviknight', 'folded_wangs')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('corviknight', 'ride_ground_run')"
}
],
@@ -136,7 +136,7 @@
"q.bedrock('corviknight', 'ground_walk')",
"q.bedrock('corviknight', 'folded_wangs')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('corviknight', 'ride_ground_walk')"
}
],
@@ -154,7 +154,7 @@
"q.bedrock('corviknight', 'ground_run')",
"q.bedrock('corviknight', 'folded_wangs')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('corviknight', 'ride_ground_run')"
}
],
@@ -26,7 +26,7 @@
"animations": [
"q.bedrock('duraludon', 'ground_idle')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('duraludon', 'ride_ground_run')"
}
],
@@ -60,7 +60,7 @@
"animations": [
"q.bedrock('duraludon', 'ground_walk')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('duraludon', 'ride_ground_run')"
}
],
@@ -77,7 +77,7 @@
"animations": [
"q.bedrock('duraludon', 'ground_run')",
{
"condition": "q.is_ridden && q.has_entity",
"condition": "q.has_entity",
"animation": "q.bedrock('duraludon', 'ride_ground_run')"
}
],
@@ -90,10 +90,7 @@
"condition": "q.in_air",
"animations": [
"q.bedrock('duraludon', 'ride_jump')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('duraludon', 'ride_ground_run')"
}
"q.bedrock('duraludon', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('duraludon', 'blink')"
@@ -56,10 +56,7 @@
"poseTypes": ["WALK"],
"condition": "!q.in_air && !q.is_sprinting",
"animations": ["q.look('head')", "q.bedrock('dragapult', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('dragapult', 'ride_ground_walk')"
}
"q.bedrock('dragapult', 'ride_ground_walk')"
],
"quirks": [
"q.bedrock_quirk('dragapult', 'blink')",
@@ -72,10 +69,7 @@
"poseTypes": ["WALK"],
"condition": "!q.in_air && q.is_sprinting",
"animations": ["q.look('head')", "q.bedrock('dragapult', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('dragapult', 'ride_ground_run')"
}
"q.bedrock('dragapult', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('dragapult', 'blink')",
@@ -104,10 +98,7 @@
"animations": [
"q.pitch_tilt('body')",
"q.bedrock('dragapult', 'air_glide')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('dragapult', 'ride_air_glide')"
}
"q.bedrock('dragapult', 'ride_air_glide')"
],
"quirks": [
"q.bedrock_quirk('dragapult', 'blink')",
@@ -125,10 +116,7 @@
"q.look('head')",
"q.pitch_tilt('body')",
"q.bedrock('dragapult', 'air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('dragapult', 'ride_air_fly')"
}
"q.bedrock('dragapult', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('dragapult', 'blink')",
@@ -159,10 +147,7 @@
"q.pitch_tilt('body')",
"q.look('head')",
"q.bedrock('dragapult', 'air_fly')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('dragapult', 'ride_air_fly')"
}
"q.bedrock('dragapult', 'ride_air_fly')"
],
"quirks": [
"q.bedrock_quirk('dragapult', 'blink')",
@@ -36,10 +36,7 @@
"animations": [
"q.look('head')",
"q.bedrock('wyrdeer', 'ground_idle')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('wyrdeer', 'ride_ground_run')"
}
"q.bedrock('wyrdeer', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('wyrdeer', 'blink')","q.bedrock_quirk('wyrdeer', 'quirk_idle', 30, 60, 1)"
@@ -71,10 +68,7 @@
"animations": [
"q.look('head')",
"q.bedrock('wyrdeer', 'ground_walk')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('wyrdeer', 'ride_ground_run')"
}
"q.bedrock('wyrdeer', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('wyrdeer', 'blink')"
@@ -89,10 +83,7 @@
"animations": [
"q.look('head')",
"q.bedrock('wyrdeer', 'ground_run')",
{
"condition": "q.is_ridden",
"animation": "q.bedrock('wyrdeer', 'ride_ground_run')"
}
"q.bedrock('wyrdeer', 'ride_ground_run')"
],
"quirks": [
"q.bedrock_quirk('wyrdeer', 'blink')"

Some files were not shown because too many files have changed in this diff Show More