Compare commits

...
Author SHA1 Message Date
JacksonB 308c9d61c6 Practically an overhaul of bird. Also everything but altitude implemented. 2025-03-14 21:11:02 -05:00
JacksonB 07ad22608d Migrate bird logic over to rideVectors and add lateral and vertical movement to bird 2025-03-14 12:58:02 -05:00
JacksonB b50f924ce0 Jet fully transferred to velocity based calculations rather than speed. A few tweaks to generic land as well. 2025-03-13 23:43:04 -05:00
JacksonB c0b55ebb6f Further dirty this branch before cleaning :( 2025-03-13 20:18:05 -05:00
JacksonB cdc0a790fb Simplification of data driven function to control rideControllers base stat interpolation. Min and max are now inside the function 2025-03-03 22:51:49 -06:00
JacksonB 13e8084f0d merge and fixes to merge of controller work 2025-03-02 12:39:06 -06:00
JacksonB 3daa3d31a3 initial fixes 2025-03-02 12:02:29 -06:00
JacksonB 585371bb08 initial commit 2 2025-03-01 22:35:38 -06:00
JacksonB aee97ff96e initial commit 2025-03-01 22:35:20 -06:00
JacksonB fbbd450d0d Address MR comments. Also implement a seperate jet flight composite controller to handle transition from jetstate data to groundstate data. 2025-02-25 19:24:23 -06:00
JacksonB 83612b50fc Initial cleanup before MR 2025-02-22 19:07:51 -06:00
JacksonB 434faea210 Merge branch 'jackowes/ride_controllers' into jackowes/ride_cleanup 2025-02-22 17:40:24 -06:00
JacksonB 6e24ee6b4e last commit before cleanup and MR 2025-02-22 17:29:18 -06:00
JacksonB 3d6a3fbfec Fix Jet sensitivity issues 2025-02-20 20:12:54 -06:00
JacksonB 708b810e6b Set up begginings of stamina using vanilla jump bar and change jet turning to accumulated input method. 2025-02-20 00:25:02 -06:00
JacksonB 509016e133 Changes to how ride handling is handled and jet controls 2025-02-19 15:07:19 -06:00
JacksonB 690183b994 Squashed commit of the following:
commit 9497175627
Author: JacksonB <78700961+jackowes@users.noreply.github.com>
Date:   Thu Feb 13 00:05:03 2025 -0600

    mk2 bird flight controller

commit 0defd2614f
Author: JacksonB <78700961+jackowes@users.noreply.github.com>
Date:   Wed Feb 12 20:01:57 2025 -0600

    Initial bird controller changes and refactoring of how riding controllers control rotation mouse+rotVel

commit 181af6847d
Author: JacksonB <78700961+jackowes@users.noreply.github.com>
Date:   Wed Feb 12 12:29:35 2025 -0600

    Add updated version of Helicopter ride controller. Tie in some static pokemonentity functions to be riding configurable. Modify the cameramixins, currently there is a temp change to them on this branch.

commit ac1cdb0ba6
Author: JacksonB <78700961+jackowes@users.noreply.github.com>
Date:   Sat Feb 8 03:02:35 2025 -0600

    Add angular velocity constant modifier (roll can now influence yaw so the dolphin can turn). Spruce up dolphin swim controller.

commit 59190209a2
Author: JacksonB <78700961+jackowes@users.noreply.github.com>
Date:   Wed Feb 5 19:30:21 2025 -0600

    initial commit with a bunch of poking around code
2025-02-18 18:20:31 -06:00
JacksonB 33d56124f5 Controller fixes due to some refactoring and velocity handling changes 2025-02-17 18:11:51 -06:00
JacksonB a5a65abe2b Changes to how velocity is handled and some fixes to jet and bird. 2025-02-17 02:01:20 -06:00
JacksonB f73ab3b001 Jet implementation and intermediate velocity and speed handling changes( this commit has broken ride controllers due to this) 2025-02-16 13:14:56 -06:00
JacksonB 9a571984db Rebase off of main transport branch. Make some tweaks to bird and helicopter controls as well as adding a WIP jet controller. 2025-02-14 16:08:13 -06:00
27 changed files with 1212 additions and 391 deletions
@@ -9,8 +9,10 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.client.CobblemonClient;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.pokedex.scanner.PokedexUsageContext;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.client.player.LocalPlayer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
@@ -30,5 +32,15 @@ public class AbstractClientPlayerMixin {
if (usageContext.getScanningGuiOpen()) {
cir.setReturnValue(usageContext.getFovMultiplier());
}
//TODO: find a better solution that just tagging this logic on here. Likely just change it to a more general
//name?
LocalPlayer player = (LocalPlayer) (Object) this;
if (player != null && player.isPassenger() && player.getVehicle() instanceof PokemonEntity) {
PokemonEntity ride = (PokemonEntity) player.getVehicle();
//Return custom fov mult
cir.setReturnValue(ride.rideFovMult());
}
}
}
@@ -61,6 +61,7 @@ import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
import net.minecraft.sounds.SoundEvent
import net.minecraft.world.effect.MobEffect
import net.minecraft.world.entity.EntityDimensions
import net.minecraft.world.item.Item
@@ -108,6 +109,7 @@ object PokemonSpecies : JsonDataRegistry<Species> {
.registerTypeAdapter(ItemPredicate::class.java, LegacyItemConditionWrapperAdapter)
.registerTypeAdapter(RideController::class.java, RideControllerAdapter)
.registerTypeAdapter(RidingStatDefinition::class.java, RidingStatDefinitionAdapter)
.registerTypeAdapter(SoundEvent::class.java, SoundEventAdapter)
.registerTypeAdapter(Expression::class.java, ExpressionAdapter)
.disableHtmlEscaping()
.enableComplexMapKeySerialization()
@@ -15,6 +15,7 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.riding.controller.RideController
import com.cobblemon.mod.common.api.riding.stats.RidingStat
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.toProperties
import com.cobblemon.mod.common.util.withQueryValue
import net.minecraft.ChatFormatting
import net.minecraft.network.chat.Component
@@ -25,6 +26,7 @@ import net.minecraft.world.entity.player.Player
import net.minecraft.world.phys.Vec2
import net.minecraft.world.phys.Vec3
import kotlin.math.sin
import kotlin.math.sin
data class RidingManager(val entity: PokemonEntity) {
var lastSpeed = 0F
@@ -40,8 +42,13 @@ data class RidingManager(val entity: PokemonEntity) {
it.environment.query.addFunction("get_ride_stats") { params ->
val rideStat = RidingStat.valueOf(params.getString(0).uppercase())
val rideStyle = RidingStyle.valueOf(params.getString(1).uppercase())
val maxVal = params.getDouble(2)
val minVal = params.getDouble(3)
//TODO: Use the mons actual boost once implemented
DoubleValue(entity.rideProp.calculate(rideStat, rideStyle, 0))
val normalizedStat = entity.rideProp.calculate(rideStat, rideStyle, 0) / 100.0f
val trueStatVal = (normalizedStat * (maxVal - minVal)) + minVal
DoubleValue(trueStatVal)
}
}
}
@@ -75,6 +82,11 @@ data class RidingManager(val entity: PokemonEntity) {
//val speedEntity = entity.deltaMovement.length() //* 20 * 60 * 60) / ( 1000 )
//println(speedEntity)
//driver.displayClientMessage(Component.literal("Speed: ").withStyle { it.withColor(ChatFormatting.GREEN) }.append(Component.literal("${String.format("%.2f", speedEntity)} km/h")), true)
//val speedPlayer = driver.deltaMovement.horizontalDistance()
//driver.displayClientMessage(Component.literal("Speed player: ").withStyle { it.withColor(ChatFormatting.GREEN) }.append(Component.literal("$speedPlayer b/t")), true)
//val speedEntity = entity.deltaMovement.length() //* 20 * 60 * 60) / ( 1000 )
//println(speedEntity)
//driver.displayClientMessage(Component.literal("Speed: ").withStyle { it.withColor(ChatFormatting.GREEN) }.append(Component.literal("${String.format("%.2f", speedEntity)} km/h")), true)
}
@@ -112,6 +124,11 @@ data class RidingManager(val entity: PokemonEntity) {
return controller.setRideBar(entity, driver)
}
fun rideFovMult(entity: PokemonEntity, driver: Player): Float {
val controller = getController(entity) ?: return 1.0f
return controller.rideFovMult(entity, driver)
}
fun canJump(entity: PokemonEntity, driver: Player): Boolean {
val controller = getController(entity) ?: return false
return controller.canJump(entity, driver)
@@ -0,0 +1,50 @@
/*
* 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
import net.minecraft.client.player.LocalPlayer
import net.minecraft.client.resources.sounds.AbstractTickableSoundInstance
import net.minecraft.client.resources.sounds.SoundInstance
import net.minecraft.sounds.SoundEvent
import net.minecraft.sounds.SoundSource
/**
* put what it is here
*
* @author Jackowes
* @since March 4th, 2025
*/
class RidingOnPlayerSoundEventInstance(
private val player: LocalPlayer,
soundEvent: SoundEvent
) : AbstractTickableSoundInstance(soundEvent, SoundSource.PLAYERS, SoundInstance.createUnseededRandom()) {
init {
this.looping = true
this.delay = 0
this.volume = 0.1f
}
fun setPitch( pitch: Float ){
this.pitch = pitch
}
fun setVolume( volume: Float){
this.volume = volume
}
override fun tick() {
//TODO: implement effectively
//For now it should just play the sound?
setPitch(1.0f)
println("ticking")
}
}
@@ -96,8 +96,9 @@ interface RideController : Encodable, Decodable {
fun setRideBar(entity: PokemonEntity, driver: Player): Float = 0.0f
fun canJump(entity: PokemonEntity, driver: Player) : Boolean
fun rideFovMult(entity: PokemonEntity, driver: Player): Float = 1.0f
fun canJump(entity: PokemonEntity, driver: Player) : Boolean
fun jumpForce(entity: PokemonEntity, driver: Player, jumpStrength: Int) : Vec3
fun gravity(entity: PokemonEntity, regularGravity: Double) : Double? = null
@@ -1417,6 +1417,7 @@ open class PokemonEntity(
return this.deltaMovement
}
/*
override fun shouldDiscardFriction(): Boolean {
val riders = this.passengers.filterIsInstance<LivingEntity>()
if (riders.isEmpty()) {
@@ -1425,6 +1426,7 @@ open class PokemonEntity(
return true
}
}
*/
override fun travel(movementInput: Vec3) {
val prevBlockPos = this.blockPosition()
@@ -1690,6 +1692,22 @@ open class PokemonEntity(
this.yBodyRot = this.yRot
this.yRotO = this.yRot
val riders = this.passengers.filterIsInstance<LivingEntity>()
//These are the only functions that matter and I have isolated them to
//avoid minecrafts changes they do to velocity so that the ride controllers
//can be tested without that interference.
if ( false ) {
val v = Entity.getInputVector(riding.velocity(this, driver, Vec3.ZERO), 1.0f, this.getYRot());
//changing this will give the ride more or less inertia/handling/drift?
//A bit of overlap with other things but.
this.setDeltaMovement(this.deltaMovement.lerp(v, riding.inertia(this)));
//maybe move this to the travel function. The jump below never gets properly used
this.move( MoverType.SELF, this.deltaMovement );
}
if (this.riding.canJump(this, driver)) {
if (this.onGround()) {
if (this.jumpInputStrength > 0) {
@@ -1893,6 +1911,11 @@ open class PokemonEntity(
return this.riding.setRideBar(this, driver)
}
fun rideFovMult(): Float {
val driver = this.controllingPassenger as? Player ?: return 1.0f
return this.riding.rideFovMult(this, driver)
}
/**
* A utility method to resolve the [Codec] of [Pokemon] aware if the [world] is client sided or not.
*
@@ -9,18 +9,18 @@
package com.cobblemon.mod.common.pokemon.riding.controllers
import com.bedrockk.molang.Expression
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.MoLangMath.lerp
import com.cobblemon.mod.common.Rollable
import com.cobblemon.mod.common.api.riding.RidingStyle
import com.cobblemon.mod.common.api.riding.controller.RideController
import com.cobblemon.mod.common.api.riding.controller.posing.PoseOption
import com.cobblemon.mod.common.api.riding.controller.posing.PoseProvider
import com.cobblemon.mod.common.api.riding.stats.RidingStat
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.riding.states.BirdAirState
import com.cobblemon.mod.common.util.*
import net.minecraft.client.Minecraft
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.util.Mth
import net.minecraft.util.SmoothDouble
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.player.Player
@@ -33,7 +33,16 @@ class BirdAirController : RideController {
override val poseProvider = PoseProvider(PoseType.HOVER)
.with(PoseOption(PoseType.FLY) {
val player = it.passengers.firstOrNull() as? Player ?: return@PoseOption false
it.deltaMovement.length() > 0.3 })
it.deltaMovement.length() > 0.5 })
/*
.with(PoseOption(PoseType.FLY) {
val player = it.passengers.firstOrNull() as? Player ?: return@PoseOption false
player.zza > 0.0f // Checks if the player is actively attempting to move forward
})
*/
.with(PoseOption(PoseType.FLY) {
val player = it.passengers.firstOrNull() as? Player ?: return@PoseOption false
it.deltaMovement.length() > 0.5 })
/*
.with(PoseOption(PoseType.FLY) {
val player = it.passengers.firstOrNull() as? Player ?: return@PoseOption false
@@ -43,8 +52,6 @@ class BirdAirController : RideController {
override val condition: (PokemonEntity) -> Boolean = { true }
var jumpVector = listOf("0".asExpression(), "0.3".asExpression(), "0".asExpression())
private set
var gravity: Expression = "0".asExpression()
private set
var horizontalAcceleration: Expression = "0.1".asExpression()
@@ -52,62 +59,29 @@ class BirdAirController : RideController {
var verticalAcceleration: Expression = "0.1".asExpression()
private set
// Where 90 is the width of the range and 45 is the offset.
// So the handling interpolates between 135 and 45
var handlingExpr: Expression = "(q.get_ride_stats('SKILL', 'AIR') / 100) * 90 + 45".asExpression()
var handlingExpr: Expression = "q.get_ride_stats('SKILL', 'AIR', 135.0, 45.0)".asExpression()
private set
var topSpeedExpr: Expression = "(q.get_ride_stats('SPEED', 'AIR') / 100) * (1.0 - 0.35) + 0.35".asExpression()
var topSpeedExpr: Expression = "q.get_ride_stats('SPEED', 'AIR', 1.0, 0.35)".asExpression()
private set
//Reaches max top speed from a standstill at minimum 8 seconds and max 3 seconds.
//This is a bit of a confusing expression and likely should be cleaned up to be more readable
var accelExpr: Expression = "(q.get_ride_stats('ACCELERATION', 'AIR') / 100) * (0.01666666666 - 0.00625) + 0.00625".asExpression()
var glideTopSpeedExpr: Expression = "q.get_ride_stats('SPEED', 'AIR', 2.0, 1.0)".asExpression()
private set
// Max accel is a whole 1.0 in 3 seconds. The conversion in the function below is to convert seconds to ticks
var accelExpr: Expression = "q.get_ride_stats('ACCELERATION', 'AIR', (1.0 / (20.0 * 3.0)), (1.0 / (20.0 * 8.0)))".asExpression()
private set
//Seconds self propelled flight (glide is not self propelled in this case)
var staminaExpr: Expression = "q.get_ride_stats('STAMINA', 'AIR', 60.0, 20.0)".asExpression()
private set
var infiniteStamina: Expression = "false".asExpression()
private set
override fun speed(entity: PokemonEntity, driver: Player): Float {
//Retrieve the current bird controller state
//val state = entity.riding.getState(KEY) { e -> BirdAirControllerState(e) }
//Retrieve the current bird controller state and stats
val state = getState(entity, ::BirdAirState)
//retrieve stats
val topSpeed = getRuntime(entity).resolveDouble(topSpeedExpr)
val accel = getRuntime(entity).resolveDouble(accelExpr)
val maxGlideSpeed = topSpeed * 2
var glideSpeedChange = 0.00
//speed up and slow down based on input
if (driver.zza > 0.0 && state.currSpeed < topSpeed) {
state.currSpeed = min(state.currSpeed + accel , topSpeed)
} else if (driver.zza < 0.0 && state.currSpeed > 0.0) {
//Decelerate is currently always a constant half of max acceleration.
state.currSpeed = max(state.currSpeed - (0.0166 / 2), 0.0)
}
val rollable = driver as? Rollable
if (rollable != null) {
glideSpeedChange = sin(Math.toRadians(rollable.pitch.toDouble()))
glideSpeedChange = glideSpeedChange.pow(3)
//TODO: Possibly create a deadzone around parallel where speed isn't taken away from?
if (glideSpeedChange <= 0.0) {
//Ensures that a propelling force is still able to be applied when
//climbing in height
if (driver.zza <= 0) {
//speed decrease should be 2x speed increase?
state.currSpeed = max(state.currSpeed + (0.0166) * glideSpeedChange, 0.0 )
}
} else {
// only add to the speed if it hasn't exceeded the current
//glide angles maximum amount of speed that it can give.
state.currSpeed = min(state.currSpeed + ((0.0166 * 2) * glideSpeedChange), maxGlideSpeed)
}
}
//air resistance
state.currSpeed = max(state.currSpeed - 0.005, 0.0)
return state.currSpeed.toFloat()
return state.rideVel.length().toFloat()
}
override fun rotation(entity: PokemonEntity, driver: LivingEntity): Vec2 {
@@ -119,57 +93,54 @@ class BirdAirController : RideController {
var leftForce = 0.0
var upForce = 0.0
var forwardForce = 0.0
val state = getState(entity, ::BirdAirState)
//Perform ride velocity update
calculateRideSpaceVel(entity, driver, state)
//Translate ride space velocity to world space velocity.
val rollable = driver as? Rollable
if (rollable != null) {
//Need to deadzone this when straight up or down
// test with and without strafing added. May be wanted while moving
leftForce = -1.0 * sin(Math.toRadians(rollable.roll.toDouble()))
upForce = -1.0 * sin(Math.toRadians(rollable.pitch.toDouble()))
upForce += -1.0 * sin(Math.toRadians(rollable.pitch.toDouble())) * state.rideVel.z
forwardForce += cos(Math.toRadians(rollable.pitch.toDouble())) * state.rideVel.z
forwardForce = cos(Math.toRadians(rollable.pitch.toDouble()))
upForce += cos(Math.toRadians(rollable.pitch.toDouble())) * state.rideVel.y
forwardForce += sin(Math.toRadians(rollable.pitch.toDouble())) * state.rideVel.y
}
//TODO: Add in an upwards and downwards force when pressing space maybe?
//TODO: implement altitude limit
//Leave out tap to flap right now
/*
var jumpVector = Vec3.ZERO
if (entity.jumpInputStrength > 0) {
val runtime = getRuntime(entity)
runtime.environment.query.addFunction("jump_strength") { DoubleValue(entity.jumpInputStrength.toDouble()) }
val jumpForces = this.jumpVector.map { runtime.resolveFloat(it) }
jumpVector = Vec3(jumpForces[0].toDouble(), jumpForces[1].toDouble(), jumpForces[2].toDouble())
jumpVector = jumpVector.toVector3f().mul((driver as Rollable).orientation).toVec3d()
entity.jumpInputStrength = 0
entity.addDeltaMovement(jumpVector)
//Bring the ride out of the sky when stamina is depleted.
if (state.stamina <= 0.0) {
upForce -= 0.3
}
*/
//Retrieve the current composite controller state
val state = getState(entity, ::BirdAirState)
val velocity = Vec3(0.0 , upForce * state.currSpeed, forwardForce * state.currSpeed)
val velocity = Vec3(state.rideVel.x , upForce, forwardForce)
return velocity
}
override fun angRollVel(entity: PokemonEntity, driver: Player, deltaTime: Double): Vec3 {
val rollable = driver as? Rollable
val state = getState(entity, ::BirdAirState)
//TODO: Tie in handling
val handling = getRuntime(entity).resolveDouble(handlingExpr)
val topSpeed = getRuntime(entity).resolveDouble(topSpeedExpr)
val rotationChangeRate = 10.0
if (rollable != null) {
var yawForce = rotationChangeRate * sin(Math.toRadians(rollable.roll.toDouble()))
//for a bit of correction on the rolls limit it to a quarter the amount
var pitchForce = -0.25 * rotationChangeRate * abs(sin(Math.toRadians(rollable.roll.toDouble())))
var pitchForce = -0.35 * rotationChangeRate * abs(sin(Math.toRadians(rollable.roll.toDouble())))
//limit rotation modulation when pitched up heavily or pitched down heavily
yawForce *= abs(cos(Math.toRadians(rollable.pitch.toDouble())))
pitchForce *= abs(cos(Math.toRadians(rollable.pitch.toDouble()))) * 1.5
//Create a deadzone for easier leveling out
if (rollable.roll <= 0.0 && rollable.roll >= -0.0) {
yawForce = 0.0
@@ -192,40 +163,54 @@ class BirdAirController : RideController {
deltaTime: Double
): Vec3 {
if (driver !is Rollable) return Vec3.ZERO
val rollable = driver as Rollable
val state = getState(entity, ::BirdAirState)
val handling = getRuntime(entity).resolveDouble(handlingExpr)
val topSpeed = getRuntime(entity).resolveDouble(topSpeedExpr)
//Smooth out mouse input.
val smoothingSpeed = 4
var xInput = xMouseSmoother.getNewDeltaValue(xMouse * 0.1, deltaTime * smoothingSpeed);
val YInput = yMouseSmoother.getNewDeltaValue(yMouse * 0.1, deltaTime * smoothingSpeed);
val xInput = xMouseSmoother.getNewDeltaValue(xMouse * 0.1, deltaTime * smoothingSpeed);
val yInput = yMouseSmoother.getNewDeltaValue(yMouse * 0.1, deltaTime * smoothingSpeed);
//limit rolling based on healing
val rotLimit = handling
//limit rolling based on handling and current speed.
//modulated by speed so that when flapping idle in air you are ont wobbling around to look around
val rotMin = 15.0
var rollForce = xInput
val rotLimit = max(handling * normalizeVal(state.rideVel.length(), 0.0, topSpeed).pow(3), rotMin)
//Limit roll by non linearly decreasing inputs towards
// a rotation limit based on the current distance from
// that rotation limit
if (abs(rollable.roll + xInput) < rotLimit) {
if (sign(xInput) == sign(rollable.roll).toDouble()) {
if (abs(rollable.roll + rollForce) < rotLimit) {
if (sign(rollForce) == sign(rollable.roll).toDouble()) {
val d = abs(abs(rollable.roll) - rotLimit)
xInput *= (d.pow(2)) / (rotLimit.pow(2))
rollForce *= (d.pow(2)) / (rotLimit.pow(2))
}
} else if (sign(xInput) == sign(rollable.roll).toDouble()) {
xInput = 0.0
} else if (sign(rollForce) == sign(rollable.roll).toDouble()) {
rollForce = 0.0
}
//Give the ability to yaw with x mouse input when at low speeds.
val yawForce = xInput * ( 1.0 - normalizeVal(state.rideVel.length(), 0.0, topSpeed).pow(3))
//yaw, pitch, roll
return Vec3(0.0, YInput, xInput)
return Vec3(yawForce, yInput, rollForce)
}
override fun canJump(
entity: PokemonEntity,
driver: Player
): Boolean {
return true
return false
}
override fun setRideBar(entity: PokemonEntity, driver: Player): Float {
//Retrieve stamina from state
val state = getState(entity, ::BirdAirState)
return (state.stamina / 1.0f)
}
override fun jumpForce(
@@ -242,6 +227,19 @@ class BirdAirController : RideController {
override fun shouldRoll(entity: PokemonEntity) = true
override fun rideFovMult(entity: PokemonEntity, driver: Player): Float {
val topSpeed = getRuntime(entity).resolveDouble(topSpeedExpr)
val glideTopSpeed = getRuntime(entity).resolveDouble(glideTopSpeedExpr)
val state = getState(entity, ::BirdAirState)
//Must I ensure that topspeed is greater than minimum?
val normalizedGlideSpeed = normalizeVal(state.rideVel.length(), topSpeed, glideTopSpeed)
//Only ever want the fov change to be a max of 0.2 and for it to have non linear scaling.
return 1.0f + normalizedGlideSpeed.pow(2).toFloat() * 0.2f
}
override fun useAngVelSmoothing(entity: PokemonEntity): Boolean = false
override fun encode(buffer: RegistryFriendlyByteBuf) {
@@ -252,6 +250,9 @@ class BirdAirController : RideController {
buffer.writeString(topSpeedExpr.toString())
buffer.writeString(accelExpr.toString())
buffer.writeString(handlingExpr.toString())
buffer.writeString(topSpeedExpr.toString())
buffer.writeString(accelExpr.toString())
buffer.writeString(handlingExpr.toString())
}
override fun decode(buffer: RegistryFriendlyByteBuf) {
@@ -261,10 +262,144 @@ class BirdAirController : RideController {
topSpeedExpr = buffer.readString().asExpression()
accelExpr = buffer.readString().asExpression()
handlingExpr = buffer.readString().asExpression()
topSpeedExpr = buffer.readString().asExpression()
accelExpr = buffer.readString().asExpression()
handlingExpr = buffer.readString().asExpression()
}
companion object {
val KEY = cobblemonResource("air/bird")
}
/*
* Normalizes the given value between a min and a max.
* The result is clamped between 0.0 and 1.0, where 0.0 represents x is at or below min
* and 1.0 represents x is at or above it.
*/
fun normalizeVal(x: Double, min: Double, max: Double): Double {
require(max > min) { "max must be greater than min" }
return ((x - min) / (max - min)).coerceIn(0.0, 1.0)
}
/*
* Calculates the change in the ride space vector due to player input and ride state
*/
fun calculateRideSpaceVel( entity: PokemonEntity, driver: Player, state: BirdAirState){
//retrieve stats
val topSpeed = getRuntime(entity).resolveDouble(topSpeedExpr)
val glideTopSpeed = getRuntime(entity).resolveDouble(glideTopSpeedExpr)
val accel = getRuntime(entity).resolveDouble(accelExpr)
val staminaStat = getRuntime(entity).resolveDouble(staminaExpr)
var glideSpeedChange = 0.00
val currSpeed = state.rideVel.length()
//Flag for determining if stamina should be lost
var loseStamina = false
//speed up and slow down based on input
if (driver.zza != 0.0f && state.stamina > 0.0) {
//make sure it can't exceed top speed
val forwardInput = when {
driver.zza > 0 && state.rideVel.z > topSpeed -> 0.0
driver.zza < 0 && state.rideVel.z < (-topSpeed / 3.0) -> 0.0
else -> driver.zza.sign
}
state.rideVel = Vec3(
state.rideVel.x,
state.rideVel.y,
(state.rideVel.z + (accel * forwardInput.toDouble())))
loseStamina = true
}
val rollable = driver as? Rollable
if (rollable != null) {
//Base glide speed change on current pitch of the ride.
glideSpeedChange = sin(Math.toRadians(rollable.pitch.toDouble()))
glideSpeedChange = glideSpeedChange.pow(3) * 0.5
//TODO: Possibly create a deadzone around parallel where glide doesn't affect speed?
if (glideSpeedChange <= 0.0) {
//Ensures that a propelling force is still able to be applied when
//climbing in height
if (driver.zza <= 0) {
//speed decrease should be 2x speed increase?
//state.currSpeed = max(state.currSpeed + (0.0166) * glideSpeedChange, 0.0 )
state.rideVel = Vec3(
state.rideVel.x,
state.rideVel.y,
lerp( state.rideVel.z, 0.0,glideSpeedChange * -0.0166 * 2 ))
}
} else {
// only add to the speed if it hasn't exceeded the current
//glide angles maximum amount of speed that it can give.
//state.currSpeed = min(state.currSpeed + ((0.0166 * 2) * glideSpeedChange), maxGlideSpeed)
state.rideVel = Vec3(
state.rideVel.x,
state.rideVel.y,
min(state.rideVel.z + ((0.0166 * 2) * glideSpeedChange), glideTopSpeed))
}
}
//Lateral movement based on driver input.
val latTopSpeed = topSpeed / 2.0
if (driver.xxa != 0.0f && state.stamina > 0.0) {
state.rideVel = Vec3(
(state.rideVel.x + (accel * driver.xxa)).coerceIn(-latTopSpeed, latTopSpeed),
state.rideVel.y,
state.rideVel.z)
loseStamina = true
}
else {
state.rideVel = Vec3(
lerp(state.rideVel.x, 0.0, latTopSpeed / 20.0),
state.rideVel.y,
state.rideVel.z)
}
//Vertical movement based on driver input.
val vertTopSpeed = topSpeed / 2.0
val vertInput = when {
Minecraft.getInstance().options.keyJump.isDown() -> 1.0
Minecraft.getInstance().options.keyShift.isDown() -> -1.0
else -> 0.0
}
if (vertInput != 0.0 && state.stamina > 0.0) {
state.rideVel = Vec3(
state.rideVel.x,
(state.rideVel.y + (accel * vertInput)).coerceIn(-vertTopSpeed, vertTopSpeed),
state.rideVel.z)
loseStamina = true
}
else {
state.rideVel = Vec3(
state.rideVel.x,
lerp(state.rideVel.y, 0.0, vertTopSpeed / 20.0),
state.rideVel.z)
}
//air resistance
//TODO: This should not affect the ride if the player is inputting a force?
//state.currSpeed = max(state.currSpeed - 0.005, 0.0)
//Ensure it would take ten whole seconds to come to a complete stop through air resistance.
if ( loseStamina ) {
state.stamina -= (0.05 / staminaStat).toFloat()
}
//Lose a base amount of stamina just for being airborne
state.stamina -= (0.01 / staminaStat).toFloat()
state.rideVel = Vec3(
state.rideVel.x,
state.rideVel.y,
lerp( state.rideVel.z,0.0, topSpeed / ( 20.0 * 30.0)))
}
}
@@ -8,6 +8,7 @@
package com.cobblemon.mod.common.pokemon.riding.controllers
import com.bedrockk.molang.Expression
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.Rollable
import com.cobblemon.mod.common.api.riding.controller.RideController
@@ -16,6 +17,7 @@ import com.cobblemon.mod.common.api.riding.controller.posing.PoseProvider
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.riding.states.DolphinState
import com.cobblemon.mod.common.util.*
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
@@ -25,33 +27,13 @@ import net.minecraft.world.entity.player.Player
import net.minecraft.world.phys.Vec2
import net.minecraft.world.phys.Vec3
import net.minecraft.world.phys.shapes.Shapes
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.math.*
class DolphinController : RideController {
companion object {
val KEY: ResourceLocation = cobblemonResource("swim/dolphin")
}
var canJump = "true".asExpression()
private set
var jumpVector = listOf("0".asExpression(), "0.3".asExpression(), "0".asExpression())
private set
var speed = "1.0".asExpression()
private set
var driveFactor = "1.0".asExpression()
private set
var reverseDriveFactor = "0.25".asExpression()
private set
var strafeFactor = "0.2".asExpression()
private set
var lastVelocity = Vec3(0.0,0.0,0.0)
private set
private val rollSmoother = SmoothDouble()
override val key: ResourceLocation = KEY
override val poseProvider: PoseProvider = PoseProvider(PoseType.FLOAT)
.with(PoseOption(PoseType.SWIM) { it.deltaMovement.length() >= 0.05 })
@@ -69,9 +51,43 @@ class DolphinController : RideController {
}
}
var handlingExpr: Expression = "(q.get_ride_stats('SKILL', 'LAND') / 100) * (140.0 - 20.0) + 20.0".asExpression()
private set
var topSpeedExpr: Expression = "(q.get_ride_stats('SPEED', 'LAND') / 100) * (1.0 - 0.5) + 0.5".asExpression()
private set
var accelExpr: Expression = ("(q.get_ride_stats('ACCELERATION', 'LAND') / 100.0) * " +
"( (1.0 / (1.0 * 2)) - (1.0 / (20 * 6.0))) + (1.0 / (20 * 6.0))").asExpression()
private set
var staminaExpr: Expression = "(q.get_ride_stats('SKILL', 'LAND') / 100) * (60.0 - 10.0) + 10.0".asExpression()
private set
var jumpExpr: Expression = "(q.get_ride_stats('SKILL', 'LAND') / 100) * (300.0 - 128.0) + 128.0".asExpression()
private set
override fun speed(entity: PokemonEntity, driver: Player): Float {
return getRuntime(entity).resolveFloat(speed)
//Retrieve Vehicle state
val state = getState(entity, ::DolphinState)
//retrieve stats
val topSpeed = getRuntime(entity).resolveDouble(topSpeedExpr) * 2
val accel = getRuntime(entity).resolveDouble(accelExpr) * 2
//speed up and slow down based on input
if( driver.zza > 0.0 && state.currSpeed < topSpeed)
{
state.currSpeed = min( state.currSpeed + accel , topSpeed)
}
else if ( driver.zza < 0.0 && state.currSpeed > 0.0)
{
//Decelerate is currently always half of accel
state.currSpeed = max( state.currSpeed - (accel * 2) , 0.0)
}
//If neither forcing deccel or accel then apply water resistance
else{
state.currSpeed = max( state.currSpeed - (accel / 4.0) , 0.0)
}
return state.currSpeed.toFloat()
}
override fun rotation(entity: PokemonEntity, driver: LivingEntity): Vec2 {
@@ -84,7 +100,7 @@ class DolphinController : RideController {
override fun inertia(entity: PokemonEntity ): Double
{
return 0.05
return 0.1
}
override fun velocity(entity: PokemonEntity, driver: Player, input: Vec3): Vec3 {
@@ -92,20 +108,12 @@ class DolphinController : RideController {
if (!entity.isInWater && !entity.isUnderWater)
{
lastVelocity = Vec3(lastVelocity.x, lastVelocity.y - 0.035, lastVelocity.z)
return lastVelocity
}
val driveFactor = runtime.resolveFloat(this.driveFactor)
val strafeFactor = runtime.resolveFloat(this.strafeFactor)
val f = driver.xxa * strafeFactor
var g = driver.zza * driveFactor
if (g <= 0.0f) {
g *= runtime.resolveFloat(this.reverseDriveFactor)
return Vec3(entity.deltaMovement.x, entity.deltaMovement.x - 0.035, entity.deltaMovement.z )
}
var yComp = 0.0
//TODO:Determine a better way to move around at low speed.
//Get roll to add a left and right strafe during roll
//Not sure if this is desired? Will need to mess around with this
//a bit more
@@ -114,50 +122,52 @@ class DolphinController : RideController {
if (rollable != null)
{
//Can be used again if I better figure out how to deadzone it near the tops?
zComp = -1.0 * g.toDouble() * sin(Math.toRadians(rollable.roll.toDouble()))
yComp = -1.0 * g.toDouble() * sin(Math.toRadians(rollable.pitch.toDouble()))
zComp = -1.0 * sin(Math.toRadians( rollable.roll.toDouble() ) )
yComp = -1.0 * sin(Math.toRadians( rollable.pitch.toDouble() ) )
}
val currVelocity = Vec3(0.0, yComp , g.toDouble())
lastVelocity = currVelocity
val currVelocity = Vec3(0.0, yComp , 1.0)
return currVelocity
}
override fun angRollVel(entity: PokemonEntity, driver: Player, deltaTime: Double): Vec3 {
if (!entity.isInWater && !entity.isUnderWater)
{
return Vec3(0.0, 0.0, 0.0)
}
val rollable = driver as? Rollable
//this should be changed to be speed maybe?
val movingForce = driver.zza
if (rollable != null) {
var yawAngVel = 3 * sin(Math.toRadians(rollable.roll.toDouble())).toFloat()
var pitchAngVel = -1 * Math.abs(sin(Math.toRadians(rollable.roll.toDouble())).toFloat())
//TODO: Tie in handling
val handling = getRuntime(entity).resolveDouble(handlingExpr)
val rotationChangeRate = 10.0
if( rollable != null )
{
var yawForce = rotationChangeRate * sin( Math.toRadians( rollable.roll.toDouble() ))
//for a bit of correction on the rolls limit it to a quarter the amount
var pitchForce = -0.25 * rotationChangeRate * abs(sin( Math.toRadians( rollable.roll.toDouble() )))
//limit rotation modulation when pitched up heavily or pitched down heavily
yawAngVel *= (abs(cos(Math.toRadians(rollable.pitch.toDouble())))).toFloat()
pitchAngVel *= (abs(cos(Math.toRadians(rollable.pitch.toDouble())))).toFloat()
//if you are not pressing forward then don't turn
//yawAngVel *= movingForce
//Ignore x,y,z its angular velocity:
//yaw, pitch, roll
return Vec3(yawAngVel.toDouble(), pitchAngVel.toDouble(), 0.0)
yawForce *= abs(cos( Math.toRadians( rollable.pitch.toDouble() )))
pitchForce *= abs(cos( Math.toRadians( rollable.pitch.toDouble() ))) * 1.5
//Create a deadzone for easier leveling out
if( rollable.roll <= 0.0 && rollable.roll >= -0.0)
{
yawForce = 0.0
pitchForce = 0.0
}
return Vec3( yawForce, pitchForce, 0.0)
}
return Vec3(0.0, 0.0, 0.0)
}
override fun canJump(entity: PokemonEntity, driver: Player) = getRuntime(entity).resolveBoolean(canJump)
override fun canJump(entity: PokemonEntity, driver: Player) = false
override fun jumpForce(entity: PokemonEntity, driver: Player, jumpStrength: Int): Vec3 {
val runtime = getRuntime(entity)
runtime.environment.query.addFunction("jump_strength") { DoubleValue(jumpStrength.toDouble()) }
val jumpVector = jumpVector.map { runtime.resolveFloat(it) }
return Vec3(jumpVector[0].toDouble(), jumpVector[1].toDouble(), jumpVector[2].toDouble())
override fun jumpForce(
entity: PokemonEntity,
driver: Player,
jumpStrength: Int
): Vec3 {
return Vec3.ZERO
}
override fun shouldRoll(entity: PokemonEntity) = true
@@ -168,35 +178,20 @@ class DolphinController : RideController {
override fun encode(buffer: RegistryFriendlyByteBuf) {
super.encode(buffer)
buffer.writeString(this.speed.getString())
buffer.writeString(this.canJump.getString())
buffer.writeString(this.jumpVector[0].getString())
buffer.writeString(this.jumpVector[1].getString())
buffer.writeString(this.jumpVector[2].getString())
buffer.writeString(this.driveFactor.getString())
buffer.writeString(this.reverseDriveFactor.getString())
buffer.writeString(this.strafeFactor.getString())
buffer.writeDouble(this.lastVelocity.x)
buffer.writeDouble(this.lastVelocity.y)
buffer.writeDouble(this.lastVelocity.z)
buffer.writeString(this.handlingExpr.getString())
buffer.writeString(this.staminaExpr.getString())
buffer.writeString(this.jumpExpr.getString())
buffer.writeString(this.topSpeedExpr.getString())
buffer.writeString(this.accelExpr.getString())
}
override fun decode(buffer: RegistryFriendlyByteBuf) {
this.speed = buffer.readString().asExpression()
this.canJump = buffer.readString().asExpression()
this.jumpVector = listOf(
buffer.readString().asExpression(),
buffer.readString().asExpression(),
buffer.readString().asExpression()
)
this.driveFactor = buffer.readString().asExpression()
this.reverseDriveFactor = buffer.readString().asExpression()
this.strafeFactor = buffer.readString().asExpression()
this.strafeFactor = buffer.readString().asExpression()
val x = buffer.readDouble()
val y = buffer.readDouble()
val z = buffer.readDouble()
this.lastVelocity = Vec3(x, y, z)
this.handlingExpr = buffer.readString().asExpression()
this.staminaExpr = buffer.readString().asExpression()
this.jumpExpr = buffer.readString().asExpression()
this.topSpeedExpr = buffer.readString().asExpression()
this.accelExpr = buffer.readString().asExpression()
}
}
}
@@ -8,30 +8,27 @@
package com.cobblemon.mod.common.pokemon.riding.controllers
import com.bedrockk.molang.Expression
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.Rollable
import com.cobblemon.mod.common.api.riding.controller.RideController
import com.cobblemon.mod.common.api.riding.controller.posing.PoseOption
import com.cobblemon.mod.common.api.riding.controller.posing.PoseProvider
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.riding.states.GenericLandState
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.blockPositionsAsListRounded
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.getString
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.resolveBoolean
import com.cobblemon.mod.common.util.resolveFloat
import com.cobblemon.mod.common.util.writeString
import com.cobblemon.mod.common.pokemon.riding.states.JetAirState
import com.cobblemon.mod.common.util.*
import net.minecraft.client.Minecraft
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.SmoothDouble
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.player.Player
import net.minecraft.world.phys.Vec2
import net.minecraft.world.phys.Vec3
import net.minecraft.world.phys.shapes.Shapes
import kotlin.math.max
import kotlin.math.min
import kotlin.math.*
class GenericLandController : RideController {
companion object {
@@ -53,6 +50,20 @@ class GenericLandController : RideController {
var strafeFactor = "0.2".asExpression()
private set
var handlingExpr: Expression = "q.get_ride_stats('SKILL', 'LAND', 140.0, 20.0)".asExpression()
private set
var topSpeedExpr: Expression = "q.get_ride_stats('SPEED', 'LAND', 1.0, 0.3)".asExpression()
private set
// Max accel is a whole 1.0 in 1 second. The conversion in the function below is to convert seconds to ticks
var accelExpr: Expression = "q.get_ride_stats('ACCELERATION', 'LAND', (1.0 / (20.0 * 0.5)), (1.0 / (20.0 * 5.0)))".asExpression()
private set
// Between 30 seconds and 10 seconds at the lowest when at full speed.
var staminaExpr: Expression = "q.get_ride_stats('SKILL', 'LAND', 30.0, 10.0)".asExpression()
private set
//Between a one block jump and a ten block jump
var jumpExpr: Expression = "q.get_ride_stats('SKILL', 'LAND', 10.0, 1.0)".asExpression()
private set
@Transient
override val key: ResourceLocation = KEY
@Transient
@@ -78,8 +89,7 @@ class GenericLandController : RideController {
override fun speed(entity: PokemonEntity, driver: Player): Float {
val state = getState(entity, ::GenericLandState)
state.currSpeed = 1.0
return state.currSpeed.toFloat()
return state.rideVel.length().toFloat()
}
override fun rotation(entity: PokemonEntity, driver: LivingEntity): Vec2 {
@@ -97,16 +107,54 @@ class GenericLandController : RideController {
if (g <= 0.0f) {
g *= runtime.resolveFloat(this.reverseDriveFactor)
}
val gravity = -0.8
val gravity = -1.0
val state = getState(entity, ::GenericLandState)
val velocity = Vec3(f.toDouble() * state.currSpeed, gravity, g.toDouble() * state.currSpeed)
return velocity
calculateRideSpaceVel(entity, driver, state)
//Jump the thang!
if (Minecraft.getInstance().options.keyJump.isDown() && entity.onGround()) {
state.rideVel = Vec3(state.rideVel.x, 2.0, state.rideVel.z)
}
//This is cheap.
//Also make it stop quicker the slower it is.
val maxSpeed = 1.0
//state.currVel = state.currVel.lerp( Vec3(0.0, state.currVel.y, 0.0), 1.0/20.0 )
//entity.deltaMovement = entity.deltaMovement.lerp(velocity, 1.0)
//state.currVel = state.currVel.add( f.toDouble() / 20.0, gravity / 20.0 , g.toDouble() / 20.0)
return state.rideVel
}
override fun canJump(entity: PokemonEntity, driver: Player) = getRuntime(entity).resolveBoolean(canJump)
override fun shouldRoll(entity: PokemonEntity): Boolean {
return false
}
override fun rotationOnMouseXY(
entity: PokemonEntity,
driver: Player,
yMouse: Double,
xMouse: Double,
yMouseSmoother: SmoothDouble,
xMouseSmoother: SmoothDouble,
sensitivity: Double,
deltaTime: Double
): Vec3 {
//Smooth out mouse input.
val smoothingSpeed = 4
val xInput = xMouseSmoother.getNewDeltaValue(xMouse * 0.1, deltaTime * smoothingSpeed);
val yInput = yMouseSmoother.getNewDeltaValue(yMouse * 0.1, deltaTime * smoothingSpeed);
//yaw, pitch, roll
return Vec3(0.0, yInput, xInput)
}
override fun canJump(entity: PokemonEntity, driver: Player) = false
//TODO: bring in stamina stats
override fun setRideBar(entity: PokemonEntity, driver: Player): Float {
@@ -155,4 +203,50 @@ class GenericLandController : RideController {
this.reverseDriveFactor = buffer.readString().asExpression()
this.strafeFactor = buffer.readString().asExpression()
}
/*
* Normalizes the current speed between minSpeed and maxSpeed.
* The result is clamped between 0.0 and 1.0, where 0.0 represents minSpeed and 1.0 represents maxSpeed.
*/
fun normalizeSpeed(currSpeed: Double, minSpeed: Double, maxSpeed: Double): Double {
require(maxSpeed > minSpeed) { "maxSpeed must be greater than minSpeed" }
return ((currSpeed - minSpeed) / (maxSpeed - minSpeed)).coerceIn(0.0, 1.0)
}
fun calculateRideSpaceVel( entity: PokemonEntity, driver: Player, state: GenericLandState){
val topSpeed = getRuntime(entity).resolveDouble(topSpeedExpr)
val accel = getRuntime(entity).resolveDouble(accelExpr)
val speed = state.rideVel.length()
//Minimum speed aka how fast you can go backwards.
//val minSpeed = -( topSpeed / 3.0)
val minSpeed = 0.0
//speed up and slow down based on input
if( driver.zza > 0.0 && speed <= topSpeed && state.stamina > 0.0f ) {
//modify acceleration to be slower when at closer speeds to top speed
val accelMod = max( (normalizeSpeed(speed, minSpeed, topSpeed) - 1).pow(2), 0.1)
state.rideVel = Vec3(state.rideVel.x, state.rideVel.y, min( state.rideVel.z + (accel * accelMod) , topSpeed))
}
else if( driver.zza >= 0.0 && (state.stamina == 0.0f) || speed > topSpeed) {
state.rideVel = Vec3(state.rideVel.x, state.rideVel.y, max( state.rideVel.z - ((accel) / 4) , minSpeed))
}
//The minimumspeed you can
else if ( driver.zza < 0.0 && speed > minSpeed) {
//modify deccel to be slower when at closer speeds to minimum speed
val deccelMod = max( -(normalizeSpeed(speed, minSpeed, topSpeed)) + 1, 0.0)
//Decelerate currently always a constant half of max acceleration.
state.rideVel = Vec3(state.rideVel.x, state.rideVel.y, max( state.rideVel.z - ((accel * deccelMod) / 2) , minSpeed))
}
state.rideVel= Vec3(state.rideVel.x, max(state.rideVel.y - 0.2, -1.0) , state.rideVel.z)
if( entity.onGround())
{
state.rideVel= Vec3(state.rideVel.x, 0.0 , state.rideVel.z)
}
}
}
@@ -35,52 +35,63 @@ class JetAirController : RideController {
override val condition: (PokemonEntity) -> Boolean = { true }
var jumpVector = listOf("0".asExpression(), "0.3".asExpression(), "0".asExpression())
private set
var gravity: Expression = "0".asExpression()
private set
var horizontalAcceleration: Expression = "0.1".asExpression()
var minSpeed: Expression = "1.2".asExpression()
private set
var verticalAcceleration: Expression = "0.1".asExpression()
var handlingExpr: Expression = "q.get_ride_stats('SKILL', 'AIR', 140.0, 20.0)".asExpression()
private set
var handlingYawExpr: Expression = "q.get_ride_stats('SKILL', 'AIR', 25.0, 8.0)".asExpression()
private set
var topSpeedExpr: Expression = "q.get_ride_stats('SPEED', 'AIR', 2.5, 1.0)".asExpression()
private set
// Max accel is a whole 1.0 in 1 second. The conversion in the function below is to convert seconds to ticks
var accelExpr: Expression = "q.get_ride_stats('ACCELERATION', 'AIR', (1.0 / (20.0 * 1.0)), (1.0 / (20.0 * 5.0)))".asExpression()
private set
// Between 60 seconds and 10 seconds at the lowest when at full speed.
var staminaExpr: Expression = "q.get_ride_stats('SKILL', 'AIR', 60.0, 10.0)".asExpression()
private set
var jumpExpr: Expression = "q.get_ride_stats('SKILL', 'AIR', 300.0, 128.0)".asExpression()
private set
//5 bl/s. Minimum speed so long as jets have no gravity.
var minSpeed: Expression = "0.8".asExpression()
// Make configurable by json
var infiniteStamina: Expression = "false".asExpression()
private set
var handlingExpr: Expression = "(q.get_ride_stats('SKILL', 'AIR') / 100) * (140.0 - 20.0) + 20.0".asExpression()
private set
var handlingYawExpr: Expression = "(q.get_ride_stats('SKILL', 'AIR') / 100) * (16.0 - 8.0) + 20.0".asExpression()
private set
var topSpeedExpr: Expression = "(q.get_ride_stats('SPEED', 'AIR') / 100) * (2.5 - 1.0) + 1.0".asExpression()
private set
//Same equation as above except the minimum accel is 8 seconds from the minimum accel to 1 second.
//This likely needs simplified to probably just the amount of seconds to reach max from zero?
var accelExpr: Expression = ("(q.get_ride_stats('ACCELERATION', 'AIR') / 100.0) * " +
"( ((2.5 - 0.35) / 20) - ((2.5 - 0.35) / (20 * 5))) + ((2.5 - 0.35) / (20 * 5))").asExpression()
var infiniteAltitude: Expression = "false".asExpression()
private set
override fun speed(entity: PokemonEntity, driver: Player): Float {
//retrieve stats
val topSpeed = getRuntime(entity).resolveDouble(topSpeedExpr)
val accel = getRuntime(entity).resolveDouble(accelExpr)
val staminaStat = getRuntime(entity).resolveDouble(staminaExpr)
//retrieve state
val state = getState(entity, ::JetAirState)
//retrieve minSpeed
val mSpeed = getRuntime(entity).resolveDouble(minSpeed)
val minSpeed = getRuntime(entity).resolveDouble(minSpeed)
//speed up and slow down based on input
if (driver.zza > 0.0 && state.currSpeed < topSpeed) {
state.currSpeed = min(state.currSpeed + accel, topSpeed)
} else if (driver.zza < 0.0 && state.currSpeed > getRuntime(entity).resolveDouble(minSpeed)) {
//Decelerate currently always a constant half of max acceleration.
state.currSpeed = max(state.currSpeed - (((2.5 - 0.35) / 20) / 2), 1.2)
//Reduce stamina unless stamina is infinite
if( !getRuntime(entity).resolveBoolean(infiniteStamina)) {
//Calculate stamina loss due to speed
//At max speed it will tick down 0.1 a second so the stamina will last ten seconds
//There has got to be a better way to express this equation. It interpolates between 0.5 and 1.0
var staminaRate = (normalizeSpeed(state.rideVel.length(), minSpeed, topSpeed))
//interpolate between 0.25 and 1.0 so that you always have at least a min of 0.25 stam loss
staminaRate = 0.25 + (0.75 * staminaRate.pow(3))
//Calculate stamina loss in seconds achievable at top speed
val staminaLoss = staminaRate * (1.0 / (20.0 * staminaStat))
state.stamina = max(state.stamina - staminaLoss, 0.0).toFloat()
}
else{
state.stamina = 1.0f
}
//Returning is redundant as the logic is not connected currently
return state.currSpeed.toFloat()
return state.rideVel.length().toFloat()
}
override fun rotation(entity: PokemonEntity, driver: LivingEntity): Vec2 {
@@ -88,19 +99,41 @@ class JetAirController : RideController {
}
override fun velocity(entity: PokemonEntity, driver: Player, input: Vec3): Vec3 {
var upForce = 0.0
var forwardForce = 0.0
val rollable = driver as? Rollable
if (rollable != null) {
upForce = -1.0 * sin(Math.toRadians(rollable.pitch.toDouble()))
forwardForce = cos(Math.toRadians(rollable.pitch.toDouble()))
}
//Gather state information
val state = getState(entity, ::JetAirState)
val velocity = Vec3(0.0 , (upForce)* state.currSpeed, forwardForce * state.currSpeed)
val rollable = driver as? Rollable
//Calculate ride space velocity
calculateRideSpaceVel(entity, driver, state)
//Translate ride space velocity to world space velocity.
if( rollable != null ) {
upForce = -1.0 * sin(Math.toRadians( rollable.pitch.toDouble() )) * state.rideVel.z
forwardForce = cos(Math.toRadians( rollable.pitch.toDouble() )) * state.rideVel.z
}
//If stamina has run out then initiate forced glide down.
upForce = if(state.stamina > 0.0) upForce else -0.7
val altitudeLimit = getRuntime(entity).resolveDouble(jumpExpr)
val pushingHeightLimit = if(getRuntime(entity).resolveBoolean(infiniteAltitude)) false
else (entity.y >= 128 && entity.xRot <= 0)
//Incorporate the altitude limiter.
//Apply downwards force if limit is reached. Resist upwards force on a buffer of 30 blocks past the limit.
upForce = if(pushingHeightLimit) upForce - upForce * ((entity.y - altitudeLimit) / 30) - 0.5
else upForce
val velocity = Vec3(0.0 , upForce, forwardForce )
return velocity
}
@@ -108,18 +141,25 @@ class JetAirController : RideController {
override fun useAngVelSmoothing(entity: PokemonEntity): Boolean = true
override fun angRollVel(entity: PokemonEntity, driver: Player, deltaTime: Double): Vec3 {
//Retrieve state
val state = getState(entity, ::JetAirState)
//Cap at a rate of 5fps so frame skips dont lead to huge jumps
val cappedDeltaTime = min( deltaTime, 0.2)
//Get handling in degrees per second
val yawRotRate = getRuntime(entity).resolveDouble(handlingYawExpr)
//Base the change off of deltatime.
val handlingYaw = yawRotRate * (deltaTime)
var handlingYaw = yawRotRate * (cappedDeltaTime)
//apply stamina debuff if applicable
handlingYaw *= if(state.stamina > 0.0) 1.0 else 0.5
//A+D to yaw
//TODO: Currently doubled, check if this makes the most sense to have it this high.
val yawForce = -2.0 * driver.xxa * handlingYaw
val yawForce = driver.xxa * handlingYaw * -1
return Vec3(yawForce, 0.0, 0.0)
return Vec3( yawForce, 0.0, 0.0)
}
override fun rotationOnMouseXY
@@ -133,32 +173,35 @@ class JetAirController : RideController {
deltaTime: Double ): Vec3
{
if(driver !is Rollable) return Vec3.ZERO
//TODO: figure out a cleaner solution to this issue of large jumps when skipping frames or lagging
//Cap at a rate of 5fps so frame skips dont lead to huge jumps
val cappedDeltaTime = min(deltaTime, 0.2)
val cappedDeltaTime = min( deltaTime, 0.2)
//Retrieve state
val state = getState(entity, ::JetAirState)
// Accumulate the mouse input
state.currMouseXForce = (state.currMouseXForce + (0.0015 * xMouse)).coerceIn(-1.0, 1.0)
state.currMouseYForce = (state.currMouseYForce + (0.0015 * yMouse)).coerceIn(-1.0, 1.0)
state.currMouseXForce = (state.currMouseXForce + ( 0.0015 * xMouse )).coerceIn(-1.0, 1.0)
state.currMouseYForce = (state.currMouseYForce + ( 0.0015 * yMouse )).coerceIn(-1.0, 1.0)
//Get handling in degrees per second
var handling = getRuntime(entity).resolveDouble(handlingExpr)
//convert it to partial ticks
handling *= (deltaTime)
//convert it to delta time
handling *= (cappedDeltaTime)
//apply stamina debuff if applicable
handling *= if(state.stamina > 0.0) 1.0 else 0.5
val poke = driver.vehicle as? PokemonEntity
//TODO: reevaluate if deadzones are needed and if they are still causing issues.
//create deadzones for the constant input values.
val xInput = remapWithDeadzone(state.currMouseXForce, 0.025, 1.0)
val yInput = remapWithDeadzone(state.currMouseYForce, 0.025, 1.0)
//val xInput = remapWithDeadzone(state.currMouseXForce, 0.025, 1.0)
//val yInput = remapWithDeadzone(state.currMouseYForce, 0.025, 1.0)
val pitchRot = handling * state.currMouseYForce
//Roll is 1.5 times as fast as pitch
val rollRot = handling * 1.5 * state.currMouseXForce
@@ -166,19 +209,11 @@ class JetAirController : RideController {
return Vec3(0.0, pitchRot, rollRot)
}
//TODO: bring in stamina stats
override fun setRideBar(entity: PokemonEntity, driver: Player): Float {
val topSpeed = getRuntime(entity).resolveDouble(topSpeedExpr)
val mSpeed = getRuntime(entity).resolveDouble(minSpeed)
//Retrieve stamina from state and tick down based on current speed vs top speed.
//Retrieve stamina from state
val state = getState(entity, ::JetAirState)
//For now 1.0 is a temp min speed.
//At max speed it will tick down 0.1 a second so the stamina will last ten seconds
//There has got to be a better way to express this equation. It interpolates between 0.5 and 1.0
val staminaLoss = (((state.currSpeed - 1.2) / (topSpeed - 1.2)) / 2 + 0.5) * ((1.0 / 20.0) * 0.1)
state.stamina = max(state.stamina - staminaLoss, 0.0).toFloat()
return state.stamina / 1.0f
return (state.stamina / 1.0f)
}
override fun canJump(
@@ -197,47 +232,105 @@ class JetAirController : RideController {
}
override fun gravity(entity: PokemonEntity, regularGravity: Double): Double {
var grav = 0.0
//TODO: create a better gravity equation.
//if (maxSpeed <= 0.5) grav = 0.5 - maxSpeed
val grav = 0.0
return grav
}
override fun shouldRoll(entity: PokemonEntity) = true
override fun rideFovMult(entity: PokemonEntity, driver: Player): Float {
val topSpeed = getRuntime(entity).resolveDouble(topSpeedExpr)
val minSpeed = getRuntime(entity).resolveDouble(minSpeed)
val state = getState(entity, ::JetAirState)
//Must I ensure that topspeed is greater than minimum?
val normalizedSpeed = normalizeSpeed(state.rideVel.length(), minSpeed, topSpeed)
//TODO: Determine if this should be based on max possible speed instead of top speed.
//Only ever want the fov change to be a max of 0.2 and for it to have non linear scaling.
return 1.0f + normalizedSpeed.pow(2).toFloat() * 0.2f
}
override fun encode(buffer: RegistryFriendlyByteBuf) {
super.encode(buffer)
buffer.writeString(gravity.toString())
buffer.writeString(horizontalAcceleration.toString())
buffer.writeString(verticalAcceleration.toString())
buffer.writeString(minSpeed.toString())
buffer.writeString(handlingExpr.toString())
buffer.writeString(handlingYawExpr.toString())
buffer.writeString(topSpeedExpr.toString())
buffer.writeString(accelExpr.toString())
buffer.writeString(jumpExpr.toString())
buffer.writeString(staminaExpr.toString())
buffer.writeString(infiniteStamina.toString())
buffer.writeString(infiniteAltitude.toString())
}
override fun decode(buffer: RegistryFriendlyByteBuf) {
gravity = buffer.readString().asExpression()
horizontalAcceleration = buffer.readString().asExpression()
verticalAcceleration = buffer.readString().asExpression()
minSpeed = buffer.readString().asExpression()
handlingExpr = buffer.readString().asExpression()
handlingYawExpr = buffer.readString().asExpression()
topSpeedExpr = buffer.readString().asExpression()
accelExpr = buffer.readString().asExpression()
jumpExpr = buffer.readString().asExpression()
staminaExpr = buffer.readString().asExpression()
infiniteStamina = buffer.readString().asExpression()
infiniteAltitude = buffer.readString().asExpression()
}
companion object {
val KEY = cobblemonResource("air/jet")
}
//This likely needs moved to somewhere that all ride contollers can share it. Maybe a ride utilities file?
//Creates a deadzone around zero that extends out to deadzoneLow in both +- and then remaps
//the value linearly between -1.0 and 1.0 outside of the deadzone
/*
* Calculates the change in the ride space vector due to player input and ride state
*/
fun calculateRideSpaceVel( entity: PokemonEntity, driver: Player, state: JetAirState){
val topSpeed = getRuntime(entity).resolveDouble(topSpeedExpr)
val accel = getRuntime(entity).resolveDouble(accelExpr)
val altitudeLimit = getRuntime(entity).resolveDouble(jumpExpr)
val minSpeed = getRuntime(entity).resolveDouble(minSpeed)
val speed = state.rideVel.length()
//Give no altitude limit if at max jump stat.
val pushingHeightLimit = if(getRuntime(entity).resolveBoolean(infiniteStamina)) false
else (entity.y >= altitudeLimit && entity.xRot <= 0)
//speed up and slow down based on input
if( driver.zza > 0.0 && speed < topSpeed && state.stamina > 0.0f && !pushingHeightLimit) {
//modify acceleration to be slower when at closer speeds to top speed
val accelMod = max( -(normalizeSpeed(speed, minSpeed, topSpeed)) + 1, 0.0)
state.rideVel = Vec3(state.rideVel.x, state.rideVel.y, min( state.rideVel.z + (accel * accelMod) , topSpeed))
}
else if( driver.zza >= 0.0 && (state.stamina == 0.0f || pushingHeightLimit) ) {
state.rideVel = Vec3(state.rideVel.x, state.rideVel.y, max( state.rideVel.z - ((accel) / 4) , minSpeed))
}
else if ( driver.zza < 0.0 && speed > minSpeed) {
//modify deccel to be slower when at closer speeds to minimum speed
val deccelMod = max( (normalizeSpeed(speed, minSpeed, topSpeed) - 1).pow(2), 0.1)
//Decelerate currently always a constant half of max acceleration.
state.rideVel = Vec3(state.rideVel.x, state.rideVel.y, max( state.rideVel.z - ((accel * deccelMod) / 2) , minSpeed))
}
}
//TODO: Move these functions to a riding util class.
/*
* Normalizes the current speed between minSpeed and maxSpeed.
* The result is clamped between 0.0 and 1.0, where 0.0 represents minSpeed and 1.0 represents maxSpeed.
*/
fun normalizeSpeed(currSpeed: Double, minSpeed: Double, maxSpeed: Double): Double {
require(maxSpeed > minSpeed) { "maxSpeed must be greater than minSpeed" }
return ((currSpeed - minSpeed) / (maxSpeed - minSpeed)).coerceIn(0.0, 1.0)
}
/*
* Creates a deadzone around zero that extends out to deadzoneLow in both +- and then remaps
* the value linearly between -1.0 and 1.0 outside of the deadzone
*/
fun remapWithDeadzone(value: Double, deadzoneLow: Double = 0.2, deadzoneHigh: Double = 1.0): Double {
// If the absolute value is below the deadzone, return 0.
if (kotlin.math.abs(value) < deadzoneLow) return 0.0
@@ -247,4 +340,6 @@ class JetAirController : RideController {
//Attach the sign back to the vale
return adjusted * sign
}
}
}
@@ -23,6 +23,7 @@ import com.cobblemon.mod.common.util.getString
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.resolveInt
import com.cobblemon.mod.common.util.writeString
import net.minecraft.client.Minecraft
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.util.SmoothDouble
import net.minecraft.world.entity.LivingEntity
@@ -74,12 +75,32 @@ class RunUpToFlightCompositeController : RideController {
if (state.activeController == flightController && entity.onGround() && state.timeTransitioned + 20 < entity.level().gameTime) {
//Pass the speed to the next state
val flightState = flightController.getState(entity, ::BirdAirState)
val groundState = landController.getState(entity, ::GenericLandState)
val flightState = flightController.getState( entity, ::BirdAirState )
val groundState = landController.getState( entity, ::GenericLandState )
groundState.currSpeed = flightState.currSpeed
groundState.rideVel = flightState.rideVel
groundState.stamina = flightState.stamina
getState(entity, ::RunUpToFlightCompositeState).activeController = landController
entity.setBehaviourFlag(PokemonBehaviourFlag.FLYING, false)
getState(entity, ::RunUpToFlightCompositeState).let {
it.activeController = landController
entity.setBehaviourFlag(PokemonBehaviourFlag.FLYING, false)
it.timeTransitioned = entity.level().gameTime
}
}
else if (state.activeController == landController && Minecraft.getInstance().options.keyJump.isDown && state.timeTransitioned + 20 < entity.level().gameTime) {
//Pass the speed to the next state
val flightState = flightController.getState( entity, ::BirdAirState )
val groundState = landController.getState( entity, ::GenericLandState )
flightState.currSpeed = groundState.currSpeed
flightState.rideVel = groundState.rideVel
flightState.stamina = groundState.stamina
getState(entity, ::RunUpToFlightCompositeState).let {
it.activeController = flightController
entity.setBehaviourFlag(PokemonBehaviourFlag.FLYING, true)
it.timeTransitioned = entity.level().gameTime
}
}
return getActiveController(entity).speed(entity, driver)
@@ -101,6 +122,14 @@ class RunUpToFlightCompositeController : RideController {
return getActiveController(entity).angRollVel(entity, driver, deltaTime)
}
override fun rideFovMult(entity: PokemonEntity, driver: Player): Float {
return getActiveController(entity).rideFovMult(entity, driver)
}
override fun setRideBar(entity: PokemonEntity, driver: Player): Float {
return getActiveController(entity).setRideBar(entity, driver)
}
override fun canJump(entity: PokemonEntity, driver: Player): Boolean {
return getActiveController(entity).canJump(entity, driver)
}
@@ -111,19 +140,9 @@ class RunUpToFlightCompositeController : RideController {
jumpStrength: Int
): Vec3 {
val controller = getActiveController(entity)
if (controller == landController && jumpStrength >= getRuntime(entity).resolveInt(minimumJump)) {
val state = getState(entity, ::CompositeState)
val isJumping = Minecraft.getInstance().options.keyJump.isDown
//Pass the speed to the next state
val flightState = flightController.getState(entity, ::BirdAirState )
val groundState = landController.getState(entity, ::GenericLandState)
flightState.currSpeed = groundState.currSpeed
getState(entity, ::RunUpToFlightCompositeState).let {
it.activeController = flightController
entity.setBehaviourFlag(PokemonBehaviourFlag.FLYING, true)
it.timeTransitioned = entity.level().gameTime
}
}
return controller.jumpForce(entity, driver, jumpStrength)
}
@@ -21,6 +21,7 @@ import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.getString
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.resolveInt
import com.cobblemon.mod.common.util.writeString
import net.minecraft.client.Minecraft
import net.minecraft.network.RegistryFriendlyByteBuf
@@ -73,14 +74,15 @@ class RunUpToJetFlightCompositeController : RideController {
driver: Player
): Float {
val state = getState(entity, ::RunUpToFlightCompositeState)
if ((state.activeController == flightController && entity.onGround() && state.timeTransitioned + 20 < entity.level().gameTime) ||
(Minecraft.getInstance().options.keySprint.isDown && state.activeController == flightController && state.timeTransitioned + 20 < entity.level().gameTime)) {
if (( state.activeController == flightController && entity.onGround() && state.timeTransitioned + 20 < entity.level().gameTime) ||
( Minecraft.getInstance().options.keySprint.isDown() && state.activeController == flightController && state.timeTransitioned + 20 < entity.level().gameTime)) {
//Pass the data to the next state
val flightState = flightController.getState(entity, ::JetAirState)
val groundState = landController.getState(entity, ::GenericLandState)
val flightState = flightController.getState( entity, ::JetAirState )
val groundState = landController.getState( entity, ::GenericLandState )
groundState.currSpeed = flightState.currSpeed
groundState.stamina = flightState.stamina
groundState.rideVel = flightState.rideVel
//Clear accumulated rotation input when transferring to ground input
flightState.currMouseXForce = 0.0
@@ -102,10 +104,11 @@ class RunUpToJetFlightCompositeController : RideController {
)
{
//Pass the data to the next state
val flightState = flightController.getState(entity, ::JetAirState)
val groundState = landController.getState(entity, ::GenericLandState)
val flightState = flightController.getState( entity, ::JetAirState )
val groundState = landController.getState( entity, ::GenericLandState )
flightState.currSpeed = groundState.currSpeed
flightState.stamina = groundState.stamina
flightState.rideVel = groundState.rideVel
getState(entity, ::RunUpToFlightCompositeState).let {
it.activeController = flightController
@@ -141,28 +144,18 @@ class RunUpToJetFlightCompositeController : RideController {
return getActiveController(entity).setRideBar(entity, driver)
}
override fun rideFovMult(entity: PokemonEntity, driver: Player): Float {
return getActiveController(entity).rideFovMult(entity, driver)
}
override fun jumpForce(
entity: PokemonEntity,
driver: Player,
jumpStrength: Int
): Vec3 {
val controller = getActiveController(entity)
/*
if (controller == landController && jumpStrength >= getRuntime(entity).resolveInt(minimumJump)) {
//Pass the data to the next state
val flightState = flightController.getState( entity, ::JetAirState )
val groundState = landController.getState( entity, ::GenericLandState )
flightState.currSpeed = groundState.currSpeed
flightState.stamina = groundState.stamina
getState(entity, ::RunUpToFlightCompositeState).let {
it.activeController = flightController
entity.setBehaviourFlag(PokemonBehaviourFlag.FLYING, true)
it.timeTransitioned = entity.level().gameTime
}
}
*/
return controller.jumpForce(entity, driver, jumpStrength)
}
@@ -227,4 +220,4 @@ class RunUpToJetFlightCompositeController : RideController {
companion object {
val KEY = cobblemonResource("composite/run_up_to_jet_flight")
}
}
}
@@ -8,54 +8,35 @@
package com.cobblemon.mod.common.pokemon.riding.controllers
import com.bedrockk.molang.Expression
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.api.riding.RidingStyle
import com.cobblemon.mod.common.api.riding.controller.RideController
import com.cobblemon.mod.common.api.riding.controller.posing.PoseOption
import com.cobblemon.mod.common.api.riding.controller.posing.PoseProvider
import com.cobblemon.mod.common.api.riding.stats.RidingStat
import com.cobblemon.mod.common.api.riding.RidingOnPlayerSoundEventInstance
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.riding.controllers.BirdAirController.Companion
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.blockPositionsAsListRounded
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.getString
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.resolveBoolean
import com.cobblemon.mod.common.util.resolveFloat
import com.cobblemon.mod.common.util.writeString
import kotlin.math.absoluteValue
import com.cobblemon.mod.common.pokemon.riding.states.LandVehicleState
import com.cobblemon.mod.common.util.*
import net.minecraft.client.Minecraft
import net.minecraft.client.player.LocalPlayer
import net.minecraft.client.resources.sounds.Sound
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import net.minecraft.sounds.SoundEvent
import net.minecraft.sounds.SoundEvents
import net.minecraft.util.Mth
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.player.Player
import net.minecraft.world.phys.Vec2
import net.minecraft.world.phys.Vec3
import net.minecraft.world.phys.shapes.Shapes
import kotlin.math.max
import kotlin.math.min
import kotlin.math.*
class VehicleLandController : RideController {
companion object {
val KEY: ResourceLocation = cobblemonResource("land/vehicle")
val MAXTOPSPEED = 1.0 // 20 bl/s
val MINTOPSPEED = 0.35 // 7 bl/s
//val MINSPEED = 0.25 // 5 bl/s
//Accel will lie between 1.0 second and 5.0 seconds
val MAXACCEL = (MAXTOPSPEED ) / (20*3) //3.0 second to max speed
val MINACCEL = (MAXTOPSPEED ) / (20*8) // 8 seconds to max speed
//Can rotate 90 degrees
val MAXHANDLING = 90.0
val MINHANDLING = 30.0
val MAXYAWHANDLING = 16.0
val MINYAWHANDLING = 8.0
}
var canJump = "true".asExpression()
@@ -69,6 +50,9 @@ class VehicleLandController : RideController {
var reverseDriveFactor = "0.25".asExpression()
private set
var sound = "minecraft:item.elytra.flying".asExpression()
private set
var minimumSpeedToTurn = "0.1".asExpression()
private set
var rotationSpeed = "45/20".asExpression()
@@ -76,8 +60,20 @@ class VehicleLandController : RideController {
var lookYawLimit = "101".asExpression()
private set
var currSpeed = 0.0
// TODO: Tweak these stats to a workable state.
var handlingExpr: Expression = "q.get_ride_stats('SKILL', 'AIR', 140.0, 20.0)".asExpression()
private set
var topSpeedExpr: Expression = "q.get_ride_stats('SPEED', 'AIR', 1.0, 0.5)".asExpression()
private set
// Max accel is a whole 1.0 in 2 seconds. The conversion in the function below is to convert seconds to ticks
var accelExpr: Expression = "q.get_ride_stats('ACCELERATION', 'AIR', (1.0 / (20.0 * 2.0)), (1.0 / (20.0 * 6.0)))".asExpression()
private set
// Between 60 seconds and 10 seconds at the lowest when at full speed.
var staminaExpr: Expression = "q.get_ride_stats('SKILL', 'AIR', 60.0, 10.0)".asExpression()
private set
var jumpExpr: Expression = "q.get_ride_stats('SKILL', 'AIR', 300.0, 128.0)".asExpression()
private set
@Transient
override val key: ResourceLocation = KEY
@@ -93,47 +89,132 @@ class VehicleLandController : RideController {
if (entity.isInWater || entity.isUnderWater) {
return@any false
}
/*
//This might not actually work, depending on what the yPos actually is. yPos of the middle of the entity? the feet?
if (it.y.toDouble() == (entity.position().y)) {
val blockState = entity.level().getBlockState(it.below())
return@any !(!blockState.isAir && blockState.fluidState.isEmpty)
return@any (!blockState.isAir && blockState.fluidState.isEmpty)
}
*/
true
}
}
override fun speed(entity: PokemonEntity, driver: Player): Float {
val topSpeed = entity.getRideStat(RidingStat.SPEED, RidingStyle.AIR, MINTOPSPEED, MAXTOPSPEED)
val accel = entity.getRideStat(RidingStat.ACCELERATION, RidingStyle.AIR, MINACCEL, MAXACCEL)
//Retrieve Vehicle state
val state = getState(entity, ::LandVehicleState)
//retrieve stats
val topSpeed = getRuntime(entity).resolveDouble(topSpeedExpr) * 2
val accel = getRuntime(entity).resolveDouble(accelExpr) * 2
//speed up and slow down based on input
if (driver.zza > 0.0 && currSpeed < topSpeed) {
currSpeed = min(currSpeed + accel , topSpeed)
} else if (driver.zza < 0.0 && currSpeed > 0.0) {
//Decelerate is now always a constant half of max acceleration.
currSpeed = max(currSpeed - (MAXACCEL / 2), 0.0)
if( driver.zza > 0.0 && state.currSpeed < topSpeed)
{
state.currSpeed = min( state.currSpeed + accel , topSpeed)
}
else if ( driver.zza < 0.0 && state.currSpeed > 0.0)
{
//Decelerate is currently always half of accel
state.currSpeed = max( state.currSpeed - (accel * 2) , 0.0)
}
else{
state.currSpeed = max( state.currSpeed - (accel / 2.0) , 0.0)
}
return currSpeed.toFloat()
return state.currSpeed.toFloat()
}
override fun rotation(entity: PokemonEntity, driver: LivingEntity): Vec2 {
//Retrieve Vehicle state
val state = getState(entity, ::LandVehicleState)
val rotationDegrees = driver.xxa * getRuntime(entity).resolveFloat(this.rotationSpeed)
return Vec2(driver.xRot, entity.yRot - rotationDegrees)
if( Minecraft.getInstance().options.keyJump.isDown() ){
//If the current drift has not been set and the car is rotating then set drift
if( state.currDrift == 0.0 && rotationDegrees != 0.0f) {
//Set to base value which will be half.
state.currDrift = sign(rotationDegrees) * 2.5
state.turnSmoother.reset()
}
//Calculate any changes to drift
val driftChangeRate = (1.0 / (20.0 * 0.15))
//If Drift is negative already
if(state.currDrift < 0) {
state.currDrift = (state.currDrift + (sign(rotationDegrees) * driftChangeRate)).coerceIn(-5.0, -1.0)
}else{
state.currDrift = (state.currDrift + (sign(rotationDegrees) * driftChangeRate)).coerceIn(1.0, 5.0)
}
return Vec2(driver.xRot, entity.yRot - state.currDrift.toFloat())
}
//If the drift key is not held then reset the curr drift to 0 and use normal rotation
else{
//If the drift key has just been released perform boost addition and drift reset
if( state.currDrift != 0.0){
state.turnSmoother.reset()
state.currDrift = 0.0
state.driftBoost = min(state.driftBoost + 1.0, 2.0)
driver.level().playSound(
null as Player?,
driver.getX(),
driver.getY(),
driver.getZ(), SoundEvents.PLAYER_ATTACK_WEAK,
driver.getSoundSource(), 1.0f, 0.2f
)
val lp = driver as? LocalPlayer
lp?.let {
//val resourceLocation = ResourceLocation.tryParse(getRuntime(entity).resolveString(sound))
//val soundEvent = BuiltInRegistries.SOUND_EVENT.getOptional(resourceLocation).orElse(null)
val soundInstance = RidingOnPlayerSoundEventInstance(it, SoundEvents.AMBIENT_UNDERWATER_LOOP)
Minecraft.getInstance().soundManager.play(soundInstance)
}
}
//Restrict turning while boost is active
val boostRotDebuff = if( state.driftBoost != 0.0 ) 0.5f else 1.0f
return Vec2(driver.xRot, entity.yRot - (rotationDegrees * boostRotDebuff) )
//return Vec2(driver.xRot, state.turnSmoother.getNewDeltaValue((entity.yRot - rotationDegrees).toDouble(), 1.0).toFloat())
}
}
override fun velocity(entity: PokemonEntity, driver: Player, input: Vec3): Vec3 {
//Retrieve Vehicle state
val state = getState(entity, ::LandVehicleState)
val runtime = getRuntime(entity)
val driveFactor = runtime.resolveFloat(this.driveFactor)
var g = driver.zza * driveFactor
if (g <= 0.0f) {
g *= runtime.resolveFloat(this.reverseDriveFactor)
}
if (driver.xxa != 0F && g.absoluteValue < runtime.resolveFloat(this.minimumSpeedToTurn)) {
driver.xxa = 0F
val grav = -1.0
var velocity = Vec3(0.0,0.0,0.0)
//If the vehicle is currently drifting then do a "poor mans" rotation of the velocity vector to offset it
//from the front of the vehicle. This likely needs cleaned up to be
if( state.currDrift != 0.0){
//Drift speed should be slower the higher the drift is
//normalize the drift speed
val driftSpeed = (1.0 - ((abs(state.currDrift) - 1.0) / (5.0 - 1.0))) * 0.5 + 0.3
velocity = Vec3((1.0 * state.currSpeed) * (driftSpeed / 2.0) * -sign(state.currDrift) + min(state.driftBoost, 0.5) * 1.25,
grav,
(1.0 * state.currSpeed) * driftSpeed + min(state.driftBoost, 0.5) * 1.25)
}else{
velocity = Vec3(0.0 , grav, state.currSpeed + min(state.driftBoost, 0.5) * 1.25)
}
val velocity = Vec3(0.0 /* Maybe do drifting here later */, 0.0, g.toDouble() * currSpeed)
//tick down boost
state.driftBoost = max( state.driftBoost - (1.0 / (20.0 * 2)), 0.0)
return velocity
}
@@ -146,7 +227,7 @@ class VehicleLandController : RideController {
override fun clampPassengerRotation(entity: PokemonEntity, driver: LivingEntity) {
val f = Mth.wrapDegrees(driver.yRot - entity.yRot)
val runtime = getRuntime(entity)
val lookYawLimit = runtime.resolveFloat(this.lookYawLimit)
val lookYawLimit = 120.0f
val g = Mth.clamp(f, -lookYawLimit, lookYawLimit)
driver.yRotO += g - f
driver.yRot = driver.yRot + g - f
@@ -162,6 +243,27 @@ class VehicleLandController : RideController {
return Vec3(jumpVector[0].toDouble(), jumpVector[1].toDouble(), jumpVector[2].toDouble())
}
override fun inertia(entity: PokemonEntity ) : Double {
val state = getState(entity, ::LandVehicleState)
//val inertia = if(Minecraft.getInstance().options.keyJump.isDown()) 0.1 else 0.5
return 0.1
}
override fun rideFovMult(entity: PokemonEntity, driver: Player): Float {
val state = getState(entity, ::LandVehicleState)
//Only increase fov when actively using the boost.
val fovIncrease = (min( state.driftBoost, 0.5 ) * 0.3).toFloat()
return 1.0f + fovIncrease
}
override fun shouldRoll(entity: PokemonEntity): Boolean {
return false
}
override fun encode(buffer: RegistryFriendlyByteBuf) {
super.encode(buffer)
buffer.writeString(this.speed.getString())
@@ -174,6 +276,11 @@ class VehicleLandController : RideController {
buffer.writeString(this.minimumSpeedToTurn.getString())
buffer.writeString(this.rotationSpeed.getString())
buffer.writeString(this.lookYawLimit.getString())
buffer.writeString(this.handlingExpr.getString())
buffer.writeString(this.staminaExpr.getString())
buffer.writeString(this.jumpExpr.getString())
buffer.writeString(this.topSpeedExpr.getString())
buffer.writeString(this.accelExpr.getString())
}
override fun decode(buffer: RegistryFriendlyByteBuf) {
@@ -189,6 +296,11 @@ class VehicleLandController : RideController {
this.minimumSpeedToTurn = buffer.readString().asExpression()
this.rotationSpeed = buffer.readString().asExpression()
this.lookYawLimit = buffer.readString().asExpression()
this.handlingExpr = buffer.readString().asExpression()
this.staminaExpr = buffer.readString().asExpression()
this.jumpExpr = buffer.readString().asExpression()
this.topSpeedExpr = buffer.readString().asExpression()
this.accelExpr = buffer.readString().asExpression()
}
}
@@ -14,5 +14,6 @@ import net.minecraft.world.phys.Vec3
data class BirdAirState(val entity: PokemonEntity) : RidingState {
var currSpeed: Double = 0.0
var lastVelocity: Vec3 = Vec3(0.0, 0.0, 0.0)
var stamina: Float = 1.0f
var rideVel: Vec3 = Vec3.ZERO
}
@@ -0,0 +1,16 @@
/*
* 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.pokemon.riding.states
import com.cobblemon.mod.common.api.riding.RidingState
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
data class DolphinState(val entity: PokemonEntity) : RidingState {
var currSpeed: Double = 0.0
}
@@ -14,5 +14,6 @@ import net.minecraft.world.phys.Vec3
data class GenericLandState(val entity: PokemonEntity) : RidingState {
var currSpeed: Double = 0.0
var rideVel: Vec3 = Vec3.ZERO
var stamina: Float = 1.0f
}
@@ -15,6 +15,7 @@ import net.minecraft.world.phys.Vec3
data class JetAirState(val entity: PokemonEntity) : RidingState {
var currSpeed: Double = 0.0
var stamina: Float = 1.0f
var rideVel: Vec3 = Vec3.ZERO
var currMouseXForce: Double = 0.0
var currMouseYForce: Double = 0.0
}
@@ -0,0 +1,33 @@
/*
* 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.pokemon.riding.states
import com.cobblemon.mod.common.api.riding.RidingState
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import net.minecraft.util.SmoothDouble
import net.minecraft.world.phys.Vec3
data class LandVehicleState(val entity: PokemonEntity) : RidingState {
var currSpeed: Double = 0.0
var stamina: Float = 1.0f
var currMouseXForce: Double = 0.0
var currMouseYForce: Double = 0.0
//I am thinking this will either be a ranging from -1.0 to 1.0
//When drift is first activated while a turn is initiated it will go to (the sign of the turn)*0.5.
//Then you are able to change it between +-1.0 and 0.25 which will determine how much forced rotation
//The drift gives you. The second component is the offset of the velocity vector. Might simply be able
//to add an equal amount of x velocity as the current z velocity? Or maybe just a transformation to turn
//it to the right?? This will be the hardest part actually... Because if you add a similar one then the
//magnitude willbe increased? Maybe just half both and then that effectively slows it? idk.
var currDrift: Double = 0.0
//Determines how much boost is left.
var driftBoost: Double = 0.0
var turnSmoother: SmoothDouble = SmoothDouble()
}
@@ -48,6 +48,7 @@ object RideControllerAdapter : JsonDeserializer<RideController> {
VehicleLandController.KEY to VehicleLandController::class.java,
DolphinController.KEY to DolphinController::class.java,
JetAirController.KEY to JetAirController::class.java
)
override fun deserialize(element: JsonElement, type: Type, context: JsonDeserializationContext): RideController {
@@ -0,0 +1,36 @@
/*
* 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.adapters
import com.google.gson.*
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.resources.ResourceLocation
import net.minecraft.sounds.SoundEvent
import java.lang.reflect.Type
/**
* Adapter for deserializing SoundEvent types.
*
* @author Jackowes
* @since March 5th, 2025
*/
object SoundEventAdapter : JsonDeserializer<SoundEvent> {
override fun deserialize(json: JsonElement, type: Type, context: JsonDeserializationContext): SoundEvent {
val soundId = json.asString
// Attempt to grab the sound resource location. If incorrect then return null
val resourceLocation = ResourceLocation.read(soundId).result().orElse(null)
?: throw JsonParseException("Invalid sound event resource location: $soundId")
// Attempt to retrieve the SoundEvent from the registry
return BuiltInRegistries.SOUND_EVENT.getOptional(resourceLocation)
.orElseThrow { JsonParseException("Unknown sound event: $soundId") }
}
}
@@ -124,8 +124,14 @@
},
"pokeball": {
"scale": {
"0.4583": {"pre": [0, 0, 0], "post": [0.3, 0.3, 0.3]},
"0.5417": {"pre": [0.3, 0.3, 0.3], "post": [0.3, 0.3, 0.3]},
"0.4583": {
"pre": [0, 0, 0],
"post": [0.3, 0.3, 0.3]
},
"0.5417": {
"pre": [0.3, 0.3, 0.3],
"post": [0.3, 0.3, 0.3]
},
"0.7083": [1, 1, 1]
}
},
@@ -622,12 +628,21 @@
},
"pokeball": {
"scale": {
"0.9375": {"pre": [0, 0, 0], "post": [0.3, 0.3, 0.3]},
"1.0": {"pre": [0.3, 0.3, 0.3], "post": [0.3, 0.3, 0.3]},
"0.9375": {
"pre": [0, 0, 0],
"post": [0.3, 0.3, 0.3]
},
"1.0": {
"pre": [0.3, 0.3, 0.3],
"post": [0.3, 0.3, 0.3]
},
"1.1875": [1, 1, 1],
"3.0625": [1, 1, 1],
"3.25": [0.4, 0.4, 0.4],
"3.375": {"pre": [0.4, 0.4, 0.4], "post": [0, 0, 0]}
"3.375": {
"pre": [0.4, 0.4, 0.4],
"post": [0, 0, 0]
}
}
},
"leg_right": {
@@ -996,6 +1011,20 @@
"scale": 0
}
}
},
"animation.trainer_generic.ride_test": {
"loop": true,
"bones": {
"body": {
"position": [0, -8.4, 0]
},
"leg_right": {
"rotation": [-90, 5, 0]
},
"leg_left": {
"rotation": [-90, -5, 0]
}
}
}
}
}
@@ -26,7 +26,10 @@
"pivot": [0, 24.5, -1],
"cubes": [
{"origin": [-6, 18.5, -13.75], "size": [12, 12, 26], "uv": [52, 39]}
]
],
"locators": {
"seat_1": [0, 24, 0]
}
},
{
"name": "neck",
@@ -175,39 +175,7 @@
}
]
}
],
"stats": {
"SPEED": {
"ranges": {
"LAND": "10-20",
"AIR": "25-50"
}
},
"ACCELERATION": {
"ranges": {
"LAND": "90-100",
"AIR": "20-40"
}
},
"SKILL": {
"ranges": {
"LAND": "15-30",
"AIR": "50-100"
}
},
"JUMP": {
"ranges": {
"LAND": "10-20",
"AIR": "25-50"
}
},
"STAMINA": {
"ranges": {
"LAND": "15-30",
"AIR": "35-70"
}
}
}
]
},
"behaviour": {
"resting": {
@@ -88,6 +88,67 @@
}
]
},
"riding": {
"stats": {
"SPEED": {
"displayName": "Speed",
"description": "Its top speed",
"ranges": {
"LIQUID": "100-100"
}
},
"ACCELERATION": {
"displayName": "Acceleration",
"description": "How fast it can speed up",
"ranges": {
"LIQUID": "100-100"
}
},
"SKILL": {
"displayName": "Handling",
"description": "How fast it can turn",
"ranges": {
"LIQUID": "100-100"
}
},
"JUMP": {
"displayName": "Altitude",
"description": "How high it can fly",
"ranges": {
"LIQUID": "100-100"
}
},
"STAMINA": {
"displayName": "Fuel/Stamina",
"description": "How long it can stay off the ground",
"ranges": {
"LIQUID": "100-100"
}
}
},
"controller": {
"key": "cobblemon:swim/dolphin"
},
"seats": [
{
"offset": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"poseAnimations": [
{
"animations": [
{
"fileName": "trainer_generic",
"animationName": "animation.trainer_generic.ride_test"
}
]
}
]
}
]
},
"moves": [
"1:firepunch",
"1:thunderpunch",
@@ -56,7 +56,7 @@
"displayName": "Speed",
"description": "Its top speed",
"ranges": {
"LAND": "2-10",
"LAND": "100-101",
"AIR": "100-101"
}
},
@@ -64,7 +64,7 @@
"displayName": "Acceleration",
"description": "How fast it can speed up",
"ranges": {
"LAND": "2-10",
"LAND": "100-101",
"AIR": "100-101"
}
},
@@ -72,7 +72,7 @@
"displayName": "Handling",
"description": "How fast it can turn",
"ranges": {
"LAND": "2-10",
"LAND": "100-101",
"AIR": "100-101"
}
},
@@ -80,7 +80,7 @@
"displayName": "Altitude",
"description": "How high it can fly",
"ranges": {
"LAND": "2-10",
"LAND": "100-101",
"AIR": "100-101"
}
},
@@ -88,7 +88,7 @@
"displayName": "Fuel/Stamina",
"description": "How long it can stay off the ground",
"ranges": {
"LAND": "2-10",
"LAND": "100-101",
"AIR": "100-101"
}
}
@@ -43,6 +43,67 @@
"eggCycles": 20,
"baseFriendship": 50,
"baseScale": 1,
"riding": {
"stats": {
"SPEED": {
"displayName": "Speed",
"description": "Its top speed",
"ranges": {
"LAND": "100-101"
}
},
"ACCELERATION": {
"displayName": "Acceleration",
"description": "How fast it can speed up",
"ranges": {
"LAND": "100-101"
}
},
"SKILL": {
"displayName": "Handling",
"description": "How fast it can turn",
"ranges": {
"LAND": "100-101"
}
},
"JUMP": {
"displayName": "Altitude",
"description": "How high it can fly",
"ranges": {
"LAND": "100-101"
}
},
"STAMINA": {
"displayName": "Fuel/Stamina",
"description": "How long it can sprint for.",
"ranges": {
"LAND": "100-101"
}
}
},
"controller": {
"key": "cobblemon:land/generic"
},
"seats": [
{
"offset": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"poseAnimations": [
{
"animations": [
{
"fileName": "trainer_generic",
"animationName": "animation.trainer_generic.ride_test"
}
]
}
]
}
]
},
"hitbox": {
"width": 1.3,
"height": 2.6,
@@ -42,6 +42,68 @@
"catchRate": 75,
"eggCycles": 20,
"baseFriendship": 50,
"riding": {
"stats": {
"SPEED": {
"displayName": "Speed",
"description": "Its top speed",
"ranges": {
"LAND": "100-101"
}
},
"ACCELERATION": {
"displayName": "Acceleration",
"description": "How fast it can speed up",
"ranges": {
"LAND": "100-101"
}
},
"SKILL": {
"displayName": "Handling",
"description": "How fast it can turn",
"ranges": {
"LAND": "100-101"
}
},
"JUMP": {
"displayName": "Altitude",
"description": "How high it can fly",
"ranges": {
"LAND": "100-101"
}
},
"STAMINA": {
"displayName": "Fuel/Stamina",
"description": "How long it can stay off the ground",
"ranges": {
"LAND": "100-101"
}
}
},
"controller": {
"key": "cobblemon:land/vehicle",
"sound": "minecraft:entity.elder_guardian.flop"
},
"seats": [
{
"offset": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"poseAnimations": [
{
"animations": [
{
"fileName": "trainer_generic",
"animationName": "animation.trainer_generic.ride_test"
}
]
}
]
}
]
},
"behaviour": {
"resting": {
"canSleep": true,