Commit vor Änderung Umstellung auf eine einzelne Animation

This commit is contained in:
2026-06-28 22:59:36 +02:00
parent 6d061cd621
commit 63aa7aa104
9 changed files with 227 additions and 442 deletions

View File

@@ -1,27 +0,0 @@
package de.blight.game.animation;
/**
* Ein Keyframe für manuellen Positions-/Rotations-Versatz während einer blockierenden Animation.
*
* TX/TZ: Versatz in Charakter-lokalem Raum (TX=seitlich, TZ=vorwärts relativ zur Blickrichtung).
* TY: Versatz in Welt-Y (hoch/runter).
* RX/RY/RZ: Additiver Rotations-Versatz in Grad (Euler XYZ, relativ zur Startrotation).
*
* Keyframes einer Aktion werden nach {@code time} sortiert und linear interpoliert.
*/
public class AnimKeyframe {
public float time; // Sekunden seit Animations-Start
public float tx, ty, tz; // Positions-Versatz (Meter)
public float rx, ry, rz; // Rotations-Versatz (Grad)
public AnimKeyframe() {}
public AnimKeyframe(float time,
float tx, float ty, float tz,
float rx, float ry, float rz) {
this.time = time;
this.tx = tx; this.ty = ty; this.tz = tz;
this.rx = rx; this.ry = ry; this.rz = rz;
}
}

View File

@@ -26,8 +26,8 @@ public class AnimSet {
private Map<String, String> actionMap = new LinkedHashMap<>();
/** Zuletzt im Editor verwendeter Modell-Pfad (relativ zu Assets-Root). Wird beim Öffnen auto-geladen. */
private String previewModelPath = null;
/** Manueller Positions-/Rotations-Versatz: Aktion → sortierte Keyframe-Liste. */
private Map<String, List<AnimKeyframe>> motionKeyframes = new LinkedHashMap<>();
/** Manueller Positions-/Rotations-Versatz pro Clip-Name. */
private Map<String, AnimOffset> animOffsets = new LinkedHashMap<>();
public List<String> getClips() { return clips; }
public void setClips(List<String> clips) { this.clips = clips; }
@@ -35,11 +35,11 @@ public class AnimSet {
public void setActionMap(Map<String, String> actionMap) { this.actionMap = actionMap; }
public String getPreviewModelPath() { return previewModelPath; }
public void setPreviewModelPath(String previewModelPath) { this.previewModelPath = previewModelPath; }
public Map<String, List<AnimKeyframe>> getMotionKeyframes() {
return motionKeyframes != null ? motionKeyframes : new LinkedHashMap<>();
public Map<String, AnimOffset> getAnimOffsets() {
return animOffsets != null ? animOffsets : new LinkedHashMap<>();
}
public void setMotionKeyframes(Map<String, List<AnimKeyframe>> motionKeyframes) {
this.motionKeyframes = motionKeyframes;
public void setAnimOffsets(Map<String, AnimOffset> animOffsets) {
this.animOffsets = animOffsets;
}
/** Speichert dieses Set als {@code <setName>.animset.json} im Verzeichnis {@code setDir}. */

View File

@@ -1,226 +0,0 @@
package de.blight.game.animation;
import com.jme3.anim.Armature;
import com.jme3.anim.Joint;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 2-Bone-Foot-IK hält die Füße an ihrer World-Space-Position fest.
*
* Muss NACH AnimComposer und SkinningControl zur Spatial hinzugefügt werden,
* damit controlUpdate() nach der Animations-Anwendung läuft.
*/
public final class FootIKControl extends AbstractControl {
private static final Logger log = LoggerFactory.getLogger(FootIKControl.class);
private final Armature armature;
private final Joint leftThigh, leftCalf, leftFoot;
private final Joint rightThigh, rightCalf, rightFoot;
// IK-Ziele in World-Space; null = IK inaktiv
private Vector3f leftWorldTarget = null;
private Vector3f rightWorldTarget = null;
private static final float MIN_CORRECTION_DIST = 0.005f;
private int debugFramesLeft = 0;
public FootIKControl(Armature armature) {
this.armature = armature;
leftThigh = findJoint(armature, "L_Thigh", "LeftUpLeg", "mixamorig:LeftUpLeg");
leftCalf = findJoint(armature, "L_Calf", "LeftLeg", "mixamorig:LeftLeg");
leftFoot = findJoint(armature, "L_Foot", "LeftFoot", "mixamorig:LeftFoot");
rightThigh = findJoint(armature, "R_Thigh", "RightUpLeg", "mixamorig:RightUpLeg");
rightCalf = findJoint(armature, "R_Calf", "RightLeg", "mixamorig:RightLeg");
rightFoot = findJoint(armature, "R_Foot", "RightFoot", "mixamorig:RightFoot");
log.info("[FootIK] Init: L={} R={}",
leftFoot != null ? leftFoot.getName() : "n/a",
rightFoot != null ? rightFoot.getName() : "n/a");
}
/**
* Fixiert die Füße an ihrer aktuellen World-Space-Position.
* Muss aufgerufen werden NACHDEM die neue Animation gesetzt wurde,
* aber BEVOR der kfOffset die Armature verschoben hat.
*/
public void lockFeetAtCurrentWorldPos() {
Spatial sp = getSpatial();
if (sp == null || leftFoot == null || rightFoot == null) {
return;
}
leftWorldTarget = sp.localToWorld(
leftFoot.getModelTransform().getTranslation().clone(), new Vector3f());
rightWorldTarget = sp.localToWorld(
rightFoot.getModelTransform().getTranslation().clone(), new Vector3f());
debugFramesLeft = 10;
log.info("[FootIK] LockWorld: L={} R={}", fv(leftWorldTarget), fv(rightWorldTarget));
}
public void releaseFeet() {
leftWorldTarget = null;
rightWorldTarget = null;
debugFramesLeft = 0;
log.info("[FootIK] Freigegeben");
}
public boolean isActive() {
return leftWorldTarget != null || rightWorldTarget != null;
}
// ── Control-Update ────────────────────────────────────────────────────────
@Override
protected void controlUpdate(float tpf) {
if (leftWorldTarget == null && rightWorldTarget == null) {
return;
}
Spatial sp = getSpatial();
if (sp == null) {
return;
}
boolean dbg = debugFramesLeft > 0;
if (dbg) {
debugFramesLeft--;
}
if (leftWorldTarget != null && leftThigh != null && leftCalf != null && leftFoot != null) {
Vector3f modelTarget = sp.worldToLocal(leftWorldTarget, new Vector3f());
solveLeg(leftThigh, leftCalf, leftFoot, modelTarget, dbg, "L");
}
if (rightWorldTarget != null && rightThigh != null && rightCalf != null && rightFoot != null) {
Vector3f modelTarget = sp.worldToLocal(rightWorldTarget, new Vector3f());
solveLeg(rightThigh, rightCalf, rightFoot, modelTarget, dbg, "R");
}
armature.update();
}
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {}
// ── 2-Bone-IK-Solver ─────────────────────────────────────────────────────
private void solveLeg(Joint thigh, Joint calf, Joint foot,
Vector3f target, boolean dbg, String side) {
Vector3f A = thigh.getModelTransform().getTranslation().clone();
Vector3f B = calf.getModelTransform().getTranslation().clone();
Vector3f C = foot.getModelTransform().getTranslation().clone();
float L1 = A.distance(B);
float L2 = B.distance(C);
if (L1 < 0.0001f || L2 < 0.0001f) {
return;
}
float footErr = C.distance(target);
if (dbg) {
log.info("[FootIK][{}] A={} B={} C={} T={} err={} L1={} L2={}",
side, fv(A), fv(B), fv(C), fv(target),
String.format("%.4f", footErr),
String.format("%.3f", L1), String.format("%.3f", L2));
}
if (footErr < MIN_CORRECTION_DIST) {
return;
}
float d = A.distance(target);
d = FastMath.clamp(d, Math.abs(L1 - L2) + 0.0001f, L1 + L2 - 0.0001f);
float cosA = (L1 * L1 + d * d - L2 * L2) / (2f * L1 * d);
float angleA = FastMath.acos(FastMath.clamp(cosA, -1f, 1f));
Vector3f dirAT = target.subtract(A).normalizeLocal();
Vector3f kneeDir = B.subtract(A).normalizeLocal();
float proj = dirAT.dot(kneeDir);
Vector3f perp = kneeDir.subtract(dirAT.mult(proj));
if (perp.lengthSquared() < 0.0001f) {
perp = findPerp(dirAT);
}
perp.normalizeLocal();
Vector3f B_new = A.add(
dirAT.mult(L1 * FastMath.cos(angleA)).addLocal(
perp.mult(L1 * FastMath.sin(angleA)))
);
// Oberschenkel rotieren
Vector3f curThighDir = B.subtract(A).normalizeLocal();
Vector3f dstThighDir = B_new.subtract(A).normalizeLocal();
Quaternion thighArc = rotArc(curThighDir, dstThighDir);
Quaternion thighModelRot = thigh.getModelTransform().getRotation().clone();
Quaternion newThighModelRot = thighArc.mult(thighModelRot);
Joint thighParent = thigh.getParent();
Quaternion parentRot = thighParent != null
? thighParent.getModelTransform().getRotation() : new Quaternion();
thigh.setLocalRotation(parentRot.inverse().mult(newThighModelRot));
// Unterschenkel rotieren — Calf-Richtung NACH der Thigh-Rotation verwenden
Vector3f curCalfDirOrig = C.subtract(B).normalizeLocal();
Vector3f curCalfDirRotated = thighArc.mult(curCalfDirOrig);
Vector3f dstCalfDir = target.subtract(B_new).normalizeLocal();
Quaternion calfArc = rotArc(curCalfDirRotated, dstCalfDir);
Quaternion oldCalfLocalRot = calf.getLocalRotation().clone();
Quaternion actualCalfModel = newThighModelRot.mult(oldCalfLocalRot);
Quaternion newCalfModelRot = calfArc.mult(actualCalfModel);
calf.setLocalRotation(newThighModelRot.inverse().mult(newCalfModelRot));
if (dbg) {
float thighAngle = thighArc.toAngleAxis(new Vector3f()) * FastMath.RAD_TO_DEG;
float calfAngle = calfArc.toAngleAxis(new Vector3f()) * FastMath.RAD_TO_DEG;
log.info("[FootIK][{}] B_new={} thigh={}° calf={}°",
side, fv(B_new),
String.format("%.1f", thighAngle),
String.format("%.1f", calfAngle));
}
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private static Quaternion rotArc(Vector3f from, Vector3f to) {
float dot = FastMath.clamp(from.dot(to), -1f, 1f);
if (dot > 0.9999f) {
return new Quaternion();
}
if (dot < -0.9999f) {
return new Quaternion().fromAngleAxis(FastMath.PI, findPerp(from));
}
Vector3f axis = from.cross(to).normalizeLocal();
Quaternion q = new Quaternion();
q.fromAngleAxis(FastMath.acos(dot), axis);
return q;
}
private static Vector3f findPerp(Vector3f v) {
Vector3f p = v.cross(Vector3f.UNIT_X);
if (p.lengthSquared() < 0.0001f) {
p = v.cross(Vector3f.UNIT_Z);
}
return p.normalizeLocal();
}
private static Joint findJoint(Armature arm, String... names) {
for (String n : names) {
Joint j = arm.getJoint(n);
if (j != null) {
return j;
}
}
return null;
}
private static String fv(Vector3f v) {
return String.format("(%.3f,%.3f,%.3f)", v.x, v.y, v.z);
}
}

View File

@@ -9,7 +9,7 @@ import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Spatial;
import de.blight.game.animation.AnimKeyframe;
import de.blight.game.animation.AnimOffset;
import de.blight.game.animation.AnimSet;
import de.blight.game.animation.AnimationAction;
import de.blight.game.animation.AnimationLibrary;
@@ -23,7 +23,6 @@ import org.slf4j.LoggerFactory;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class PlayerInputControl {
@@ -58,20 +57,21 @@ public class PlayerInputControl {
private com.jme3.anim.SkinningControl skinningControl = null;
// ── Motion-Keyframe-Offsets ───────────────────────────────────────────────
private Map<String, List<AnimKeyframe>> motionKeyframes = new LinkedHashMap<>();
// ── Anim-Offsets ─────────────────────────────────────────────────────────
private Map<String, AnimOffset> animOffsets = new LinkedHashMap<>();
/** Basis-Local-Translation des Visual-Nodes; wird beim Laden des AnimSet einmalig gespeichert. */
private Vector3f visualBaseTranslation = new Vector3f();
private final Vector3f kfOffsetCurrent = new Vector3f();
private final Vector3f kfOffsetTarget = new Vector3f();
// Lineare Geschwindigkeit des KF-Offsets in m/s; 0 = kein aktiver Lerp
private float kfOffsetSpeed = 0f;
private final Vector3f animOffsetCurrent = new Vector3f();
private final Vector3f animOffsetTarget = new Vector3f();
private float animOffsetSpeed = 0f;
// ── Navigation ────────────────────────────────────────────────────────────
private CharacterNavigator navigator = null;
private de.blight.game.navigation.PathFinder navPathFinder = null;
private TerrainChunkState navTerrain = null;
private int jumpFrames = 0;
private int jumpFrames = 0;
private int groundGraceFrames = 0;
private double nextTransitionLength = 0.0;
private boolean pickupActive = false;
private float pickupRemaining = 0f;
@@ -146,11 +146,11 @@ public class PlayerInputControl {
}
try {
AnimSet as = AnimSet.load(assetRoot.resolve("animations/sets"), animSetName);
motionKeyframes = as.getMotionKeyframes();
log.info("[AnimCtx] {} Motion-KF-Einträge geladen.", motionKeyframes.size());
animOffsets = as.getAnimOffsets();
log.info("[AnimCtx] {} Anim-Offset-Einträge geladen.", animOffsets.size());
} catch (Exception e) {
log.warn("[AnimCtx] AnimSet-KF nicht ladbar: {}", e.getMessage());
motionKeyframes = new LinkedHashMap<>();
animOffsets = new LinkedHashMap<>();
}
// Navigator (neu) aufbauen sobald alle Abhängigkeiten bereit sind
if (navPathFinder != null && navTerrain != null && physicsChar != null && visual != null) {
@@ -219,7 +219,7 @@ public class PlayerInputControl {
duration = resolveClipLength(action, 1.5f);
}
blockingAnimActive = true;
blockingAnimRemaining = duration + (1f / 60f);
blockingAnimRemaining = duration;
blockingAnimTotal = duration;
blockingAnimCallback = onComplete;
autopilotDir = null;
@@ -310,7 +310,7 @@ public class PlayerInputControl {
forward = backward = left = right = false;
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
autopilotDir = null;
clearMotionKfOffset();
clearAnimOffset();
if (arriveRadius > 0f) {
navigator.navigateTo(target, speed, arriveRadius, onArrival, onFailed);
} else {
@@ -350,17 +350,17 @@ public class PlayerInputControl {
public void update(float tpf) {
if (physicsChar == null) return;
if (visual != null && kfOffsetSpeed > 0f) {
Vector3f delta = kfOffsetTarget.subtract(kfOffsetCurrent);
if (visual != null && animOffsetSpeed > 0f) {
Vector3f delta = animOffsetTarget.subtract(animOffsetCurrent);
float dist = delta.length();
float step = kfOffsetSpeed * tpf;
float step = animOffsetSpeed * tpf;
if (dist <= step) {
kfOffsetCurrent.set(kfOffsetTarget);
kfOffsetSpeed = 0f;
animOffsetCurrent.set(animOffsetTarget);
animOffsetSpeed = 0f;
} else {
kfOffsetCurrent.addLocal(delta.normalizeLocal().multLocal(step));
animOffsetCurrent.addLocal(delta.normalizeLocal().multLocal(step));
}
visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(kfOffsetCurrent));
visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent));
}
if (paused) {
@@ -485,9 +485,10 @@ public class PlayerInputControl {
// Animations-Auswahl
if (jumpFrames > 0) jumpFrames--;
if (groundGraceFrames > 0) groundGraceFrames--;
AnimationAction target;
if (jumpFrames > 0 || !physicsChar.onGround()) {
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
} else if (moving) {
target = walk ? AnimationAction.WALK
@@ -540,36 +541,66 @@ public class PlayerInputControl {
return null;
}
private void applyMotionKfOffset(String clip) {
private void applyAnimOffset(String clip) {
if (visual == null) return;
List<AnimKeyframe> kfs = (clip != null) ? motionKeyframes.get(clip) : null;
if (kfs == null || kfs.isEmpty()) { clearMotionKfOffset(); return; }
AnimKeyframe kf = kfs.get(0);
AnimOffset off = (clip != null) ? animOffsets.get(clip) : null;
if (off == null) { clearAnimOffset(); return; }
Quaternion facing = visual.getLocalRotation().clone();
Vector3f worldOffset = facing.mult(new Vector3f(kf.tx, kf.ty, kf.tz));
kfOffsetTarget.set(worldOffset);
if (kfOffsetCurrent.distanceSquared(worldOffset) > 1e-4f) {
kfOffsetSpeed = 5.0f;
Vector3f worldOffset = facing.mult(new Vector3f(off.tx, off.ty, off.tz));
animOffsetTarget.set(worldOffset);
if (animOffsetCurrent.distanceSquared(worldOffset) > 1e-4f) {
animOffsetSpeed = 5.0f;
}
log.info("[KF] Clip '{}' → Offset-Ziel ({},{},{})", clip, worldOffset.x, worldOffset.y, worldOffset.z);
log.info("[AnimOffset] Clip '{}' → Ziel ({},{},{})", clip, worldOffset.x, worldOffset.y, worldOffset.z);
}
public void clearKfOffset() {
log.info("[KF] clearKfOffset() → Lerp zu 0, current=({},{},{})", kfOffsetCurrent.x, kfOffsetCurrent.y, kfOffsetCurrent.z);
kfOffsetTarget.set(0, 0, 0);
kfOffsetSpeed = 5.0f;
public void clearAnimOffset() {
log.info("[AnimOffset] clearAnimOffset() → Lerp zu 0, current=({},{},{})", animOffsetCurrent.x, animOffsetCurrent.y, animOffsetCurrent.z);
animOffsetTarget.set(0, 0, 0);
animOffsetSpeed = 5.0f;
}
private void clearMotionKfOffset() {
clearKfOffset();
/**
* Setzt den Visual-Versatz sofort (kein Lerp), ohne den Physik-Körper zu bewegen.
* tx/ty/tz in charakter-lokalem Raum (tz = vorwärts/rückwärts in Blickrichtung).
*/
public void setAnimOffsetInstant(float tx, float ty, float tz) {
if (visual == null) return;
Quaternion facing = visual.getLocalRotation().clone();
Vector3f worldOffset = facing.mult(new Vector3f(tx, ty, tz));
animOffsetTarget.set(worldOffset);
animOffsetCurrent.set(worldOffset);
animOffsetSpeed = 0f;
visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent));
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). */
public void clearAnimOffsetInstant() {
animOffsetTarget.set(0, 0, 0);
animOffsetCurrent.set(0, 0, 0);
animOffsetSpeed = 0f;
if (visual != null) {
visual.setLocalTranslation(visualBaseTranslation.clone());
}
log.info("[AnimOffset] Instant-Clear");
}
/** Verhindert für {@code frames} Frames, dass die JUMP-Animation durch kurzes onGround=false nach Teleport ausgelöst wird. */
public void setGroundGrace(int frames) {
groundGraceFrames = frames;
}
/** Setzt eine einmalige Überblend-Zeit für den nächsten Animations-Wechsel (sanfter Skelett-Übergang). */
public void setNextAnimTransition(double seconds) {
nextTransitionLength = seconds;
}
/** Gibt die konfigurierte Bank-Approach-Distanz (|sitting.tz|) zurück; 0.25m als Fallback. */
public float getBenchApproachDist() {
List<AnimKeyframe> kfs = motionKeyframes.get("sitting");
if (kfs == null || kfs.isEmpty()) return 0.25f;
float tz = kfs.get(0).tz;
return Math.abs(tz) > 0.01f ? Math.abs(tz) : 0.25f;
AnimOffset off = animOffsets.get("sitting");
if (off == null) return 0.25f;
return Math.abs(off.tz) > 0.01f ? Math.abs(off.tz) : 0.25f;
}
private boolean tryPlay(String clip) {
@@ -577,6 +608,16 @@ public class PlayerInputControl {
log.info("[Anim] tryPlay('{}') → applyTo FAILED", clip);
return false;
}
// Optionale Überblend-Zeit (einmalig, wird nach Verwendung auf 0 zurückgesetzt)
double transLen = nextTransitionLength;
nextTransitionLength = 0.0;
if (transLen > 0.0) {
com.jme3.anim.tween.action.Action nextAction = animComposer.action(clip);
if (nextAction instanceof com.jme3.anim.tween.action.BlendableAction) {
((com.jme3.anim.tween.action.BlendableAction) nextAction).setTransitionLength(transLen);
log.info("[Anim] Transition {} → '{}' über {:.2f}s", runningClip, clip, transLen);
}
}
// Erst Action setzen, DANN SkinningControl aktivieren
// vermeidet 1 Frame in Bind-Pose × Armature-Rx90° = liegender Charakter.
com.jme3.anim.tween.action.Action action = animComposer.setCurrentAction(clip);
@@ -589,7 +630,7 @@ public class PlayerInputControl {
log.info("[Anim] SkinningControl aktiviert nach Action '{}'", clip);
}
runningClip = clip;
applyMotionKfOffset(clip);
applyAnimOffset(clip);
return true;
}
}

View File

@@ -9,6 +9,7 @@ import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;
import de.blight.common.PlacedModel;
import de.blight.common.PlacedModelIO;
import de.blight.common.model.Bed;
@@ -53,17 +54,20 @@ public class WorldInteractableState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(WorldInteractableState.class);
private static final float BENCH_RANGE = 5f;
private static final float BED_RANGE = 6f;
private static final float WALK_TIMEOUT = 12f;
private static final float BENCH_RANGE = 5f;
private static final float BED_RANGE = 6f;
private static final float WALK_TIMEOUT = 12f;
private static final float BENCH_SIT_MOVE_DIST = 0.5f;
// Nach dem Aufstehen: Bank-Physik erst re-enablen wenn Charakter weit genug weg
private static final float BENCH_REENABLE_DIST_SQ = 0.36f; // 0.6m Radius
private static final float BENCH_REENABLE_TIMEOUT = 8f;
private String benchPendingId = null;
private float benchPendingX = 0f;
private float benchPendingZ = 0f;
private float benchPendingTimer = 0f;
private String benchPendingId = null;
private float benchPendingX = 0f;
private float benchPendingZ = 0f;
private float benchPendingTimer = 0f;
/** Blickrichtung des Charakters beim Hinsetzen (weg von der Bank); für Positions-Versatz nach Anim. */
private Vector3f currentBenchSitDir = null;
// ── Abhängigkeiten ────────────────────────────────────────────────────────
@@ -264,6 +268,9 @@ public class WorldInteractableState extends BaseAppState {
InteractableEntry entry = entries.get(targetIdx);
float rotY = getSitFacingRotY(entry);
Vector3f sitDir = new Vector3f((float) Math.cos(rotY), 0f, (float) Math.sin(rotY));
if (isBench(entry)) {
currentBenchSitDir = sitDir.clone();
}
playerInput.requestTurn(sitDir, 0.35f, () -> startSitAnim(entry));
}
@@ -289,8 +296,20 @@ public class WorldInteractableState extends BaseAppState {
AnimationAction idleAction = isBench(entry) ? AnimationAction.SITTING : AnimationAction.LYING;
playerInput.requestAnimation(downAction, 0f, () -> {
if (!isBench(entry)) snapToSitPos(entry); // Bank: Physik bleibt am Approach-Punkt
if (isBench(entry) && currentBenchSitDir != null) {
Vector3f cur = physicsChar.getPhysicsLocation();
Vector3f delta = currentBenchSitDir.mult(-BENCH_SIT_MOVE_DIST);
physicsChar.setPhysicsLocation(cur.add(delta));
// Sofortiges Spatial-Update verhindert 1-Frame-Kamerazuckeln
Spatial phySpatial = physicsChar.getSpatial();
if (phySpatial != null) {
phySpatial.setLocalTranslation(phySpatial.getLocalTranslation().add(delta));
}
log.info("[WorldInteractable] Charakter 50cm zur Bank verschoben.");
}
if (!isBench(entry)) snapToSitPos(entry);
playerInput.lockInPlace();
if (isBench(entry)) playerInput.setNextAnimTransition(0.2);
playerInput.playLockedAnimation(idleAction);
phase = Phase.RESTING;
log.info("[WorldInteractable] Ruhezustand: {}", entry.type());
@@ -328,7 +347,21 @@ public class WorldInteractableState extends BaseAppState {
playerInput.requestAnimation(upAction, 0f, () -> {
if (isBench(entry)) {
playerInput.clearKfOffset();
// Nach stand_up_bench: Charakter 50cm von Bank wegbewegen
if (currentBenchSitDir != null) {
Vector3f cur = physicsChar.getPhysicsLocation();
Vector3f delta = currentBenchSitDir.mult(BENCH_SIT_MOVE_DIST);
physicsChar.setPhysicsLocation(cur.add(delta));
Spatial phySpatial = physicsChar.getSpatial();
if (phySpatial != null) {
phySpatial.setLocalTranslation(phySpatial.getLocalTranslation().add(delta));
}
currentBenchSitDir = null;
playerInput.setGroundGrace(4);
playerInput.setNextAnimTransition(0.2);
log.info("[WorldInteractable] Charakter 50cm von Bank wegbewegt.");
}
playerInput.clearAnimOffset();
benchPendingId = entry.interactableId();
benchPendingX = entry.worldX();
benchPendingZ = entry.worldZ();