Compare commits

..
Author SHA1 Message Date
jackowes 1c1cf52bd5 cam, surface, debug 2026-08-03 23:03:05 -05:00
jackowes 4dc6dbef20 tony hawk tuah and some temp string cheese guy asset changes 2026-08-02 22:20:14 -05:00
jackowes 1ffb60d9d8 Test out keeping an entity at a certain distance in the field. Both a spring method and a field surface normal method. 2026-08-02 01:30:19 -05:00
jackowes 6bb878b937 cleanup after squashening 2026-08-01 13:26:45 -05:00
jackowes 83ffcede88 squash merge main 2026-08-01 13:24:28 -05:00
jackowes b2d40bc41c sdf tests 2026-08-01 13:03:31 -05:00
jackowes 3fccb07dba First attempt at sticking force with gravity 2025-09-29 22:20:53 -05:00
jackowes 46dd628b16 sticking force whack but orientation seems fine. Maybe needs to be a bit more responsive? Sticking force likely needs to only apply when a certain distance away from it? 2025-09-28 22:45:42 -05:00
jackowes 676f5e63bd pause on that 2025-09-28 15:29:00 -05:00
jackowes 6dd2d3aada starting again 2025-09-28 15:28:56 -05:00
jackowes e4e0846a24 Smooth out transition to rollables. Transition from rollables to non rollables is broken. 2025-09-28 15:27:12 -05:00
jackowes 7d70af44ba WIP multi passenger rolling support (it works but will likely need a better smoothing function eventually)
Still to do:
- zero driver rotx and roty when mounting.
- Fix the odd camera transition when going from rollable to non rollable (kick out of rollable camera logic earlier probably)
- position logic for freelook camera is wrong in reverse 3rd person.
2025-09-27 23:59:39 -05:00
jackowes 7c8fcdcd48 Freelook support (position logic for 3rd person no view bob still needs fixed 2025-09-27 11:54:23 -05:00
jackowes 67684ff9e8 Merge branch 'main' into orientation-refactor
# Conflicts:
#	common/src/main/kotlin/com/cobblemon/mod/common/api/riding/behaviour/types/air/BirdBehaviour.kt
2025-09-24 21:32:45 -05:00
jackowes 7144aa8dce split orientations 2025-09-24 21:31:14 -05:00
jackowes 6801035c1d messing around with some temp changes 2025-09-23 20:18:22 -05:00
25 changed files with 2832 additions and 301 deletions

No files matched your search

-1
View File
@@ -396,7 +396,6 @@
- Fixed `/giveallpokemon` not respecting the min and max parameters
- Fixed the choose starter toast not disappearing when capturing a Pokémon instead of opening the starter selection screen
- Fixed an issue where certain Sinister Tea seasonings were only applying as instant effects and would not apply over time
- Long config option labels now scroll instead of overflowing
### Developer
- Added `PokeSnackSpawnerFactory` which allows influence registration on PokeSnack encounters
@@ -20,8 +20,6 @@ import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasLookup;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
@@ -33,9 +31,6 @@ import java.util.Optional;
@Mixin(JigsawPlacement.class)
public class JigsawPlacementMixin {
@Unique
private static final Logger cobblemon$LOGGER = LoggerFactory.getLogger("Cobblemon/HeightRangeVeto");
@Inject(
method = "addPieces(Lnet/minecraft/world/level/levelgen/structure/Structure$GenerationContext;Lnet/minecraft/core/Holder;Ljava/util/Optional;ILnet/minecraft/core/BlockPos;ZLjava/util/Optional;ILnet/minecraft/world/level/levelgen/structure/pools/alias/PoolAliasLookup;Lnet/minecraft/world/level/levelgen/structure/pools/DimensionPadding;Lnet/minecraft/world/level/levelgen/structure/templatesystem/LiquidSettings;)Ljava/util/Optional;",
at = @At("RETURN"),
@@ -59,30 +54,18 @@ public class JigsawPlacementMixin {
if (result.isEmpty()) {
return;
}
BlockPos spawnPos = result.get().position();
String poolId = startPool.unwrapKey().map(key -> key.location().toString()).orElse("unknown");
boolean canPlace = cobblemon$canStructurePlace(context, startPool, spawnPos);
cobblemon$LOGGER.info(
"[HeightRangeVeto] addPieces evaluated requestedPos={} spawnPos={} startPool={} -> canPlace={}",
pos, spawnPos, poolId, canPlace
);
boolean canPlace = cobblemon$canAnyStartElementPlace(context, startPool, spawnPos);
if (canPlace) {
return;
}
cobblemon$LOGGER.info(
"[HeightRangeVeto] Vetoing structure start at {} (startPool={})",
spawnPos, poolId
);
cir.setReturnValue(Optional.empty());
}
@Unique
private static boolean cobblemon$canStructurePlace(
private static boolean cobblemon$canAnyStartElementPlace(
Structure.GenerationContext generationContext,
Holder<StructureTemplatePool> startPool,
BlockPos startPos
@@ -91,7 +74,7 @@ public class JigsawPlacementMixin {
Optional<HeightRangeStructureProcessor> heightRangeProcessor = cobblemon$getHeightRangeProcessor(element);
if (heightRangeProcessor.isEmpty()) {
continue;
return true;
}
HeightRangeStructureProcessor processor = heightRangeProcessor.get();
@@ -102,17 +85,12 @@ public class JigsawPlacementMixin {
startPos
);
cobblemon$LOGGER.info(
"[HeightRangeVeto] template={} pos={} valid={}",
element, startPos, valid
);
if (!valid) {
return false;
if (valid) {
return true;
}
}
return true;
return false;
}
@Unique
@@ -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)
@@ -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) }
}
@@ -22,6 +22,13 @@ 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
@@ -41,16 +48,22 @@ 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.server.level.ServerLevel
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.Vector3f
import kotlin.math.*
class HorseBehaviour : RidingBehaviour<HorseSettings, HorseState> {
companion object {
@@ -118,7 +131,57 @@ class HorseBehaviour : RidingBehaviour<HorseSettings, HorseState> {
inAirCheck(state, vehicle)
tickStamina(settings, state, vehicle)
state.walking.set(state.rideVelocity.get().horizontalDistance() > 0.01 || driver.zza != 0.0f)
//surfDistCheck(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 = 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: HorseState,
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(
@@ -266,6 +329,19 @@ class HorseBehaviour : RidingBehaviour<HorseSettings, HorseState> {
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: HorseSettings,
@@ -496,7 +572,7 @@ class HorseBehaviour : RidingBehaviour<HorseSettings, HorseState> {
state: HorseState,
vehicle: PokemonEntity
): Boolean {
return false
return true
}
override fun turnOffOnGround(
@@ -639,6 +715,7 @@ class HorseState : RidingBehaviourState() {
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)
override fun encode(buffer: FriendlyByteBuf) {
super.encode(buffer)
@@ -649,6 +726,7 @@ class HorseState : RidingBehaviourState() {
buffer.writeInt(jumpTicks.get())
buffer.writeInt(sprintTickTimer.get())
buffer.writeBoolean(forwardHeldLastTick.get())
buffer.writeFloat(surfDist.get())
}
override fun decode(buffer: FriendlyByteBuf) {
@@ -660,6 +738,7 @@ class HorseState : RidingBehaviourState() {
jumpTicks.set(buffer.readInt(), forced = true)
sprintTickTimer.set(buffer.readInt(), forced = true)
forwardHeldLastTick.set(buffer.readBoolean(), forced = true)
surfDist.set(buffer.readFloat(), forced = true)
}
override fun reset() {
@@ -671,6 +750,7 @@ class HorseState : RidingBehaviourState() {
jumpTicks.set(0, forced = true)
sprintTickTimer.set(0, forced = true)
forwardHeldLastTick.set(false, forced = true)
surfDist.set(Float.POSITIVE_INFINITY, forced = true)
}
override fun copy() = HorseState().also {
@@ -683,6 +763,7 @@ class HorseState : RidingBehaviourState() {
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)
}
override fun shouldSync(previous: RidingBehaviourState): Boolean {
@@ -691,6 +772,7 @@ class HorseState : RidingBehaviourState() {
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,62 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.riding.util
import net.minecraft.core.BlockPos
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.phys.AABB
import kotlin.math.floor
/**
* Walks every block position inside [searchBox] and turns each solid block into one
* full-cube [FieldPrimitive].
*
* @author Jackson
* @since July 27th, 2026
*/
fun collectPrimitivesFromBlocks(
level: BlockGetter,
searchBox: AABB,
shape: PrimitiveShape,
includeFluids: Boolean = false
): List<FieldPrimitive> {
val primitives = mutableListOf<FieldPrimitive>()
val minX = floor(searchBox.minX).toInt()
val maxX = floor(searchBox.maxX).toInt()
val minY = floor(searchBox.minY).toInt()
val maxY = floor(searchBox.maxY).toInt()
val minZ = floor(searchBox.minZ).toInt()
val maxZ = floor(searchBox.maxZ).toInt()
val pos = BlockPos.MutableBlockPos()
for (x in minX..maxX) {
for (y in minY..maxY) {
for (z in minZ..maxZ) {
pos.set(x, y, z)
val state = level.getBlockState(pos)
val isSolid = !state.getCollisionShape(level, pos).isEmpty
val isFluid = includeFluids && !state.fluidState.isEmpty
if (!isSolid && !isFluid) continue
primitives.add(
FieldPrimitive(
shape = shape,
centerX = x + 0.5f, centerY = y + 0.5f, centerZ = z + 0.5f,
halfWidth = 0.5f, halfHeight = 0.5f, halfDepth = 0.5f
)
)
}
}
}
return primitives
}
@@ -0,0 +1,155 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.riding.util
import net.minecraft.world.phys.Vec3
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
/**
* Helpful Functions when working with signed distance fields/functions.
* https://iquilezles.org/articles/distfunctions/
*
* @author Jackson
* @since July 27th, 2026
*/
/** Which distance formula a primitive uses. */
enum class PrimitiveShape {
BOX,
ROUNDED_BOX,
ELLIPSOID
}
/** Tunable constants for the field. */
data class DistanceFieldTuning(
val smoothingRange: Float = 0.5f,
val roundingRadius: Float = 0.08f
)
/** A single shape in the field - a center position, half-extents, and which formula to use. */
data class FieldPrimitive(
val shape: PrimitiveShape,
val centerX: Float, val centerY: Float, val centerZ: Float,
val halfWidth: Float, val halfHeight: Float, val halfDepth: Float
)
/** Fast approximate 1/sqrt(x) - the Quake III trick, cheaper than a real sqrt + divide. */
fun fastInverseSqrt(value: Float): Float {
var x = value
val halfX = 0.5f * x
var bits = java.lang.Float.floatToIntBits(x)
bits = 0x5f3759df - (bits shr 1)
x = java.lang.Float.intBitsToFloat(bits)
x *= (1.5f - halfX * x * x)
return x
}
/** Exact distance from a point to an axis-aligned box. Inputs are relative to the box's own center. */
fun boxDistance(relX: Float, relY: Float, relZ: Float, halfWidth: Float, halfHeight: Float, halfDepth: Float): Float {
val qx = abs(relX) - halfWidth
val qy = abs(relY) - halfHeight
val qz = abs(relZ) - halfDepth
val ox = max(qx, 0f)
val oy = max(qy, 0f)
val oz = max(qz, 0f)
val outside = sqrt(ox * ox + oy * oy + oz * oz)
val inside = min(max(qx, max(qy, qz)), 0f)
return outside + inside
}
/** Same as boxDistance but with rounded corners/edges https://iquilezles.org/articles/distfunctions/ */
fun roundedBoxDistance(relX: Float, relY: Float, relZ: Float, halfWidth: Float, halfHeight: Float, halfDepth: Float, roundingRadius: Float): Float =
boxDistance(relX, relY, relZ, halfWidth - roundingRadius, halfHeight - roundingRadius, halfDepth - roundingRadius) - roundingRadius
/** Approximate (lower-bound) distance to an ellipsoid https://iquilezles.org/articles/ellipsoids/. */
fun ellipsoidDistance(relX: Float, relY: Float, relZ: Float, halfWidth: Float, halfHeight: Float, halfDepth: Float): Float {
val px = relX / halfWidth
val py = relY / halfHeight
val pz = relZ / halfDepth
val inverseMagnitude = fastInverseSqrt(px * px + py * py + pz * pz)
val trueDistance = sqrt(relX * relX + relY * relY + relZ * relZ)
return trueDistance * (1.0f - inverseMagnitude)
}
/** Distance from a point to one primitive, dispatched by its shape. */
fun distanceToPrimitive(primitive: FieldPrimitive, sampleX: Float, sampleY: Float, sampleZ: Float, tuning: DistanceFieldTuning): Float {
val relX = sampleX - primitive.centerX
val relY = sampleY - primitive.centerY
val relZ = sampleZ - primitive.centerZ
return when (primitive.shape) {
PrimitiveShape.BOX -> boxDistance(relX, relY, relZ, primitive.halfWidth, primitive.halfHeight, primitive.halfDepth)
PrimitiveShape.ROUNDED_BOX -> roundedBoxDistance(relX, relY, relZ, primitive.halfWidth, primitive.halfHeight, primitive.halfDepth, tuning.roundingRadius)
PrimitiveShape.ELLIPSOID -> ellipsoidDistance(relX, relY, relZ, primitive.halfWidth, primitive.halfHeight, primitive.halfDepth)
}
}
///** Normalized quadratic smooth-min https://iquilezles.org/articles/smin/ */
//fun smoothMinimum(a: Float, b: Float, smoothingRange: Float): Float {
// if (smoothingRange <= 0.001f) return min(a, b)
// val k = smoothingRange * 4.0f
// val h = (0.5f + 0.5f * (b - a) / k).coerceIn(0f, 1f)
// return b + (a - b) * h - k * h * (1f - h)
//}
//
///** Folds every primitive's distance together into one field value at a point. */
//fun sampleField(primitives: List<FieldPrimitive>, sampleX: Float, sampleY: Float, sampleZ: Float, tuning: DistanceFieldTuning): Float {
// var result = Float.POSITIVE_INFINITY
// for (primitive in primitives) {
// val distance = distanceToPrimitive(primitive, sampleX, sampleY, sampleZ, tuning)
// result = if (result == Float.POSITIVE_INFINITY) distance else smoothMinimum(result, distance, tuning.smoothingRange)
// }
// return result
//}
/** Normalized quadratic smooth-min https://iquilezles.org/articles/smin/ */
fun smoothMinimum(a: Float, b: Float, smoothingRange: Float): Float {
if (smoothingRange <= 0.001f) return min(a, b)
val k = smoothingRange * 4.0f
val h = (0.5f + 0.5f * (b - a) / k).coerceIn(0f, 1f)
return b + (a - b) * h - k * h * (1f - h)
}
/** Folds nearby primitive distances together into one field value at a point. */
fun sampleField(primitives: List<FieldPrimitive>, sampleX: Float, sampleY: Float, sampleZ: Float, tuning: DistanceFieldTuning): Float {
if (primitives.isEmpty()) return Float.POSITIVE_INFINITY
val distances = FloatArray(primitives.size) { distanceToPrimitive(primitives[it], sampleX, sampleY, sampleZ, tuning) }
val minDistance = distances.min()
if (tuning.smoothingRange <= 0.001f) return minDistance
// Anything more than a few blend-widths past the closest primitive is cut - its
// contribution would be negligible in a correct blend anyway, but left in, it still
// drags the chained result down the more of them there are.
val cutoff = tuning.smoothingRange * 3f
var result = Float.POSITIVE_INFINITY
for (distance in distances) {
if (distance - minDistance > cutoff) continue
result = if (result == Float.POSITIVE_INFINITY) distance else smoothMinimum(result, distance, tuning.smoothingRange)
}
return result
}
/** Finite-difference gradient of the field at a point - points away from the surface, magnitude ~1 for a clean field. */
fun estimateGradient(primitives: List<FieldPrimitive>, point: Vec3, tuning: DistanceFieldTuning, epsilon: Float = 0.05f): Vec3 {
val px = point.x.toFloat()
val py = point.y.toFloat()
val pz = point.z.toFloat()
val dx = sampleField(primitives, px + epsilon, py, pz, tuning) - sampleField(primitives, px - epsilon, py, pz, tuning)
val dy = sampleField(primitives, px, py + epsilon, pz, tuning) - sampleField(primitives, px, py - epsilon, pz, tuning)
val dz = sampleField(primitives, px, py, pz + epsilon, tuning) - sampleField(primitives, px, py, pz - epsilon, tuning)
val gradient = Vec3(dx.toDouble(), dy.toDouble(), dz.toDouble())
return if (gradient.lengthSqr() > 1e-8) gradient.normalize() else Vec3(0.0, 1.0, 0.0)
}
@@ -0,0 +1,105 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.riding.util
import net.minecraft.core.particles.DustParticleOptions
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.Entity
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import org.joml.Vector3f
/** One shell to draw - a target field value, a color, a particle size. */
data class DistanceFieldLayer(
val isoValue: Float,
val color: Vector3f,
val particleSize: Float = 1.0f
)
/**
* Marches a grid of vertical columns through the field around [center] and spawns a
* particle everywhere it crosses one of [layers]' target values. Server-side only.
*/
fun visualizeDistanceField(
referenceEntity: Entity,
center: Vec3,
layers: List<DistanceFieldLayer>,
shape: PrimitiveShape = PrimitiveShape.BOX,
horizontalRadius: Double = 4.0,
verticalRange: Double = 4.0,
sampleSpacing: Double = 0.5,
marchStep: Double = 0.1,
tuning: DistanceFieldTuning = DistanceFieldTuning()
) {
val level = referenceEntity.level()
if (level !is ServerLevel) return
val searchBox = AABB(
center.x - horizontalRadius, center.y - verticalRange, center.z - horizontalRadius,
center.x + horizontalRadius, center.y, center.z + horizontalRadius
)
val primitives = collectPrimitivesFromBlocks(level, searchBox, shape, includeFluids = true)
if (primitives.isEmpty()) return
var x = -horizontalRadius
while (x <= horizontalRadius) {
var z = -horizontalRadius
while (z <= horizontalRadius) {
marchColumn(level, primitives, center, x, z, verticalRange, marchStep, layers, tuning)
z += sampleSpacing
}
x += sampleSpacing
}
}
private fun marchColumn(
level: ServerLevel,
primitives: List<FieldPrimitive>,
center: Vec3,
offsetX: Double,
offsetZ: Double,
verticalRange: Double,
marchStep: Double,
layers: List<DistanceFieldLayer>,
tuning: DistanceFieldTuning
) {
val previousValue = FloatArray(layers.size) { Float.NaN }
var y = verticalRange
while (y >= -verticalRange) {
val worldX = (center.x + offsetX).toFloat()
val worldY = (center.y + y).toFloat()
val worldZ = (center.z + offsetZ).toFloat()
val fieldValue = sampleField(primitives, worldX, worldY, worldZ, tuning)
for (layerIndex in layers.indices) {
val layer = layers[layerIndex]
val previous = previousValue[layerIndex]
if (!previous.isNaN() && crossedTarget(previous, fieldValue, layer.isoValue)) {
val t = (layer.isoValue - previous) / (fieldValue - previous)
val crossingY = (y + marchStep) - marchStep * t
spawnLayerParticle(level, center.add(offsetX, crossingY, offsetZ), layer)
}
previousValue[layerIndex] = fieldValue
}
y -= marchStep
}
}
private fun crossedTarget(previous: Float, current: Float, target: Float): Boolean =
(previous - target) * (current - target) < 0f
private fun spawnLayerParticle(level: ServerLevel, pos: Vec3, layer: DistanceFieldLayer) {
val particle = DustParticleOptions(layer.color, layer.particleSize)
level.sendParticles(particle, pos.x, pos.y, pos.z, 1, 0.0, 0.0, 0.0, 0.0)
}
@@ -25,7 +25,6 @@ import net.minecraft.world.level.block.SoundType
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityTicker
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.entity.CommandBlockEntity
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BooleanProperty
@@ -81,13 +80,11 @@ class HabitatBlock(properties: Properties) : Block(properties), EntityBlock {
}
override fun useWithoutItem(state: BlockState, level: Level, pos: BlockPos, player: Player, hitResult: BlockHitResult): InteractionResult {
val blockEntity = level.getBlockEntity(pos)
val be = level.getBlockEntity(pos) as? HabitatBlockEntity ?: return InteractionResult.PASS
return if (blockEntity is HabitatBlockEntity && player.canUseGameMasterBlocks()) {
be.onUse(player, hitResult)
} else {
InteractionResult.PASS
if (!level.isClientSide) {
val be = level.getBlockEntity(pos) as? HabitatBlockEntity ?: return InteractionResult.PASS
return be.onUse(player, hitResult)
}
return InteractionResult.SUCCESS
}
override fun getDrops(state: BlockState, params: LootParams.Builder): MutableList<ItemStack> {
@@ -182,6 +182,7 @@ class RideControlsOverlay : Gui(Minecraft.getInstance()) {
showHorizontalMouse = false
}
VehicleBehaviour.KEY -> {}
BoardBehaviour.KEY -> {}
}
val centerX = minecraft.window.guiScaledWidth / 2
@@ -41,7 +41,6 @@ class CobblemonConfigScreen(
const val WIDGET_HEIGHT = SLOT_HEIGHT - PADDING
const val LABEL_Y_OFFSET = 4
const val WIDGET_Y_OFFSET = 0
const val LABEL_MAX_WIDTH = SLOT_WIDTH - RESET_SLOT_WIDTH - WIDGET_WIDTH - PADDING * 2
}
val clonedConfig = Cobblemon.config.clone()
@@ -8,18 +8,17 @@
package com.cobblemon.mod.common.client.gui.config.variable
import com.cobblemon.mod.common.client.gui.config.CobblemonConfigScreen.Companion.LABEL_MAX_WIDTH
import com.cobblemon.mod.common.client.gui.config.CobblemonConfigScreen.Companion.LABEL_Y_OFFSET
import com.cobblemon.mod.common.client.gui.config.CobblemonConfigScreen.Companion.RESET_SLOT_WIDTH
import com.cobblemon.mod.common.client.gui.config.CobblemonConfigScreen.Companion.SLOT_WIDTH
import com.cobblemon.mod.common.client.gui.config.CobblemonConfigScreen.Companion.WIDGET_HEIGHT
import com.cobblemon.mod.common.client.gui.config.CobblemonConfigScreen.Companion.WIDGET_Y_OFFSET
import com.cobblemon.mod.common.client.gui.config.CobblemonConfigVariableList
import com.cobblemon.mod.common.client.gui.config.CobblemonConfigVariableList.CobblemonConfigVariableListEntry
import com.cobblemon.mod.common.client.render.drawScrollingText
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.config.CobblemonConfig
import com.cobblemon.mod.common.config.CobblemonConfigField
import com.cobblemon.mod.common.util.lang
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.Button
import net.minecraft.client.gui.components.Tooltip
@@ -92,15 +91,12 @@ abstract class CobblemonConfigVariable(
val x = getX(left)
val y = getY(top)
drawScrollingText(
context,
Minecraft.getInstance().font,
labelLang,
x,
y + WIDGET_Y_OFFSET,
x + LABEL_MAX_WIDTH,
y + WIDGET_Y_OFFSET + WIDGET_HEIGHT,
-1
drawScaledText(
context = context,
text = labelLang,
x = getX(left),
y = getY(top) + LABEL_Y_OFFSET,
shadow = true
)
resetButton.x = x + SLOT_WIDTH - RESET_SLOT_WIDTH
@@ -18,23 +18,17 @@ import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.blaze3d.vertex.VertexConsumer
import com.mojang.math.Axis
import net.minecraft.Util
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.Font
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.client.renderer.RenderType
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.client.renderer.texture.TextureAtlas
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.FormattedCharSequence
import net.minecraft.util.Mth
import net.minecraft.world.item.ItemDisplayContext
import net.minecraft.world.item.ItemStack
import kotlin.math.cos
import kotlin.math.sin
fun renderScaledGuiItemIcon(itemStack: ItemStack, x: Double, y: Double, scale: Double = 1.0, zTranslation: Float = 100.0F, matrixStack: PoseStack? = null) {
val itemRenderer = Minecraft.getInstance().itemRenderer
@@ -103,55 +97,6 @@ fun getDepletableRedGreen(
return r.toFloat() to g.toFloat()
}
fun drawScrollingText(
context: GuiGraphics,
font: Font,
text: Component,
minX: Int,
minY: Int,
maxX: Int,
maxY: Int,
colour: Int,
minScrollVelocity: Double? = 10.0
) {
val textWidth = font.width(text)
val fittedMaxX = if (textWidth <= maxX - minX) minX + textWidth else maxX
renderScrollingString(context, font, text, minX, minY, fittedMaxX, maxY, colour, minScrollVelocity)
}
/** Copied from [net.minecraft.client.gui.components.AbstractWidget.renderScrollingString] **/
fun renderScrollingString(
context: GuiGraphics,
font: Font,
text: Component,
minX: Int,
minY: Int,
maxX: Int,
maxY: Int,
colour: Int,
minScrollVelocity: Double? = 10.0
) {
val textWidth = font.width(text)
val centerY = (minY + maxY - 9) / 2 + 1
val boxWidth = maxX - minX
if (textWidth <= boxWidth) {
val centerX = Mth.clamp((minX + maxX) / 2, minX + textWidth / 2, maxX - textWidth / 2)
context.drawCenteredString(font, text, centerX, centerY, colour)
return
}
val overflow = textWidth - boxWidth
var period = (overflow * 0.5).coerceAtLeast(3.0)
if (minScrollVelocity != null) {
period = period.coerceAtMost(2 * overflow / minScrollVelocity)
}
val time = Util.getMillis() / 1000.0
val easedProgress = sin((Math.PI / 2) * cos(2 * Math.PI * time / period)) / 2.0 + 0.5
val offset = Mth.lerp(easedProgress, 0.0, overflow.toDouble())
context.enableScissor(minX, minY, maxX, maxY)
context.drawString(font, text, minX - offset.toInt(), centerY, colour)
context.disableScissor()
}
fun drawScaledText(
context: GuiGraphics,
@@ -34,6 +34,7 @@ import com.cobblemon.mod.common.client.render.models.blockbench.quirk.ModelQuirk
import com.cobblemon.mod.common.client.render.models.blockbench.quirk.SimpleQuirk
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
import com.cobblemon.mod.common.client.render.models.blockbench.wavefunction.WaveFunction
import com.cobblemon.mod.common.client.util.exists
import com.cobblemon.mod.common.entity.PosableEntity
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.generic.GenericBedrockEntity
@@ -255,7 +256,7 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
}
fun setLayerContext(buffer: MultiBufferSource, state: PosableState, layers: Iterable<ModelLayer>) {
currentLayers = layers.sortedBy { it.translucent }
currentLayers = layers
bufferProvider = buffer
currentState = state
}
@@ -316,6 +316,7 @@ object CobblemonItemGroups {
entries.accept(CobblemonItems.PASTURE)
entries.accept(CobblemonItems.TM_MACHINE)
entries.accept(CobblemonItems.DISC_SHELF)
entries.accept(CobblemonItems.HABITAT_BLOCK)
entries.accept(CobblemonItems.GILDED_CHEST)
entries.accept(CobblemonItems.YELLOW_GILDED_CHEST)
@@ -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,
/*
@@ -20,7 +20,6 @@ import net.minecraft.world.level.levelgen.RandomState
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate
import org.slf4j.LoggerFactory
class HeightRangeStructureProcessor(
val minOffset: Int,
@@ -44,14 +43,7 @@ class HeightRangeStructureProcessor(
val referenceY = reference.getY(chunkGenerator, heightAccessor, randomState, pos)
val minY = referenceY + minOffset
val maxY = referenceY + maxOffset
val valid = pos.y in minY..maxY
LOGGER.info(
"[HeightRangeVeto] isValidPlacement pos={} reference={} referenceY={} minY={} maxY={} pos.y={} -> valid={}",
pos, reference, referenceY, minY, maxY, pos.y, valid
)
return valid
return pos.y in minY..maxY
}
override fun processBlock(
@@ -113,8 +105,6 @@ class HeightRangeStructureProcessor(
}
companion object {
private val LOGGER = LoggerFactory.getLogger("Cobblemon/HeightRangeStructureProcessor")
val CODEC: MapCodec<HeightRangeStructureProcessor> = RecordCodecBuilder.mapCodec { instance ->
instance.group(
Codec.INT.fieldOf("min_offset").forGetter { it.minOffset },
@@ -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)"
}
@@ -52,7 +52,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,