Compare commits

...
Author SHA1 Message Date
jackowes ef79cf79e3 camera fixes 2026-10-25 20:39:05 -05:00
jackowes 2b0128aed0 Port spiderpig board/climb work onto main 2026-10-25 20:28:36 -05:00
17 changed files with 2445 additions and 198 deletions
@@ -75,9 +75,10 @@ public abstract class LivingEntityMixin extends Entity implements OrientationCon
public @NotNull HitResult pick(double hitDistance, float partialTicks, boolean hitFluids) {
Entity vehicle = this.getVehicle();
if (vehicle instanceof PokemonEntity pokemonEntity && pokemonEntity.getDelegate() instanceof PokemonClientDelegate delegate) {
int seatIndex = pokemonEntity.getPassengers().indexOf(this);
Seat seat = pokemonEntity.getSeats().get(seatIndex);
MatrixWrapper locator = delegate.getLocatorStates().get(seat.getLocator());
// Passenger order isn't seat order, so look the seat up directly
Seat seat = pokemonEntity.getSeatForPassenger(this);
MatrixWrapper locator = seat != null ? delegate.getLocatorStates().get(seat.getLocator()) : null;
if (locator == null) return super.pick(hitDistance, partialTicks, hitFluids);
Vec3 locatorOffset = new Vec3(locator.getMatrix().getTranslation(new Vector3f()));
@@ -99,7 +100,8 @@ public abstract class LivingEntityMixin extends Entity implements OrientationCon
@Unique
private Vec3 cobblemon$getEyePosition(float partialTicks, PokemonEntity pokemonEntity, Vec3 locatorOffset) {
OrientationController controller = this.getOrientationController();
// The ride owns the orientation, the passenger's own controller is never active
OrientationController controller = ((OrientationControllable) pokemonEntity).getOrientationController();
float currEyeHeight = this.getEyeHeight();
Vec3 offset = locatorOffset.add(pokemonEntity.position());
@@ -107,7 +109,7 @@ public abstract class LivingEntityMixin extends Entity implements OrientationCon
Quaternionf orientation = controller.getRenderOrientation(partialTicks);
Vec3 rotatedEyeHeight = new Vec3(orientation.transform(new Vector3f(0f, currEyeHeight - (this.getBbHeight() / 2), 0f)));
offset.add(rotatedEyeHeight);
offset = offset.add(rotatedEyeHeight);
}
return offset;
@@ -50,11 +50,13 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Map;
import java.util.Objects;
@Mixin(LocalPlayer.class)
public abstract class LocalPlayerMixin extends AbstractClientPlayer implements OrientationControllable, DoubleJump {
@Unique Matrix3f cobblemon$lastOrientation;
@Unique int cobblemon$lastOrientationVehicleId = -1;
@Unique float cobblemon$lastRideXRot;
@Unique float cobblemon$lastRideYRot;
@@ -81,9 +83,18 @@ public abstract class LocalPlayerMixin extends AbstractClientPlayer implements O
private void cobblemon$updateRotationMatrixPassenger(CallbackInfo ci) {
if (!(this.getVehicle() instanceof OrientationControllable controllableVehicle)) return;
var vehicleController = controllableVehicle.getOrientationController();
if (!vehicleController.isActive() || vehicleController.getOrientation() == cobblemon$lastOrientation) return;
cobblemon$lastOrientation = vehicleController.getOrientation() != null ? new Matrix3f(vehicleController.getOrientation()) : null;
CobblemonNetwork.INSTANCE.sendToServer(new ServerboundUpdateOrientationPacket(this.getVehicle().getId(), vehicleController.getOrientation()));
if (!vehicleController.isActive()) {
// Forget the last sent value so the next ride always gets its first orientation sent
cobblemon$lastOrientation = null;
return;
}
var orientation = vehicleController.getOrientation();
var vehicleId = this.getVehicle().getId();
// Compare by value, the stored one is a copy so a reference check never matches
if (vehicleId == cobblemon$lastOrientationVehicleId && Objects.equals(orientation, cobblemon$lastOrientation)) return;
cobblemon$lastOrientation = orientation != null ? new Matrix3f(orientation) : null;
cobblemon$lastOrientationVehicleId = vehicleId;
CobblemonNetwork.INSTANCE.sendToServer(new ServerboundUpdateOrientationPacket(vehicleId, orientation));
}
@Inject(method = "rideTick", at = @At("HEAD"))
@@ -38,6 +38,9 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(Entity.class)
public abstract class EntityMixin {
// How far the clientsent ride eye can sit from the players
@Unique private static final double COBBLEMON$MAX_RIDE_EYE_OFFSET = 3.0;
@Shadow @Nullable public abstract Entity getVehicle();
@Inject(method = "updateInWaterStateAndDoWaterCurrentPushing", at = @At(value = "FIELD", target = "Lnet/minecraft/world/entity/Entity;wasTouchingWater:Z", ordinal = 1), cancellable = true)
@@ -91,6 +94,9 @@ public abstract class EntityMixin {
if (vehicleController == null) return;
Vec3 customEyePos = ridePassenger.cobblemon$getRideEyePos();
Vec3 vanillaEyePos = new Vec3(player.getX(), player.getEyeY(), player.getZ());
double maxSqr = COBBLEMON$MAX_RIDE_EYE_OFFSET * COBBLEMON$MAX_RIDE_EYE_OFFSET;
if (customEyePos == null || !(customEyePos.distanceToSqr(vanillaEyePos) <= maxSqr)) return;
cir.setReturnValue(customEyePos);
}
@@ -20,6 +20,7 @@ import com.cobblemon.mod.common.api.riding.behaviour.types.air.HelicopterSetting
import com.cobblemon.mod.common.api.riding.behaviour.types.air.HoverSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.air.JetSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.air.RocketSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.air.BoardSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.land.HorseSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.land.MinekartSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.land.VehicleSettings
@@ -56,6 +57,7 @@ object CobblemonRideSettings : DataRegistry {
var hover = HoverSettings()
var jet = JetSettings()
var rocket = RocketSettings()
var board = BoardSettings()
var horse = HorseSettings()
var minekart = MinekartSettings()
@@ -75,6 +77,7 @@ object CobblemonRideSettings : DataRegistry {
hover = hover,
jet = jet,
rocket = rocket,
board = board,
horse = horse,
minekart = minekart,
vehicle = vehicle,
@@ -93,6 +96,7 @@ object CobblemonRideSettings : DataRegistry {
hover = loadStyle(manager, "hover", HoverSettings::class.java)
jet = loadStyle(manager, "jet", JetSettings::class.java)
rocket = loadStyle(manager, "rocket", RocketSettings::class.java)
board = loadStyle(manager, "board", BoardSettings::class.java)
horse = loadStyle(manager, "horse", HorseSettings::class.java)
minekart = loadStyle(manager, "minekart", MinekartSettings::class.java)
vehicle = loadStyle(manager, "vehicle", VehicleSettings::class.java)
@@ -29,7 +29,8 @@ open class OrientationController(val entity: LivingEntity) {
val UP: Vector3f = Vector3f(0.0f, 1.0f, 0.0f)
val LEFT: Vector3f = Vector3f(-1.0f, 0.0f, 0.0f)
private const val TICK_DT = 1f / 20f // Minecraft server tick rate
private const val RENDER_SMOOTH_HALFLIFE = 0.1f // seconds to close ~half the gap
// Seconds for the render orientation to close half the gap to the real one. Raise for smoother but laggier visuals
private const val RENDER_SMOOTH_HALFLIFE = 0.15f
}
open var active: Boolean = false
@@ -101,7 +102,7 @@ open class OrientationController(val entity: LivingEntity) {
// Use a "quat spring" to smoothly move the render orientation towards the goal orientation.
// It takes into account the orientations angular velocities and should be much more resilient
// to sharp changes in goal orientation or late/missed packets.
simpleSpringDamperQuat(renderSpring, targetQuat, 0.15f, TICK_DT)
simpleSpringDamperQuat(renderSpring, targetQuat, RENDER_SMOOTH_HALFLIFE, TICK_DT)
val newRenderOrientation = Matrix3f()
renderSpring.quat.get(newRenderOrientation)
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.api.riding.behaviour
import com.cobblemon.mod.common.api.riding.behaviour.types.air.*
import com.cobblemon.mod.common.api.riding.behaviour.types.composite.CompositeBehaviour
import com.cobblemon.mod.common.api.riding.behaviour.types.land.ClimbBehaviour
import com.cobblemon.mod.common.api.riding.behaviour.types.land.HorseBehaviour
import com.cobblemon.mod.common.api.riding.behaviour.types.land.VehicleBehaviour
import com.cobblemon.mod.common.api.riding.behaviour.types.land.MinekartBehaviour
@@ -35,7 +36,9 @@ object RidingBehaviours {
register(MinekartBehaviour.KEY, MinekartBehaviour())
register(HoverBehaviour.KEY, HoverBehaviour())
register(RocketBehaviour.KEY, RocketBehaviour())
register(BoardBehaviour.KEY, BoardBehaviour())
register(SubmarineBehaviour.KEY, SubmarineBehaviour())
register(ClimbBehaviour.KEY, ClimbBehaviour())
register(CompositeBehaviour.KEY, CompositeBehaviour())
}
@@ -0,0 +1,924 @@
/*
* 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.behaviour.types.air
import com.bedrockk.molang.Expression
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.CobblemonRideSettings
import com.cobblemon.mod.common.OrientationControllable
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.orientation.OrientationController
import com.cobblemon.mod.common.api.riding.RidingStyle
import com.cobblemon.mod.common.api.riding.behaviour.RidingBehaviour
import com.cobblemon.mod.common.api.riding.behaviour.RidingBehaviourSettings
import com.cobblemon.mod.common.api.riding.behaviour.RidingBehaviourState
import com.cobblemon.mod.common.api.riding.behaviour.Side
import com.cobblemon.mod.common.api.riding.behaviour.ridingState
import com.cobblemon.mod.common.api.riding.posing.PoseOption
import com.cobblemon.mod.common.api.riding.posing.PoseProvider
import com.cobblemon.mod.common.api.riding.sound.RideSoundSettingsList
import com.cobblemon.mod.common.api.riding.stats.RidingStat
import com.cobblemon.mod.common.api.riding.util.DistanceFieldLayer
import com.cobblemon.mod.common.api.riding.util.DistanceFieldTuning
import com.cobblemon.mod.common.api.riding.util.PrimitiveShape
import com.cobblemon.mod.common.api.riding.util.collectPrimitivesFromBlocks
import com.cobblemon.mod.common.api.riding.util.estimateGradient
import com.cobblemon.mod.common.api.riding.util.sampleField
import com.cobblemon.mod.common.api.riding.util.visualizeDistanceField
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.blockPositionsAsListRounded
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readNullableExpression
import com.cobblemon.mod.common.util.readRidingStats
import com.cobblemon.mod.common.util.resolveBoolean
import com.cobblemon.mod.common.util.resolveDouble
import com.cobblemon.mod.common.util.resolveFloat
import com.cobblemon.mod.common.util.writeNullableExpression
import com.cobblemon.mod.common.util.writeRidingStats
import net.minecraft.client.Minecraft
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sign
import kotlin.math.sqrt
import net.minecraft.core.Direction
import net.minecraft.core.particles.ParticleTypes
import net.minecraft.network.FriendlyByteBuf
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.ai.attributes.Attributes
import net.minecraft.world.entity.player.Player
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec2
import net.minecraft.world.phys.Vec3
import net.minecraft.world.phys.shapes.Shapes
import org.joml.Matrix3f
import org.joml.Quaternionf
import org.joml.Vector3f
class BoardBehaviour : RidingBehaviour<BoardSettings, BoardState> {
companion object {
val KEY = cobblemonResource("air/board")
}
override val key = KEY
val globalBoard: BoardSettings
get() = CobblemonRideSettings.board
override fun getRidingStyle(settings: BoardSettings, state: BoardState): RidingStyle {
return RidingStyle.LAND
}
val poseProvider = PoseProvider<BoardSettings, BoardState>(PoseType.HOVER)
.with(PoseOption(PoseType.FLY) { _, state, vehicle ->
return@PoseOption state.walking.get()
})
override fun isActive(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity
): Boolean {
return Shapes.create(vehicle.boundingBox).blockPositionsAsListRounded().any {
// Need to check other fluids
if (vehicle.isInWater || vehicle.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() == (vehicle.position().y)) {
val blockState = vehicle.level().getBlockState(it.below())
return@any !blockState.isAir && blockState.fluidState.isEmpty
}
true
}
}
override fun pose(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity
): PoseType {
return this.poseProvider.select(settings, state, vehicle)
}
override fun speed(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity,
driver: Player
): Float {
return state.rideVelocity.get().length().toFloat()
}
override fun tick(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity,
driver: Player,
input: Vec3
) {
if (vehicle.level().isClientSide) {
handleSprinting(state, driver)
inAirCheck(state, vehicle)
tickStamina(settings, state, vehicle)
state.walking.set(state.rideVelocity.get().horizontalDistance() > 0.1 || driver.zza != 0.0f)
//surfDistCheck(state, vehicle)
}
//TODO: should be a config somewhere
val debugVisualizeField = true;
if (false && debugVisualizeField && !vehicle.level().isClientSide && vehicle.tickCount % 5 == 0) {
visualizeDistanceField(
referenceEntity = vehicle,
center = vehicle.position().add(0.0, (vehicle.bbHeight * 0.5f).toDouble(), 0.0),
layers = listOf(
DistanceFieldLayer(isoValue = 2.5f, color = Vector3f(1f, 0.2f, 0.2f))
),
shape = PrimitiveShape.ELLIPSOID,
horizontalRadius = 4.0,
verticalRange = 4.0,
sampleSpacing = 0.3,
marchStep = 0.15,
tuning = DistanceFieldTuning(smoothingRange = 0.25f)
)
}
}
/**
* Check how far the vehicle is from the "surface"
*/
fun surfDistCheck(
state: BoardState,
vehicle: PokemonEntity,
) {
val center = vehicle.position()
val horizontalRadius = 4.0;
val verticalRange = 4.0;
val level = vehicle.level()
//if (level !is ServerLevel) return
// Take a cubic area around the vehicle and gather SDF primitives from it.
val searchBox = AABB(
center.x - horizontalRadius, center.y - verticalRange, center.z - horizontalRadius,
center.x + horizontalRadius, center.y + verticalRange, center.z + horizontalRadius
)
val primitives = collectPrimitivesFromBlocks(level,
searchBox,
PrimitiveShape.ELLIPSOID,
includeFluids = true
)
val sp = center.toVector3f() // Sample Point
val tuning = DistanceFieldTuning(smoothingRange = 0.25f)
val sd = sampleField(primitives, sp.x, sp.y, sp.z, tuning)
state.surfDist.set(sd)
}
fun inAirCheck(
state: BoardState,
vehicle: PokemonEntity,
) {
// Check both vertical movement and if there are blocks below.
val posBelow = vehicle.blockPosition().below()
val blockStateBelow = vehicle.level().getBlockState(posBelow)
val isAirOrLiquid = blockStateBelow.isAir || !blockStateBelow.fluidState.isEmpty
val canSupportEntity = blockStateBelow.isFaceSturdy(vehicle.level(), posBelow, Direction.UP)
val standingOnSolid = canSupportEntity && !isAirOrLiquid
// inAir if not on the ground
val inAir = !(vehicle.deltaMovement.y == 0.0 || standingOnSolid)
state.inAir.set(true)
}
fun handleSprinting(
state: BoardState,
driver: Player
) {
val sprintTimerTickLength = 7 // Emulate vanillas tick timer length
var hasDoubleTapped = false
// If you are not sprinting and you just pressed down and theres no
if (!state.forwardHeldLastTick.get() && Minecraft.getInstance().options.keyUp.isDown() && !state.sprinting.get() && state.sprintTickTimer.get() == 0) {
state.sprintTickTimer.set(sprintTimerTickLength)
} else if (!state.forwardHeldLastTick.get() && Minecraft.getInstance().options.keyUp.isDown() && state.sprintTickTimer.get() != 0) { // If has double tapped
hasDoubleTapped = true
}else if(!state.sprinting.get() && state.sprintTickTimer.get() > 0) {
state.sprintTickTimer.set(state.sprintTickTimer.get() - 1)
} else {
state.sprintTickTimer.set(0)
}
val tryingToSprint = (Minecraft.getInstance().options.keySprint.isDown() && Minecraft.getInstance().options.keyUp.isDown()) || hasDoubleTapped
if (state.stamina.get() <= 0.0f || !Minecraft.getInstance().options.keyUp.isDown()) {
// If stamina runs out or the player is not holding forward then stop sprinting
state.sprinting.set(false)
if (state.stamina.get() <= 0.0f) {
state.sprintToggleable.set(false)
}
} else if (!state.sprinting.get() && !state.sprintToggleable.get() && state.stamina.get() > 0.33f) {
// If you are not sprinting and sprint is not toggleable and you're not trying to sprint and stamina is above 0.3 then enable the toggle
state.sprintToggleable.set(true)
} else if (tryingToSprint && state.sprintToggleable.get()) {
// If you are trying to sprint and the toggle allows it then start sprinting and disable toggle
state.sprinting.set(true)
}
// Take not of whether forward was held last tick
state.forwardHeldLastTick.set(Minecraft.getInstance().options.keyUp.isDown())
}
fun tickStamina(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity,
) {
val stam = state.stamina.get()
if (vehicle.runtime.resolveBoolean(settings.infiniteStamina ?: globalBoard.infiniteStamina!!)) {
return
}
val stamDrainRate = (1.0f / vehicle.runtime.resolveDouble(settings.staminaExpr ?: globalBoard.staminaExpr!!)).toFloat() / 20.0f
val newStam = if (state.sprinting.get()) {
max(0.0f,stam - stamDrainRate)
} else {
min(1.0f,stam + stamDrainRate * 4)
}
state.stamina.set(newStam.coerceIn(0F, 1F))
}
override fun updatePassengerRotation(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity,
driver: LivingEntity
) { return }
override fun clampPassengerRotation(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity,
driver: LivingEntity
) { }
override fun rotation(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity,
driver: LivingEntity
): Vec2 {
val turnAmount = calcRotAmount(settings, state, vehicle, driver)
return Vec2(vehicle.xRot, vehicle.yRot + turnAmount )
}
/*
* Calculates the rotation amount given the difference between
* the current player y rot and entity y rot. This gives the
* affect of influencing a rides rotation through the mouse
* without it being instant.
*/
fun calcRotAmount(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity,
driver: LivingEntity
): Float {
val topSpeed = vehicle.runtime.resolveDouble(settings.speedExpr ?: globalBoard.speedExpr!!)
val handling = vehicle.runtime.resolveDouble(settings.handlingExpr ?: globalBoard.handlingExpr!!)
val walkHandlingBoost = 5
val maxYawDiff = vehicle.runtime.resolveFloat(settings.lookYawLimit ?: globalBoard.lookYawLimit!!)
// Normalize the current rotation diff
val rotDiff = Mth.wrapDegrees(driver.yRot - vehicle.yRot).coerceIn(-maxYawDiff,maxYawDiff)
val rotDiffNorm = rotDiff / maxYawDiff//(rotDiff / maxYawDiff).coerceIn(-0.2f, 0.2f)
// Take the square root so that the ride levels out quicker when at lower differences between entity
// y and driver y
// This influences the speed of the turn based on how far in one direction you're looking
val minRotMod = 0.4f // Min speed that the rotation modulation converges towards
val rotDiffMod = ((sqrt(abs(rotDiffNorm)) * (1.0f - minRotMod)) + minRotMod) * rotDiffNorm.sign
// Turn rate should be quick when walking and slower when sprinting.
// Smoothly move between these based on speed
val walkSpeed = getWalkSpeed(vehicle)
val w = max(walkSpeed, vehicle.deltaMovement.horizontalDistance())
val invRelSpeed = (RidingBehaviour.scaleToRange(w, walkSpeed, topSpeed ) - 1.0f) * -1.0f
val turnRate = ((handling.toFloat() / 20.0f) * max(walkHandlingBoost * sqrt(abs(invRelSpeed)), 1.0)).toFloat()
// Ensure you only ever rotate as much difference as there is between the angles.
val turnSpeed = turnRate * rotDiffMod
val rotAmount = turnSpeed.coerceIn(-abs(rotDiff), abs(rotDiff))
return rotAmount
}
private fun spawnGradientDebugParticles(vehicle: PokemonEntity, normal: Vec3) {
val level = vehicle.level()
val headPos = vehicle.position().add(0.0, vehicle.bbHeight.toDouble(), 0.0)
val steps = 20
val length = 2.0
for (i in 0..steps) {
val t = i.toDouble() / steps
val point = headPos.add(normal.scale(length * t))
level.addParticle(ParticleTypes.END_ROD, point.x, point.y, point.z, 0.0, 0.0, 0.0)
}
}
override fun velocity(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity,
driver: Player,
input: Vec3
): Vec3 {
state.rideVelocity.set(calculateRideSpaceVel(settings, state, vehicle, driver))
return state.rideVelocity.get()
}
private fun calculateRideSpaceVel(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity,
driver: Player
): Vec3 {
val canSprint = vehicle.runtime.resolveBoolean(settings.canSprint ?: globalBoard.canSprint!!)
val canJump = vehicle.runtime.resolveBoolean(settings.canJump ?: globalBoard.canJump!!)
val jumpForce = vehicle.runtime.resolveDouble(settings.jumpExpr ?: globalBoard.jumpExpr!!) * 0.75
val rideTopSpeed = vehicle.runtime.resolveDouble(settings.speedExpr ?: globalBoard.speedExpr!!)
val walkSpeed = getWalkSpeed(vehicle)
val topSpeed = if(canSprint && state.sprinting.get()) rideTopSpeed else walkSpeed
val accelTime = if(state.sprinting.get()) (vehicle.runtime.resolveDouble(settings.accelerationExpr ?: globalBoard.accelerationExpr!!) * 20.0) else 0.5 * 20.0
val accel = topSpeed / accelTime
var activeInput = false
/******************************************************
* Gather the previous velocity and check for horizontal
* collisions
*****************************************************/
var newVelocity = state.rideVelocity.get() //.normalize().scale(vehicle.deltaMovement.horizontalDistance())
/******************************************************
* Gather the previous velocity and check for horizontal
* collisions
*****************************************************/
if (vehicle.horizontalCollision) {
var horizontalVec = Vec3(newVelocity.x, 0.0, newVelocity.z)
horizontalVec = horizontalVec.normalize().scale(min(horizontalVec.length(), 0.3))
newVelocity = Vec3(horizontalVec.x, newVelocity.y, horizontalVec.z)
}
/******************************************************
* Speed up and slow down based on input
*****************************************************/
if (driver.zza != 0.0f) {
// If on a tight turn then do not speed up past half of top speed in order to turn quicker
// Also determine how fast to be slowing down based on how far turned you are
val lookYawLimit = vehicle.runtime.resolveFloat(settings.lookYawLimit ?: globalBoard.lookYawLimit!!)
val percOfMaxTurnSpeed = abs(Mth.wrapDegrees(driver.yRot - vehicle.yRot) / lookYawLimit) * 100.0f
val turnPercThresh = 0.0f
val s = min(((percOfMaxTurnSpeed - turnPercThresh) / (100.0f - turnPercThresh)).pow(1),1.0f)
val effectiveTopSpeed = if (percOfMaxTurnSpeed > turnPercThresh) topSpeed / max(2.0f*s,1.0f) else topSpeed
val turningSlowDown = s * 0.1
//make sure it can't exceed top speed
val forwardInput = when {
driver.zza > 0 && newVelocity.z > effectiveTopSpeed -> 0.0
driver.zza < 0 && newVelocity.z < (-effectiveTopSpeed / 3.0) -> 0.0
else -> driver.zza.sign
}
// Add extra friction if trying to slow down to turn faster
if (newVelocity.z > effectiveTopSpeed) {
newVelocity = newVelocity.subtract(0.0, 0.0, min(turningSlowDown * newVelocity.z.sign, newVelocity.z))
}
newVelocity = Vec3(
newVelocity.x,
newVelocity.y,
(newVelocity.z + (accel * forwardInput.toDouble())))
activeInput = true
}
/******************************************************
* Gravity logic
*****************************************************/
// if(state.surfDist.get() > 1.6f) {
// val gravity = (9.8 / ( 20.0)) * 0.2 * 0.6
// val terminalVel = 2.0
// newVelocity = Vec3(newVelocity.x, max(newVelocity.y - gravity, -terminalVel), newVelocity.z)
// } else if(state.surfDist.get() > 1.4f && state.surfDist.get() <= 1.6f) {
// newVelocity = Vec3(newVelocity.x, 0.0, newVelocity.z)
// } else if(state.surfDist.get() <= 1.4f) {
// val gravity = -(9.8 / ( 20.0)) * 0.2 * 0.6
// val terminalVel = -2.0
// newVelocity = Vec3(newVelocity.x, min(newVelocity.y - gravity, -terminalVel), newVelocity.z)
// }
/******************************************************
* Hover logic - non-predictive: correct against current position
*****************************************************/
val correctionGain = 0.5
val maxCorrectionSpeed = 0.3
val center = vehicle.position()
val horizontalRadius = 4.0;
val verticalRange = 4.0;
val level = vehicle.level()
// Take a cubic area around the vehicle and gather SDF primitives from it.
val searchBox = AABB(
center.x - horizontalRadius, center.y - verticalRange, center.z - horizontalRadius,
center.x + horizontalRadius, center.y + vehicle.bbHeight, center.z + horizontalRadius
)
val primitives = collectPrimitivesFromBlocks(level,
searchBox,
PrimitiveShape.ELLIPSOID,
includeFluids = true
)
val tuning = DistanceFieldTuning(smoothingRange = 0.25f)
val currentPos = vehicle.position()
val currentDist = sampleField(primitives, currentPos.x.toFloat(), currentPos.y.toFloat(), currentPos.z.toFloat(), tuning)
val normal = estimateGradient(primitives, currentPos, tuning)
state.surfaceNormal.set(normal)
//spawnGradientDebugParticles(vehicle, normal) // LETS SEE IT!
// Only correct when under the hover surface.
// Otherwise allow gravity to do its thing
// if (currentDist < targetHoverDist) {
// val error = currentDist - targetHoverDist
// val normalSpeed = newVelocity.dot(normal)
// val tangentialVelocity = newVelocity.subtract(normal.scale(normalSpeed))
//
// val correctionSpeed = (-error * correctionGain).coerceIn(-maxCorrectionSpeed, maxCorrectionSpeed)
// newVelocity = tangentialVelocity.add(normal.scale(correctionSpeed))
// } else {
// val hoverCoolnessLowGravFactor = 0.5
// val gravity = (9.8 / ( 20.0)) * 0.2 * 0.6 * hoverCoolnessLowGravFactor
// val terminalVel = 2.0 * hoverCoolnessLowGravFactor
// newVelocity = Vec3(newVelocity.x, max(newVelocity.y - gravity, -terminalVel), newVelocity.z)
// }
/******************************************************
* Hover logic
*****************************************************/
val hoverActivationDist = 3.0f
val targetHoverDist = 1.0f
val hoverCoolnessLowGravFactor = 0.70
val gravity = (9.8 / 20.0) * 0.2 * 0.6 * hoverCoolnessLowGravFactor
val terminalVel = 2.0 * hoverCoolnessLowGravFactor
// val repelStrength = 0.05 // tested and was alright
// val hoverDamping = 0.15
val repelStrength = 0.1
val hoverDamping = 0.2
val surfDist = currentDist
if (surfDist > hoverActivationDist) {
// Free fall - no surface influence yet, same as before.
newVelocity = Vec3(newVelocity.x, max(newVelocity.y - gravity, -terminalVel), newVelocity.z)
} else {
// Getting close - push back toward targetHoverDist, harder the nearer we are.
val closeness = ((hoverActivationDist - surfDist) / (hoverActivationDist - targetHoverDist)).coerceIn(0f, 1.5f)
val repelForce = closeness * repelStrength
val dampingForce = -newVelocity.y * hoverDamping // resists overshoot/bounce
newVelocity = Vec3(newVelocity.x, newVelocity.y + repelForce + dampingForce - gravity * 0.3, newVelocity.z)
}
/******************************************************
* Ground Friction
// *****************************************************/
// if( (newVelocity.horizontalDistance() > 0 && vehicle.onGround() && !activeInput) || newVelocity.horizontalDistance() > topSpeed) {
// newVelocity = newVelocity.subtract(0.0, 0.0, min(0.03 * newVelocity.z.sign, newVelocity.z))
// }
/******************************************************
* Jump Logic
*****************************************************/
if (state.jumpTicks.get() > 0 || (state.jumpTicks.get() >= 0 && driver.jumping && vehicle.onGround() && canJump && driver.deltaMovement.y <= 0.1)) {
// Spread out jumpforce so that variable height jumps are possible
val jumpInputTicks = 6
if (driver.jumping && (state.jumpTicks.get() >= 0 && state.jumpTicks.get() < jumpInputTicks)) {
val appliedJumpForce = ((jumpForce*1.5) / jumpInputTicks) //* (1 - state.jumpTicks.get() / jumpInputTicks)
newVelocity = Vec3(newVelocity.x, newVelocity.y + appliedJumpForce, newVelocity.z)
state.jumpTicks.set(state.jumpTicks.get() + 1)
} else {
// Set delay before next jump is possible after hitting ground
val tickJumpDelay = 3
state.jumpTicks.set(-tickJumpDelay)
}
} else if (vehicle.onGround() && state.jumpTicks.get() < 0) {
// Tick off the delay once reaching the ground
state.jumpTicks.set(state.jumpTicks.get() + 1)
}
// Zero out lateral velocity possibly picked up from a controller transition
newVelocity = Vec3(0.0, newVelocity.y, newVelocity.z)
return newVelocity
}
private fun getWalkSpeed( vehicle: PokemonEntity ): Double {
val walkspeed = vehicle.runtime.resolveDouble(vehicle.behaviour.moving.walk.walkSpeed)
val movementSpeed = vehicle.attributes.getValue(Attributes.MOVEMENT_SPEED)
val speedModifier = 1.2 * 0.35
return walkspeed * movementSpeed * speedModifier
}
override fun angRollVel(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity,
driver: Player,
deltaTime: Double
): Vec3 {
if (vehicle !is OrientationControllable) return Vec3.ZERO
val controller = (vehicle as OrientationControllable).orientationController
// turning
val turnMaxRate = 5.5
val turnAccel = 1.0
val turnFriction = 0.75
val targetYawRate = -driver.xxa.toDouble() * turnMaxRate
val currentYawRate = state.yawAngularVelocity.get()
val newYawRate = if (abs(targetYawRate) > 0.001) {
moveToward(currentYawRate, targetYawRate, turnAccel * deltaTime * 20.0)
} else {
moveToward(currentYawRate, 0.0, turnFriction * deltaTime * 20.0)
}
state.yawAngularVelocity.set(newYawRate)
controller.applyGlobalYaw((newYawRate * deltaTime * 20.0).toFloat())
// up-vector alignment framerate-independent
val upAlignmentRatePerTick = 0.1 // fraction of the remaining gap closed per tick
val frameBlend = 1.0 - (1.0 - upAlignmentRatePerTick).pow(deltaTime * 20.0)
alignUpToSurfaceNormal(controller, state.surfaceNormal.get(), frameBlend.toFloat())
// lean into turns (cosmetic local roll)
val maxLeanDeg = 35.0
val leanPerYawRate = 4.0 * 3.0
val leanRatePerTick = 0.25 * 1.5
val desiredLean = (newYawRate * leanPerYawRate).coerceIn(-maxLeanDeg, maxLeanDeg)
val leanFrameBlend = 1.0 - (1.0 - leanRatePerTick).pow(deltaTime * 20.0)
val leanError = controller.roll.angleDifference(desiredLean.toFloat())
controller.rotateRoll((leanError * leanFrameBlend).toFloat())
//yaw, pitch, roll
return Vec3.ZERO
}
private fun moveToward(current: Double, target: Double, maxDelta: Double): Double {
val diff = target - current
return if (abs(diff) <= maxDelta) target else current + maxDelta * diff.sign
}
private fun alignUpToSurfaceNormal(controller: OrientationController, targetUp: Vec3, alignmentBlend: Float) {
if (targetUp.lengthSqr() < 1e-6) return
val targetUpVec = Vector3f(targetUp.x.toFloat(), targetUp.y.toFloat(), targetUp.z.toFloat()).normalize()
controller.updateOrientation { current ->
val currentUp = current.transform(Vector3f(OrientationController.UP), Vector3f())
if (currentUp.lengthSquared() < 1e-6f) return@updateOrientation current
// Full rotation that would exactly align currentUp to targetUp...
val fullCorrection = Quaternionf().rotationTo(currentUp, targetUpVec)
// ...but only take a partial step toward it, so alignment is smooth, not instant.
val steppedCorrection = Quaternionf().slerp(fullCorrection, alignmentBlend)
// Pre-multiply (world-space correction), same pattern applyGlobalYaw uses.
Matrix3f().set(steppedCorrection).mul(current, Matrix3f())
}
}
override fun rotationOnMouseXY(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity,
driver: Player,
mouseY: Double,
mouseX: Double,
mouseYSmoother: SmoothDouble,
mouseXSmoother: SmoothDouble,
sensitivity: Double,
deltaTime: Double
): Vec3 {
//Smooth out mouse input.
val smoothingSpeed = 4
val xInput = mouseXSmoother.getNewDeltaValue(mouseX * 0.1, deltaTime * smoothingSpeed);
val yInput = mouseYSmoother.getNewDeltaValue(mouseY * 0.1, deltaTime * smoothingSpeed);
//yaw, pitch, roll
return Vec3(0.0, yInput, xInput)
}
override fun canJump(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity,
driver: Player
): Boolean {
return false
}
override fun setRideBar(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity,
driver: Player
): Float {
return (state.stamina.get() / 1.0f)
}
override fun jumpForce(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity,
driver: Player,
jumpStrength: Int
): Vec3 {
return Vec3.ZERO
}
override fun gravity(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity,
regularGravity: Double
): Double {
return 0.0
}
override fun rideFovMultiplier(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity,
driver: Player
): Float {
return if (state.sprinting.get()) 1.15f else 1.0f
}
override fun useAngVelSmoothing(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity
): Boolean {
return false
}
override fun inertia(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity
): Double {
return 1.0
}
override fun shouldRoll(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity
): Boolean {
return true
}
override fun turnOffOnGround(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity
): Boolean {
return false
}
override fun dismountOnShift(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity
): Boolean {
return false
}
override fun shouldRotatePokemonHead(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity
): Boolean {
return false
}
override fun shouldRotateRiderHead(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity
): Boolean {
return true
}
override fun maxUpStep(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity
): Float? {
return if (state.inAir.get()) 0.0f else 1.2f
}
override fun getRideSounds(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity
): RideSoundSettingsList {
return settings.rideSounds
}
override fun createDefaultState(settings: BoardSettings) = BoardState()
override fun asMoLangValue(
settings: BoardSettings,
state: BoardState,
vehicle: PokemonEntity
): ObjectValue<RidingBehaviour<BoardSettings, BoardState>> {
val canJump = vehicle.runtime.resolveBoolean(settings.canJump ?: globalBoard.canJump!!)
val value = super.asMoLangValue(settings, state, vehicle)
value.functions.put("sprinting") { DoubleValue(state.sprinting.get()) }
value.functions.put("walking") { DoubleValue(state.walking.get()) }
value.functions.put("in_air") { DoubleValue(state.inAir.get() || (state.rideVelocity.get().y >= 0.0 && (vehicle.controllingPassenger as? Player)?.jumping == true && canJump) || (state.rideVelocity.get().y > 0.0)) }
return value
}
}
class BoardSettings : RidingBehaviourSettings {
override val key = BoardBehaviour.KEY
override val stats = mutableMapOf<RidingStat, IntRange>()
var infiniteStamina: Expression? = null
private set
var canJump: Expression? = null
private set
var canSprint: Expression? = null
private set
var lookYawLimit: Expression? = null
private set
var speedExpr: Expression? = null
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 accelerationExpr: Expression? = null
private set
// Between 30 seconds and 10 seconds at the lowest when at full speed.
var staminaExpr: Expression? = null
private set
//Between a one block jump and a six block jump
var jumpExpr: Expression? = null
private set
var handlingExpr: Expression? = null
private set
var rideSounds: RideSoundSettingsList = RideSoundSettingsList()
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeRidingStats(stats)
rideSounds.encode(buffer)
buffer.writeNullableExpression(infiniteStamina)
buffer.writeNullableExpression(canJump)
buffer.writeNullableExpression(canSprint)
buffer.writeNullableExpression(lookYawLimit)
buffer.writeNullableExpression(speedExpr)
buffer.writeNullableExpression(accelerationExpr)
buffer.writeNullableExpression(staminaExpr)
buffer.writeNullableExpression(jumpExpr)
buffer.writeNullableExpression(handlingExpr)
buffer.writeNullableExpression(canJump)
buffer.writeNullableExpression(canSprint)
}
override fun decode(buffer: RegistryFriendlyByteBuf) {
stats.putAll(buffer.readRidingStats())
rideSounds = RideSoundSettingsList.decode(buffer)
infiniteStamina = buffer.readNullableExpression()
canJump = buffer.readNullableExpression()
canSprint = buffer.readNullableExpression()
lookYawLimit = buffer.readNullableExpression()
speedExpr = buffer.readNullableExpression()
accelerationExpr = buffer.readNullableExpression()
staminaExpr = buffer.readNullableExpression()
jumpExpr = buffer.readNullableExpression()
handlingExpr = buffer.readNullableExpression()
canJump = buffer.readNullableExpression()
canSprint = buffer.readNullableExpression()
}
}
class BoardState : RidingBehaviourState() {
var sprinting = ridingState(false, Side.CLIENT)
var walking = ridingState(false, Side.BOTH)
var sprintToggleable = ridingState(false, Side.CLIENT)
var inAir = ridingState(false, Side.CLIENT)
var jumpTicks = ridingState(0, Side.CLIENT)
var sprintTickTimer = ridingState(0, Side.CLIENT)
var forwardHeldLastTick = ridingState(false, Side.CLIENT)
var surfDist = ridingState(Float.POSITIVE_INFINITY, Side.BOTH)
var surfaceNormal = ridingState(Vec3(0.0, 1.0, 0.0), Side.CLIENT)
var yawAngularVelocity = ridingState(0.0, Side.CLIENT)
override fun encode(buffer: FriendlyByteBuf) {
super.encode(buffer)
buffer.writeBoolean(sprinting.get())
buffer.writeBoolean(walking.get())
buffer.writeBoolean(sprintToggleable.get())
buffer.writeBoolean(inAir.get())
buffer.writeInt(jumpTicks.get())
buffer.writeInt(sprintTickTimer.get())
buffer.writeBoolean(forwardHeldLastTick.get())
buffer.writeFloat(surfDist.get())
buffer.writeVec3(surfaceNormal.get())
buffer.writeDouble(yawAngularVelocity.get())
}
override fun decode(buffer: FriendlyByteBuf) {
super.decode(buffer)
sprinting.set(buffer.readBoolean(), forced = true)
walking.set(buffer.readBoolean(), forced = true)
sprintToggleable.set(buffer.readBoolean(), forced = true)
inAir.set(buffer.readBoolean(), forced = true)
jumpTicks.set(buffer.readInt(), forced = true)
sprintTickTimer.set(buffer.readInt(), forced = true)
forwardHeldLastTick.set(buffer.readBoolean(), forced = true)
surfDist.set(buffer.readFloat(), forced = true)
surfaceNormal.set(buffer.readVec3(), forced = true)
yawAngularVelocity.set(buffer.readDouble(), forced = true)
}
override fun reset() {
super.reset()
sprinting.set(false, forced = true)
walking.set(false, forced = true)
sprintToggleable.set(false, forced = true)
inAir.set(false, forced = true)
jumpTicks.set(0, forced = true)
sprintTickTimer.set(0, forced = true)
forwardHeldLastTick.set(false, forced = true)
surfDist.set(Float.POSITIVE_INFINITY, forced = true)
surfaceNormal.set(Vec3(0.0, 1.0, 0.0), forced = true)
yawAngularVelocity.set(0.0, forced = true)
}
override fun copy() = BoardState().also {
it.rideVelocity.set(this.rideVelocity.get(), forced = true)
it.stamina.set(this.stamina.get(), forced = true)
it.sprinting.set(this.sprinting.get(), forced = true)
it.walking.set(this.sprinting.get(), forced = true)
it.sprintToggleable.set(this.sprintToggleable.get(), forced = true)
it.inAir.set(this.inAir.get(), forced = true)
it.jumpTicks.set(this.jumpTicks.get(), forced = true)
it.sprintTickTimer.set(this.sprintTickTimer.get(), forced = true)
it.forwardHeldLastTick.set(this.forwardHeldLastTick.get(), forced = true)
it.surfDist.set(this.surfDist.get(), forced = true)
it.surfaceNormal.set(this.surfaceNormal.get(), forced = true)
it.yawAngularVelocity.set(this.yawAngularVelocity.get(), forced = true)
}
override fun shouldSync(previous: RidingBehaviourState): Boolean {
if (previous !is BoardState) return false
if (previous.sprinting.get() != sprinting.get()) return true
if (previous.walking.get() != walking.get()) return true
if (previous.sprintToggleable.get() != sprintToggleable.get()) return true
if (previous.inAir.get() != inAir.get()) return true
if (previous.surfDist.get() != surfDist.get()) return true
return super.shouldSync(previous)
}
}
@@ -0,0 +1,783 @@
/*
* 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.behaviour.types.land
import com.bedrockk.molang.Expression
import com.cobblemon.mod.common.CobblemonRideSettings
import com.cobblemon.mod.common.OrientationControllable
import com.cobblemon.mod.common.api.orientation.OrientationController
import com.cobblemon.mod.common.api.orientation.OrientationController.Companion.UP
import com.cobblemon.mod.common.api.riding.RidingStyle
import com.cobblemon.mod.common.api.riding.behaviour.*
import com.cobblemon.mod.common.api.riding.posing.PoseOption
import com.cobblemon.mod.common.api.riding.posing.PoseProvider
import com.cobblemon.mod.common.api.riding.util.DistanceFieldLayer
import com.cobblemon.mod.common.api.riding.util.FieldPrimitive
import com.cobblemon.mod.common.api.riding.util.PrimitiveShape
import com.cobblemon.mod.common.api.riding.util.collectPrimitivesFromBlocks
import com.cobblemon.mod.common.api.riding.util.estimateGradient
import com.cobblemon.mod.common.api.riding.util.sampleField
import com.cobblemon.mod.common.api.riding.util.visualizeDistanceField
import com.cobblemon.mod.common.api.riding.sound.RideSoundSettingsList
import com.cobblemon.mod.common.api.riding.stats.RidingStat
import com.cobblemon.mod.common.api.riding.util.DistanceFieldTuning
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.*
import com.cobblemon.mod.common.util.math.geometry.toRadians
import net.minecraft.core.Direction
import net.minecraft.core.particles.ParticleTypes
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.Mth
import net.minecraft.util.SmoothDouble
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.attributes.Attributes
import net.minecraft.world.entity.player.Player
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec2
import net.minecraft.world.phys.Vec3
import net.minecraft.world.phys.shapes.Shapes
import org.joml.Matrix3f
import org.joml.Quaternionf
import org.joml.Vector3f
import kotlin.math.*
class ClimbBehaviour : RidingBehaviour<ClimbSettings, ClimbState> {
companion object {
val KEY = cobblemonResource("land/climb")
}
override val key = KEY
override fun getRidingStyle(settings: ClimbSettings, state: ClimbState): RidingStyle {
return RidingStyle.LAND
}
val poseProvider = PoseProvider<ClimbSettings, ClimbState>(PoseType.STAND)
.with(PoseOption(PoseType.WALK) { _, state, vehicle ->
return@PoseOption abs(state.rideVelocity.get().horizontalDistance()) > 0.0
})
override fun isActive(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity
): Boolean {
return Shapes.create(vehicle.boundingBox).blockPositionsAsListRounded().any {
// Need to check other fluids
if (vehicle.isInWater || vehicle.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() == (vehicle.position().y)) {
val blockState = vehicle.level().getBlockState(it.below())
return@any !blockState.isAir && blockState.fluidState.isEmpty
}
true
}
}
override fun pose(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity
): PoseType {
return this.poseProvider.select(settings, state, vehicle)
}
override fun speed(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity,
driver: Player
): Float {
return state.rideVelocity.get().length().toFloat()
}
override fun tick(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity,
driver: Player,
input: Vec3
) {
handleSprinting(state, driver)
inAirCheck(state, vehicle)
tickStamina(settings, state, vehicle)
// attachmentNormal: discrete, physics-facing - which face are we touching, used
// for the sticking force (newDownForce) in velocity().
// renderSurfaceNormal: smoothed field-gradient - purely cosmetic, used to slowly
// rotate the model's up vector in angRollVel(). The two are intentionally
// separate and should not be conflated.
updateAttachment(state, vehicle)
updateRenderSurfaceNormal(state, vehicle)
//TODO: should be a config somewhere
val debugVisualizeField = true;
if (debugVisualizeField && !vehicle.level().isClientSide && vehicle.tickCount % 5 == 0) {
visualizeDistanceField(
referenceEntity = vehicle,
center = vehicle.position().add(0.0, (vehicle.bbHeight * 0.5f).toDouble(), 0.0),
layers = listOf(
DistanceFieldLayer(isoValue = 0.0f, color = Vector3f(1f, 0.2f, 0.2f)),
DistanceFieldLayer(isoValue = 1.0f, color = Vector3f(0.2f, 0.6f, 1f))
),
horizontalRadius = 2.0,
verticalRange = 2.0,
sampleSpacing = 0.35,
marchStep = 0.15,
tuning = DistanceFieldTuning(smoothingRange = 0.5f)
)
}
}
/**
* Computes the smoothed field-gradient normal used for rendering only (up-vector
* alignment in angRollVel). Cached here at tick rate since collecting primitives
* and sampling the field is real work - actually applying the rotation happens
* separately at frame rate, same pattern as HorseBehaviour's surfDist/BoardBehaviour's
* surfaceNormal.
*/
fun updateRenderSurfaceNormal(state: ClimbState, vehicle: PokemonEntity) {
val level = vehicle.level()
val center = vehicle.position().add(0.0, (vehicle.bbHeight * 0.5f).toDouble(), 0.0)
val horizontalRadius = 1.5
val verticalRange = 1.5
val searchBox = AABB(
center.x - horizontalRadius, center.y - verticalRange, center.z - horizontalRadius,
center.x + horizontalRadius, center.y + verticalRange, center.z + horizontalRadius
)
val primitives: List<FieldPrimitive> = collectPrimitivesFromBlocks(level, searchBox, PrimitiveShape.ELLIPSOID)
if (primitives.isEmpty()) return
val tuning = DistanceFieldTuning(smoothingRange = 0.5f)
val gradient = estimateGradient(primitives, center, tuning)
state.renderSurfaceNormal.set(gradient)
}
/** Debug viz: draws a short line of particles from the vehicle out along the current attachment normal. */
fun spawnAttachmentNormalParticles(vehicle: PokemonEntity, normal: Vec3) {
val level = vehicle.level()
if (!level.isClientSide) return
if (normal.lengthSqr() < 1e-6) return
val origin = vehicle.position().add(0.0, (vehicle.bbHeight * 0.5f).toDouble(), 0.0)
val length = 1.5
val steps = 15
for (i in 0..steps) {
val t = i.toDouble() / steps
val point = origin.add(normal.scale(length * t))
level.addParticle(ParticleTypes.END_ROD, point.x, point.y, point.z, 0.0, 0.0, 0.0)
}
}
fun inAirCheck(
state: ClimbState,
vehicle: PokemonEntity,
) {
// Check both vertical movement and if there are blocks below.
val posBelow = vehicle.blockPosition().below()
val blockStateBelow = vehicle.level().getBlockState(posBelow)
val isAirOrLiquid = blockStateBelow.isAir || !blockStateBelow.fluidState.isEmpty
val canSupportEntity = blockStateBelow.isFaceSturdy(vehicle.level(), posBelow, Direction.UP)
val standingOnSolid = canSupportEntity && !isAirOrLiquid
// inAir if not on the ground
val inAir = !(vehicle.deltaMovement.y == 0.0 || standingOnSolid)
state.inAir.set(inAir)
}
fun handleSprinting(
state: ClimbState,
driver: Player
) {
if (state.sprinting.get()) {
state.sprinting.set(driver.isSprinting && state.stamina.get() > 0.0f)
} else {
// Only allow sprinting to start if over x percentage of stamina left
val stamSprintPerc = 25.0f
state.sprinting.set(driver.isSprinting && state.stamina.get() > stamSprintPerc / 100.0f)
}
}
fun tickStamina(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity,
) {
val stam = state.stamina.get()
var newStam = stam
val stamDrainRate = (1.0f / vehicle.runtime.resolveDouble(settings.staminaExpr)).toFloat() / 20.0f
if (state.sprinting.get()) {
newStam = max(0.0f,stam - stamDrainRate)
} else {
newStam = min(1.0f,stam + stamDrainRate * 4)
}
state.stamina.set(newStam)
}
override fun updatePassengerRotation(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity,
driver: LivingEntity
) { return }
override fun clampPassengerRotation(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity,
driver: LivingEntity
) { }
override fun rotation(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity,
driver: LivingEntity
): Vec2 {
return Vec2(vehicle.xRot, vehicle.yRot )
}
/*
* Calculates the rotation amount given the difference between
* the current player y rot and entity y rot. This gives the
* affect of influencing a rides rotation through the mouse
* without it being instant.
*
* THIS IS ADAPTED FOR 3D ORIENTATION
*/
fun calcRotAmount(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity,
driver: LivingEntity
): Float {
val topSpeed = vehicle.runtime.resolveDouble(settings.speedExpr)
val handling = vehicle.runtime.resolveDouble(settings.handlingExpr)
val walkHandlingBoost = 5
val maxYawDiff = vehicle.runtime.resolveFloat(settings.lookYawLimit)
// Normalize the current rotation diff
val controller = (vehicle as? OrientationControllable)?.orientationController
if (controller == null || controller.orientation == null) return 0.0f
val rotDiff = Mth.wrapDegrees(Mth.wrapDegrees(driver.yRot) - controller.yaw).coerceIn(-maxYawDiff,maxYawDiff)
val rotDiffNorm = rotDiff / maxYawDiff
// Take the square root so that the ride levels out quicker when at lower differences between entity
// y and driver y
// This influences the speed of the turn based on how far in one direction you're looking
val minRotMod = 0.4f // Min speed that the rotation modulation converges towards
val rotDiffMod = ((sqrt(abs(rotDiffNorm)) * (1.0f - minRotMod)) + minRotMod) * rotDiffNorm.sign
// Turn rate should be quick when walking and slower when sprinting.
// Smoothly move between these based on speed
val walkSpeed = getWalkSpeed(vehicle)
val w = max(walkSpeed, vehicle.deltaMovement.horizontalDistance())
val invRelSpeed = (RidingBehaviour.scaleToRange(w, walkSpeed, topSpeed ) - 1.0f) * -1.0f
val turnRate = ((handling.toFloat() / 20.0f) * max(walkHandlingBoost * invRelSpeed, 1.0)).toFloat()
// Ensure you only ever rotate as much difference as there is between the angles.
val turnSpeed = turnRate * rotDiffMod
val rotAmount = turnSpeed.coerceIn(-abs(rotDiff), abs(rotDiff))
return rotAmount
}
override fun velocity(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity,
driver: Player,
input: Vec3
): Vec3 {
state.rideVelocity.set(calculateRideSpaceVel(settings, state, vehicle, driver))
return state.rideVelocity.get()
}
private fun calculateRideSpaceVel(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity,
driver: Player
): Vec3 {
val globalHorse: HorseSettings = CobblemonRideSettings.horse
val canSprint = vehicle.runtime.resolveBoolean(settings.canSprint ?: globalHorse.canSprint!!)
val canJump = vehicle.runtime.resolveBoolean(settings.canJump ?: globalHorse.canJump!!)
val jumpForce = vehicle.runtime.resolveDouble(settings.jumpExpr ?: globalHorse.jumpExpr!!) * 0.75
val rideTopSpeed = vehicle.runtime.resolveDouble(settings.speedExpr ?: globalHorse.speedExpr!!)
val walkSpeed = getWalkSpeed(vehicle)
val topSpeed = if(canSprint && state.sprinting.get()) rideTopSpeed else walkSpeed
val accelTime = if(state.sprinting.get()) (vehicle.runtime.resolveDouble(settings.accelerationExpr ?: globalHorse.accelerationExpr!!) * 20.0) else 0.5 * 20.0
val accel = topSpeed / accelTime
var activeInput = false
/******************************************************
* Gather the previous velocity and check for horizontal
* collisions
*****************************************************/
var newVelocity = state.rideVelocity.get() //.normalize().scale(vehicle.deltaMovement.horizontalDistance())
/******************************************************
* Gather the previous velocity and check for horizontal
* collisions
*****************************************************/
if (vehicle.horizontalCollision) {
var horizontalVec = Vec3(newVelocity.x, 0.0, newVelocity.z)
horizontalVec = horizontalVec.normalize().scale(min(horizontalVec.length(), 0.3))
newVelocity = Vec3(horizontalVec.x, newVelocity.y, horizontalVec.z)
}
/******************************************************
* Speed up and slow down based on input
*****************************************************/
if (driver.zza != 0.0f) {
// If on a tight turn then do not speed up past half of top speed in order to turn quicker
// Also determine how fast to be slowing down based on how far turned you are
val lookYawLimit = vehicle.runtime.resolveFloat(settings.lookYawLimit ?: globalHorse.lookYawLimit!!)
val percOfMaxTurnSpeed = abs(Mth.wrapDegrees(driver.yRot - vehicle.yRot) / lookYawLimit) * 100.0f
val turnPercThresh = 0.0f
val s = min(((percOfMaxTurnSpeed - turnPercThresh) / (100.0f - turnPercThresh)).pow(1),1.0f)
val effectiveTopSpeed = if (percOfMaxTurnSpeed > turnPercThresh) topSpeed / max(2.0f*s,1.0f) else topSpeed
val turningSlowDown = s * 0.1
//make sure it can't exceed top speed
val forwardInput = when {
driver.zza > 0 && newVelocity.z > effectiveTopSpeed -> 0.0
driver.zza < 0 && newVelocity.z < (-effectiveTopSpeed / 3.0) -> 0.0
else -> driver.zza.sign
}
// Add extra friction if trying to slow down to turn faster
if (newVelocity.z > effectiveTopSpeed) {
newVelocity = newVelocity.subtract(0.0, 0.0, min(turningSlowDown * newVelocity.z.sign, newVelocity.z))
}
newVelocity = Vec3(
newVelocity.x,
newVelocity.y,
(newVelocity.z + (accel * forwardInput.toDouble())))
activeInput = true
}
/******************************************************
* Gravity logic
*****************************************************/
val isOnTopSurface = state.attachmentNormal.get() == Vec3(0.0, 1.0, 0.0)
if (isOnTopSurface) {
if (!vehicle.onGround() && state.jumpTicks.get() <= 0) {
val gravity = (9.8 / ( 20.0)) * 0.2 * 0.6
val terminalVel = 2.0
newVelocity = Vec3(newVelocity.x, max(newVelocity.y - gravity, -terminalVel), newVelocity.z)
} else if(vehicle.onGround()) {
newVelocity = Vec3(newVelocity.x, 0.0, newVelocity.z)
}
} else {
// Wall/ceiling - small constant pull to stay stuck, not real gravity.
val stickForce = 0.08
newVelocity = Vec3(newVelocity.x, newVelocity.y - stickForce, newVelocity.z)
}
/******************************************************
* Ground Friction
*****************************************************/
if( (newVelocity.horizontalDistance() > 0 && vehicle.onGround() && !activeInput) || newVelocity.horizontalDistance() > topSpeed) {
newVelocity = newVelocity.subtract(0.0, 0.0, min(0.03 * newVelocity.z.sign, newVelocity.z))
}
/******************************************************
* Jump Logic
*****************************************************/
if (state.jumpTicks.get() > 0 || (state.jumpTicks.get() >= 0 && driver.jumping && vehicle.onGround() && canJump && driver.deltaMovement.y <= 0.1)) {
// Spread out jumpforce so that variable height jumps are possible
val jumpInputTicks = 6
if (driver.jumping && (state.jumpTicks.get() >= 0 && state.jumpTicks.get() < jumpInputTicks)) {
val appliedJumpForce = ((jumpForce*1.5) / jumpInputTicks) //* (1 - state.jumpTicks.get() / jumpInputTicks)
newVelocity = Vec3(newVelocity.x, newVelocity.y + appliedJumpForce, newVelocity.z)
state.jumpTicks.set(state.jumpTicks.get() + 1)
} else {
// Set delay before next jump is possible after hitting ground
val tickJumpDelay = 3
state.jumpTicks.set(-tickJumpDelay)
}
} else if (vehicle.onGround() && state.jumpTicks.get() < 0) {
// Tick off the delay once reaching the ground
state.jumpTicks.set(state.jumpTicks.get() + 1)
}
// Zero out lateral velocity possibly picked up from a controller transition
newVelocity = Vec3(0.0, newVelocity.y, newVelocity.z)
return newVelocity
}
private fun getWalkSpeed( vehicle: PokemonEntity ): Double {
val walkspeed = vehicle.runtime.resolveDouble(vehicle.behaviour.moving.walk.walkSpeed)
val movementSpeed = vehicle.attributes.getValue(Attributes.MOVEMENT_SPEED)
val speedModifier = 1.2 * 0.35
return walkspeed * movementSpeed * speedModifier
}
override fun angRollVel(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity,
driver: Player,
deltaTime: Double
): Vec3 {
if (vehicle !is OrientationControllable) return Vec3.ZERO
val controller = vehicle.orientationController
// Smoothly rotate the model's up vector toward the (separately computed,
// smoothed) field-gradient normal. Purely cosmetic - does not affect attachment
// or movement, which are driven by state.attachmentNormal instead.
val upAlignmentRatePerTick = 0.2
val frameBlend = 1.0 - (1.0 - upAlignmentRatePerTick).pow(deltaTime * 20.0)
alignUpToSurfaceNormal(controller, state.renderSurfaceNormal.get(), frameBlend.toFloat())
return Vec3.ZERO
}
/**
* Rotates [controller]'s orientation so its up vector moves a fraction of the way
* toward [targetUp] - partial per call, not instant, so alignment settles smoothly
* rather than snapping every time the target changes.
*/
private fun alignUpToSurfaceNormal(controller: OrientationController, targetUp: Vec3, alignmentBlend: Float) {
if (targetUp.lengthSqr() < 1e-6) return
val targetUpVec = Vector3f(targetUp.x.toFloat(), targetUp.y.toFloat(), targetUp.z.toFloat()).normalize()
controller.updateOrientation { current ->
val currentUp = current.transform(Vector3f(UP), Vector3f())
if (currentUp.lengthSquared() < 1e-6f) return@updateOrientation current
// Full rotation that would exactly align currentUp to targetUp...
val fullCorrection = Quaternionf().rotationTo(currentUp, targetUpVec)
// ...but only take a partial step toward it, so alignment is smooth, not instant.
val steppedCorrection = Quaternionf().slerp(fullCorrection, alignmentBlend)
Matrix3f().set(steppedCorrection).mul(current, Matrix3f())
}
}
override fun rotationOnMouseXY(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity,
driver: Player,
mouseY: Double,
mouseX: Double,
mouseYSmoother: SmoothDouble,
mouseXSmoother: SmoothDouble,
sensitivity: Double,
deltaTime: Double
): Vec3 {
if (vehicle !is OrientationControllable) return Vec3.ZERO
val vehicleController = vehicle.orientationController
val smoothingSpeed = 4
val xInput = mouseXSmoother.getNewDeltaValue(mouseX * 0.1, deltaTime * smoothingSpeed)
val yInput = mouseYSmoother.getNewDeltaValue(mouseY * 0.1, deltaTime * smoothingSpeed)
//yaw, pitch, roll
return Vec3(xInput, 0.0, 0.0)
}
override fun canJump(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity,
driver: Player
): Boolean {
return false
}
override fun setRideBar(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity,
driver: Player
): Float {
return (state.stamina.get() / 1.0f)
}
override fun jumpForce(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity,
driver: Player,
jumpStrength: Int
): Vec3 {
return Vec3.ZERO
}
override fun gravity(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity,
regularGravity: Double
): Double {
return 0.0
}
override fun rideFovMultiplier(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity,
driver: Player
): Float {
return 1.0f
}
override fun useAngVelSmoothing(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity
): Boolean {
return false
}
override fun inertia(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity
): Double {
return 1.0
}
override fun shouldRoll(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity
): Boolean {
return true
}
override fun turnOffOnGround(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity
): Boolean {
return false
}
override fun dismountOnShift(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity
): Boolean {
return false
}
override fun shouldRotatePokemonHead(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity
): Boolean {
return false
}
override fun shouldRotateRiderHead(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity
): Boolean {
return true
}
override fun getRideSounds(
settings: ClimbSettings,
state: ClimbState,
vehicle: PokemonEntity
): RideSoundSettingsList {
return settings.rideSounds
}
override fun createDefaultState(settings: ClimbSettings) = ClimbState()
}
class ClimbSettings : RidingBehaviourSettings {
override val key = ClimbBehaviour.KEY
override val stats = mutableMapOf<RidingStat, IntRange>()
var canJump = "true".asExpression()
private set
var canSprint = "true".asExpression()
private set
var lookYawLimit = "90.0f".asExpression()
private set
var speedExpr: Expression = "q.get_ride_stats('SPEED', 'LAND', 1.0, 0.4)".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 accelerationExpr: Expression =
"q.get_ride_stats('ACCELERATION', 'LAND', 0.5, 2.0)".asExpression()
private set
// Between 30 seconds and 10 seconds at the lowest when at full speed.
var staminaExpr: Expression = "q.get_ride_stats('STAMINA', 'LAND', 30.0, 10.0)".asExpression()
private set
//Between a one block jump and a six block jump
var jumpExpr: Expression = "q.get_ride_stats('JUMP', 'LAND', 1.0, 0.38)".asExpression()
private set
var handlingExpr: Expression = "q.get_ride_stats('SKILL', 'LAND', 180.0, 40.0)".asExpression()
private set
var rideSounds: RideSoundSettingsList = RideSoundSettingsList()
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeResourceLocation(key)
buffer.writeRidingStats(stats)
rideSounds.encode(buffer)
buffer.writeExpression(speedExpr)
buffer.writeExpression(accelerationExpr)
buffer.writeExpression(staminaExpr)
buffer.writeExpression(jumpExpr)
buffer.writeExpression(handlingExpr)
buffer.writeExpression(canJump)
buffer.writeExpression(canSprint)
}
override fun decode(buffer: RegistryFriendlyByteBuf) {
stats.putAll(buffer.readRidingStats())
rideSounds = RideSoundSettingsList.decode(buffer)
speedExpr = buffer.readExpression()
accelerationExpr = buffer.readExpression()
staminaExpr = buffer.readExpression()
jumpExpr = buffer.readExpression()
handlingExpr = buffer.readExpression()
canJump = buffer.readExpression()
canSprint = buffer.readExpression()
}
}
class ClimbState : RidingBehaviourState() {
var sprinting = ridingState(false, Side.CLIENT)
var inAir = ridingState(false, Side.CLIENT)
var jumpTicks = ridingState(0, Side.CLIENT)
// Discrete, physics-facing: which of the 6 faces we're attached to right now.
// Drives newDownForce in velocity(). Set by updateAttachment().
var attachmentNormal = ridingState(Vec3(0.0, 1.0, 0.0), Side.CLIENT)
var attachmentPoint = ridingState(Vec3(0.0, 1.0, 0.0), Side.CLIENT)
// Smoothed, rendering-facing: the field-gradient normal used only to slowly rotate
// the model's visual up vector in angRollVel(). Deliberately separate from
// attachmentNormal - see comments in tick(). Set by updateRenderSurfaceNormal().
var renderSurfaceNormal = ridingState(Vec3(0.0, 1.0, 0.0), Side.CLIENT)
override fun encode(buffer: FriendlyByteBuf) {
super.encode(buffer)
buffer.writeBoolean(sprinting.get())
buffer.writeBoolean(inAir.get())
buffer.writeInt(jumpTicks.get())
}
override fun decode(buffer: FriendlyByteBuf) {
super.decode(buffer)
sprinting.set(buffer.readBoolean(), forced = true)
inAir.set(buffer.readBoolean(), forced = true)
jumpTicks.set(buffer.readInt(), forced = true)
}
override fun reset() {
super.reset()
sprinting.set(false, forced = true)
inAir.set(false, forced = true)
jumpTicks.set(0, forced = true)
attachmentNormal.set(Vec3(0.0, 1.0, 0.0), forced = true)
attachmentPoint.set(Vec3(0.0, 1.0, 0.0), forced = true)
renderSurfaceNormal.set(Vec3(0.0, 1.0, 0.0), forced = true)
}
override fun copy() = ClimbState().also {
it.rideVelocity.set(this.rideVelocity.get(), forced = true)
it.stamina.set(this.stamina.get(), forced = true)
it.sprinting.set(this.sprinting.get(), forced = true)
it.inAir.set(this.inAir.get(), forced = true)
it.jumpTicks.set(this.jumpTicks.get(), forced = true)
it.attachmentNormal.set(this.attachmentNormal.get(), forced = true)
it.attachmentPoint.set(this.attachmentNormal.get(), forced = true)
it.renderSurfaceNormal.set(this.renderSurfaceNormal.get(), forced = true)
}
override fun shouldSync(previous: RidingBehaviourState): Boolean {
if (previous !is ClimbState) return false
if (previous.sprinting.get() != sprinting.get()) return true
if (previous.inAir.get() != inAir.get()) return true
return super.shouldSync(previous)
}
}
fun updateAttachment(state: ClimbState, vehicle: PokemonEntity) {
val probeMargin = 0.15
val currentFace = AttachmentFace.entries.firstOrNull { it.normal == state.attachmentNormal.get() }
val face = determineAttachmentFace(vehicle, currentFace, probeMargin)
if (face != null) {
state.attachmentNormal.set(face.normal)
}
// face == null means nothing's currently in reach - leave attachmentNormal as
// whatever it last was until the jump/detach state takes over.
}
enum class AttachmentFace(val normal: Vec3) {
UP(Vec3(0.0, 1.0, 0.0)),
DOWN(Vec3(0.0, -1.0, 0.0)),
NORTH(Vec3(0.0, 0.0, -1.0)),
SOUTH(Vec3(0.0, 0.0, 1.0)),
EAST(Vec3(1.0, 0.0, 0.0)),
WEST(Vec3(-1.0, 0.0, 0.0))
}
/** Thin slab test on one face of the vehicle's actual bounding box - true if anything solid is touching that side. */
fun probeFaceContact(vehicle: PokemonEntity, face: AttachmentFace, probeMargin: Double): Boolean {
val box = vehicle.boundingBox
val testBox = when (face) {
AttachmentFace.UP -> AABB(box.minX, box.maxY, box.minZ, box.maxX, box.maxY + probeMargin, box.maxZ)
AttachmentFace.DOWN -> AABB(box.minX, box.minY - probeMargin, box.minZ, box.maxX, box.minY, box.maxZ)
AttachmentFace.NORTH -> AABB(box.minX, box.minY, box.minZ - probeMargin, box.maxX, box.maxY, box.minZ)
AttachmentFace.SOUTH -> AABB(box.minX, box.minY, box.maxZ, box.maxX, box.maxY, box.maxZ + probeMargin)
AttachmentFace.EAST -> AABB(box.maxX, box.minY, box.minZ, box.maxX + probeMargin, box.maxY, box.maxZ)
AttachmentFace.WEST -> AABB(box.minX - probeMargin, box.minY, box.minZ, box.minX, box.maxY, box.maxZ)
}
return !vehicle.level().noCollision(vehicle, testBox)
}
/** Stays on the current face if it's still touching, otherwise scans for a new one. */
fun determineAttachmentFace(vehicle: PokemonEntity, currentFace: AttachmentFace?, probeMargin: Double): AttachmentFace? {
if (currentFace != null && probeFaceContact(vehicle, currentFace, probeMargin)) {
return currentFace
}
return AttachmentFace.entries.firstOrNull { probeFaceContact(vehicle, it, probeMargin) }
}
@@ -182,6 +182,7 @@ class RideControlsOverlay : Gui(Minecraft.getInstance()) {
showHorizontalMouse = false
}
VehicleBehaviour.KEY -> {}
BoardBehaviour.KEY -> {}
}
val centerX = minecraft.window.guiScaledWidth / 2
@@ -41,6 +41,8 @@ import kotlin.math.sign
import kotlin.math.sqrt
object MountedCameraRenderer {
private const val MIN_OFFSET_LENGTH = 1.0E-6
private var returnTimer: Float = 0f
private var lastHandledRotationTime: Double = Double.MIN_VALUE
private var frameTime: Double = 0.0
@@ -79,7 +81,7 @@ object MountedCameraRenderer {
val locator = delegate.locatorStates[locatorName] ?: return null
// Get additional offset from poser and add to the eyeHeight offset
val currEyeHeight: Double = Mth.lerp(instance.partialTickTime.toDouble(), eyeHeight, eyeHeightOld)
val currEyeHeight: Double = Mth.lerp(instance.partialTickTime.toDouble(), eyeHeightOld, eyeHeight)
var offset = Vec3(0.0, currEyeHeight - (driver.bbHeight / 2), 0.0)
var eyeOffset = Vec3(0.0, currEyeHeight - (driver.bbHeight / 2), 0.0)
@@ -126,10 +128,10 @@ object MountedCameraRenderer {
} */
val offsetDistance = offset.length()
val offsetDirection = offset.scale(1 / offsetDistance)
val offsetDirection = safeDirection(offset, offsetDistance)
val eyeOffsetDistance = eyeOffset.length()
val eyeOffsetDirection = eyeOffset.scale(1 / eyeOffsetDistance)
val eyeOffsetDirection = safeDirection(eyeOffset, eyeOffsetDistance)
// Use getMaxZoom to calculate clipping
val maxZoom: Double = getMaxZoom(offsetDistance, offsetDirection, pos, instance)
@@ -329,7 +331,7 @@ object MountedCameraRenderer {
val offset = if (thirdPersonReverse && cameraOffsets.containsKey(locatorName + "_reverse")) {
cameraOffsets[locatorName + "_reverse"]!!
} else if (thirdPersonReverse && cameraOffsets.containsKey("seat_default_reverse")) {
cameraOffsets[locatorName + "seat_default_reverse"]!!
cameraOffsets["seat_default_reverse"]!!
} else if (cameraOffsets.containsKey(locatorName)) {
cameraOffsets[locatorName]!!
} else if (cameraOffsets.containsKey("seat_default")) {
@@ -354,6 +356,10 @@ object MountedCameraRenderer {
}
/** Unit direction of [offset], or zero when it's too short to have one (avoids NaN from 0 / 0). */
private fun safeDirection(offset: Vec3, length: Double): Vec3 =
if (length > MIN_OFFSET_LENGTH) offset.scale(1 / length) else Vec3.ZERO
private fun getMaxZoom(maxZoom: Double, directionVector: Vec3, positionVector: Vec3, instance: Camera): Double {
var maxZoom = maxZoom
for (i in 0 .. 7) {
@@ -15,6 +15,7 @@ import com.cobblemon.mod.common.api.riding.behaviour.types.air.HelicopterSetting
import com.cobblemon.mod.common.api.riding.behaviour.types.air.HoverSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.air.JetSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.air.RocketSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.air.BoardSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.land.HorseSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.land.MinekartSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.land.VehicleSettings
@@ -35,6 +36,7 @@ class RideSettingsSyncPacket(
val hover: HoverSettings,
val jet: JetSettings,
val rocket: RocketSettings,
val board: BoardSettings,
val horse: HorseSettings,
val minekart: MinekartSettings,
val vehicle: VehicleSettings,
@@ -52,6 +54,7 @@ class RideSettingsSyncPacket(
hover.encode(buffer)
jet.encode(buffer)
rocket.encode(buffer)
board.encode(buffer)
horse.encode(buffer)
minekart.encode(buffer)
vehicle.encode(buffer)
@@ -71,6 +74,7 @@ class RideSettingsSyncPacket(
hover = HoverSettings(),
jet = JetSettings(),
rocket = RocketSettings(),
board = BoardSettings(),
horse = HorseSettings(),
minekart = MinekartSettings(),
vehicle = VehicleSettings(),
@@ -85,6 +89,7 @@ class RideSettingsSyncPacket(
hover.decode(buffer)
jet.decode(buffer)
rocket.decode(buffer)
board.decode(buffer)
horse.decode(buffer)
minekart.decode(buffer)
vehicle.decode(buffer)
@@ -17,12 +17,7 @@ import com.cobblemon.mod.common.api.riding.behaviour.types.composite.strategies.
import com.cobblemon.mod.common.api.riding.behaviour.types.composite.strategies.FallStrategy
import com.cobblemon.mod.common.api.riding.behaviour.types.composite.strategies.JumpStrategy
import com.cobblemon.mod.common.api.riding.behaviour.types.composite.strategies.RunStrategy
import com.cobblemon.mod.common.api.riding.behaviour.types.land.HorseBehaviour
import com.cobblemon.mod.common.api.riding.behaviour.types.land.HorseSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.land.MinekartBehaviour
import com.cobblemon.mod.common.api.riding.behaviour.types.land.MinekartSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.land.VehicleBehaviour
import com.cobblemon.mod.common.api.riding.behaviour.types.land.VehicleSettings
import com.cobblemon.mod.common.api.riding.behaviour.types.land.*
import com.cobblemon.mod.common.api.riding.behaviour.types.liquid.*
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.google.gson.JsonDeserializationContext
@@ -50,7 +45,9 @@ object RidingBehaviourSettingsAdapter : JsonDeserializer<RidingBehaviourSettings
MinekartBehaviour.KEY to MinekartSettings::class.java,
HoverBehaviour.KEY to HoverSettings::class.java,
RocketBehaviour.KEY to RocketSettings::class.java,
BoardBehaviour.KEY to BoardSettings::class.java,
SubmarineBehaviour.KEY to SubmarineSettings::class.java,
ClimbBehaviour.KEY to ClimbSettings::class.java,
CompositeBehaviour.KEY to CompositeSettings::class.java,
/*
@@ -846,6 +846,14 @@
"cubes": [
{"origin": [-2, -1, 27], "size": [4, 1, 4], "inflate": -0.01, "uv": [104, 47]}
]
},
{
"name": "locator_seat_1",
"parent": "surfboard",
"pivot": [0, 14.5, 3],
"locators": {
"seat_1": [19, 1.5, 0]
}
}
]
}
@@ -0,0 +1,11 @@
{
"infiniteStamina": "false",
"canJump": "true",
"canSprint": "true",
"lookYawLimit": "90.0",
"speedExpr": "q.get_ride_stats('SPEED', 'LAND', 1.2, 0.1)",
"accelerationExpr": "q.get_ride_stats('ACCELERATION', 'LAND', 0.1, 12.0)",
"staminaExpr": "q.get_ride_stats('STAMINA', 'LAND', 120.0, 4.0)",
"jumpExpr": "q.get_ride_stats('JUMP', 'LAND', 1.6, 0.2)",
"handlingExpr": "q.get_ride_stats('SKILL', 'LAND', 90.0, 30.0)"
}
@@ -53,7 +53,7 @@
"riding": {
"behaviours": {
"LAND": {
"key": "cobblemon:land/horse",
"key": "cobblemon:land/climb",
"rideSounds": [],
"stats": {
"ACCELERATION": "55-85",
@@ -48,6 +48,26 @@
"height": 2.15,
"fixed": false
},
"riding": {
"behaviours": {
"LAND": {
"key": "cobblemon:air/board",
"rideSounds": [],
"stats": {
"ACCELERATION": "50-100",
"JUMP": "50-100",
"SKILL": "50-100",
"SPEED": "50-100",
"STAMINA": "50-100"
}
}
},
"seats": [
{
"locator": "seat_1"
}
]
},
"behaviour": {
"resting": {
"canSleep": true,