mirror of
https://gitlab.com/cable-mc/cobblemon.git
synced 2026-10-23 15:46:24 -04:00
Compare commits
15
Commits
main
...
mon-smoothening
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d54e51fb9 | ||
|
|
e3c1fff02a | ||
|
|
c672a4d690 | ||
|
|
e64df48441 | ||
|
|
acd375f533 | ||
|
|
9076c659de | ||
|
|
9292504f8c | ||
|
|
ad484abf56 | ||
|
|
fc1c7cf6ca | ||
|
|
33abca9b43 | ||
|
|
ea3c051772 | ||
|
|
4cde730259 | ||
|
|
2bcce06318 | ||
|
|
930c3568bb | ||
|
|
ffe480b12f |
@@ -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
|
||||
|
||||
+9
-3
@@ -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
|
||||
)
|
||||
|
||||
+9
-4
@@ -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
|
||||
|
||||
+47
-31
@@ -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) }
|
||||
}
|
||||
}
|
||||
+240
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -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)
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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())
|
||||
|
||||
+6
-36
@@ -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()))
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -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) {
|
||||
|
||||
+15
-3
@@ -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)
|
||||
|
||||
+5
-2
@@ -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)
|
||||
}
|
||||
+97
@@ -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)
|
||||
}
|
||||
}
|
||||
+66
@@ -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)
|
||||
}
|
||||
}
|
||||
+258
-40
@@ -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.
|
||||
|
||||
+26
-6
@@ -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
|
||||
|
||||
+61
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -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) {}
|
||||
}
|
||||
+35
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -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)
|
||||
}
|
||||
}
|
||||
+31
@@ -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()
|
||||
}
|
||||
}
|
||||
+31
@@ -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)
|
||||
}
|
||||
+3
-12
@@ -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')"
|
||||
|
||||
+4
-16
@@ -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')"
|
||||
|
||||
+3
-12
@@ -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')"
|
||||
|
||||
+1
-4
@@ -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')"
|
||||
|
||||
+2
-2
@@ -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')"
|
||||
}
|
||||
],
|
||||
|
||||
+3
-12
@@ -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')"]
|
||||
},
|
||||
|
||||
+4
-4
@@ -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')"
|
||||
}
|
||||
],
|
||||
|
||||
+4
-16
@@ -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')"]
|
||||
},
|
||||
|
||||
+4
-16
@@ -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')"]
|
||||
},
|
||||
|
||||
+4
-16
@@ -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')"]
|
||||
},
|
||||
|
||||
+7
-28
@@ -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')"]
|
||||
},
|
||||
|
||||
+4
-16
@@ -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')"]
|
||||
},
|
||||
|
||||
+8
-32
@@ -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')",
|
||||
|
||||
+4
-16
@@ -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')",
|
||||
|
||||
+4
-16
@@ -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')"
|
||||
|
||||
+4
-4
@@ -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')"
|
||||
}
|
||||
],
|
||||
|
||||
+2
-8
@@ -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')"
|
||||
|
||||
+2
-2
@@ -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')"
|
||||
}
|
||||
],
|
||||
|
||||
+4
-16
@@ -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')"
|
||||
|
||||
+2
-8
@@ -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
|
||||
|
||||
+2
-8
@@ -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')"
|
||||
|
||||
+5
-20
@@ -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')"
|
||||
|
||||
+2
-8
@@ -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')"]
|
||||
},
|
||||
|
||||
+3
-12
@@ -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')"]
|
||||
},
|
||||
|
||||
+3
-12
@@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+3
-12
@@ -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')"]
|
||||
}
|
||||
|
||||
+4
-16
@@ -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')"
|
||||
|
||||
+2
-8
@@ -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')"
|
||||
|
||||
+3
-12
@@ -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')"]
|
||||
},
|
||||
|
||||
+3
-12
@@ -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')"]
|
||||
},
|
||||
|
||||
+2
-8
@@ -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')"
|
||||
|
||||
+6
-6
@@ -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')"
|
||||
}
|
||||
],
|
||||
|
||||
+1
-4
@@ -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')"
|
||||
|
||||
+2
-8
@@ -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')"
|
||||
]
|
||||
|
||||
+3
-3
@@ -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')"
|
||||
},
|
||||
{
|
||||
|
||||
+2
-8
@@ -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": {
|
||||
|
||||
+2
-8
@@ -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": {
|
||||
|
||||
+5
-20
@@ -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')"
|
||||
|
||||
+6
-24
@@ -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')"]
|
||||
},
|
||||
|
||||
+2
-2
@@ -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')"
|
||||
}
|
||||
],
|
||||
|
||||
+3
-12
@@ -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')"]
|
||||
},
|
||||
|
||||
+2
-8
@@ -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')"
|
||||
|
||||
+2
-8
@@ -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')"]
|
||||
},
|
||||
|
||||
+2
-2
@@ -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')"
|
||||
}
|
||||
],
|
||||
|
||||
+2
-8
@@ -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')"]
|
||||
},
|
||||
|
||||
+2
-2
@@ -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')"
|
||||
}
|
||||
],
|
||||
|
||||
+3
-12
@@ -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')"]
|
||||
},
|
||||
|
||||
+2
-8
@@ -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')"]
|
||||
},
|
||||
|
||||
+2
-8
@@ -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')"
|
||||
|
||||
+1
-4
@@ -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')"
|
||||
|
||||
+2
-8
@@ -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
|
||||
|
||||
+3
-3
@@ -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')"
|
||||
}
|
||||
],
|
||||
|
||||
+3
-3
@@ -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')"
|
||||
}
|
||||
],
|
||||
|
||||
+1
-1
@@ -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')"
|
||||
}
|
||||
],
|
||||
|
||||
+2
-2
@@ -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')"
|
||||
}
|
||||
],
|
||||
|
||||
+2
-2
@@ -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')"
|
||||
}
|
||||
],
|
||||
|
||||
+2
-8
@@ -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": {
|
||||
|
||||
+5
-20
@@ -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')"
|
||||
|
||||
+2
-8
@@ -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": []
|
||||
}
|
||||
|
||||
+5
-5
@@ -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')"
|
||||
}
|
||||
],
|
||||
|
||||
+4
-7
@@ -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')"
|
||||
|
||||
+5
-20
@@ -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')",
|
||||
|
||||
+3
-12
@@ -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
Reference in New Issue
Block a user