Compare commits
3 Commits
ba0b80f524
...
6d061cd621
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d061cd621 | |||
| cd350a92fa | |||
| b44d583dc3 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"clips": [
|
||||
"get_up_sitting",
|
||||
"alive_again",
|
||||
"idle",
|
||||
"idle_jump",
|
||||
"pickup",
|
||||
"running",
|
||||
"running_jump",
|
||||
"sitting",
|
||||
"sit_down_bench",
|
||||
"sitting_bench",
|
||||
"sitting_floor",
|
||||
"sprinting",
|
||||
"stand_up",
|
||||
"stand_up_bench",
|
||||
"tpose",
|
||||
"walking",
|
||||
"sit_down_bench"
|
||||
"walking"
|
||||
],
|
||||
"actionMap": {
|
||||
"DEFAULT": "tpose",
|
||||
@@ -20,32 +21,33 @@
|
||||
"WALK": "walking",
|
||||
"RUN": "running",
|
||||
"SPRINT": "sprinting",
|
||||
"RUNNING_JUMP": "running_jump",
|
||||
"JUMP": "idle_jump",
|
||||
"RUNNING_JUMP": "running_jump",
|
||||
"PICK_UP": "pickup",
|
||||
"SITTING": "sitting",
|
||||
"SIT_DOWN": "sit_down_bench",
|
||||
"SIT_UP": "get_up_sitting"
|
||||
"SIT_UP": "stand_up_bench",
|
||||
"SITTING": "sitting_bench",
|
||||
"REVIVE": "alive_again"
|
||||
},
|
||||
"previewModelPath": "Models/Chars/mainchar.j3o",
|
||||
"motionKeyframes": {
|
||||
"get_up_sitting": [
|
||||
{
|
||||
"time": 0.0,
|
||||
"tx": 0.0,
|
||||
"ty": 0.0,
|
||||
"tz": -0.25,
|
||||
"rx": 0.0,
|
||||
"ry": 0.0,
|
||||
"rz": 0.0
|
||||
}
|
||||
],
|
||||
"sitting": [
|
||||
{
|
||||
"time": 0.0,
|
||||
"tx": 0.0,
|
||||
"ty": 0.0,
|
||||
"tz": -0.25,
|
||||
"tz": -0.5,
|
||||
"rx": 0.0,
|
||||
"ry": 0.0,
|
||||
"rz": 0.0
|
||||
}
|
||||
],
|
||||
"get_up_sitting": [
|
||||
{
|
||||
"time": 0.0,
|
||||
"tx": 0.0,
|
||||
"ty": 0.0,
|
||||
"tz": -0.5,
|
||||
"rx": 0.0,
|
||||
"ry": 0.0,
|
||||
"rz": 0.0
|
||||
|
||||
@@ -603,6 +603,17 @@ public class EditorApp extends Application {
|
||||
updateSpawnFields(input.pickedSpawnInfo);
|
||||
}
|
||||
|
||||
// Modell-Editor: gebakte Scale aus j3o erkannt → Spinner aktualisieren
|
||||
if (input.modelEditorBakedScaleDetected) {
|
||||
input.modelEditorBakedScaleDetected = false;
|
||||
double bsX = input.modelEditorScaleX;
|
||||
double bsY = input.modelEditorScaleY;
|
||||
double bsZ = input.modelEditorScaleZ;
|
||||
if (modelEditorSpinX != null) modelEditorSpinX.getValueFactory().setValue(bsX);
|
||||
if (modelEditorSpinY != null) modelEditorSpinY.getValueFactory().setValue(bsY);
|
||||
if (modelEditorSpinZ != null) modelEditorSpinZ.getValueFactory().setValue(bsZ);
|
||||
}
|
||||
|
||||
// Modell-Editor: Bounds-Aktualisierung
|
||||
if (input.modelEditorBoundsReady) {
|
||||
input.modelEditorBoundsReady = false;
|
||||
@@ -5999,6 +6010,8 @@ public class EditorApp extends Application {
|
||||
input.modelInteractableOffsetY = meta.interactableOffsetY();
|
||||
input.modelInteractableOffsetZ = meta.interactableOffsetZ();
|
||||
input.modelInteractableRotY = meta.interactableRotY();
|
||||
input.modelInteractableActive = meta.interactableType() == de.blight.common.model.InteractableType.BED
|
||||
|| meta.interactableType() == de.blight.common.model.InteractableType.BENCH;
|
||||
input.modelInteractableOffsetChanged = true;
|
||||
|
||||
Label restPosHint = new Label("Klicke auf das Modell um den Ruhepunkt zu setzen:");
|
||||
@@ -6116,8 +6129,9 @@ public class EditorApp extends Application {
|
||||
|| nv == de.blight.common.model.InteractableType.BENCH;
|
||||
restPointBox.setVisible(show);
|
||||
restPointBox.setManaged(show);
|
||||
// Pfeil ein-/ausblenden — über SharedInput-Flag signalisieren
|
||||
input.modelInteractableOffsetChanged = show;
|
||||
// Pfeil ein-/ausblenden — immer rebuilden, damit Sichtbarkeit aktualisiert wird
|
||||
input.modelInteractableActive = show;
|
||||
input.modelInteractableOffsetChanged = true;
|
||||
});
|
||||
|
||||
// ── Buttons ───────────────────────────────────────────────────────────
|
||||
@@ -6654,8 +6668,9 @@ public class EditorApp extends Application {
|
||||
de.blight.common.model.InteractableType interactableType,
|
||||
float interactableOffsetX, float interactableOffsetY,
|
||||
float interactableOffsetZ, float interactableRotY) {
|
||||
// Scale wird in j3o eingebrannt → Meta bekommt immer 1.0 (kein doppelter Scale beim Laden)
|
||||
de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta(
|
||||
name, category, tags, sx, sy, sz, uniform,
|
||||
name, category, tags, 1f, 1f, 1f, uniform,
|
||||
pivotY, placeY, solid, cast, receive, rndMin, rndMax,
|
||||
lod1Path, lod2Path, 30f, 80f, 120f,
|
||||
lights, emitters,
|
||||
@@ -6712,12 +6727,22 @@ public class EditorApp extends Application {
|
||||
// Asset-Tree aktualisieren
|
||||
input.refreshAssets = true;
|
||||
|
||||
// Thumbnail generieren (JME3-Thread liest das Flag und rendert)
|
||||
java.nio.file.Path finalJ3o = category.isEmpty() ? absolutePath
|
||||
: ASSET_ROOT.resolve("Models")
|
||||
.resolve(java.nio.file.Path.of(category.replace('/', java.io.File.separatorChar)))
|
||||
.resolve(name.isEmpty() ? absolutePath.getFileName().toString()
|
||||
: name.replaceAll("[\\\\/:*?\"<>|]", "_") + ".j3o");
|
||||
|
||||
// Scale in j3o einbrennen (JME3-Thread) – muss vor Thumbnail passieren
|
||||
if (sx != 1f || sy != 1f || sz != 1f) {
|
||||
input.modelEditorScaleX = sx;
|
||||
input.modelEditorScaleY = sy;
|
||||
input.modelEditorScaleZ = sz;
|
||||
input.modelEditorBakeScalePath = finalJ3o;
|
||||
input.modelEditorBakeScaleRequest = true;
|
||||
}
|
||||
|
||||
// Thumbnail generieren (JME3-Thread liest das Flag und rendert)
|
||||
input.modelEditorThumbnailRequest = finalJ3o;
|
||||
}
|
||||
|
||||
|
||||
@@ -674,6 +674,13 @@ public class SharedInput {
|
||||
*/
|
||||
public volatile java.nio.file.Path modelEditorThumbnailRequest = null;
|
||||
|
||||
/** JFX → JME: Scale in j3o einbrennen (für animierte Modelle als Spatial-Transform, sonst Vertex-Bake). */
|
||||
public volatile boolean modelEditorBakeScaleRequest = false;
|
||||
public volatile java.nio.file.Path modelEditorBakeScalePath = null;
|
||||
|
||||
/** JME → JFX: j3o hatte eine gebakte Scale, die von der Meta abwich – Spinner aktualisieren. */
|
||||
public volatile boolean modelEditorBakedScaleDetected = false;
|
||||
|
||||
/** JME → JFX: true wenn das geladene Modell eingebettete LOD-Kinder hat (kein separater Pfad nötig). */
|
||||
public volatile boolean modelEditorHasEmbeddedLods = false;
|
||||
|
||||
@@ -882,6 +889,8 @@ public class SharedInput {
|
||||
public volatile float modelInteractableOffsetZ = 0f;
|
||||
public volatile float modelInteractableRotY = 0f;
|
||||
public volatile boolean modelInteractableOffsetChanged = false;
|
||||
/** true wenn Interactable-Typ aktiv (BED/BENCH) – steuert Pfeil-Sichtbarkeit. */
|
||||
public volatile boolean modelInteractableActive = false;
|
||||
/** Gesetzt vom JME-Thread nach Raycast-Klick, damit JFX-Spinner aktualisiert werden. */
|
||||
public volatile boolean modelInteractablePosSetFromJme = false;
|
||||
|
||||
|
||||
@@ -225,10 +225,9 @@ public class AnimPreviewState extends BaseAppState {
|
||||
previewTarget.z + FastMath.cos(rotY) * FastMath.cos(rotX) * dist));
|
||||
c.lookAt(previewTarget, Vector3f.UNIT_Y);
|
||||
|
||||
// Achsen: Größe proportional zur Kameradistanz, immer am Weltpunkt (0,0,0)
|
||||
// Achsen: feste 1m Weltlänge (Schaft = 0.5 lokal → Scale 2.0)
|
||||
if (axesNode != null) {
|
||||
float s = previewCamDist * input.animPreviewZoom * 0.18f;
|
||||
axesNode.setLocalScale(s);
|
||||
axesNode.setLocalScale(2.0f);
|
||||
axesNode.setLocalTranslation(Vector3f.ZERO);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.jme3.scene.shape.Box;
|
||||
import com.jme3.scene.shape.Cylinder;
|
||||
import com.jme3.scene.shape.Dome;
|
||||
import com.jme3.scene.shape.Sphere;
|
||||
import com.jme3.anim.SkinningControl;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.editor.SharedInput;
|
||||
import org.slf4j.Logger;
|
||||
@@ -219,6 +220,18 @@ public class ModelEditorState extends BaseAppState {
|
||||
input.modelEditorAttachedEmitters);
|
||||
}
|
||||
|
||||
// Scale in j3o einbrennen (vor Thumbnail, damit Thumbnail die gebackene Datei erhält)
|
||||
if (input.modelEditorBakeScaleRequest) {
|
||||
input.modelEditorBakeScaleRequest = false;
|
||||
Path bakePath = input.modelEditorBakeScalePath;
|
||||
if (bakePath != null) {
|
||||
bakeScaleIntoModel(bakePath,
|
||||
input.modelEditorScaleX,
|
||||
input.modelEditorScaleY,
|
||||
input.modelEditorScaleZ);
|
||||
}
|
||||
}
|
||||
|
||||
// Thumbnail auf Anforderung generieren
|
||||
Path thumbReq = input.modelEditorThumbnailRequest;
|
||||
if (thumbReq != null && modelWrapper != null) {
|
||||
@@ -350,6 +363,18 @@ public class ModelEditorState extends BaseAppState {
|
||||
modelWrapper.attachChild(box);
|
||||
}
|
||||
|
||||
// Gebakte Spatial-Scale erkennen: j3o hat explizit gesetzte Scale, Meta sagt 1.0
|
||||
// → Scale aus j3o übernehmen und JavaFX-Spinner aktualisieren lassen
|
||||
com.jme3.math.Vector3f jScale = modelWrapper.getChildren().isEmpty()
|
||||
? com.jme3.math.Vector3f.UNIT_XYZ
|
||||
: modelWrapper.getChild(0).getLocalScale();
|
||||
if (Math.abs(jScale.y - 1f) > 0.001f && Math.abs(input.modelEditorScaleY - 1f) < 0.001f) {
|
||||
input.modelEditorScaleX = jScale.x;
|
||||
input.modelEditorScaleY = jScale.y;
|
||||
input.modelEditorScaleZ = jScale.z;
|
||||
input.modelEditorBakedScaleDetected = true;
|
||||
}
|
||||
|
||||
// Skalierung aus SharedInput anwenden
|
||||
applyScale(input.modelEditorScaleX, input.modelEditorScaleY, input.modelEditorScaleZ);
|
||||
applyPivot(input.modelEditorPivotY);
|
||||
@@ -767,6 +792,48 @@ public class ModelEditorState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Scale-Bake ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Brennt den Scale (sx,sy,sz) in die j3o-Datei ein.
|
||||
* Animierte Modelle (SkinningControl vorhanden): Scale als Spatial-Transform gespeichert.
|
||||
* Statische Modelle: Scale in Vertex-Positionen gebacken (wie ModelImportState beim Import).
|
||||
*/
|
||||
private void bakeScaleIntoModel(Path j3oPath, float sx, float sy, float sz) {
|
||||
try {
|
||||
BinaryImporter importer = BinaryImporter.getInstance();
|
||||
importer.setAssetManager(app.getAssetManager());
|
||||
Savable savable = importer.load(j3oPath.toFile());
|
||||
if (!(savable instanceof Spatial root)) {
|
||||
log.warn("[ModelEditor] Bake: kein Spatial in {}", j3oPath.getFileName());
|
||||
return;
|
||||
}
|
||||
if (hasSkinningControl(root)) {
|
||||
root.setLocalScale(sx, sy, sz);
|
||||
log.info("[ModelEditor] Animiert: Scale ({},{},{}) als Spatial-Transform gespeichert", sx, sy, sz);
|
||||
} else {
|
||||
root.setLocalScale(sx, sy, sz);
|
||||
ModelImportState.stripControls(root);
|
||||
ModelImportState.bakeTransform(root, new Matrix4f());
|
||||
log.info("[ModelEditor] Statisch: Scale ({},{},{}) in Vertices gebacken", sx, sy, sz);
|
||||
}
|
||||
BinaryExporter.getInstance().save(root, j3oPath.toFile());
|
||||
log.info("[ModelEditor] j3o nach Bake gespeichert: {}", j3oPath.getFileName());
|
||||
} catch (Exception e) {
|
||||
log.error("[ModelEditor] Scale-Bake fehlgeschlagen: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasSkinningControl(Spatial s) {
|
||||
if (s.getControl(SkinningControl.class) != null) return true;
|
||||
if (s instanceof Node n) {
|
||||
for (Spatial c : n.getChildren()) {
|
||||
if (hasSkinningControl(c)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Thumbnail ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void generateThumbnail(Path j3oPath) {
|
||||
@@ -842,7 +909,8 @@ public class ModelEditorState extends BaseAppState {
|
||||
group.attachChild(shaft);
|
||||
group.attachChild(head);
|
||||
interactableArrowNode.attachChild(group);
|
||||
interactableArrowNode.setCullHint(Spatial.CullHint.Inherit);
|
||||
interactableArrowNode.setCullHint(
|
||||
input.modelInteractableActive ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
}
|
||||
|
||||
/** Setzt den Pfeil sichtbar/unsichtbar. */
|
||||
|
||||
@@ -619,7 +619,7 @@ public class ModelImportState extends BaseAppState {
|
||||
bakeTransform(s, new Matrix4f());
|
||||
}
|
||||
|
||||
private static void bakeTransform(Spatial s, Matrix4f accum) {
|
||||
static void bakeTransform(Spatial s, Matrix4f accum) {
|
||||
Matrix4f localMat = new Matrix4f();
|
||||
s.getLocalTransform().toTransformMatrix(localMat);
|
||||
Matrix4f combined = accum.mult(localMat);
|
||||
@@ -637,7 +637,7 @@ public class ModelImportState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyMatrixToMesh(Geometry g, Matrix4f mat) {
|
||||
static void applyMatrixToMesh(Geometry g, Matrix4f mat) {
|
||||
Mesh newMesh = g.getMesh().deepClone();
|
||||
|
||||
FloatBuffer pos = newMesh.getFloatBuffer(VertexBuffer.Type.Position);
|
||||
@@ -673,7 +673,7 @@ public class ModelImportState extends BaseAppState {
|
||||
g.setMesh(newMesh);
|
||||
}
|
||||
|
||||
private static Matrix3f buildNormalMatrix(Matrix4f mat) {
|
||||
static Matrix3f buildNormalMatrix(Matrix4f mat) {
|
||||
Matrix3f m3 = new Matrix3f(
|
||||
mat.m00, mat.m01, mat.m02,
|
||||
mat.m10, mat.m11, mat.m12,
|
||||
@@ -683,7 +683,7 @@ public class ModelImportState extends BaseAppState {
|
||||
return m3;
|
||||
}
|
||||
|
||||
private static void stripControls(Spatial s) {
|
||||
static void stripControls(Spatial s) {
|
||||
while (s.getNumControls() > 0) s.removeControl(s.getControl(0));
|
||||
if (s instanceof Node n) n.getChildren().forEach(ModelImportState::stripControls);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<logger name="com.jme3.util.TangentBinormalGenerator" level="ERROR"/>
|
||||
<!-- Material warnt bei linear-color-space Texturen ohne passenden Parameter – bekannt, kein Fehler -->
|
||||
<logger name="com.jme3.material.Material" level="ERROR"/>
|
||||
<logger name="de.blight.game.animation.FootIKControl" level="DEBUG"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package de.blight.game.animation;
|
||||
|
||||
/**
|
||||
* Positions-/Rotations-Versatz für eine blockierende 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).
|
||||
*/
|
||||
public class AnimOffset {
|
||||
|
||||
public float tx, ty, tz; // Positions-Versatz (Meter)
|
||||
public float rx, ry, rz; // Rotations-Versatz (Grad)
|
||||
|
||||
public AnimOffset() {}
|
||||
|
||||
public AnimOffset(float tx, float ty, float tz,
|
||||
float rx, float ry, float rz) {
|
||||
this.tx = tx; this.ty = ty; this.tz = tz;
|
||||
this.rx = rx; this.ry = ry; this.rz = rz;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ 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.AnimSet;
|
||||
import de.blight.game.animation.AnimationAction;
|
||||
import de.blight.game.animation.AnimationLibrary;
|
||||
import de.blight.game.animation.RetargetingSystem;
|
||||
@@ -20,6 +22,9 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class PlayerInputControl {
|
||||
|
||||
@@ -53,6 +58,15 @@ public class PlayerInputControl {
|
||||
|
||||
private com.jme3.anim.SkinningControl skinningControl = null;
|
||||
|
||||
// ── Motion-Keyframe-Offsets ───────────────────────────────────────────────
|
||||
private Map<String, List<AnimKeyframe>> motionKeyframes = 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;
|
||||
|
||||
// ── Navigation ────────────────────────────────────────────────────────────
|
||||
private CharacterNavigator navigator = null;
|
||||
private de.blight.game.navigation.PathFinder navPathFinder = null;
|
||||
@@ -127,6 +141,17 @@ public class PlayerInputControl {
|
||||
log.info("[AnimCtx] AnimComposer gefunden: {}", animComposer != null);
|
||||
skinningControl = findSkinningControl(visual);
|
||||
log.info("[AnimCtx] SkinningControl gefunden: {}", skinningControl != null);
|
||||
if (visual != null) {
|
||||
visualBaseTranslation = visual.getLocalTranslation().clone();
|
||||
}
|
||||
try {
|
||||
AnimSet as = AnimSet.load(assetRoot.resolve("animations/sets"), animSetName);
|
||||
motionKeyframes = as.getMotionKeyframes();
|
||||
log.info("[AnimCtx] {} Motion-KF-Einträge geladen.", motionKeyframes.size());
|
||||
} catch (Exception e) {
|
||||
log.warn("[AnimCtx] AnimSet-KF nicht ladbar: {}", e.getMessage());
|
||||
motionKeyframes = new LinkedHashMap<>();
|
||||
}
|
||||
// Navigator (neu) aufbauen sobald alle Abhängigkeiten bereit sind
|
||||
if (navPathFinder != null && navTerrain != null && physicsChar != null && visual != null) {
|
||||
navigator = new CharacterNavigator(physicsChar, visual, navPathFinder, navTerrain);
|
||||
@@ -194,7 +219,7 @@ public class PlayerInputControl {
|
||||
duration = resolveClipLength(action, 1.5f);
|
||||
}
|
||||
blockingAnimActive = true;
|
||||
blockingAnimRemaining = duration;
|
||||
blockingAnimRemaining = duration + (1f / 60f);
|
||||
blockingAnimTotal = duration;
|
||||
blockingAnimCallback = onComplete;
|
||||
autopilotDir = null;
|
||||
@@ -268,6 +293,15 @@ public class PlayerInputControl {
|
||||
*/
|
||||
public void navigateTo(Vector3f target, CharacterNavigator.Speed speed,
|
||||
Runnable onArrival, Runnable onFailed) {
|
||||
navigateTo(target, speed, -1f, onArrival, onFailed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wie {@link #navigateTo} mit explizitem Ankunftsradius (Meter).
|
||||
* Werte <= 0 verwenden den Navigator-Standard (0.45m).
|
||||
*/
|
||||
public void navigateTo(Vector3f target, CharacterNavigator.Speed speed, float arriveRadius,
|
||||
Runnable onArrival, Runnable onFailed) {
|
||||
if (navigator == null) {
|
||||
log.warn("[Nav] navigateTo: kein CharacterNavigator – setNavigationSources() vorher aufrufen.");
|
||||
if (onFailed != null) onFailed.run();
|
||||
@@ -276,7 +310,12 @@ public class PlayerInputControl {
|
||||
forward = backward = left = right = false;
|
||||
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
autopilotDir = null;
|
||||
navigator.navigateTo(target, speed, onArrival, onFailed);
|
||||
clearMotionKfOffset();
|
||||
if (arriveRadius > 0f) {
|
||||
navigator.navigateTo(target, speed, arriveRadius, onArrival, onFailed);
|
||||
} else {
|
||||
navigator.navigateTo(target, speed, onArrival, onFailed);
|
||||
}
|
||||
}
|
||||
|
||||
/** Bricht eine laufende Navigation ab (kein Callback). */
|
||||
@@ -291,8 +330,6 @@ public class PlayerInputControl {
|
||||
/**
|
||||
* Spielt eine Animations-Aktion als Dauer-Loop während des Ruhezustands.
|
||||
* Nur sinnvoll nach {@link #lockInPlace()}.
|
||||
* Hat die Aktion Motion Keyframes, wird der erste Keyframe (time=0) als statischer
|
||||
* Versatz auf den Visual-Node angewendet.
|
||||
*/
|
||||
public void playLockedAnimation(AnimationAction action) {
|
||||
playAction(action);
|
||||
@@ -313,6 +350,19 @@ public class PlayerInputControl {
|
||||
public void update(float tpf) {
|
||||
if (physicsChar == null) return;
|
||||
|
||||
if (visual != null && kfOffsetSpeed > 0f) {
|
||||
Vector3f delta = kfOffsetTarget.subtract(kfOffsetCurrent);
|
||||
float dist = delta.length();
|
||||
float step = kfOffsetSpeed * tpf;
|
||||
if (dist <= step) {
|
||||
kfOffsetCurrent.set(kfOffsetTarget);
|
||||
kfOffsetSpeed = 0f;
|
||||
} else {
|
||||
kfOffsetCurrent.addLocal(delta.normalizeLocal().multLocal(step));
|
||||
}
|
||||
visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(kfOffsetCurrent));
|
||||
}
|
||||
|
||||
if (paused) {
|
||||
if (autopilotDir != null) {
|
||||
autopilotDir = null;
|
||||
@@ -490,6 +540,38 @@ public class PlayerInputControl {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void applyMotionKfOffset(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);
|
||||
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;
|
||||
}
|
||||
log.info("[KF] Clip '{}' → Offset-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;
|
||||
}
|
||||
|
||||
private void clearMotionKfOffset() {
|
||||
clearKfOffset();
|
||||
}
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
private boolean tryPlay(String clip) {
|
||||
if (animComposer == null || !animLib.applyTo(clip, visual)) {
|
||||
log.info("[Anim] tryPlay('{}') → applyTo FAILED", clip);
|
||||
@@ -507,6 +589,7 @@ public class PlayerInputControl {
|
||||
log.info("[Anim] SkinningControl aktiviert nach Action '{}'", clip);
|
||||
}
|
||||
runningClip = clip;
|
||||
applyMotionKfOffset(clip);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,10 @@ public class CharacterNavigator {
|
||||
|
||||
// ── Tuneable ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Ankunftsradius zum finalen Ziel (m). */
|
||||
private static final float ARRIVE_RADIUS = 0.45f;
|
||||
/** Standard-Ankunftsradius zum finalen Ziel (m). */
|
||||
private static final float DEFAULT_ARRIVE_RADIUS = 0.45f;
|
||||
/** Aktueller Ankunftsradius; kann pro navigateTo-Aufruf überschrieben werden. */
|
||||
private float arriveRadius = DEFAULT_ARRIVE_RADIUS;
|
||||
/** Radius zum Weiterspringen auf den nächsten Wegpunkt (m). */
|
||||
private static final float WAYPOINT_RADIUS = 0.9f;
|
||||
/** Rotationsgeschwindigkeit (rad/s). */
|
||||
@@ -133,6 +135,16 @@ public class CharacterNavigator {
|
||||
*/
|
||||
public void navigateTo(Vector3f target, Speed speed,
|
||||
Runnable onArrival, Runnable onFailed) {
|
||||
navigateTo(target, speed, DEFAULT_ARRIVE_RADIUS, onArrival, onFailed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wie {@link #navigateTo} mit explizitem Ankunftsradius.
|
||||
* Kleinere Werte (z. B. 0.05m für Sitzpunkte) reduzieren den Versatz beim End-Snap.
|
||||
*/
|
||||
public void navigateTo(Vector3f target, Speed speed, float customArriveRadius,
|
||||
Runnable onArrival, Runnable onFailed) {
|
||||
this.arriveRadius = customArriveRadius;
|
||||
this.speed = speed;
|
||||
this.onArrival = onArrival;
|
||||
this.onFailed = onFailed;
|
||||
@@ -158,7 +170,7 @@ public class CharacterNavigator {
|
||||
|
||||
/** Wie {@link #navigateTo} ohne Fehler-Callback. */
|
||||
public void navigateTo(Vector3f target, Speed speed, Runnable onArrival) {
|
||||
navigateTo(target, speed, onArrival, null);
|
||||
navigateTo(target, speed, DEFAULT_ARRIVE_RADIUS, onArrival, null);
|
||||
}
|
||||
|
||||
/** Bricht die Navigation sofort ab, kein Callback wird ausgeführt. */
|
||||
@@ -190,7 +202,7 @@ public class CharacterNavigator {
|
||||
if (path.isEmpty()) { arrive(); return; }
|
||||
WorldPoint goal = path.get(path.size() - 1);
|
||||
float gdx = goal.x - pos.x, gdz = goal.z - pos.z;
|
||||
if (gdx * gdx + gdz * gdz <= ARRIVE_RADIUS * ARRIVE_RADIUS) { arrive(); return; }
|
||||
if (gdx * gdx + gdz * gdz <= arriveRadius * arriveRadius) { arrive(); return; }
|
||||
|
||||
// ── Wegpunkte überspringen die bereits passiert sind ──────────────────
|
||||
while (pathStep < path.size() - 1) {
|
||||
|
||||
@@ -340,7 +340,13 @@ public class WorldScene extends BaseAppState {
|
||||
float modelHeight = yRange[1] - yRange[0];
|
||||
log.info("[WorldScene] Vertex-Y-Range: min={} max={} height={}", yRange[0], yRange[1], modelHeight);
|
||||
float offsetY;
|
||||
if (modelHeight > 0.1f) {
|
||||
com.jme3.math.Vector3f savedScale = loaded.getLocalScale();
|
||||
if (Math.abs(savedScale.y - 1f) > 0.001f) {
|
||||
// Im j3o eingebrannte Skalierung verwenden (nicht auto-berechnen)
|
||||
float scale = savedScale.y;
|
||||
offsetY = -(0.9f + scale * yRange[0]);
|
||||
log.info("[WorldScene] Charakter: gespeicherte Skalierung {}x offsetY={}", scale, offsetY);
|
||||
} else if (modelHeight > 0.1f) {
|
||||
float scale = 1.8f / modelHeight;
|
||||
loaded.setLocalScale(scale);
|
||||
// Füße des Modells (scale * minY in loaded-Local) auf Kapsel-Unterkante legen
|
||||
|
||||
@@ -208,7 +208,8 @@ public class GrassState extends BaseAppState
|
||||
node.attachChild(geo);
|
||||
}
|
||||
if (node.getChildren().isEmpty()) return;
|
||||
node.setCullHint(Spatial.CullHint.Always); // sichtbar erst wenn LOD0
|
||||
boolean visibleNow = terrainChunkState.getChunkLod(cx, cz) == 0;
|
||||
node.setCullHint(visibleNow ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
chunkNodes[idx] = node;
|
||||
grassNode.attachChild(node);
|
||||
}
|
||||
|
||||
@@ -257,6 +257,13 @@ public class TerrainChunkState extends BaseAppState {
|
||||
public void addChunkListener(ChunkListener l) { listeners.add(l); }
|
||||
public void removeChunkListener(ChunkListener l) { listeners.remove(l); }
|
||||
|
||||
/** Aktueller LOD eines Chunks (cx/cz in Chunk-Koordinaten), -1 = nicht geladen/versteckt. */
|
||||
public int getChunkLod(int cx, int cz) {
|
||||
int ci = ChunkTerrainIO.chunkIndex(cx, cz);
|
||||
if (ci < 0 || ci >= TOTAL) return -1;
|
||||
return chunkLod[ci];
|
||||
}
|
||||
|
||||
// ── Chunk-Mesh-Aufbau ─────────────────────────────────────────────────────
|
||||
|
||||
private void rebuildChunkMesh(int cx, int cz, int lod, int[] targetLod) {
|
||||
|
||||
@@ -57,6 +57,14 @@ public class WorldInteractableState extends BaseAppState {
|
||||
private static final float BED_RANGE = 6f;
|
||||
private static final float WALK_TIMEOUT = 12f;
|
||||
|
||||
// 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;
|
||||
|
||||
// ── Abhängigkeiten ────────────────────────────────────────────────────────
|
||||
|
||||
private final KeyBindings keyBindings;
|
||||
@@ -146,9 +154,16 @@ public class WorldInteractableState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
// Navigation läuft im CharacterNavigator (PlayerInputControl.update).
|
||||
// Nur walkTimer für Bett-Rückkehr brauchen wir noch.
|
||||
if (phase == Phase.WALKING_BACK) walkTimer += tpf;
|
||||
if (benchPendingId != null) {
|
||||
benchPendingTimer += tpf;
|
||||
Vector3f pos = physicsChar.getPhysicsLocation();
|
||||
float dx = pos.x - benchPendingX;
|
||||
float dz = pos.z - benchPendingZ;
|
||||
if (dx * dx + dz * dz >= BENCH_REENABLE_DIST_SQ || benchPendingTimer >= BENCH_REENABLE_TIMEOUT) {
|
||||
flushBenchReEnable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Listener ─────────────────────────────────────────────────────────────
|
||||
@@ -197,10 +212,14 @@ public class WorldInteractableState extends BaseAppState {
|
||||
return;
|
||||
}
|
||||
|
||||
if (benchPendingId != null && benchPendingId.equals(entry.interactableId())) {
|
||||
benchPendingId = null; // gleiche Bank – wird gleich wieder disabled
|
||||
}
|
||||
phase = Phase.WALKING;
|
||||
log.info("[WorldInteractable] Annäherung {} [{}]", entry.type(), entry.interactableId());
|
||||
|
||||
playerInput.navigateTo(target, CharacterNavigator.Speed.WALK,
|
||||
float radius = isBench(entry) ? 0.05f : -1f;
|
||||
playerInput.navigateTo(target, CharacterNavigator.Speed.WALK, radius,
|
||||
this::onApproachArrived,
|
||||
this::cancelInteraction);
|
||||
}
|
||||
@@ -217,7 +236,13 @@ public class WorldInteractableState extends BaseAppState {
|
||||
log.warn("[WorldInteractable] Bank {} hat keinen Sitzpunkt.", entry.interactableId());
|
||||
return null;
|
||||
}
|
||||
return new Vector3f(bench.getSitzX(), bench.getSitzY(), bench.getSitzZ());
|
||||
// Approach 17.5cm kürzer als KF-Offset → Charakter läuft näher ran, Visual landet tiefer zur Bank.
|
||||
float approachDist = Math.max(playerInput.getBenchApproachDist() - 0.175f, 0.05f);
|
||||
float rotY = bench.getSitzRotY();
|
||||
float dx = (float) Math.cos(rotY) * approachDist;
|
||||
float dz = (float) Math.sin(rotY) * approachDist;
|
||||
log.info("[WorldInteractable] Bank approach: {}m von Sitzpunkt", approachDist);
|
||||
return new Vector3f(bench.getSitzX() + dx, bench.getSitzY(), bench.getSitzZ() + dz);
|
||||
} else {
|
||||
Bed bed = BedIO.load(entry.interactableId()).orElse(null);
|
||||
if (bed == null || !bed.isLiegeSet()) {
|
||||
@@ -238,7 +263,6 @@ public class WorldInteractableState extends BaseAppState {
|
||||
phase = Phase.PLAY_ANIM;
|
||||
InteractableEntry entry = entries.get(targetIdx);
|
||||
float rotY = getSitFacingRotY(entry);
|
||||
// Charakter dreht sich so dass der Rücken zur Bank / zum Bett zeigt
|
||||
Vector3f sitDir = new Vector3f((float) Math.cos(rotY), 0f, (float) Math.sin(rotY));
|
||||
playerInput.requestTurn(sitDir, 0.35f, () -> startSitAnim(entry));
|
||||
}
|
||||
@@ -265,7 +289,7 @@ public class WorldInteractableState extends BaseAppState {
|
||||
AnimationAction idleAction = isBench(entry) ? AnimationAction.SITTING : AnimationAction.LYING;
|
||||
|
||||
playerInput.requestAnimation(downAction, 0f, () -> {
|
||||
snapToSitPos(entry);
|
||||
if (!isBench(entry)) snapToSitPos(entry); // Bank: Physik bleibt am Approach-Punkt
|
||||
playerInput.lockInPlace();
|
||||
playerInput.playLockedAnimation(idleAction);
|
||||
phase = Phase.RESTING;
|
||||
@@ -303,10 +327,12 @@ public class WorldInteractableState extends BaseAppState {
|
||||
AnimationAction upAction = isBench(entry) ? AnimationAction.SIT_UP : AnimationAction.LIE_UP;
|
||||
|
||||
playerInput.requestAnimation(upAction, 0f, () -> {
|
||||
setTargetPhysicsEnabled(entry, true);
|
||||
|
||||
if (isBench(entry)) {
|
||||
// Bank: Charakter bleibt stehen, Spieler übernimmt wieder
|
||||
playerInput.clearKfOffset();
|
||||
benchPendingId = entry.interactableId();
|
||||
benchPendingX = entry.worldX();
|
||||
benchPendingZ = entry.worldZ();
|
||||
benchPendingTimer = 0f;
|
||||
phase = Phase.IDLE;
|
||||
targetIdx = -1;
|
||||
log.info("[WorldInteractable] Bank verlassen.");
|
||||
@@ -337,10 +363,18 @@ public class WorldInteractableState extends BaseAppState {
|
||||
if (targetIdx >= 0 && targetIdx < entries.size()) {
|
||||
setTargetPhysicsEnabled(entries.get(targetIdx), true);
|
||||
}
|
||||
flushBenchReEnable();
|
||||
phase = Phase.IDLE;
|
||||
targetIdx = -1;
|
||||
}
|
||||
|
||||
private void flushBenchReEnable() {
|
||||
if (benchPendingId == null) return;
|
||||
WorldObjectsState wos = getApplication().getStateManager().getState(WorldObjectsState.class);
|
||||
if (wos != null) wos.setInteractablePhysicsEnabled(benchPendingId, true);
|
||||
benchPendingId = null;
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
private static boolean isBench(InteractableEntry e) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>ERROR</level>
|
||||
<level>INFO</level>
|
||||
</filter>
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} %-5level [%logger{30}] %msg%n%ex</pattern>
|
||||
|
||||
Reference in New Issue
Block a user