Completed pokeball capturing outside of battle, improved pokeball collision and motion, added some useful maths for parabolas

This commit is contained in:
Paul
2021-12-29 16:53:38 +10:00
committed by Paul
parent 84d630b759
commit f3906147ce
33 changed files with 507 additions and 253 deletions
Binary file not shown.
@@ -0,0 +1,34 @@
package com.cablemc.pokemoncobbled.client.entity
import com.cablemc.pokemoncobbled.client.render.models.blockbench.PoseableEntityState
import com.cablemc.pokemoncobbled.client.render.models.blockbench.frame.PokeBallFrame
import com.cablemc.pokemoncobbled.client.render.pokeball.animation.OpenAnimation
import com.cablemc.pokemoncobbled.client.render.pokeball.animation.ShakeAnimation
import com.cablemc.pokemoncobbled.common.api.entity.EntitySideDelegate
import com.cablemc.pokemoncobbled.common.api.reactive.Observable.Companion.emitWhile
import com.cablemc.pokemoncobbled.common.entity.pokeball.EmptyPokeBallEntity
import com.cablemc.pokemoncobbled.common.entity.pokeball.EmptyPokeBallEntity.CaptureState
import com.cablemc.pokemoncobbled.common.entity.pokeball.EmptyPokeBallEntity.CaptureState.HIT
import com.cablemc.pokemoncobbled.common.entity.pokeball.EmptyPokeBallEntity.CaptureState.SHAKE
import com.cablemc.pokemoncobbled.common.entity.pokeball.PokeBallDelegate
class EmptyPokeBallClientDelegate : PoseableEntityState<EmptyPokeBallEntity>(), PokeBallDelegate<EmptyPokeBallEntity> {
override fun initialize(entity: EmptyPokeBallEntity) {
entity.captureState.subscribe {
when (CaptureState.values()[it.toInt()]) {
HIT -> {
statefulAnimations.add(OpenAnimation())
}
CaptureState.FALL -> {
statefulAnimations.clear()
}
SHAKE -> {
entity.shakeEmitter
.pipe(emitWhile { entity.captureState.get() == SHAKE.ordinal.toByte() })
.subscribe { statefulAnimations.add(ShakeAnimation(0.8F)) }
}
else -> {}
}
}
}
}
@@ -0,0 +1,7 @@
package com.cablemc.pokemoncobbled.client.entity
import com.cablemc.pokemoncobbled.client.render.models.blockbench.PoseableEntityState
import com.cablemc.pokemoncobbled.common.entity.pokeball.OccupiedPokeBallEntity
import com.cablemc.pokemoncobbled.common.entity.pokeball.PokeBallDelegate
class OccupiedPokeBallClientDelegate : PoseableEntityState<OccupiedPokeBallEntity>(), PokeBallDelegate<OccupiedPokeBallEntity>
@@ -1,32 +0,0 @@
package com.cablemc.pokemoncobbled.client.entity
import com.cablemc.pokemoncobbled.client.render.models.blockbench.PoseableEntityState
import com.cablemc.pokemoncobbled.client.render.models.blockbench.frame.PokeBallFrame
import com.cablemc.pokemoncobbled.client.render.pokeball.animation.OpenAnimation
import com.cablemc.pokemoncobbled.common.api.entity.EntitySideDelegate
import com.cablemc.pokemoncobbled.common.entity.pokeball.EmptyPokeBallEntity
import com.cablemc.pokemoncobbled.common.entity.pokeball.EmptyPokeBallEntity.CaptureState
import com.cablemc.pokemoncobbled.common.entity.pokeball.EmptyPokeBallEntity.CaptureState.HIT
import com.cablemc.pokemoncobbled.common.entity.pokeball.EmptyPokeBallEntity.CaptureState.SHAKE
import com.cablemc.pokemoncobbled.common.entity.pokeball.PokeBallEntity
class PokeBallClientDelegate : PoseableEntityState<PokeBallEntity>(), EntitySideDelegate<PokeBallEntity> {
override fun initialize(entity: PokeBallEntity) {
if (entity is EmptyPokeBallEntity) {
entity.captureState.subscribe {
when (CaptureState.values()[it.toInt()]) {
HIT -> {
statefulAnimations.add(OpenAnimation(currentModel as PokeBallFrame))
}
CaptureState.FALL -> {
}
SHAKE -> {
}
else -> {}
}
}
}
}
}
@@ -98,7 +98,6 @@ abstract class PoseableEntityModel<T : Entity> : EntityModel<T>(), ModelFrame {
if (previousPose != null && pose.transformTicks > 0) {
state.statefulAnimations.add(
PoseTransitionAnimation(
frame = this,
beforePose = previousPose,
afterPose = pose,
durationTicks = pose.transformTicks
@@ -10,7 +10,7 @@ import net.minecraft.world.entity.Entity
/**
* Represents the entity-specific state for a poseable model. The implementation is responsible for
* handling all of the state for an entity's model, and needs to be conscious of the fact that the
* handling all the state for an entity's model, and needs to be conscious of the fact that the
* model may change without this state changing.
*
* @author Hiroku
@@ -49,7 +49,7 @@ abstract class PoseableEntityState<T : Entity> {
return
}
val animation = PoseTransitionAnimation(model, beforePose, afterPose, durationTicks)
val animation = PoseTransitionAnimation(beforePose, afterPose, durationTicks)
statefulAnimations.add(animation)
}
@@ -16,11 +16,10 @@ import java.lang.Float.min
* @since December 5th, 2021
*/
class PoseTransitionAnimation<T : Entity>(
frame: ModelFrame,
beforePose: Pose<T, *>,
val afterPose: Pose<T, *>,
private val durationTicks: Int = 20
) : StatefulAnimation<T, ModelFrame>(frame) {
) : StatefulAnimation<T, ModelFrame> {
var passedTicks = 0F
val transforms = mutableListOf<GradualTransform>()
@@ -0,0 +1,31 @@
package com.cablemc.pokemoncobbled.client.render.models.blockbench.animation
import com.cablemc.pokemoncobbled.client.render.models.blockbench.PoseableEntityModel
import com.cablemc.pokemoncobbled.client.render.models.blockbench.frame.PokeBallFrame
import com.cablemc.pokemoncobbled.common.entity.pokeball.EmptyPokeBallEntity
import com.cablemc.pokemoncobbled.common.entity.pokeball.PokeBallEntity
import net.minecraft.util.Mth
import net.minecraft.util.Mth.PI
/**
* A simple idle animation that will point the PokéBall in the direction of the target it hit. Does nothing
* for occupied PokéBalls.
*
* @author Hiroku
* @since December 25th, 2021
*/
class RootPokeBallLookAnimation(frame: PokeBallFrame) : StatelessAnimation<PokeBallEntity, PokeBallFrame>(frame) {
override val targetFrame = PokeBallFrame::class.java
override fun setAngles(entity: PokeBallEntity?, model: PoseableEntityModel<PokeBallEntity>, limbSwing: Float, limbSwingAmount: Float, ageInTicks: Float, headYaw: Float, headPitch: Float) {
val yRot = entity?.let {
if (it is EmptyPokeBallEntity) {
val dispX = it.hitTargetPosition.get().x - it.x
val dispZ = it.hitTargetPosition.get().z - it.z
Mth.atan2(-dispZ, dispX) + PI / 2
} else {
0F
}
}?.toFloat() ?: 0F
model.rootPart.yRot += yRot
}
}
@@ -12,14 +12,14 @@ import net.minecraft.world.entity.Entity
* @author Hiroku
* @since December 5th, 2021
*/
abstract class StatefulAnimation<T : Entity, F : ModelFrame>(val frame: F) {
interface StatefulAnimation<T : Entity, F : ModelFrame> {
/**
* Whether this animation should prevent the given idle animation from occurring.
*
* This is for cases where this animation and the idle animation work on the same parts
* of the model and would conflict.
*/
abstract fun preventsIdle(entity: T, idleAnimation: StatelessAnimation<T, *>): Boolean
/** Runs the animation. You can access the frame from the class properties. Returns true if the animation should continue. */
abstract fun run(entity: T, model: PoseableEntityModel<T>): Boolean
fun preventsIdle(entity: T, idleAnimation: StatelessAnimation<T, *>): Boolean
/** Runs the animation. You can check that the model fits a particular frame. Returns true if the animation should continue. */
fun run(entity: T, model: PoseableEntityModel<T>): Boolean
}
@@ -3,5 +3,6 @@ package com.cablemc.pokemoncobbled.client.render.models.blockbench.frame
import net.minecraft.client.model.geom.ModelPart
interface PokeBallFrame : ModelFrame {
val subRoot: ModelPart
val lid: ModelPart
}
@@ -1,11 +1,15 @@
package com.cablemc.pokemoncobbled.client.render.models.blockbench.pokeball
import com.cablemc.pokemoncobbled.client.entity.PokeBallClientDelegate
import com.cablemc.pokemoncobbled.client.render.models.blockbench.PoseableEntityModel
import com.cablemc.pokemoncobbled.client.render.models.blockbench.animation.RotationFunctionStatelessAnimation
import com.cablemc.pokemoncobbled.client.render.models.blockbench.PoseableEntityState
import com.cablemc.pokemoncobbled.client.render.models.blockbench.animation.RootPokeBallLookAnimation
import com.cablemc.pokemoncobbled.client.render.models.blockbench.frame.PokeBallFrame
import com.cablemc.pokemoncobbled.client.render.models.blockbench.getChildOf
import com.cablemc.pokemoncobbled.client.render.models.blockbench.pose.PoseType
import com.cablemc.pokemoncobbled.client.render.models.blockbench.pose.TransformedModelPart.Companion.Y_AXIS
import com.cablemc.pokemoncobbled.common.PokemonCobbled
import com.cablemc.pokemoncobbled.common.entity.pokeball.EmptyPokeBallEntity
import com.cablemc.pokemoncobbled.common.entity.pokeball.OccupiedPokeBallEntity
import com.cablemc.pokemoncobbled.common.entity.pokeball.PokeBallEntity
import net.minecraft.client.model.geom.ModelLayerLocation
import net.minecraft.client.model.geom.ModelPart
@@ -18,18 +22,38 @@ import net.minecraft.resources.ResourceLocation
import net.minecraft.util.Mth.PI
class PokeBallModel(root: ModelPart) : PoseableEntityModel<PokeBallEntity>(), PokeBallFrame {
override val rootPart = registerRelevantPart(root.getChild("pokeball"))
override val lid = registerRelevantPart(root.getChild("pokeball").getChild("pokeball_lid"))
override val rootPart = registerRelevantPart(root.getChild("root"))
override val subRoot = registerRelevantPart(rootPart.getChild("pokeball"))
override val lid = registerRelevantPart(subRoot.getChild("pokeball_lid"))
override fun registerPoses() {
registerPose(
poseType = PoseType.NONE,
condition = { true },
condition = { it is EmptyPokeBallEntity && it.captureState.get() != EmptyPokeBallEntity.CaptureState.NOT.ordinal.toByte() },
idleAnimations = arrayOf(RootPokeBallLookAnimation(this)),
transformTicks = 0,
transformedParts = arrayOf()
)
registerPose(
poseType = PoseType.WALK,
condition = { it is OccupiedPokeBallEntity || (it as EmptyPokeBallEntity).captureState.get() == EmptyPokeBallEntity.CaptureState.NOT.ordinal.toByte() },
transformTicks = 0,
idleAnimations = arrayOf(
rootPart.rotation(
function = { t -> t * PI / 10 }, // 1 rotation per second = 2pi per 20 ticks = 2pi / 20 = pi / 10 per tick
axis = Y_AXIS,
timeVariable = { _, _, ageInTicks -> ageInTicks }
),
rootPart.translation(
function = { t ->
if (t > 8) {
0F
} else {
-(8F - t) * 3F
}
},
axis = Y_AXIS,
timeVariable = { _, _, ageInTicks -> ageInTicks }
)
),
transformedParts = arrayOf()
@@ -38,15 +62,20 @@ class PokeBallModel(root: ModelPart) : PoseableEntityModel<PokeBallEntity>(), Po
companion object {
// This layer location should be baked with EntityRendererProvider.Context in the entity renderer and passed into this model's constructor
val LAYER_LOCATION = ModelLayerLocation(ResourceLocation("modid", "pokeball"), "main")
val LAYER_LOCATION = ModelLayerLocation(ResourceLocation(PokemonCobbled.MODID, "pokeball"), "main")
fun createBodyLayer(): LayerDefinition {
val meshdefinition = MeshDefinition()
val partdefinition = meshdefinition.root
val pokeball = partdefinition.addOrReplaceChild(
val root = partdefinition.addOrReplaceChild(
"root",
CubeListBuilder.create(),
PartPose.offsetAndRotation(0.0f, 0.0f, 0.0f, PI, 0F, 0F)
)
val pokeball = root.addOrReplaceChild(
"pokeball",
CubeListBuilder.create().texOffs(0, 0)
.addBox(-4.0f, -4.0f, -4.0f, 8.0f, 4.0f, 8.0f, CubeDeformation(0.0f)),
PartPose.offsetAndRotation(0.0f, 24.0f, 0.0f, PI, 0F, 0F)
PartPose.offsetAndRotation(0.0f, 0.0f, 0.0f, 0F, 0F, 0F)
)
val pokeball_lid = pokeball.addOrReplaceChild(
"pokeball_lid",
@@ -58,5 +87,5 @@ class PokeBallModel(root: ModelPart) : PoseableEntityModel<PokeBallEntity>(), Po
}
}
override fun getState(entity: PokeBallEntity) = entity.delegate as PokeBallClientDelegate
override fun getState(entity: PokeBallEntity) = entity.delegate as PoseableEntityState<PokeBallEntity>
}
@@ -1,8 +1,11 @@
package com.cablemc.pokemoncobbled.client.render.models.blockbench.wavefunction
import net.minecraft.util.Mth.PI
import net.minecraft.util.Mth.sqrt
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sign
import kotlin.math.sin
typealias WaveFunction = (Float) -> Float
@@ -14,12 +17,65 @@ fun gradient(function: WaveFunction, t0: Float, t1: Float) = (function(t1) - fun
operator fun WaveFunction.plus(other: WaveFunction): WaveFunction = { t -> this(t) + other(t) }
operator fun WaveFunction.times(other: WaveFunction): WaveFunction = { t -> this(t) * other(t) }
fun WaveFunction.aggregate(func: WaveFunction): WaveFunction = { t -> func(this(t)) }
fun linearFunction(gradient: Float = 1F, yIntercept: Float = 0F): WaveFunction = { t -> gradient * t + yIntercept }
fun sineFunction(amplitude: Float = 1F, period: Float = 1F, phaseShift: Float = 0F, verticalShift: Float = 0F): WaveFunction = { t -> sin(2*PI/period * (t - phaseShift)) * amplitude + verticalShift }
fun cosineFunction(amplitude: Float = 1F, period: Float = 1F, phaseShift: Float = 0F, verticalShift: Float = 0F): WaveFunction = { t -> cos(2*PI/period * (t - phaseShift)) * amplitude + verticalShift }
fun triangleFunction(amplitude: Float = 1F, period: Float = 1F, phaseShift: Float = 0F, verticalShift: Float = 0F): WaveFunction = { t ->
val timeTerm = ((t + 3 * period / 4 - phaseShift) % period) - period/2
var time = t
while (time < 0) {
time += period
}
val timeTerm = ((time + 3 * period / 4 - phaseShift) % period) - period/2
val value = 4 * amplitude / period * abs(timeTerm) - amplitude + verticalShift
value
}
/**
* Returns a parabola that intercepts the y axis at zero, intercepts the x axis again
* at [period], and at its furthest from y = 0 will reach [peak].
*
* The maths used to calculate this was a bit confusing but I've added my working to the
* docs/parabolaworking.pdf file in the Cobbled repository.
*/
fun parabolaFunction(
peak: Float,
period: Float
) = parabolaFunction(
tightness = -4 * peak / period.pow(2),
verticalShift = peak,
phaseShift = period / 2
)
fun parabolaFunction(
tightness: Float = -1F,
phaseShift: Float = 0F,
verticalShift: Float = 1F
): WaveFunction {
/*
* To understand the parameters for this, type y=t(x-p)+v into https://www.desmos.com/calculator and play
* around with tightness, phase shift, and vertical shift
*/
val a = tightness
val b = -2 * phaseShift * tightness
val c = tightness * phaseShift * phaseShift + verticalShift
val root1 = (-b - sqrt(b*b - 4 * a * c)) / (2 * tightness)
val root2 = (-b + sqrt(b*b - 4 * a * c)) / (2 * tightness)
val tMin = if (root1 < root2) root1 else root2
val tMax = if (root1 < root2) root2 else root1
val period = tMax - tMin
return { t ->
var time = t
while (time < tMin) {
time += period
}
while (time > tMax) {
time -= period
}
tightness * (time - phaseShift).pow(2) + verticalShift
}
}
@@ -30,9 +30,9 @@ class PokeBallRenderer<T : PokeBallEntity>(context: EntityRendererProvider.Conte
) {
val model = PokeBallModelRepository.getModel(pEntity.pokeBall).entityModel
pMatrixStack.pushPose()
pMatrixStack.translate(0.0, -1.0, 0.0)
pMatrixStack.scale(0.7F, 0.7F, 0.7F)
val vertexconsumer = ItemRenderer.getFoilBufferDirect(pBuffer, model.renderType(getTextureLocation(pEntity)), false, false)
model.setupAnim(pEntity, 0f, 0f, pEntity.tickCount + pPartialTicks, pEntity.yRot, pEntity.xRot)
model.setupAnim(pEntity, 0f, 0f, pEntity.tickCount + pPartialTicks, pEntity.yRotO, pEntity.xRotO)
model.renderToBuffer(pMatrixStack, vertexconsumer, pPackedLight, OverlayTexture.NO_OVERLAY, 1.0f, 1.0f, 1.0f, 1.0f)
pMatrixStack.popPose()
super.render(pEntity, pEntityYaw, pPartialTicks, pMatrixStack, pBuffer, pPackedLight)
@@ -1,15 +0,0 @@
package com.cablemc.pokemoncobbled.client.render.pokeball.animation
import net.minecraft.client.model.EntityModel
interface ModelAnimation<T : EntityModel<*>> {
/** The current frame the animation is on. If this is null, the animation is not currently running. */
val currentFrame: Int?
/** Animates the model. */
fun animate(model: T, limbSwing: Float, limbSwingAmout: Float, ageInTicks: Float, netHeadYaw: Float, headPitch: Float, frame: Int = 0)
fun resetAnimation()
}
@@ -4,18 +4,38 @@ import com.cablemc.pokemoncobbled.client.render.models.blockbench.PoseableEntity
import com.cablemc.pokemoncobbled.client.render.models.blockbench.animation.StatefulAnimation
import com.cablemc.pokemoncobbled.client.render.models.blockbench.animation.StatelessAnimation
import com.cablemc.pokemoncobbled.client.render.models.blockbench.frame.PokeBallFrame
import com.cablemc.pokemoncobbled.common.entity.pokeball.PokeBallEntity
import com.cablemc.pokemoncobbled.common.entity.pokeball.EmptyPokeBallEntity
import com.cablemc.pokemoncobbled.common.util.math.geometry.toRadians
import com.cablemc.pokemoncobbled.common.util.math.toVec3
import net.minecraft.commands.arguments.EntityAnchorArgument
import net.minecraft.util.Mth
import net.minecraft.util.Mth.PI
import net.minecraft.world.phys.Vec3
import kotlin.math.min
class OpenAnimation(frame: PokeBallFrame) : StatefulAnimation<PokeBallEntity, PokeBallFrame>(frame) {
/**
* Animation that opens a PokéBall upon hitting a Pokémon, and then shuts it.
*
* @author Hiroku
* @since December 24th, 2021
*/
class OpenAnimation : StatefulAnimation<EmptyPokeBallEntity, PokeBallFrame> {
companion object {
const val DURATION_SECONDS = 3F
const val OPEN_START = 0.1F
const val OPEN_END = 0.3F
const val CLOSE_START = 1F
const val CLOSE_END = 1.2F
const val OPEN_ANGLE = PI / 3F
}
var initialized = false
override fun preventsIdle(entity: PokeBallEntity, idleAnimation: StatelessAnimation<PokeBallEntity, *>) = true
override fun run(entity: PokeBallEntity, model: PoseableEntityModel<PokeBallEntity>): Boolean {
var startedClosing = false
var maxPitch = 0F
override fun preventsIdle(entity: EmptyPokeBallEntity, idleAnimation: StatelessAnimation<EmptyPokeBallEntity, *>) = true
override fun run(entity: EmptyPokeBallEntity, model: PoseableEntityModel<EmptyPokeBallEntity>): Boolean {
val frame = model as PokeBallFrame
if (!initialized) {
model.getState(entity).animationSeconds = 0F
initialized = true
@@ -23,19 +43,29 @@ class OpenAnimation(frame: PokeBallFrame) : StatefulAnimation<PokeBallEntity, Po
val animationSeconds = model.getState(entity).animationSeconds
when {
animationSeconds <= 1 -> {
frame.lid.xRot = -animationSeconds * PI / 3
}
animationSeconds > 2 -> {
frame.lid.xRot = -30f.toRadians() + ((animationSeconds - 2) * (1.5f).toRadians()) / 20F
}
else -> {
frame.lid.xRot = 30f.toRadians()
return false
entity.lookAt(EntityAnchorArgument.Anchor.EYES, entity.hitTargetPosition.get().toVec3())
val xDist = entity.hitTargetPosition.get().x - entity.x
val zDist = entity.hitTargetPosition.get().z - entity.z
frame.rootPart.yRot = Mth.atan2(-zDist, xDist).toFloat() + PI / 2
frame.rootPart.xRot = -entity.xRotO.toRadians() - PI / 2
val minPitch = -PI
if (animationSeconds >= OPEN_START && animationSeconds < CLOSE_START) {
val portion = min((animationSeconds - OPEN_START)/OPEN_END, 1F)
frame.lid.xRot = -portion * OPEN_ANGLE
} else if (animationSeconds >= CLOSE_START) {
if (!startedClosing) {
startedClosing = true
maxPitch = frame.rootPart.xRot
}
val portion = min((animationSeconds - CLOSE_START) / (CLOSE_END - CLOSE_START), 1F)
frame.lid.xRot = (portion - 1) * OPEN_ANGLE
val dist = maxPitch - minPitch
frame.rootPart.xRot = minPitch + (1 - portion) * dist
}
return true
return entity.captureState.get() == EmptyPokeBallEntity.CaptureState.HIT.ordinal.toByte()
}
}
@@ -1,47 +1,51 @@
package com.cablemc.pokemoncobbled.client.render.pokeball.animation
import com.cablemc.pokemoncobbled.client.render.models.blockbench.pokeball.PokeBallModel
import com.cablemc.pokemoncobbled.common.util.math.geometry.toRadians
import net.minecraft.util.Mth
import com.cablemc.pokemoncobbled.client.render.models.blockbench.PoseableEntityModel
import com.cablemc.pokemoncobbled.client.render.models.blockbench.addRotation
import com.cablemc.pokemoncobbled.client.render.models.blockbench.animation.StatefulAnimation
import com.cablemc.pokemoncobbled.client.render.models.blockbench.animation.StatelessAnimation
import com.cablemc.pokemoncobbled.client.render.models.blockbench.frame.PokeBallFrame
import com.cablemc.pokemoncobbled.client.render.models.blockbench.pose.TransformedModelPart.Companion.Z_AXIS
import com.cablemc.pokemoncobbled.client.render.models.blockbench.wavefunction.linearFunction
import com.cablemc.pokemoncobbled.client.render.models.blockbench.wavefunction.sineFunction
import com.cablemc.pokemoncobbled.client.render.models.blockbench.wavefunction.times
import com.cablemc.pokemoncobbled.common.entity.pokeball.EmptyPokeBallEntity
import net.minecraft.util.Mth.PI
class ShakeAnimation(val numShakes: Int) : ModelAnimation<PokeBallModel> {
/**
* Animation that shakes a PokéBall on the specified axis, with the specified force.
* This will move it along that axis along a sine function whose highest amplitude is
* the force multiplied by PI / 3, meaning at force 1 it rotates 60 degrees to either
* side. This shaking will be dampened to nothing over the course of 1 second.
*
* @author Hiroku
* @since December 25th, 2021
*/
class ShakeAnimation(
val force: Float,
val axis: Int = Z_AXIS
) : StatefulAnimation<EmptyPokeBallEntity, PokeBallFrame> {
val shakeFunction = sineFunction(
amplitude = force * PI / 3,
period = 0.7F
) * linearFunction(
gradient = -1F,
yIntercept = 1F
)
override var currentFrame: Int? = null
private set
private var shakesComplete = 0
var initialized = false
override fun animate(
model: PokeBallModel,
limbSwing: Float,
limbSwingAmout: Float,
ageInTicks: Float,
netHeadYaw: Float,
headPitch: Float,
frame: Int
) {
currentFrame = frame % 63
model.rootPart.y = 20f
model.rootPart.xRot = 180f.toRadians()
model.rootPart.yRot = 0f
model.lid.xRot = 0f
if (shakesComplete < numShakes) {
if (currentFrame == 62) {
currentFrame = 0
shakesComplete++
}
if (currentFrame!! <= 42) {
model.rootPart.zRot = Mth.sin(currentFrame!!.toFloat() * 0.15f) * 30f.toRadians()
}
else {
model.rootPart.zRot = 0f
}
override fun preventsIdle(entity: EmptyPokeBallEntity, idleAnimation: StatelessAnimation<EmptyPokeBallEntity, *>) = false
override fun run(entity: EmptyPokeBallEntity, model: PoseableEntityModel<EmptyPokeBallEntity>): Boolean {
val frame = model as PokeBallFrame
val state = model.getState(entity)
if (!initialized) {
state.animationSeconds = 0F
initialized = true
}
else {
model.rootPart.zRot = 0f
}
}
override fun resetAnimation() {
currentFrame = 0
frame.subRoot.addRotation(axis = axis, differenceInRadians = shakeFunction(state.animationSeconds))
return state.animationSeconds < 1F
}
}
@@ -5,6 +5,7 @@ import com.cablemc.pokemoncobbled.common.api.pokemon.stats.Stats
import com.cablemc.pokemoncobbled.common.pokeball.PokeBall
import com.cablemc.pokemoncobbled.common.pokemon.Pokemon
import net.minecraft.server.level.ServerPlayer
import kotlin.math.pow
import kotlin.random.Random.Default.nextInt
/**
@@ -22,7 +23,7 @@ class Gen7CaptureCalculator : CaptureCalculator {
return CaptureContext(isSuccessfulCapture = true, isCriticalCapture = true, numberOfShakes = 1)
}
else {
val shakeProbability = (25536 / Math.pow(255 / catchRate.toDouble(), 3.0 / 16))
val shakeProbability = (25536 / (255 / catchRate.toDouble()).pow(3.0 / 16))
var numShakes = 0
for (i in 0..3) {
@@ -31,13 +32,13 @@ class Gen7CaptureCalculator : CaptureCalculator {
}
numShakes++
}
return CaptureContext(isSuccessfulCapture = true, isCriticalCapture = false, numberOfShakes = numShakes)
return CaptureContext(isSuccessfulCapture = numShakes == 4, isCriticalCapture = false, numberOfShakes = numShakes)
}
}
fun getCatchRate(player: ServerPlayer, pokemon: Pokemon, pokeBall: PokeBall): Float {
var catchRate = pokemon.species.catchRate.toFloat()
pokeBall.catchRateModifiers.forEach { modifier -> catchRate = modifier.modifyCatchRate(catchRate, player, pokemon) }
pokeBall.catchRateModifiers.forEach { catchRate = it.modifyCatchRate(catchRate, player, pokemon) }
val maxHealth = pokemon.getMaxHealth()
val currentHealth = pokemon.health
val statusBonus = getStatusBonus(pokemon)
@@ -1,6 +1,7 @@
package com.cablemc.pokemoncobbled.common.api.scheduling
import net.minecraft.util.Mth.ceil
import kotlin.math.roundToInt
fun after(ticks: Int = 0, seconds: Float = 0F, action: () -> Unit) {
ScheduledTaskTracker.addTask(
@@ -11,4 +12,19 @@ fun after(ticks: Int = 0, seconds: Float = 0F, action: () -> Unit) {
)
}
fun lerp(ticks: Int, action: (Float) -> Unit) {
var tick = 0F
action(tick / ticks)
taskBuilder().interval(1).iterations(Int.MAX_VALUE).execute { task ->
tick++
action(tick / ticks)
if (tick >= ticks) {
task.expire()
}
}
}
fun lerp(seconds: Float, action: (Float) -> Unit) = lerp(ticks = (seconds * 20F).roundToInt(), action)
fun taskBuilder() = ScheduledTask.Builder()
@@ -1,63 +0,0 @@
package com.cablemc.pokemoncobbled.common.entity.animation
import com.cablemc.pokemoncobbled.client.render.pokeball.animation.ModelAnimation
import net.minecraft.client.model.EntityModel
import net.minecraft.world.entity.Entity
import net.minecraftforge.eventbus.api.EventPriority
/**
* Controls which animation an entity is currently running for it's model.
* The idea here is to register animations with a predicate, to determine when the animation should run.
* Any time this is ticked, it will determine if the animation should change or continue based on the predicates.
*
* This likely will need to be expanded upon in the future or refactored.
*
* @author landonjw
* @since December 3, 2021
*/
class AnimationController<T : Entity> {
/**
* Animations registered in the controller based on priority of the animation.
* Low priorities should be for animations that will act as default animations, or animations that will very
* commonly run.
*/
private var animationsByPriority = Array(EventPriority.values().size) { mutableListOf<AnimationRegistry<T, ModelAnimation<out EntityModel<T>>>>() }
/** The current animation that is running. If this is null, there is currently no animation for the model and it will t-pose. */
var currentAnimation: ModelAnimation<out EntityModel<T>>? = null
private set
private var currentAnimationFrame = 0
/**
* Ticks the animation controller for a given entity.
* If given an animation, it will force the animation onto the entity, ticking it if necessary.
* Otherwise, this will determine the appropriate animation based on the priority sets.
*/
fun tick(entity: T, animation: ModelAnimation<EntityModel<T>>? = null) {
val animationToRun = animation ?: getAnimationFromPrioritySet(entity)
if (currentAnimation == animationToRun && animationToRun != null) {
currentAnimationFrame++
} else {
currentAnimationFrame = 0
currentAnimation = animationToRun
}
// TODO: Run the animation frame
}
private fun getAnimationFromPrioritySet(entity: T): ModelAnimation<out EntityModel<T>>? {
for (prioritySet in animationsByPriority) {
for (animationRegistry in prioritySet) {
if (animationRegistry.predicate(entity)) {
return animationRegistry.animation
}
}
}
return null
}
fun <M: EntityModel<T>, A : ModelAnimation<M>> registerAnimation(priority: EventPriority, animation: A, animationPredicate: (T) -> Boolean) {
animationsByPriority[priority.ordinal].add(AnimationRegistry(animationPredicate, animation))
}
}
@@ -1,11 +0,0 @@
package com.cablemc.pokemoncobbled.common.entity.animation
import com.cablemc.pokemoncobbled.client.render.pokeball.animation.ModelAnimation
import net.minecraft.client.model.EntityModel
import net.minecraft.world.entity.Entity
data class AnimationRegistry<T: Entity, A: ModelAnimation<out EntityModel<T>>>(
/** Determines if the animation can run or not. For example, a swim animation should only run if the entity is in water. */
val predicate: (T) -> Boolean,
val animation: A
)
@@ -2,16 +2,24 @@ package com.cablemc.pokemoncobbled.common.entity.pokeball
import com.cablemc.pokemoncobbled.common.api.pokeball.PokeBalls
import com.cablemc.pokemoncobbled.common.api.scheduling.after
import com.cablemc.pokemoncobbled.common.api.scheduling.taskBuilder
import com.cablemc.pokemoncobbled.common.api.storage.PokemonStoreManager
import com.cablemc.pokemoncobbled.common.entity.EntityRegistry
import com.cablemc.pokemoncobbled.common.entity.pokemon.PokemonEntity
import com.cablemc.pokemoncobbled.common.pokeball.PokeBall
import com.cablemc.pokemoncobbled.common.util.isServerSide
import com.cablemc.pokemoncobbled.common.util.math.toRotations
import com.cablemc.pokemoncobbled.mod.PokemonCobbledMod
import net.minecraft.core.Rotations
import net.minecraft.network.syncher.EntityDataSerializers
import net.minecraft.network.syncher.SynchedEntityData
import net.minecraft.server.level.ServerPlayer
import net.minecraft.util.Mth.PI
import net.minecraft.world.entity.EntityType
import net.minecraft.world.level.Level
import net.minecraft.world.phys.BlockHitResult
import net.minecraft.world.phys.EntityHitResult
import net.minecraft.world.phys.Vec3
class EmptyPokeBallEntity(
pokeBall: PokeBall,
@@ -28,20 +36,31 @@ class EmptyPokeBallEntity(
companion object {
private val CAPTURE_STATE = SynchedEntityData.defineId(EmptyPokeBallEntity::class.java, EntityDataSerializers.BYTE)
private val HIT_TARGET_POSITION = SynchedEntityData.defineId(EmptyPokeBallEntity::class.java, EntityDataSerializers.ROTATIONS)
private val HIT_VELOCITY = SynchedEntityData.defineId(EmptyPokeBallEntity::class.java, EntityDataSerializers.ROTATIONS)
private val SHAKE = SynchedEntityData.defineId(EmptyPokeBallEntity::class.java, EntityDataSerializers.BOOLEAN)
const val TICKS_BETWEEN_SHAKES = 40
const val TICKS_BEFORE_SHAKE = 10
}
var capturingPokemon: PokemonEntity? = null
val captureState = addEntityProperty(CAPTURE_STATE, CaptureState.NOT.ordinal.toByte())
val hitTargetPosition = addEntityProperty(HIT_TARGET_POSITION, Rotations(0F, 0F, 0F))
val hitVelocity = addEntityProperty(HIT_VELOCITY, Rotations(0F, 0F, 0F))
val shakeEmitter = addEntityProperty(SHAKE, false)
override val delegate = if (level.isClientSide) {
com.cablemc.pokemoncobbled.client.entity.EmptyPokeBallClientDelegate()
} else {
EmptyPokeBallServerDelegate()
}
init {
delegate.initialize(this)
captureState.subscribe { state ->
when (CaptureState.values()[state.toInt()]) {
CaptureState.NOT -> isNoGravity = false
CaptureState.HIT -> {
isNoGravity = true
setDeltaMovement(0.0, 0.0, 0.0)
}
CaptureState.HIT -> {}
CaptureState.FALL -> isNoGravity = false
CaptureState.SHAKE -> isNoGravity = false
}
@@ -51,42 +70,121 @@ class EmptyPokeBallEntity(
private var isAttemptingCatch = false
constructor(entityType: EntityType<out EmptyPokeBallEntity>, level: Level) : this(PokeBalls.POKE_BALL, entityType, level)
constructor(pokeBall: PokeBall, level: Level) : this(pokeBall, EntityRegistry.EMPTY_POKEBALL.get(), level)
override fun onHitBlock(hitResult: BlockHitResult) {
super.onHitBlock(hitResult)
kill()
spawnAtLocation(defaultItem)
if (captureState.get() == CaptureState.NOT.ordinal.toByte()) {
if (level.isServerSide()) {
super.onHitBlock(hitResult)
kill()
spawnAtLocation(defaultItem)
}
} else {
isNoGravity = false
deltaMovement = Vec3.ZERO
isOnGround = true
}
}
override fun onHitEntity(hitResult: EntityHitResult) {
if (hitResult.entity is PokemonEntity) {
attemptCatch(hitResult.entity as PokemonEntity)
if (captureState.get() == CaptureState.NOT.ordinal.toByte()) {
if (hitResult.entity is PokemonEntity && level.isServerSide()) {
val pokemon = hitResult.entity as PokemonEntity
if (pokemon.isBeingCaptured || !pokemon.pokemon.isWild()) {
kill()
return
}
capturingPokemon = pokemon
hitVelocity.set(deltaMovement.normalize().toRotations())
hitTargetPosition.set(hitResult.location.toRotations())
attemptCatch(pokemon)
return
}
}
super.onHitEntity(hitResult)
}
override fun tick() {
delegate.tick(this)
// if (isAttemptingCatch) {
// setDeltaMovement(0.0, 0.0, 0.0)
// when {
// currentAnimation is OpenAnimation && (currentAnimation as OpenAnimation).isComplete() -> {
// currentAnimation = ShakeAnimation(3)
// }
// }
// }
super.tick()
if (level.isServerSide()) {
if (owner == null || !owner!!.isAlive || (captureState.get() != CaptureState.NOT.ordinal.toByte() && capturingPokemon?.isAlive != true)) {
kill()
return
}
if (isOnGround && captureState.get() == CaptureState.FALL.ordinal.toByte()) {
captureState.set(CaptureState.SHAKE.ordinal.toByte())
val captureResult = PokemonCobbledMod.captureCalculator.processCapture(owner as ServerPlayer, capturingPokemon!!.pokemon, pokeBall)
var rollsRemaining = captureResult.numberOfShakes
if (rollsRemaining == 4) {
rollsRemaining--
}
taskBuilder()
.iterations(captureResult.numberOfShakes + 1)
.delay(TICKS_BEFORE_SHAKE)
.interval(TICKS_BETWEEN_SHAKES)
.execute {
if (capturingPokemon?.isAlive != true) {
kill()
}
if (!isAlive) {
it.expire()
return@execute
}
if (rollsRemaining <= 0) {
if (captureResult.isSuccessfulCapture) {
// Do a capture
capturingPokemon!!.kill()
val party = PokemonStoreManager.getParty(this.owner!!.uuid)
party.add(capturingPokemon!!.pokemon)
} else {
breakFree()
}
return@execute
}
rollsRemaining--
shakeEmitter.set(!shakeEmitter.get())
}
.build()
}
}
}
private fun breakFree() {
capturingPokemon!!.setPos(position())
capturingPokemon!!.isInvisible = false
capturingPokemon!!.isBeingCaptured = false
kill()
}
private fun attemptCatch(pokemonEntity: PokemonEntity) {
// TODO: Validate pokemon is not owned by a player
if (!isAttemptingCatch) {
isAttemptingCatch = true
pokemonEntity.isBeingCaptured = true
val displace = deltaMovement
captureState.set(CaptureState.HIT.ordinal.toByte())
pokemonEntity.isInvisible = true // TODO this is not the way
after(seconds = 2F) { captureState.set(CaptureState.FALL.ordinal.toByte()) }
val mul = if (random.nextBoolean()) 1 else -1
deltaMovement = displace.multiply(-1.0, 0.0, -1.0).normalize().yRot(mul * PI/3).multiply(0.1, 0.0, 0.1).add(0.0, 1.0 / 3, 0.0)
after(seconds = 0.7F) {
deltaMovement = Vec3.ZERO
isNoGravity = true
}
after(seconds = 2.2F) {
pokemonEntity.isInvisible = true
pokemonEntity.setPos(position())
// TODO can we turn off collision or something
captureState.set(CaptureState.FALL.ordinal.toByte())
}
}
}
}
@@ -0,0 +1,4 @@
package com.cablemc.pokemoncobbled.common.entity.pokeball
class EmptyPokeBallServerDelegate : PokeBallDelegate<EmptyPokeBallEntity> {
}
@@ -13,6 +13,12 @@ class OccupiedPokeBallEntity(
entityType: EntityType<out OccupiedPokeBallEntity>,
level: Level
) : PokeBallEntity(pokeBall, entityType, level) {
override val delegate = if (level.isClientSide) {
com.cablemc.pokemoncobbled.client.entity.OccupiedPokeBallClientDelegate()
} else {
OccupiedPokeBallServerDelegate()
}
init {
delegate.initialize(this)
}
@@ -0,0 +1,3 @@
package com.cablemc.pokemoncobbled.common.entity.pokeball
class OccupiedPokeBallServerDelegate : PokeBallDelegate<OccupiedPokeBallEntity>
@@ -2,6 +2,4 @@ package com.cablemc.pokemoncobbled.common.entity.pokeball
import com.cablemc.pokemoncobbled.common.api.entity.EntitySideDelegate
class PokeBallServerDelegate : EntitySideDelegate<PokeBallEntity> {
}
interface PokeBallDelegate<T : PokeBallEntity> : EntitySideDelegate<T>
@@ -1,13 +1,13 @@
package com.cablemc.pokemoncobbled.common.entity.pokeball
import com.cablemc.pokemoncobbled.client.render.models.blockbench.pokeball.PokeBallModel
import com.cablemc.pokemoncobbled.client.render.pokeball.animation.ModelAnimation
import com.cablemc.pokemoncobbled.common.entity.EntityProperty
import com.cablemc.pokemoncobbled.common.item.ItemRegistry
import com.cablemc.pokemoncobbled.common.pokeball.PokeBall
import net.minecraft.network.protocol.Packet
import net.minecraft.network.syncher.EntityDataAccessor
import net.minecraft.world.entity.EntityDimensions
import net.minecraft.world.entity.EntityType
import net.minecraft.world.entity.Pose
import net.minecraft.world.entity.projectile.ThrowableItemProjectile
import net.minecraft.world.item.Item
import net.minecraft.world.level.Level
@@ -18,18 +18,10 @@ abstract class PokeBallEntity(
entityType: EntityType<out PokeBallEntity>,
level: Level
) : ThrowableItemProjectile(entityType, level) {
val DIMENSIONS = EntityDimensions(0.4F, 0.4F, true)
val entityProperties = mutableListOf<EntityProperty<*>>()
var currentAnimation: ModelAnimation<PokeBallModel>? = null
protected set
val delegate = if (level.isClientSide) {
// Don't import because scanning for imports is a CI job we'll do later to detect errant access to client from server
com.cablemc.pokemoncobbled.client.entity.PokeBallClientDelegate()
} else {
PokeBallServerDelegate()
}
abstract val delegate: PokeBallDelegate<out PokeBallEntity>
override fun getDefaultItem(): Item = ItemRegistry.POKE_BALL.get()
@@ -39,10 +31,10 @@ abstract class PokeBallEntity(
override fun tick() {
super.tick()
delegate.tick(this)
entityProperties.forEach { it.checkForUpdate() }
}
override fun getDimensions(pPose: Pose) = DIMENSIONS
fun <T> addEntityProperty(accessor: EntityDataAccessor<T>, initialValue: T): EntityProperty<T> {
val property = EntityProperty(
entityData = entityData,
@@ -42,6 +42,8 @@ class PokemonEntity(
PokemonServerDelegate()
}
var isBeingCaptured = false
val entityProperties = mutableListOf<EntityProperty<*>>()
val dexNumber = addEntityProperty(SPECIES_DEX, pokemon.species.nationalPokedexNumber)
@@ -3,6 +3,7 @@ package com.cablemc.pokemoncobbled.common.item
import com.cablemc.pokemoncobbled.common.entity.pokeball.EmptyPokeBallEntity
import com.cablemc.pokemoncobbled.common.item.creativetabs.PokeBallTab
import com.cablemc.pokemoncobbled.common.pokeball.PokeBall
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.InteractionHand
import net.minecraft.world.InteractionResultHolder
import net.minecraft.world.entity.player.Player
@@ -16,7 +17,7 @@ class PokeBallItem(
override fun use(level: Level, player: Player, usedHand: InteractionHand): InteractionResultHolder<ItemStack> {
val itemStack = player.getItemInHand(usedHand)
if (!level.isClientSide) {
throwPokeBall(level, player)
throwPokeBall(level, player as ServerPlayer)
}
if (!player.abilities.instabuild) {
itemStack.shrink(1)
@@ -24,12 +25,12 @@ class PokeBallItem(
return InteractionResultHolder.sidedSuccess(itemStack, level.isClientSide)
}
private fun throwPokeBall(level: Level, player: Player) {
private fun throwPokeBall(level: Level, player: ServerPlayer) {
val pokeBallEntity = EmptyPokeBallEntity(pokeBall, player.level).apply {
setPos(player.x, player.y, player.z)
setPos(player.x, player.y + 1.5, player.z)
shootFromRotation(player, player.xRot - 7, player.yRot, 0.0f, 1.5f, 1.0f)
owner = player
}
level.addFreshEntity(pokeBallEntity)
}
}
@@ -161,5 +161,8 @@ class Pokemon {
private val _level = registerObservable(SimpleObservable<Int>()) { LevelUpdatePacket(this, it) }
private val _health = SimpleObservable<Int>()
fun getMaxHealth(): Int = TODO("needs to be calculated properly. NOT just the base stat value")
val ivHP = 1
val evHP = 1
fun getMaxHealth(): Int = (2 * stats[Stats.HP]!! + ivHP + (evHP / 4) * level) / 100 + level + 10
}
@@ -2,6 +2,7 @@ package com.cablemc.pokemoncobbled.common.util
import com.cablemc.pokemoncobbled.common.api.reactive.Observable
import net.minecraft.util.thread.BlockableEventLoop
import net.minecraft.world.level.Level
import net.minecraftforge.api.distmarker.Dist
import net.minecraftforge.fml.DistExecutor
import net.minecraftforge.fml.LogicalSide
@@ -42,4 +43,6 @@ internal fun <T> runOnSide(side: LogicalSide, block: () -> T): CompletableFuture
val future = CompletableFuture<T>()
LogicalSidedProvider.WORKQUEUE.get<BlockableEventLoop<*>>(side).submit { future.complete(block()) }
return future
}
}
fun Level.isServerSide() = !isClientSide
@@ -1,7 +1,12 @@
package com.cablemc.pokemoncobbled.common.util.math
import net.minecraft.core.Rotations
import net.minecraft.world.phys.Vec3
import kotlin.math.pow
infix fun Int.pow(power: Int): Int {
return toDouble().pow(power.toDouble()).toInt()
}
}
fun Vec3.toRotations() = Rotations(x.toFloat(), y.toFloat(), z.toFloat())
fun Rotations.toVec3() = Vec3(x.toDouble(), y.toDouble(), z.toDouble())
@@ -5,6 +5,8 @@ import com.cablemc.pokemoncobbled.client.render.models.blockbench.repository.Pok
import com.cablemc.pokemoncobbled.client.render.models.blockbench.repository.PokemonModelRepository
import com.cablemc.pokemoncobbled.common.CommandRegistrar
import com.cablemc.pokemoncobbled.common.PokemonCobbled
import com.cablemc.pokemoncobbled.common.api.pokeball.catching.calculators.CaptureCalculator
import com.cablemc.pokemoncobbled.common.api.pokeball.catching.calculators.Gen7CaptureCalculator
import com.cablemc.pokemoncobbled.common.api.pokemon.PokemonSpecies
import com.cablemc.pokemoncobbled.common.api.scheduling.ScheduledTaskListener
import com.cablemc.pokemoncobbled.common.api.storage.PokemonStoreManager
@@ -38,6 +40,7 @@ import thedarkcolour.kotlinforforge.forge.MOD_CONTEXT
object PokemonCobbledMod {
val LOGGER = LogManager.getLogger()
val EVENT_BUS = BusBuilder.builder().build()
var captureCalculator: CaptureCalculator = Gen7CaptureCalculator()
init {
with(MOD_CONTEXT.getKEventBus()) {
@@ -96,15 +99,4 @@ object PokemonCobbledMod {
fun on(event: EntityAttributeCreationEvent) {
EntityRegistry.registerAttributes(event)
}
// // You can use EventBusSubscriber to automatically subscribe events on the contained class (this is subscribing to the MOD
// // Event bus for receiving Registry Events)
// @EventBusSubscriber(bus = EventBusSubscriber.Bus.MOD)
// object RegistryEvents {
// @SubscribeEvent
// fun onBlocksRegistry(blockRegistryEvent: Register<Block?>?) {
// // register a new block here
// LOGGER.info("HELLO from Register Block")
// }
// }
}
@@ -0,0 +1,61 @@
package com.cablemc.pokemoncobbled.client.render.models.blockbench.animation
import com.cablemc.pokemoncobbled.common.util.math.geometry.toDegrees
import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.math.Vector3f
import net.minecraft.util.Mth.PI
import org.junit.jupiter.api.Test
import kotlin.math.acos
import kotlin.math.asin
import kotlin.math.atan2
import kotlin.math.cos
internal class RootPokeBallLookAnimationTest {
@Test
fun `attempts at getting rotation logic working, i am not done trying angryface`() {
val xRot = PI / 3
val yRot = PI / 3
val zRot = 0F
val desiredZ = PI / 3
val poseStack = PoseStack()
poseStack.mulPose(Vector3f.ZP.rotation(zRot))
poseStack.mulPose(Vector3f.YP.rotation(yRot))
poseStack.mulPose(Vector3f.XP.rotation(xRot))
val zAxis = Vector3f.ZP.copy()
zAxis.transform(poseStack.last().normal())
poseStack.mulPose(zAxis.rotation(desiredZ))
val y = Vector3f.YP.copy()
y.transform(poseStack.last().normal())
val py1 = y.x()
val z = Vector3f.ZP.copy()
z.transform(poseStack.last().normal())
val pz1 = z.x()
val pz3 = z.z()
// val beta = atan2(d.y(), d.x())
// val alpha = asin(d.z())
val beta = asin(pz1)
val alpha = acos(pz3 / cos(beta))
val gamma = asin(-py1 / cos(beta))
val poseStack2 = PoseStack()
poseStack2.mulPose(Vector3f.ZP.rotation(gamma))
poseStack2.mulPose(Vector3f.YP.rotation(beta))
poseStack2.mulPose(Vector3f.XP.rotation(alpha))
val y2 = Vector3f(3F, 0F, -4F)
y2.transform(poseStack.last().normal())
println(y2)
val alphaD = alpha.toDegrees()
val betaD = beta.toDegrees()
val gammaD = gamma.toDegrees()
println("xRot: $alphaD, yRot: $betaD, zRot: $gammaD")
}
}