@@ -64,8 +64,6 @@ public class PlayerInputControl {
// Bone-Namen nach Phase-0-Bone-Log eintragen:
// Bone-Namen nach Phase-0-Bone-Log eintragen:
private static final String BONE_LEFT_FOOT = " mixamorig:LeftFoot " ;
private static final String BONE_LEFT_FOOT = " mixamorig:LeftFoot " ;
private static final String BONE_RIGHT_FOOT = " mixamorig:RightFoot " ;
private static final String BONE_RIGHT_FOOT = " mixamorig:RightFoot " ;
// Y-Schwelle im Model-Space: Fuß gilt als am Boden wenn Y < FOOT_GROUND_Y
private static final float FOOT_GROUND_Y = 0 . 05f ;
/** Wassertiefe (m) ab der Laufen/Sprinten gesperrt ist und Watgeräusch spielt. */
/** Wassertiefe (m) ab der Laufen/Sprinten gesperrt ist und Watgeräusch spielt. */
public static final float WATER_WADING_DEPTH = 0 . 5f ;
public static final float WATER_WADING_DEPTH = 0 . 5f ;
@@ -75,16 +73,23 @@ public class PlayerInputControl {
private com . jme3 . audio . AudioNode wadingNode ;
private com . jme3 . audio . AudioNode wadingNode ;
/** Aktuell berechnete Wassertiefe – einmal pro Update() gesetzt, in pollFootsteps() gelesen. */
/** Aktuell berechnete Wassertiefe – einmal pro Update() gesetzt, in pollFootsteps() gelesen. */
private float currentWaterDepth = 0f ;
private float currentWaterDepth = 0f ;
private float lastLeftFootY = Float . MAX_VALUE ;
// Velocity-based footstep detection: fires at the local Y-minimum of each foot (no fixed threshold).
private float lastRigh tFootY = Float . MAX_VALUE ;
private float lastLef tFootY = Float . MAX_VALUE ;
private float lastRightFootY = Float . MAX_VALUE ;
private boolean leftFootDescending = false ;
private boolean rightFootDescending = false ;
private float stepCooldown = 0f ;
private static final float MIN_STEP_INTERVAL = 0 . 15f ;
// ── Anim-Offsets ─────────────────────────────────────────────────────────
// ── Anim-Offsets ─────────────────────────────────────────────────────────
private Map < String , AnimOffset > animOffsets = new LinkedHashMap < > ( ) ;
private Map < String , AnimOffset > animOffsets = new LinkedHashMap < > ( ) ;
/** Basis-Local-Translation des Visual-Nodes; wird beim Laden des AnimSet einmalig gespeichert. */
/** Basis-Local-Translation des Visual-Nodes; wird beim Laden des AnimSet einmalig gespeichert. */
private Vector3f visualBaseTranslation = new Vector3f ( ) ;
private Vector3f visualBaseTranslation = new Vector3f ( ) ;
private final Vector3f animOffsetCurrent = new Vector3f ( ) ;
private final Vector3f animOffsetCurrent = new Vector3f ( ) ;
private final Vector3f animOffsetFrom = new Vector3f ( ) ;
private final Vector3f animOffsetTarget = new Vector3f ( ) ;
private final Vector3f animOffsetTarget = new Vector3f ( ) ;
private float animOffsetSpeed = 0f ;
// Zeit-basierter Lerp über AnimPlayer.BLEND_DURATION – exakt wie im AnimPreviewState (Editor).
private float animOffsetElapsed = Float . MAX_VALUE ; // MAX_VALUE = stabil / kein Lerp aktiv
// ── Navigation ────────────────────────────────────────────────────────────
// ── Navigation ────────────────────────────────────────────────────────────
private CharacterNavigator navigator = null ;
private CharacterNavigator navigator = null ;
@@ -422,6 +427,7 @@ public class PlayerInputControl {
if ( physicsChar ! = null ) physicsChar . setWalkDirection ( Vector3f . ZERO ) ;
if ( physicsChar ! = null ) physicsChar . setWalkDirection ( Vector3f . ZERO ) ;
autopilotDir = null ;
autopilotDir = null ;
clearAnimOffset ( ) ;
clearAnimOffset ( ) ;
if ( animPlayer ! = null ) animPlayer . stop ( ) ;
if ( arriveRadius > 0f ) {
if ( arriveRadius > 0f ) {
navigator . navigateTo ( target , speed , arriveRadius , onArrival , onFailed ) ;
navigator . navigateTo ( target , speed , arriveRadius , onArrival , onFailed ) ;
} else {
} else {
@@ -466,16 +472,11 @@ public class PlayerInputControl {
runningClip = animPlayer . getCurrentClip ( ) ;
runningClip = animPlayer . getCurrentClip ( ) ;
}
}
if ( visual ! = null & & animOffsetSpe ed > 0f ) {
if ( visual ! = null & & animOffsetElaps ed < AnimPlayer . BLEND_DURATION ) {
Vector3f delta = animOffsetTarget . subtract ( animOffsetCurrent ) ;
animOffsetElapsed + = tpf ;
float dis t = delta . length ( ) ;
float t = Math . min ( 1f , animOffsetElapsed / AnimPlayer . BLEND_DURATION ) ;
float step = animOffsetSpeed * tpf ;
animOffsetCurrent . set ( animOffsetFrom ) ;
if ( dist < = step ) {
animOffsetCurrent . interpolateLocal ( animOffsetTarget , t ) ;
animOffsetCurrent . set ( animOffsetTarget ) ;
animOffsetSpeed = 0f ;
} else {
animOffsetCurrent . addLocal ( delta . normalizeLocal ( ) . multLocal ( step ) ) ;
}
visual . setLocalTranslation ( visualBaseTranslation . clone ( ) . addLocal ( animOffsetCurrent ) ) ;
visual . setLocalTranslation ( visualBaseTranslation . clone ( ) . addLocal ( animOffsetCurrent ) ) ;
}
}
@@ -626,7 +627,7 @@ public class PlayerInputControl {
currentAnim = target ;
currentAnim = target ;
}
}
pollFootsteps ( ) ;
pollFootsteps ( tpf ) ;
}
}
private void playAction ( AnimationAction action ) {
private void playAction ( AnimationAction action ) {
@@ -670,26 +671,31 @@ public class PlayerInputControl {
log . info ( " {} " , sb ) ;
log . info ( " {} " , sb ) ;
}
}
private void poll Footsteps ( ) {
private void reset FootstepState ( ) {
lastLeftFootY = Float . MAX_VALUE ;
lastRightFootY = Float . MAX_VALUE ;
leftFootDescending = false ;
rightFootDescending = false ;
stepCooldown = 0f ;
}
private void pollFootsteps ( float tpf ) {
if ( ! physicsChar . onGround ( ) | | blockingAnimActive | | lockedInPlace ) {
if ( ! physicsChar . onGround ( ) | | blockingAnimActive | | lockedInPlace ) {
stopWading ( ) ;
stopWading ( ) ;
lastLeftFootY = Flo at. MAX_VALUE ;
resetFootstepSt ate ( ) ;
lastRightFootY = Float . MAX_VALUE ;
return ;
return ;
}
}
float speed = physicsChar . getWalkDirection ( ) . length ( ) ;
float speed = physicsChar . getWalkDirection ( ) . length ( ) ;
if ( speed < 0 . 001f ) {
if ( speed < 0 . 001f ) {
stopWading ( ) ;
stopWading ( ) ;
lastLeftFootY = Flo at. MAX_VALUE ;
resetFootstepSt ate ( ) ;
lastRightFootY = Float . MAX_VALUE ;
return ;
return ;
}
}
if ( currentWaterDepth > WATER_WADING_DEPTH ) {
if ( currentWaterDepth > WATER_WADING_DEPTH ) {
// Im Wasser: Watgeräusch abspielen, Fußgeräusche unterdrücken
// Im Wasser: Watgeräusch abspielen, Fußgeräusche unterdrücken
startWading ( ) ;
startWading ( ) ;
lastLeftFootY = Flo at. MAX_VALUE ;
resetFootstepSt ate ( ) ;
lastRightFootY = Float . MAX_VALUE ;
return ;
return ;
}
}
@@ -699,20 +705,29 @@ public class PlayerInputControl {
String gait = currentAnimToGait ( currentAnim ) ;
String gait = currentAnimToGait ( currentAnim ) ;
if ( gait = = null ) return ;
if ( gait = = null ) return ;
stepCooldown = Math . max ( 0f , stepCooldown - tpf ) ;
com . jme3 . anim . Joint lf = armature . getJoint ( BONE_LEFT_FOOT ) ;
com . jme3 . anim . Joint lf = armature . getJoint ( BONE_LEFT_FOOT ) ;
com . jme3 . anim . Joint rf = armature . getJoint ( BONE_RIGHT_FOOT ) ;
com . jme3 . anim . Joint rf = armature . getJoint ( BONE_RIGHT_FOOT ) ;
if ( lf ! = null ) {
if ( lf ! = null ) {
float y = lf . getModelTransform ( ) . getTranslation ( ) . y ;
float y = lf . getModelTransform ( ) . getTranslation ( ) . y ;
if ( y < FOOT_GROUND_Y & & lastLeftFootY > = FOOT_GROUND_Y ) {
// Descend→ascend transition = local Y-minimum = foot touches ground
boolean desc = ( lastLeftFootY - y ) > 0 . 001f ;
if ( ! desc & & leftFootDescending & & stepCooldown < = 0f ) {
triggerStep ( gait ) ;
triggerStep ( gait ) ;
stepCooldown = MIN_STEP_INTERVAL ;
}
}
leftFootDescending = desc ;
lastLeftFootY = y ;
lastLeftFootY = y ;
}
}
if ( rf ! = null ) {
if ( rf ! = null ) {
float y = rf . getModelTransform ( ) . getTranslation ( ) . y ;
float y = rf . getModelTransform ( ) . getTranslation ( ) . y ;
if ( y < FOOT_GROUND_Y & & lastRightFootY > = FOOT_GROUND_Y ) {
boolean desc = ( lastRightFootY - y ) > 0 . 001f ;
if ( ! desc & & rightFootDescending & & stepCooldown < = 0f ) {
triggerStep ( gait ) ;
triggerStep ( gait ) ;
stepCooldown = MIN_STEP_INTERVAL ;
}
}
rightFootDescending = desc ;
lastRightFootY = y ;
lastRightFootY = y ;
}
}
}
}
@@ -770,9 +785,10 @@ public class PlayerInputControl {
if ( off = = null ) { clearAnimOffset ( ) ; return ; }
if ( off = = null ) { clearAnimOffset ( ) ; return ; }
Quaternion facing = visual . getLocalRotation ( ) . clone ( ) ;
Quaternion facing = visual . getLocalRotation ( ) . clone ( ) ;
Vector3f worldOffset = facing . mult ( new Vector3f ( off . tx , off . ty , off . tz ) ) ;
Vector3f worldOffset = facing . mult ( new Vector3f ( off . tx , off . ty , off . tz ) ) ;
animOffsetTarget . set ( worldOffset ) ;
if ( animOffsetCurrent . distanceSquared ( worldOffset ) > 1e - 4f ) {
if ( animOffsetCurrent . distanceSquared ( worldOffset ) > 1e - 4f ) {
animOffsetSpeed = 5 . 0f ;
animOffsetFrom . set ( animOffsetCurrent ) ;
animOffsetTarget . set ( worldOffset ) ;
animOffsetElapsed = 0f ;
}
}
log . debug ( " [AnimOffset] Clip '{}' → Ziel ({},{},{}) " , clip , worldOffset . x , worldOffset . y , worldOffset . z ) ;
log . debug ( " [AnimOffset] Clip '{}' → Ziel ({},{},{}) " , clip , worldOffset . x , worldOffset . y , worldOffset . z ) ;
}
}
@@ -790,9 +806,10 @@ public class PlayerInputControl {
if ( off = = null ) { clearAnimOffsetInstant ( ) ; return ; }
if ( off = = null ) { clearAnimOffsetInstant ( ) ; return ; }
Quaternion facing = visual . getLocalRotation ( ) . clone ( ) ;
Quaternion facing = visual . getLocalRotation ( ) . clone ( ) ;
Vector3f worldOffset = facing . mult ( new Vector3f ( off . tx , off . ty , off . tz ) ) ;
Vector3f worldOffset = facing . mult ( new Vector3f ( off . tx , off . ty , off . tz ) ) ;
animOffsetFrom . set ( worldOffset ) ;
animOffsetTarget . set ( worldOffset ) ;
animOffsetTarget . set ( worldOffset ) ;
animOffsetCurrent . set ( worldOffset ) ;
animOffsetCurrent . set ( worldOffset ) ;
animOffsetSpe ed = 0f ;
animOffsetElaps ed = Float . MAX_VALUE ;
visual . setLocalTranslation ( visualBaseTranslation . clone ( ) . addLocal ( animOffsetCurrent ) ) ;
visual . setLocalTranslation ( visualBaseTranslation . clone ( ) . addLocal ( animOffsetCurrent ) ) ;
log . info ( " [AnimOffset] Sofort: action={} clip='{}' → ({},{},{}) " , action , clip ,
log . info ( " [AnimOffset] Sofort: action={} clip='{}' → ({},{},{}) " , action , clip ,
worldOffset . x , worldOffset . y , worldOffset . z ) ;
worldOffset . x , worldOffset . y , worldOffset . z ) ;
@@ -800,8 +817,11 @@ public class PlayerInputControl {
public void clearAnimOffset ( ) {
public void clearAnimOffset ( ) {
log . info ( " [AnimOffset] clearAnimOffset() → Lerp zu 0, current=({},{},{}) " , animOffsetCurrent . x , animOffsetCurrent . y , animOffsetCurrent . z ) ;
log . info ( " [AnimOffset] clearAnimOffset() → Lerp zu 0, current=({},{},{}) " , animOffsetCurrent . x , animOffsetCurrent . y , animOffsetCurrent . z ) ;
animOffsetTarget . set ( 0 , 0 , 0 ) ;
if ( animOffsetCurrent . lengthSquared ( ) > 1e - 4f ) {
animOffsetSpeed = 5 . 0f ;
animOffsetFrom . set ( animOffsetCurrent ) ;
animOffsetTarget . set ( 0 , 0 , 0 ) ;
animOffsetElapsed = 0f ;
}
}
}
/**
/**
@@ -812,18 +832,20 @@ public class PlayerInputControl {
if ( visual = = null ) return ;
if ( visual = = null ) return ;
Quaternion facing = visual . getLocalRotation ( ) . clone ( ) ;
Quaternion facing = visual . getLocalRotation ( ) . clone ( ) ;
Vector3f worldOffset = facing . mult ( new Vector3f ( tx , ty , tz ) ) ;
Vector3f worldOffset = facing . mult ( new Vector3f ( tx , ty , tz ) ) ;
animOffsetFrom . set ( worldOffset ) ;
animOffsetTarget . set ( worldOffset ) ;
animOffsetTarget . set ( worldOffset ) ;
animOffsetCurrent . set ( worldOffset ) ;
animOffsetCurrent . set ( worldOffset ) ;
animOffsetSpe ed = 0f ;
animOffsetElaps ed = Float . MAX_VALUE ;
visual . setLocalTranslation ( visualBaseTranslation . clone ( ) . addLocal ( animOffsetCurrent ) ) ;
visual . setLocalTranslation ( visualBaseTranslation . clone ( ) . addLocal ( animOffsetCurrent ) ) ;
log . info ( " [AnimOffset] Instant: ({},{},{}) → world ({},{},{}) " , tx , ty , tz , worldOffset . x , worldOffset . y , worldOffset . z ) ;
log . info ( " [AnimOffset] Instant: ({},{},{}) → world ({},{},{}) " , tx , ty , tz , worldOffset . x , worldOffset . y , worldOffset . z ) ;
}
}
/** Setzt den Visual-Versatz sofort auf 0 zurück (kein Lerp). */
/** Setzt den Visual-Versatz sofort auf 0 zurück (kein Lerp). */
public void clearAnimOffsetInstant ( ) {
public void clearAnimOffsetInstant ( ) {
animOffsetFrom . set ( 0 , 0 , 0 ) ;
animOffsetTarget . set ( 0 , 0 , 0 ) ;
animOffsetTarget . set ( 0 , 0 , 0 ) ;
animOffsetCurrent . set ( 0 , 0 , 0 ) ;
animOffsetCurrent . set ( 0 , 0 , 0 ) ;
animOffsetSpe ed = 0f ;
animOffsetElaps ed = Float . MAX_VALUE ;
if ( visual ! = null ) {
if ( visual ! = null ) {
visual . setLocalTranslation ( visualBaseTranslation . clone ( ) ) ;
visual . setLocalTranslation ( visualBaseTranslation . clone ( ) ) ;
}
}