Kampfsystem, hier: Positionierung des Schwertes, weitere Features für die Griffe ergänzt

This commit is contained in:
2026-10-12 21:07:46 +02:00
parent 9ff911eaa6
commit 21c8af0105
27 changed files with 1063 additions and 162 deletions

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -194,6 +194,14 @@
"Roll",
"Sneaking Forward",
"Sword And Shield Jump Running",
"Sword And Shield Jump"
"Sword And Shield Jump",
"Great Sword Walk backwards",
"Great Sword Walk",
"Great Sword Strafe left",
"Great Sword Strafe Right",
"Great Sword Running Jump",
"Great Sword Run",
"Great Sword Heavy Attack",
"Great Sword Attack 3"
]
}

View File

@@ -0,0 +1,30 @@
#Tue Oct 06 21:54:42 CEST 2026
attachedEmitters.count=0
attachedLights.count=0
attachedSounds.count=0
castShadow=true
category=
cullDistance=120.0
footstepSurface=
interactableOffsetX=0.0
interactableOffsetY=0.5
interactableOffsetZ=0.0
interactableRotY=0.0
interactableType=NONE
lod1Distance=30.0
lod1Path=
lod2Distance=80.0
lod2Path=
modelCategory=NONE
name=rostiger+zweihaender
pivotOffsetY=0.0
placementOffsetY=0.0
randomScaleMax=1.0
randomScaleMin=1.0
receiveShadow=true
scaleX=1.0
scaleY=1.0
scaleZ=1.0
solid=true
tags=
uniformScale=true

View File

@@ -0,0 +1,30 @@
#Mon Oct 12 14:17:44 CEST 2026
attachedEmitters.count=0
attachedLights.count=0
attachedSounds.count=0
castShadow=true
category=
cullDistance=120.0
footstepSurface=
interactableOffsetX=0.0
interactableOffsetY=0.5
interactableOffsetZ=0.0
interactableRotY=0.0
interactableType=NONE
lod1Distance=30.0
lod1Path=
lod2Distance=80.0
lod2Path=
modelCategory=NONE
name=rostiger_zweihaender
pivotOffsetY=0.0
placementOffsetY=0.0
randomScaleMax=1.0
randomScaleMin=1.0
receiveShadow=true
scaleX=1.0
scaleY=1.0
scaleZ=1.0
solid=false
tags=
uniformScale=true

View File

@@ -16,7 +16,13 @@
"posZ": -0.13,
"rotX": 90.0,
"rotY": 0.0,
"rotZ": 90.0
"rotZ": 90.0,
"grip2PosX": 0.0,
"grip2PosY": 0.0,
"grip2PosZ": 0.0,
"handOffsetX": 0.0,
"handOffsetY": 0.0,
"handOffsetZ": 0.0
},
"modelRef": {
"path": "Models/waffen/schilder/einfacher schild.j3o"

View File

@@ -0,0 +1,31 @@
{
"itemId": "rostiger_zweihaender",
"category": "WEAPON",
"subCategory": "TWO_HANDED_SWORD",
"name": {
"id": "rostiger_zweihaender.name"
},
"description": {
"id": "rostiger_zweihaender.description"
},
"worthGold": 0,
"damage": 50,
"grip": {
"posX": 0.10248406,
"posY": 0.104730606,
"posZ": 0.5130257,
"rotX": 0.0,
"rotY": 0.0,
"rotZ": 0.0,
"grip2PosX": 0.0024840608,
"grip2PosY": 0.104730606,
"grip2PosZ": 0.5630257,
"handOffsetX": 0.0,
"handOffsetY": 0.0,
"handOffsetZ": 0.0
},
"modelRef": {
"path": "Models/waffen/schwerter/rostiger_zweihaender.j3o"
},
"consumable": false
}

View File

@@ -16,7 +16,13 @@
"posZ": 0.0,
"rotX": 0.0,
"rotY": -90.0,
"rotZ": 90.0
"rotZ": 90.0,
"grip2PosX": 0.0,
"grip2PosY": 0.0,
"grip2PosZ": 0.0,
"handOffsetX": 0.0,
"handOffsetY": 0.0,
"handOffsetZ": 0.0
},
"modelRef": {
"path": "Models/waffen/schwerter/rostiges_kurzschwert.j3o"

View File

@@ -7,6 +7,15 @@ import lombok.Setter;
@Getter
@Setter
public class ItemGrip {
/** Offset (Modell-local), den das Runtime-System beim Anbringen der Waffe addiert. */
public static final float HAND_GRIP_OFFSET_X = 0.1f;
public static final float HAND_GRIP_OFFSET_Y = 0.0f;
public static final float HAND_GRIP_OFFSET_Z = 0.05f;
private float posX, posY, posZ;
private float rotX, rotY, rotZ;
/** Position des linken Hand-Griffs im Modell-Space (nur Zweihänder). */
private float grip2PosX, grip2PosY, grip2PosZ;
/** Zusätzlicher Offset in Character-Space (X=rechts, Y=oben, Z=vorwärts). */
private float handOffsetX, handOffsetY, handOffsetZ;
}

View File

@@ -197,7 +197,7 @@ public class EditorApp extends Application {
stoneController = new de.blight.editor.toolset.stone.StoneController(toolCtx);
contentController = new de.blight.editor.toolset.content.ContentController(toolCtx);
contentController.setOnEnterContentMode(() -> {
if (assetController.isOverlayOpen()) assetController.closeOverlay();
assetController.closeOverlayImmediate();
assetController.hideTabBtn();
});
tripoController = new de.blight.editor.toolset.tripo.TripoController(toolCtx);
@@ -281,10 +281,13 @@ public class EditorApp extends Application {
return;
}
}
// Tab: Sub-Modus innerhalb des aktiven Tools cyclen
// Tab: Sub-Modus innerhalb des aktiven Tools cyclen.
// Nicht auslösen wenn Fokus auf einem Eingabefeld liegt (TextInputControl oder Spinner –
// Spinner.getFocusOwner() gibt den Spinner selbst zurück, nicht das interne TextField).
if (e.getCode() == KeyCode.TAB && "world".equals(currentTool)
&& !e.isAltDown() && !e.isControlDown()
&& !(scene.getFocusOwner() instanceof javafx.scene.control.TextInputControl)) {
&& !(scene.getFocusOwner() instanceof javafx.scene.control.TextInputControl)
&& !(scene.getFocusOwner() instanceof javafx.scene.control.Spinner)) {
e.consume();
cycleToolSubMode();
return;
@@ -618,13 +621,14 @@ public class EditorApp extends Application {
private void switchToWorldEditor() {
boolean wasObjEditor = "objecteditor".equals(registry.activeToolId()) || "objecteditor".equals(currentTool);
boolean wasModelEditor = "modeleditor".equals(registry.activeToolId()) || "modeleditor".equals(currentTool);
boolean wasGripEditor = "gripEditor".equals(currentTool);
boolean wasThumbnailManager = "thumbnailmanager".equals(currentTool);
registry.deactivateAll();
currentTool = "world";
if (jmeApp != null) jmeApp.setRenderThrottled(false);
root.setLeft(null); // ggf. gesetzten Content-Tree entfernen
assetController.showTabBtn();
if (wasThumbnailManager) assetController.openOverlay();
assetController.openOverlay();
topBar.getChildren().set(1, topBarController.getWorldToolBar());
if (contentController.hasRoutineJmeSlot()) {
contentController.detachRoutineJmeSlot();
@@ -647,9 +651,22 @@ public class EditorApp extends Application {
input.activeLayer = 0;
input.modelEditorOpenPath = null;
topBarController.getBaseBtn().setSelected(true);
} else if (wasGripEditor) {
input.activeLayer = 0;
input.gripEditorActive = false;
input.gripEditorIsShield = false;
Platform.runLater(() -> topBarController.getAuswahlBtn().fire());
}
}
/** Wechselt ggf. zum Welteditor und feuert dann den gewünschten Toolbar-Button. */
private void fireWorldToolBtn(Runnable btn) {
Platform.runLater(() -> {
if (!"world".equals(currentTool)) switchToWorldEditor();
btn.run();
});
}
private void ensureNotContentEditor() {
if (contentController.isSingleWindowContentActive(currentTool)) {
switchToWorldEditor();
@@ -665,7 +682,7 @@ public class EditorApp extends Application {
thumbnailManagerView.refresh();
}
// Asset-Baum-Overlay ausblenden – wird in switchToWorldEditor() wiederhergestellt
if (assetController.isOverlayOpen()) assetController.closeOverlay();
assetController.closeOverlayImmediate();
assetController.hideTabBtn();
topBar.getChildren().set(1, buildThumbnailManagerToolBar());
root.setLeft(null);
@@ -1067,16 +1084,18 @@ public class EditorApp extends Application {
input.topologyRequest = sel ? 1 : 2;
}
}
case F1 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getAuswahlBtn().fire()); }
case F2 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getBaseBtn().fire()); }
case F3 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getVoxelMergedBtn().fire()); }
case F4 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getTextureBtn().fire()); }
case F1 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getAuswahlBtn().fire()); }
case F2 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getBaseBtn().fire()); }
case F3 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getVoxelMergedBtn().fire()); }
case F4 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getTextureBtn().fire()); }
case F5 -> {
if (pressed) {
if ("world".equals(currentTool)) { Platform.runLater(() -> topBarController.getGrassBtn().fire()); }
else {
ToolController active = registry.getActive();
if (active != null) { active.onPreview(); } else if (onF5 != null) { onF5.run(); }
if (active != null) { active.onPreview(); }
else if (onF5 != null) { onF5.run(); }
else { fireWorldToolBtn(() -> topBarController.getGrassBtn().fire()); }
}
}
}
@@ -1085,28 +1104,29 @@ public class EditorApp extends Application {
if ("world".equals(currentTool)) { Platform.runLater(() -> topBarController.getStoneBtn().fire()); }
else {
ToolController active = registry.getActive();
if (active != null) { active.onRandomSeed(); } else if (onF6 != null) { onF6.run(); }
if (active != null) { active.onRandomSeed(); }
else if (onF6 != null) { onF6.run(); }
else { fireWorldToolBtn(() -> topBarController.getStoneBtn().fire()); }
}
}
}
case F7 -> {
if (pressed && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getObjPlaceBtn().fire()); }
else if (pressed && "objecteditor".equals(registry.activeToolId())) { Platform.runLater(objectController::activatePlaceMode); }
if (pressed && "objecteditor".equals(registry.activeToolId())) { Platform.runLater(objectController::activatePlaceMode); }
else if (pressed) { fireWorldToolBtn(() -> topBarController.getObjPlaceBtn().fire()); }
}
case F8 -> {
if (pressed) {
if ("world".equals(currentTool)) { Platform.runLater(() -> topBarController.getAtmosphaereBtn().fire()); }
else if (input.ctrlHeld) { input.debugNoLightToggle = true; }
} else if (input.ctrlHeld) { input.debugNoLightToggle = true; }
if (pressed && input.ctrlHeld) { input.debugNoLightToggle = true; }
else if (!pressed && input.ctrlHeld) { input.debugNoLightToggle = true; }
else if (pressed) { fireWorldToolBtn(() -> topBarController.getAtmosphaereBtn().fire()); }
}
case F9 -> {
if (pressed && input.ctrlHeld) { Platform.runLater(() ->
contentController.activateContentSection(8, currentTool)); }
else if (pressed && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getWasserMergedBtn().fire()); }
else if (pressed) { fireWorldToolBtn(() -> topBarController.getWasserMergedBtn().fire()); }
}
case F10 -> { if (pressed && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getWegeBtn().fire()); } }
case F11 -> { if (pressed && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getZonenBtn().fire()); } }
case F12 -> { if (pressed && !input.ctrlHeld && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getPlayToolBtn().fire()); } }
case F10 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getWegeBtn().fire()); }
case F11 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getZonenBtn().fire()); }
case F12 -> { if (pressed && !input.ctrlHeld) fireWorldToolBtn(() -> topBarController.getPlayToolBtn().fire()); }
}
}

View File

@@ -878,6 +878,12 @@ public class SharedInput {
public volatile boolean modelEditorBakeScaleRequest = false;
public volatile java.nio.file.Path modelEditorBakeScalePath = null;
/** JFX → JME: Waffe an Z-Achse ausrichten und Mitte in Ursprung setzen. */
public volatile boolean modelEditorAlignWeaponRequest = false;
public volatile java.nio.file.Path modelEditorAlignWeaponPath = null;
/** JME → JFX: Ergebnis der letzten Ausrichte-Operation (null = kein Ergebnis). */
public volatile String modelEditorAlignWeaponResult = null;
/** JME → JFX: j3o hatte eine gebakte Scale, die von der Meta abwich – Spinner aktualisieren. */
public volatile boolean modelEditorBakedScaleDetected = false;
@@ -934,6 +940,8 @@ public class SharedInput {
public volatile String bakeStatusMsg = null;
/** JME → JFX: Anzahl dirty Chunks in der Session (0 = nichts zu backen). */
public volatile int sessionDirtyVoxelCount = 0;
/** JFX → JME: Pfad zum aktiven Session-Verzeichnis (gesetzt von GameController beim Start). */
public volatile java.nio.file.Path activeSessionDir = null;
/** JFX → JME: Nur dirty Chunks backen (Bake-on-Exit). */
public volatile boolean bakeOnExitRequested = false;
/** JFX → JME: Laufenden Bake-on-Exit abbrechen. */
@@ -1209,10 +1217,25 @@ public class SharedInput {
public volatile float gripEditorRotX = 0f;
public volatile float gripEditorRotY = 0f;
public volatile float gripEditorRotZ = 0f;
public volatile float gripEditorGrip2PosX = 0f;
public volatile float gripEditorGrip2PosY = 0f;
public volatile float gripEditorGrip2PosZ = 0f;
public volatile float gripEditorHandOffX = 0f;
public volatile float gripEditorHandOffY = 0f;
public volatile float gripEditorHandOffZ = 0f;
/** Gesetzt vom JFX-Thread wenn Spinner geändert → JME baut Gizmo neu. */
public volatile boolean gripEditorGizmoDirty = false;
/** true → Gizmo ist ein 28cm-Unterarm-Pfeil (Schild); false → 15cm-Waffen-Pfeil. */
public volatile boolean gripEditorIsShield = false;
/** JFX → JME: Griff-Position aus Modell-Geometrie schätzen (Parierstange erkennen). */
public volatile boolean gripEditorGuessRequest = false;
/** JME → JFX: Ergebnis der Griff-Schätzung — X/Y = Querschnittsmitte, Z = Griff-Z mit Puffer.
* gripEditorGuessPosZ = Float.NaN bedeutet: kein Ergebnis vorhanden. */
public volatile float gripEditorGuessPosX = 0f;
public volatile float gripEditorGuessPosY = 0f;
public volatile float gripEditorGuessPosZ = Float.NaN;
/** Klingenrichtung im Modellraum: -1 = Klinge bei -Z, +1 = Klinge bei +Z. Wird von guessGripPosition gesetzt. */
public volatile float gripEditorBladeDirZ = -1f;
/** Klick/Drag im Viewport im Sculpt-Modus. action 0=links, 1=rechts. */
public record SculptEdit(float screenX, float screenY, int action) {}

View File

@@ -279,6 +279,7 @@ public class ItemPlacementState extends BaseAppState {
try {
PlacedItemIO.save(items);
syncBpiToSession();
log.info("[ItemPlacement] Item '{}' platziert bei ({}, {}, {})", pi.itemId(), pt.x, pt.y, pt.z);
} catch (IOException e) {
log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage());
@@ -664,6 +665,7 @@ public class ItemPlacementState extends BaseAppState {
nodes.remove(idx);
try {
PlacedItemIO.save(items);
syncBpiToSession();
} catch (IOException e) {
log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage());
}
@@ -681,6 +683,7 @@ public class ItemPlacementState extends BaseAppState {
try {
PlacedItemIO.save(items);
syncBpiToSession();
} catch (IOException e) {
log.warn("[ItemPlacement] Speichern nach Edit fehlgeschlagen: {}", e.getMessage());
}
@@ -689,6 +692,7 @@ public class ItemPlacementState extends BaseAppState {
private void saveItems() {
try {
PlacedItemIO.save(items);
syncBpiToSession();
} catch (IOException e) {
log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage());
}
@@ -696,6 +700,17 @@ public class ItemPlacementState extends BaseAppState {
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private void syncBpiToSession() {
Path sd = input.activeSessionDir;
if (sd == null) return;
try {
java.nio.file.Files.copy(PlacedItemIO.getPath(), sd.resolve("blight_placed_items.bpi"),
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} catch (IOException ex) {
log.warn("[ItemPlacement] Session-Sync fehlgeschlagen: {}", ex.getMessage());
}
}
private static void applyRot(Node n, float rotX, float rotY, float rotZ) {
Quaternion q = new Quaternion();
q.fromAngles(rotX, rotY, rotZ);

View File

@@ -106,6 +106,7 @@ public class ModelEditorState extends BaseAppState {
private Node gripGizmoNode = null;
private float gripPosX = 0f, gripPosY = 0f, gripPosZ = 0f;
private float gripRotX = 0f, gripRotY = 0f, gripRotZ = 0f;
private float grip2PosX = 0f, grip2PosY = 0f, grip2PosZ = 0f;
private float interactableOffsetZ = 0f;
private float interactableRotY = 0f;
@@ -197,6 +198,10 @@ public class ModelEditorState extends BaseAppState {
app.getStateManager().detach(previewEnvCam);
previewEnvCam = null;
}
// Probe jetzt zur Szene hinzufügen – erst hier ist sie korrekt initialisiert
if (previewRoot != null) {
previewRoot.addLight(previewProbe);
}
previewProbeReady = true;
// Emissive-Fallback von bereits geladenen Materialien entfernen
for (Material m : pbrMaterials) {
@@ -223,6 +228,22 @@ public class ModelEditorState extends BaseAppState {
}
}
if (input.modelEditorAlignWeaponRequest) {
input.modelEditorAlignWeaponRequest = false;
Path alignPath = input.modelEditorAlignWeaponPath;
if (alignPath != null) {
alignWeaponToZ(alignPath);
} else {
log.warn("[ModelEditor] AlignWeapon: kein Pfad gesetzt");
input.modelEditorAlignWeaponResult = "FEHLER: kein Pfad";
}
}
if (input.gripEditorGuessRequest) {
input.gripEditorGuessRequest = false;
guessGripPosition();
}
if (input.modelEditorEmbedLodsRequest) {
input.modelEditorEmbedLodsRequest = false;
embedLodsAndSave(input.modelEditorEmbedLodsPath);
@@ -248,6 +269,9 @@ public class ModelEditorState extends BaseAppState {
gripRotX = input.gripEditorRotX;
gripRotY = input.gripEditorRotY;
gripRotZ = input.gripEditorRotZ;
grip2PosX = input.gripEditorGrip2PosX;
grip2PosY = input.gripEditorGrip2PosY;
grip2PosZ = input.gripEditorGrip2PosZ;
rebuildGripGizmo();
}
@@ -472,7 +496,22 @@ public class ModelEditorState extends BaseAppState {
// Rendering schwarz erscheinen lassen.
try { app.getAssetManager().deleteFromCache(new com.jme3.asset.ModelKey(assetPath)); }
catch (Exception ignored) {}
Spatial model = app.getAssetManager().loadModel(assetPath);
// Direkt aus ASSET_ROOT (src/main/resources) laden – umgeht den ClasspathLocator,
// der bin/main bevorzugt und nach einem Bake möglicherweise die veraltete Kopie liefert.
java.nio.file.Path absFile = ASSET_ROOT.resolve(
java.nio.file.Paths.get(assetPath.replace('/', java.io.File.separatorChar)));
Spatial model;
if (absFile.toFile().exists()) {
BinaryImporter directImp = BinaryImporter.getInstance();
directImp.setAssetManager(app.getAssetManager());
Savable directLoaded = directImp.load(absFile.toFile());
log.info("[ModelEditor] Direkt geladen: {} ({}B)", absFile.getFileName(), absFile.toFile().length());
model = (directLoaded instanceof Spatial) ? (Spatial) directLoaded
: app.getAssetManager().loadModel(assetPath);
} else {
log.warn("[ModelEditor] Nicht in ASSET_ROOT, lade via AM: {}", assetPath);
model = app.getAssetManager().loadModel(assetPath);
}
stripControls(model);
initImpostorControls(model);
recalcNormalsIfMissing(model);
@@ -696,12 +735,12 @@ public class ModelEditorState extends BaseAppState {
new ColorRGBA(0.4f, 0.4f, 0.5f, 1f)));
previewRoot.addLight(new AmbientLight(new ColorRGBA(0.65f, 0.65f, 0.7f, 1f)));
// LightProbe: JME3-PBR braucht Probe damit g_AmbientLightColor verwendet wird.
// EnvironmentCamera backt die Probe aus dem hellen Übergangshintergrund heraus;
// danach wird der dunkle Display-Hintergrund wiederhergestellt.
// LightProbe: erst nach dem Backen zur Szene hinzufügen – eine leere (uninitialisierte)
// Probe stört den PBR-Shader und macht das Modell dunkel, bevor der Bake fertig ist.
// Bis dahin sorgen DirectionalLights + AmbientLight für sichtbare Beleuchtung.
previewProbe = new LightProbe();
previewProbe.getArea().setRadius(1000f);
previewRoot.addLight(previewProbe);
// previewRoot.addLight(previewProbe) → wird in done()-Callback aufgerufen
previewEnvCam = new EnvironmentCamera(256, Vector3f.ZERO.clone());
app.getStateManager().attach(previewEnvCam);
previewProbeFrame = 0;
@@ -734,8 +773,11 @@ public class ModelEditorState extends BaseAppState {
gripRotX = input.gripEditorRotX;
gripRotY = input.gripEditorRotY;
gripRotZ = input.gripEditorRotZ;
grip2PosX = input.gripEditorGrip2PosX;
grip2PosY = input.gripEditorGrip2PosY;
grip2PosZ = input.gripEditorGrip2PosZ;
orbitYaw = 30f;
orbitYaw = input.gripEditorActive ? 90f : 30f;
orbitPitch = 25f;
}
@@ -1218,9 +1260,15 @@ public class ModelEditorState extends BaseAppState {
projectRoot.resolve("blight-assets").resolve("bin").resolve("main").resolve(rel),
projectRoot.resolve("blight-assets").resolve("build").resolve("resources").resolve("main").resolve(rel),
};
for (java.nio.file.Path mirror : mirrors) {
if (java.nio.file.Files.exists(mirror)) {
java.nio.file.Path[] mirrorRoots = {
projectRoot.resolve("blight-assets").resolve("bin").resolve("main"),
projectRoot.resolve("blight-assets").resolve("build").resolve("resources").resolve("main"),
};
for (int i = 0; i < mirrors.length; i++) {
java.nio.file.Path mirror = mirrors[i];
if (!java.nio.file.Files.isDirectory(mirrorRoots[i])) continue;
try {
java.nio.file.Files.createDirectories(mirror.getParent());
java.nio.file.Files.copy(savedFile, mirror,
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
log.info("[ModelEditor] Spiegel aktualisiert: {}", mirror);
@@ -1229,7 +1277,6 @@ public class ModelEditorState extends BaseAppState {
}
}
}
}
private void embedLodsAndSave(Path j3oPath) {
log.info("[ModelEditor] embedLodsAndSave aufgerufen: path={}, originalSpatial={}",
@@ -1424,6 +1471,296 @@ public class ModelEditorState extends BaseAppState {
}
}
private void alignWeaponToZ(Path j3oPath) {
log.info("[ModelEditor] alignWeaponToZ GESTARTET: {}", j3oPath);
try {
BinaryImporter importer = BinaryImporter.getInstance();
importer.setAssetManager(app.getAssetManager());
Savable savable = importer.load(j3oPath.toFile());
if (!(savable instanceof Spatial root)) {
log.warn("[ModelEditor] AlignWeapon: kein Spatial in {}", j3oPath.getFileName());
input.modelEditorAlignWeaponResult = "FEHLER: kein Spatial";
return;
}
root.setLocalTranslation(Vector3f.ZERO);
root.setLocalRotation(Quaternion.IDENTITY);
root.setLocalScale(1f);
root.updateGeometricState();
// Schritt 1: alle Vertex-Positionen sammeln
java.util.List<Vector3f> verts = new java.util.ArrayList<>();
collectAllVertices(root, new Matrix4f(), verts);
if (verts.size() < 2) {
log.warn("[ModelEditor] AlignWeapon: zu wenige Vertices ({})", verts.size());
return;
}
// Schritt 2: Die zwei am weitesten entfernten Punkte finden (Durchmesser-Approx.)
// Startet von beliebigem Punkt → A = Farthest(v0) → B = Farthest(A)
Vector3f A = awFarthestFrom3D(verts, verts.get(0));
Vector3f B = awFarthestFrom3D(verts, A);
log.info("[ModelEditor] AlignWeapon: Hauptachse {} → {} Länge={}", A, B, A.distance(B));
// Schritt 3: Richtung A→B auf +Z ausrichten
Vector3f bladeDir = B.subtract(A).normalizeLocal();
if (bladeDir.z < 0) bladeDir.negateLocal(); // +Z-Richtung bevorzugen
Quaternion rot1 = awRotateToZ(bladeDir);
// Mitte der Hauptachse → Ursprung
Vector3f midpoint = A.add(B).multLocal(0.5f);
// Schritt 4: Vertices in neues Koordinatensystem transformieren (rot1 + Translation)
// v' = rot1 * (v - midpoint)
java.util.List<Vector3f> verts2 = new java.util.ArrayList<>(verts.size());
for (Vector3f v : verts) {
verts2.add(rot1.mult(v.subtract(midpoint)));
}
// Schritt 5: In der XY-Ebene die zwei weitesten Punkte finden (Z ignoriert)
Vector3f C = awFarthestFromXY(verts2, verts2.get(0));
Vector3f D = awFarthestFromXY(verts2, C);
log.info("[ModelEditor] AlignWeapon: Breitenachse ({},{}) → ({},{})", C.x, C.y, D.x, D.y);
// Schritt 6: Rotation um Z, damit die Breitenrichtung auf X liegt
Quaternion rot2 = new Quaternion();
float wx = D.x - C.x, wy = D.y - C.y;
if (wx*wx + wy*wy > 1e-8f) {
float angle = FastMath.atan2(wy, wx);
rot2.fromAngles(0f, 0f, -angle);
}
// Schritt 7: Gesamt-Rotation = rot2 ∘ rot1 (erst rot1, dann rot2)
Quaternion combined = rot2.mult(rot1);
// Schritt 7b: Bestimme Klingen-Richtung anhand der Parierstangen-Position.
// Die Parierstange (breitester Querschnitt) liegt immer an der Klingenbasis.
// normZ >0.5 → Parierstange in +Z-Hälfte → Pommel bei +Z → flip nötig.
// normZ <0.5 → Parierstange in -Z-Hälfte → Klinge bereits bei +Z → kein flip.
{
final int BKTS = 40;
float zMin2 = Float.MAX_VALUE, zMax2 = -Float.MAX_VALUE;
for (Vector3f v : verts2) { if (v.z < zMin2) zMin2 = v.z; if (v.z > zMax2) zMax2 = v.z; }
float zR2 = zMax2 - zMin2;
if (zR2 > 0.01f) {
float[] xmx = new float[BKTS], xmn = new float[BKTS];
float[] ymx = new float[BKTS], ymn = new float[BKTS];
java.util.Arrays.fill(xmx, -Float.MAX_VALUE); java.util.Arrays.fill(xmn, Float.MAX_VALUE);
java.util.Arrays.fill(ymx, -Float.MAX_VALUE); java.util.Arrays.fill(ymn, Float.MAX_VALUE);
for (Vector3f v : verts2) {
int b = Math.max(0, Math.min(BKTS - 1, (int) ((v.z - zMin2) / zR2 * (BKTS - 1))));
if (v.x > xmx[b]) xmx[b] = v.x; if (v.x < xmn[b]) xmn[b] = v.x;
if (v.y > ymx[b]) ymx[b] = v.y; if (v.y < ymn[b]) ymn[b] = v.y;
}
float maxScore2 = 0f;
int guardBucket = 0;
for (int i = 0; i < BKTS; i++) {
float s = Math.max(xmx[i] > xmn[i] ? xmx[i] - xmn[i] : 0f,
ymx[i] > ymn[i] ? ymx[i] - ymn[i] : 0f);
if (s > maxScore2) { maxScore2 = s; guardBucket = i; }
}
float guardNorm = (float) guardBucket / (BKTS - 1);
log.info("[ModelEditor] AlignWeapon: Parierstange bei normZ={} (Bucket {}/{})",
String.format("%.2f", guardNorm), guardBucket, BKTS);
if (guardNorm > 0.5f) {
// Parierstange in +Z-Hälfte → Pommel bei +Z, Klinge bei -Z → 180° um X flippen
Quaternion flip = new Quaternion().fromAngles(FastMath.PI, 0f, 0f);
combined = flip.mult(combined);
log.info("[ModelEditor] AlignWeapon: Pommel bei +Z detektiert → Schwert geflippt");
}
}
}
Vector3f rotatedMid = combined.mult(midpoint);
root.setLocalRotation(combined);
root.setLocalTranslation(rotatedMid.negate());
root.setLocalScale(1f);
ModelExportUtil.stripControls(root);
ModelExportUtil.bakeTransform(root, new Matrix4f());
// Vertex-Bounds nach Baking prüfen (Debug)
java.util.List<Vector3f> bakedVerts = new java.util.ArrayList<>();
collectAllVertices(root, new Matrix4f(), bakedVerts);
float xMin = Float.MAX_VALUE, xMax = -Float.MAX_VALUE;
float yMin = Float.MAX_VALUE, yMax = -Float.MAX_VALUE;
float zMin = Float.MAX_VALUE, zMax = -Float.MAX_VALUE;
for (Vector3f v : bakedVerts) {
if (v.x < xMin) xMin = v.x; if (v.x > xMax) xMax = v.x;
if (v.y < yMin) yMin = v.y; if (v.y > yMax) yMax = v.y;
if (v.z < zMin) zMin = v.z; if (v.z > zMax) zMax = v.z;
}
log.info("[ModelEditor] AlignWeapon nach Baking: X[{},{}] Y[{},{}] Z[{},{}]",
String.format("%.3f",xMin), String.format("%.3f",xMax),
String.format("%.3f",yMin), String.format("%.3f",yMax),
String.format("%.3f",zMin), String.format("%.3f",zMax));
BinaryExporter.getInstance().save(root, j3oPath.toFile());
syncToMirrors(j3oPath);
input.modelEditorScaleX = 1.0f;
input.modelEditorScaleY = 1.0f;
input.modelEditorScaleZ = 1.0f;
input.modelEditorBakedScaleDetected = true;
input.modelEditorOpenPath = currentPath;
input.modelEditorAlignWeaponResult = String.format(
"OK: Z=%.2fm X=%.2fm Y=%.2fm",
zMax - zMin, xMax - xMin, yMax - yMin);
log.info("[ModelEditor] Waffe ausgerichtet und gespeichert: {}", j3oPath.getFileName());
} catch (Exception e) {
log.error("[ModelEditor] AlignWeapon fehlgeschlagen: {}", e.getMessage(), e);
input.modelEditorAlignWeaponResult = "FEHLER: " + e.getMessage();
}
}
/** Sammelt alle Vertex-Positionen aus dem Spatial-Baum, transformiert durch den akkumulierten Matrix. */
private static void collectAllVertices(Spatial s, Matrix4f parentMat, java.util.List<Vector3f> out) {
Matrix4f localMat = new Matrix4f();
s.getLocalTransform().toTransformMatrix(localMat);
Matrix4f worldMat = parentMat.mult(localMat);
if (s instanceof Geometry g) {
FloatBuffer pos = g.getMesh().getFloatBuffer(VertexBuffer.Type.Position);
if (pos != null) {
pos.rewind();
while (pos.remaining() >= 3) {
Vector3f v = new Vector3f(pos.get(), pos.get(), pos.get());
worldMat.mult(v, v);
out.add(v);
}
pos.rewind();
}
} else if (s instanceof Node n) {
for (Spatial child : n.getChildren()) collectAllVertices(child, worldMat, out);
}
}
/** Farthest-Point in 3D von einem Ausgangspunkt. */
private static Vector3f awFarthestFrom3D(java.util.List<Vector3f> verts, Vector3f from) {
Vector3f best = verts.get(0);
float bestDist = from.distanceSquared(best);
for (Vector3f v : verts) {
float d = from.distanceSquared(v);
if (d > bestDist) { bestDist = d; best = v; }
}
return best;
}
/** Farthest-Point nur in XY-Ebene (Z wird ignoriert). */
private static Vector3f awFarthestFromXY(java.util.List<Vector3f> verts, Vector3f from) {
Vector3f best = verts.get(0);
float bx = from.x - best.x, by = from.y - best.y;
float bestDist = bx*bx + by*by;
for (Vector3f v : verts) {
float dx = from.x - v.x, dy = from.y - v.y;
float d = dx*dx + dy*dy;
if (d > bestDist) { bestDist = d; best = v; }
}
return best;
}
/** Quaternion, der den Vektor dir auf +Z dreht. */
private static Quaternion awRotateToZ(Vector3f dir) {
Quaternion q = new Quaternion();
float dot = FastMath.clamp(dir.dot(Vector3f.UNIT_Z), -1f, 1f);
if (dot >= 1f - 1e-6f) return q; // bereits +Z
if (dot <= -1f + 1e-6f) {
q.fromAngles(FastMath.PI, 0f, 0f); // genau gegenüber: 180° um X
return q;
}
Vector3f axis = dir.cross(Vector3f.UNIT_Z).normalizeLocal();
q.fromAngleNormalAxis(FastMath.acos(dot), axis);
return q;
}
/**
* Schätzt Position und Richtung des Griffes anhand der Modell-Geometrie.
* Erkennt die Parierstange (breitester Querschnitt in X oder Y) und verschiebt
* den Griff-Kontaktpunkt 8 cm in Richtung Pommel (weg von der Klingenspitze).
* Voraussetzung: Klinge liegt auf Z-Achse (nach Ausrichten).
* Ergebnis wird in input.gripEditorGuessPosX/Y/Z geschrieben.
*/
private void guessGripPosition() {
input.gripEditorGuessPosZ = Float.NaN; // Sentinel zurücksetzen
if (originalSpatial == null) return;
java.util.List<Vector3f> verts = new java.util.ArrayList<>();
collectAllVertices(originalSpatial, new Matrix4f(), verts);
if (verts.size() < 10) return;
float zMin = Float.MAX_VALUE, zMax = -Float.MAX_VALUE;
for (Vector3f v : verts) {
if (v.z < zMin) zMin = v.z;
if (v.z > zMax) zMax = v.z;
}
float zRange = zMax - zMin;
if (zRange < 0.1f) return;
final int BUCKETS = 80;
float[] xMaxB = new float[BUCKETS];
float[] xMinB = new float[BUCKETS];
float[] yMaxB = new float[BUCKETS];
float[] yMinB = new float[BUCKETS];
java.util.Arrays.fill(xMaxB, -Float.MAX_VALUE);
java.util.Arrays.fill(xMinB, Float.MAX_VALUE);
java.util.Arrays.fill(yMaxB, -Float.MAX_VALUE);
java.util.Arrays.fill(yMinB, Float.MAX_VALUE);
for (Vector3f v : verts) {
int b = (int) ((v.z - zMin) / zRange * (BUCKETS - 1));
b = Math.max(0, Math.min(BUCKETS - 1, b));
if (v.x > xMaxB[b]) xMaxB[b] = v.x;
if (v.x < xMinB[b]) xMinB[b] = v.x;
if (v.y > yMaxB[b]) yMaxB[b] = v.y;
if (v.y < yMinB[b]) yMinB[b] = v.y;
}
// Score = max(X-Spanne, Y-Spanne) — robust wenn Parierstange in X oder Y breiter ist
float[] score = new float[BUCKETS];
for (int i = 0; i < BUCKETS; i++) {
float xSpan = (xMaxB[i] > xMinB[i]) ? xMaxB[i] - xMinB[i] : 0f;
float ySpan = (yMaxB[i] > yMinB[i]) ? yMaxB[i] - yMinB[i] : 0f;
score[i] = Math.max(xSpan, ySpan);
}
// Bucket mit höchstem Score = Parierstange
int bestBucket = 0;
for (int i = 1; i < BUCKETS; i++) {
if (score[i] > score[bestBucket]) bestBucket = i;
}
float guardZ = zMin + (bestBucket + 0.5f) / BUCKETS * zRange;
// X: globaler Midpoint — Klinge läuft entlang Z, X-Mitte ist über alle Buckets konstant
float xGlobalMin = Float.MAX_VALUE, xGlobalMax = -Float.MAX_VALUE;
for (int i = 0; i < BUCKETS; i++) {
if (xMinB[i] < xGlobalMin) xGlobalMin = xMinB[i];
if (xMaxB[i] > xGlobalMax) xGlobalMax = xMaxB[i];
}
float guardX = (xGlobalMax + xGlobalMin) * 0.5f;
// Y: Midpoint im Crossguard-Bucket (Höhe der Parierstange)
float guardY = (yMaxB[bestBucket] > yMinB[bestBucket]) ? (yMaxB[bestBucket] + yMinB[bestBucket]) * 0.5f : 0f;
// Pommel-Richtung: Klingenspitze = dünnstes Ende → Pommel = dickeres Ende
int endB = Math.max(1, BUCKETS / 12); // ~7 Buckets je Seite
float scoreLow = 0f, scoreHigh = 0f;
for (int i = 0; i < endB; i++) scoreLow += score[i];
for (int i = BUCKETS - endB; i < BUCKETS; i++) scoreHigh += score[i];
float pommelDirZ = (scoreHigh > scoreLow) ? +1f : -1f;
input.gripEditorBladeDirZ = -pommelDirZ; // Klinge = entgegengesetzt zum Pommel
// 17 cm Puffer in Richtung Pommel: Hand liegt knapp unterhalb der Parierstange im Griffbereich
final float BUFFER = 0.17f;
float gripZ = guardZ + pommelDirZ * BUFFER;
log.info("[ModelEditor] GripGuess: Parierstange Z={}, pommelDir={}, Griff Z={} X={} Y={}",
String.format("%.3f", guardZ), pommelDirZ > 0 ? "+Z" : "-Z",
String.format("%.3f", gripZ),
String.format("%.3f", guardX),
String.format("%.3f", guardY));
input.gripEditorGuessPosX = guardX;
input.gripEditorGuessPosY = guardY;
input.gripEditorGuessPosZ = gripZ;
}
private static boolean hasSkinningControl(Spatial s) {
if (s.getControl(SkinningControl.class) != null) return true;
if (s instanceof Node n) {
@@ -1533,66 +1870,77 @@ public class ModelEditorState extends BaseAppState {
input.modelInteractableActive ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
}
/** Zeichnet / aktualisiert den Grip-Gizmo: 15cm orange (Waffe) oder 28cm cyan (Schild/Unterarm). */
/** Zeichnet / aktualisiert den Grip-Gizmo: 15cm orange (Waffe/Grip1), blau (Grip2), cyan (Schild). */
public void rebuildGripGizmo() {
if (gripGizmoNode == null) return;
gripGizmoNode.detachAllChildren();
boolean shieldMode = input.gripEditorIsShield;
// Waffe: 15cm orange | Schild: 28cm cyan
float shaftLen = shieldMode ? 0.24f : 0.12f;
float shaftRad = shieldMode ? 0.010f : 0.008f;
float headRad = shieldMode ? 0.026f : 0.022f;
float headLen = shieldMode ? 0.04f : 0.03f;
float axisLen = shieldMode ? 0.08f : 0.06f;
float markerSz = shieldMode ? 0.018f : 0.015f;
float bladeDirZ = input.gripEditorBladeDirZ;
ColorRGBA shaftColor = shieldMode
? new ColorRGBA(0f, 0.75f, 0.9f, 1f)
: new ColorRGBA(1f, 0.55f, 0f, 1f);
ColorRGBA headColor = shieldMode
? new ColorRGBA(0f, 0.5f, 0.75f, 1f)
: new ColorRGBA(1f, 0.3f, 0f, 1f);
ColorRGBA axisColor = shieldMode
? new ColorRGBA(0.6f, 1f, 0.6f, 1f)
: new ColorRGBA(1f, 0.8f, 0.1f, 1f);
// Grip 1: orange (Waffe) oder cyan (Schild)
ColorRGBA c1s = shieldMode ? new ColorRGBA(0f, 0.75f, 0.9f, 1f) : new ColorRGBA(1f, 0.55f, 0f, 1f);
ColorRGBA c1h = shieldMode ? new ColorRGBA(0f, 0.5f, 0.75f, 1f) : new ColorRGBA(1f, 0.3f, 0f, 1f);
ColorRGBA c1a = shieldMode ? new ColorRGBA(0.6f, 1f, 0.6f, 1f) : new ColorRGBA(1f, 0.8f, 0.1f, 1f);
gripGizmoNode.attachChild(buildGripGroup("gripGroup",
gripPosX, gripPosY, gripPosZ, gripRotX, gripRotY, gripRotZ,
bladeDirZ, c1s, c1h, c1a, shaftLen, shaftRad, headRad, axisLen, markerSz));
// Grip 2: blau — nur für Waffen, nur wenn Position gesetzt
if (!shieldMode && (grip2PosX != 0f || grip2PosY != 0f || grip2PosZ != 0f)) {
gripGizmoNode.attachChild(buildGripGroup("grip2Group",
grip2PosX, grip2PosY, grip2PosZ, 0, 0, 0,
bladeDirZ,
new ColorRGBA(0.2f, 0.5f, 1f, 1f),
new ColorRGBA(0.1f, 0.3f, 0.9f, 1f),
new ColorRGBA(0.5f, 0.75f, 1f, 1f),
shaftLen, shaftRad, headRad, axisLen, markerSz));
}
gripGizmoNode.setCullHint(Spatial.CullHint.Inherit);
}
private Node buildGripGroup(String name,
float posX, float posY, float posZ, float rotXDeg, float rotYDeg, float rotZDeg,
float bladeDirZ, ColorRGBA shaftColor, ColorRGBA headColor, ColorRGBA axisColor,
float shaftLen, float shaftRad, float headRad, float axisLen, float markerSz) {
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", shaftColor);
Material matHead = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
matHead.setColor("Color", headColor);
Node group = new Node("gripGroup");
group.setLocalTranslation(gripPosX, gripPosY, gripPosZ);
Quaternion rot = new Quaternion();
rot.fromAngles(
com.jme3.math.FastMath.DEG_TO_RAD * gripRotX,
com.jme3.math.FastMath.DEG_TO_RAD * gripRotY,
com.jme3.math.FastMath.DEG_TO_RAD * gripRotZ);
group.setLocalRotation(rot);
float markerSize = shieldMode ? 0.018f : 0.015f;
Geometry marker = new Geometry("gMarker", new Box(markerSize, markerSize, markerSize));
marker.setMaterial(mat);
// Schaft entlang +Z
Geometry shaft = new Geometry("gShaft", new Cylinder(4, 8, shaftRad, shaftLen, true));
shaft.setMaterial(mat);
shaft.setLocalTranslation(0f, 0f, shaftLen * 0.5f);
// Kegelspitze: Dome zeigt per Default nach +Y → 90° um X → zeigt nach +Z
Geometry head = new Geometry("gHead", new Dome(Vector3f.ZERO, 2, 8, headRad, false));
head.setMaterial(matHead);
head.setLocalTranslation(0f, 0f, shaftLen);
Quaternion headRot = new Quaternion();
headRot.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X);
head.setLocalRotation(headRot);
// Querachsen-Zylinder entlang +Y: zeigt Roll-/Orientierungsachse
Material matAxis = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
matAxis.setColor("Color", axisColor);
float axisLen = shieldMode ? 0.08f : 0.06f;
Geometry gripAxis = new Geometry("gGripAxis", new Cylinder(4, 8, 0.006f, axisLen, true));
Node group = new Node(name);
group.setLocalTranslation(posX, posY, posZ);
if (rotXDeg != 0 || rotYDeg != 0 || rotZDeg != 0) {
Quaternion rot = new Quaternion();
rot.fromAngles(FastMath.DEG_TO_RAD * rotXDeg, FastMath.DEG_TO_RAD * rotYDeg, FastMath.DEG_TO_RAD * rotZDeg);
group.setLocalRotation(rot);
}
Geometry marker = new Geometry(name + "M", new Box(markerSz, markerSz, markerSz));
marker.setMaterial(mat);
Geometry shaft = new Geometry(name + "S", new Cylinder(4, 8, shaftRad, shaftLen, true));
shaft.setMaterial(mat);
shaft.setLocalTranslation(0f, 0f, bladeDirZ * shaftLen * 0.5f);
Geometry head = new Geometry(name + "H", new Dome(Vector3f.ZERO, 2, 8, headRad, false));
head.setMaterial(matHead);
head.setLocalTranslation(0f, 0f, bladeDirZ * shaftLen);
Quaternion headRot = new Quaternion();
headRot.fromAngleAxis(bladeDirZ * FastMath.HALF_PI, Vector3f.UNIT_X);
head.setLocalRotation(headRot);
// Orientierungsstab +Y (oben): JME-Cylinder liegt auf Z, Rx(-90°) → +Y
Geometry gripAxis = new Geometry(name + "A", new Cylinder(4, 8, 0.006f, axisLen, true));
gripAxis.setMaterial(matAxis);
Quaternion axisRot = new Quaternion();
axisRot.fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X);
@@ -1603,8 +1951,7 @@ public class ModelEditorState extends BaseAppState {
group.attachChild(shaft);
group.attachChild(gripAxis);
group.attachChild(head);
gripGizmoNode.attachChild(group);
gripGizmoNode.setCullHint(Spatial.CullHint.Inherit);
return group;
}
/** Blendet den Grip-Gizmo ein oder aus. */

View File

@@ -646,6 +646,12 @@ public class AssetTreeController {
tt.play();
}
public void closeOverlayImmediate() {
assetOverlayOpen = false;
assetTabBtn.setText("›");
assetPanel.setTranslateX(-420);
}
// ── Asset-Tree-Cell (DnD + Kontextmenü) ──────────────────────────────────
private TreeCell<String> buildAssetCell() {

View File

@@ -113,6 +113,14 @@ public class ContentController {
/** Per-Frame-Polling: animEmbedStatus + animStripStatus → charEditorStatusLabel. */
public void onFrame() {
float guessZ = ctx.input().gripEditorGuessPosZ;
if (!Float.isNaN(guessZ)) {
float guessX = ctx.input().gripEditorGuessPosX;
float guessY = ctx.input().gripEditorGuessPosY;
ctx.input().gripEditorGuessPosZ = Float.NaN;
gripEditorController.applyGripGuess(guessX, guessY, guessZ);
}
String embedStatus = ctx.input().animEmbedStatus;
if (embedStatus != null) {
ctx.input().animEmbedStatus = null;
@@ -349,24 +357,22 @@ public class ContentController {
showContentEditor();
contentLastSection = "item";
if (contentItemBtn != null) contentItemBtn.setSelected(true);
de.blight.editor.ui.ItemEditorView view =
new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
wireItemView(view);
setContentCenter(view);
activeItemView = new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
wireItemView(activeItemView);
setContentCenter(activeItemView);
setContentRight(null);
if (selectItemId != null) view.selectItem(selectItemId);
if (selectItemId != null) activeItemView.selectItem(selectItemId);
return;
}
ctx.setCurrentTool().accept("itemEditor");
ToolBar tb = makeSingleWindowToolBar("Item-Manager");
ctx.setMainToolbar().accept(tb);
ctx.setLeftPanel().accept(buildContentTreePanel());
de.blight.editor.ui.ItemEditorView view =
new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
wireItemView(view);
ctx.setCenterView().accept(view);
activeItemView = new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
wireItemView(activeItemView);
ctx.setCenterView().accept(activeItemView);
ctx.setRightPanel().accept(null);
if (selectItemId != null) view.selectItem(selectItemId);
if (selectItemId != null) activeItemView.selectItem(selectItemId);
}
/** Prefs-Restore: stellt Dual-Window-Position und Content-Section wieder her. */

View File

@@ -23,6 +23,8 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
private Spinner<Double> posXSpin, posYSpin, posZSpin;
private Spinner<Double> rotXSpin, rotYSpin, rotZSpin;
private Spinner<Double> grip2XSpin, grip2YSpin, grip2ZSpin;
private Spinner<Double> handOffXSpin, handOffYSpin, handOffZSpin;
private Consumer<ItemGrip> pendingOnSave;
private Runnable onReturn;
@@ -31,6 +33,28 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
public void setOnReturn(Runnable r) { this.onReturn = r; }
/** Wird aus ContentController.onFrame() aufgerufen wenn JME Position und Richtung des Griffes ermittelt hat. */
public void applyGripGuess(float gX, float gY, float gZ) {
if (posXSpin == null) return;
suppressSpinnerEvents = true;
posXSpin.getValueFactory().setValue((double) gX);
posYSpin.getValueFactory().setValue((double) gY);
posZSpin.getValueFactory().setValue((double) gZ);
// Rotation zurücksetzen: Schaft zeigt zur Klinge, gelber Stab nach oben
rotXSpin.getValueFactory().setValue(0.0);
rotYSpin.getValueFactory().setValue(0.0);
rotZSpin.getValueFactory().setValue(0.0);
// 2. Griff (linke Hand): direkt angrenzend an den 1. Griff, 5cm weiter Richtung Pommel.
// Grip2-Spinner = Modell-Koordinaten direkt (kein HAND_GRIP_OFFSET), deshalb JSON-Pos von Grip1 verwenden.
float bladeDirZ = ctx.input().gripEditorBladeDirZ; // -1 = Klinge bei -Z, +1 = Klinge bei +Z
float grip1JsonZ = gZ + ItemGrip.HAND_GRIP_OFFSET_Z;
grip2XSpin.getValueFactory().setValue((double) gX);
grip2YSpin.getValueFactory().setValue((double) gY);
grip2ZSpin.getValueFactory().setValue((double)(grip1JsonZ - bladeDirZ * 0.05f));
suppressSpinnerEvents = false;
onSpinnerChanged();
}
public GripEditorController(ToolContext ctx) {
this.ctx = ctx;
}
@@ -47,13 +71,20 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
input.gripEditorIsShield = isShield;
// Grip-Werte aus Item übernehmen (oder Defaults)
// Spinner/Gizmo zeigen den Handflächen-Kontaktpunkt (= JSON-pos minus HAND_GRIP_OFFSET)
ItemGrip g = currentGrip != null ? currentGrip : new ItemGrip();
input.gripEditorPosX = g.getPosX();
input.gripEditorPosY = g.getPosY();
input.gripEditorPosZ = g.getPosZ();
input.gripEditorPosX = g.getPosX() - ItemGrip.HAND_GRIP_OFFSET_X;
input.gripEditorPosY = g.getPosY() - ItemGrip.HAND_GRIP_OFFSET_Y;
input.gripEditorPosZ = g.getPosZ() - ItemGrip.HAND_GRIP_OFFSET_Z;
input.gripEditorRotX = g.getRotX();
input.gripEditorRotY = g.getRotY();
input.gripEditorRotZ = g.getRotZ();
input.gripEditorGrip2PosX = g.getGrip2PosX();
input.gripEditorGrip2PosY = g.getGrip2PosY();
input.gripEditorGrip2PosZ = g.getGrip2PosZ();
input.gripEditorHandOffX = g.getHandOffsetX();
input.gripEditorHandOffY = g.getHandOffsetY();
input.gripEditorHandOffZ = g.getHandOffsetZ();
input.gripEditorGizmoDirty = true;
// Waffenmodell im Modell-Editor laden
@@ -61,12 +92,30 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
input.modelEditorOpenPath = modelPath;
}
input.activeLayer = SharedInput.LAYER_MODEL_EDITOR;
// JME vollständig aktivieren, damit LightProbe-Bake (8 Frames) nicht gedrosselt läuft
if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(false);
// Hauptfenster: JME-Viewport + Grip-Panel
ctx.setCenterView().accept(ctx.viewportSupplier().get());
ctx.setCurrentTool().accept("gripEditor");
ctx.setMainToolbar().accept(buildToolbar());
ctx.setRightPanel().accept(buildPanel(g, isShield));
VBox innerPanel = buildPanel(g, isShield);
ScrollPane scroll = new ScrollPane(innerPanel);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color:transparent;-fx-background:transparent;");
scroll.skinProperty().addListener((obs, o, n) -> {
javafx.scene.Node vp = scroll.lookup(".viewport");
if (vp != null) vp.setStyle("-fx-background-color: #252535;");
});
VBox outerPanel = new VBox(scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
outerPanel.setPrefWidth(220);
outerPanel.setMaxWidth(220);
outerPanel.setStyle("-fx-background-color: #252535;");
ctx.setRightPanel().accept(outerPanel);
ctx.setLeftPanel().accept(null);
ctx.setBottomPanel().accept(null);
}
@@ -86,12 +135,18 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
// ── Panel ─────────────────────────────────────────────────────────────────
private VBox buildPanel(ItemGrip g, boolean isShield) {
posXSpin = doubleSpin(g.getPosX(), -2.0, 2.0, 0.01, "m");
posYSpin = doubleSpin(g.getPosY(), -2.0, 2.0, 0.01, "m");
posZSpin = doubleSpin(g.getPosZ(), -2.0, 2.0, 0.01, "m");
posXSpin = doubleSpin(g.getPosX() - ItemGrip.HAND_GRIP_OFFSET_X, -2.0, 2.0, 0.01, "m");
posYSpin = doubleSpin(g.getPosY() - ItemGrip.HAND_GRIP_OFFSET_Y, -2.0, 2.0, 0.01, "m");
posZSpin = doubleSpin(g.getPosZ() - ItemGrip.HAND_GRIP_OFFSET_Z, -2.0, 2.0, 0.01, "m");
rotXSpin = doubleSpin(g.getRotX(), -180.0, 180.0, 1.0, "°");
rotYSpin = doubleSpin(g.getRotY(), -180.0, 180.0, 1.0, "°");
rotZSpin = doubleSpin(g.getRotZ(), -180.0, 180.0, 1.0, "°");
grip2XSpin = doubleSpin(g.getGrip2PosX(), -2.0, 2.0, 0.01, "m");
grip2YSpin = doubleSpin(g.getGrip2PosY(), -2.0, 2.0, 0.01, "m");
grip2ZSpin = doubleSpin(g.getGrip2PosZ(), -2.0, 2.0, 0.01, "m");
handOffXSpin = doubleSpin(g.getHandOffsetX(), -1.0, 1.0, 0.01, "m");
handOffYSpin = doubleSpin(g.getHandOffsetY(), -1.0, 1.0, 0.01, "m");
handOffZSpin = doubleSpin(g.getHandOffsetZ(), -1.0, 1.0, 0.01, "m");
posXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
posYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
@@ -99,12 +154,48 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
rotXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
rotYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
rotZSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
grip2XSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
grip2YSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
grip2ZSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
handOffXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
handOffYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
handOffZSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
Button saveBtn = new Button("Speichern");
saveBtn.setMaxWidth(Double.MAX_VALUE);
saveBtn.setStyle("-fx-background-color: #2a5a2a; -fx-text-fill: #aaffaa; -fx-cursor: hand; -fx-font-weight: bold;");
saveBtn.setOnAction(e -> onSave());
Button guessBtn = new Button("Griff raten");
guessBtn.setMaxWidth(Double.MAX_VALUE);
guessBtn.setStyle("-fx-background-color: #1a3a5a; -fx-text-fill: #88ccff; -fx-cursor: hand;");
guessBtn.setTooltip(new javafx.scene.control.Tooltip(
"Erkennt die Parierstange im Modell und setzt\n" +
"den Griff-Kontaktpunkt an diese Position.\n" +
"Voraussetzung: Klinge liegt auf der Z-Achse."));
guessBtn.setOnAction(e -> ctx.input().gripEditorGuessRequest = true);
Button resetBtn = new Button("Griff zurücksetzen");
resetBtn.setMaxWidth(Double.MAX_VALUE);
resetBtn.setStyle("-fx-background-color: #5a3a1a; -fx-text-fill: #ffcc88; -fx-cursor: hand;");
resetBtn.setOnAction(e -> {
suppressSpinnerEvents = true;
posXSpin.getValueFactory().setValue(0.0);
posYSpin.getValueFactory().setValue(0.0);
posZSpin.getValueFactory().setValue(0.0);
rotXSpin.getValueFactory().setValue(0.0);
rotYSpin.getValueFactory().setValue(0.0);
rotZSpin.getValueFactory().setValue(0.0);
grip2XSpin.getValueFactory().setValue(0.0);
grip2YSpin.getValueFactory().setValue(0.0);
grip2ZSpin.getValueFactory().setValue(0.0);
handOffXSpin.getValueFactory().setValue(0.0);
handOffYSpin.getValueFactory().setValue(0.0);
handOffZSpin.getValueFactory().setValue(0.0);
suppressSpinnerEvents = false;
onSpinnerChanged();
});
Button cancelBtn = new Button("Abbrechen");
cancelBtn.setMaxWidth(Double.MAX_VALUE);
cancelBtn.setStyle("-fx-background-color: #3a3a3a; -fx-text-fill: #cccccc; -fx-cursor: hand;");
@@ -116,21 +207,37 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
panel.setPrefWidth(220);
panel.getChildren().addAll(
styledTitle("Position (m)"),
styledTitle("1. Griff – Position (m)"),
styledHint("Handflächen-Kontaktpunkt im Modellraum (oranges Gizmo)."),
row("X:", posXSpin),
row("Y:", posYSpin),
row("Z:", posZSpin),
new Separator(),
styledTitle("Rotation (°)"),
styledTitle("Rotation Hand (°)"),
styledHint("Dreht den gesamten Griff-Gizmo. 0° = Hand oben (+Y), Klinge in -Z."),
row("X:", rotXSpin),
row("Y:", rotYSpin),
row("Z:", rotZSpin),
new Separator(),
styledTitle("2. Griff – Linke Hand (m)"),
styledHint("Zweiter Griffpunkt (blaues Gizmo) für Zweihänder.\nDirekt angrenzend an den 1. Griff, Richtung Knauf."),
row("X:", grip2XSpin),
row("Y:", grip2YSpin),
row("Z:", grip2ZSpin),
new Separator(),
styledTitle("Hand-Offset (Char-Space, m)"),
styledHint("Verschiebt die sichtbare Hand relativ zum Griff-Kontaktpunkt.\nX=rechts, Y=oben, Z=vorwärts."),
row("X:", handOffXSpin),
row("Y:", handOffYSpin),
row("Z:", handOffZSpin),
new Separator(),
styledHint(isShield
? "Pfeil zeigt vom Ellbogen zur Hand.\n28cm = Unterarmlänge."
: "Pfeil zeigt Klingenrichtung.\n15cm Länge = Maßstab."),
? "Cyan-Pfeil zeigt vom Ellbogen zur Hand.\n28cm = Unterarmlänge."
: "Orange-Pfeil zeigt zur Klinge, gelber Stab nach oben (+Y).\nBlauer Pfeil = 2. Griff (linke Hand)."),
new Separator(),
saveBtn,
guessBtn,
resetBtn,
cancelBtn
);
return panel;
@@ -147,6 +254,12 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
input.gripEditorRotX = rotXSpin.getValue().floatValue();
input.gripEditorRotY = rotYSpin.getValue().floatValue();
input.gripEditorRotZ = rotZSpin.getValue().floatValue();
input.gripEditorGrip2PosX = grip2XSpin.getValue().floatValue();
input.gripEditorGrip2PosY = grip2YSpin.getValue().floatValue();
input.gripEditorGrip2PosZ = grip2ZSpin.getValue().floatValue();
input.gripEditorHandOffX = handOffXSpin.getValue().floatValue();
input.gripEditorHandOffY = handOffYSpin.getValue().floatValue();
input.gripEditorHandOffZ = handOffZSpin.getValue().floatValue();
input.gripEditorGizmoDirty = true;
}
@@ -154,12 +267,19 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
private void onSave() {
ItemGrip grip = new ItemGrip();
grip.setPosX(posXSpin.getValue().floatValue());
grip.setPosY(posYSpin.getValue().floatValue());
grip.setPosZ(posZSpin.getValue().floatValue());
// Spinner zeigen Handflächen-Position → HAND_GRIP_OFFSET wieder addieren für JSON
grip.setPosX(posXSpin.getValue().floatValue() + ItemGrip.HAND_GRIP_OFFSET_X);
grip.setPosY(posYSpin.getValue().floatValue() + ItemGrip.HAND_GRIP_OFFSET_Y);
grip.setPosZ(posZSpin.getValue().floatValue() + ItemGrip.HAND_GRIP_OFFSET_Z);
grip.setRotX(rotXSpin.getValue().floatValue());
grip.setRotY(rotYSpin.getValue().floatValue());
grip.setRotZ(rotZSpin.getValue().floatValue());
grip.setGrip2PosX(grip2XSpin.getValue().floatValue());
grip.setGrip2PosY(grip2YSpin.getValue().floatValue());
grip.setGrip2PosZ(grip2ZSpin.getValue().floatValue());
grip.setHandOffsetX(handOffXSpin.getValue().floatValue());
grip.setHandOffsetY(handOffYSpin.getValue().floatValue());
grip.setHandOffsetZ(handOffZSpin.getValue().floatValue());
SharedInput input = ctx.input();
input.gripEditorActive = false;

View File

@@ -247,6 +247,7 @@ public class GameController {
Path runDir = ProjectRoot.PATH.resolve("run");
Path sessionDir = runDir.resolve("session-" + System.currentTimeMillis());
copyDirectory(srcMapDir, sessionDir);
in.activeSessionDir = sessionDir;
try {
java.util.List<Path> sessions;
@@ -310,6 +311,7 @@ public class GameController {
}
}
consoleBuffer.offer("--- Spiel beendet ---");
in.activeSessionDir = null;
Platform.runLater(() -> {
if (primary != null) primary.setIconified(false);
if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); }

View File

@@ -98,6 +98,7 @@ public class ModelEditorController implements ToolController {
modelEditorCurrentPath = relPath;
ctx.input().activeLayer = SharedInput.LAYER_MODEL_EDITOR;
if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(false);
ctx.setCenterView().accept(ctx.viewportSupplier().get());
openAssetOverlay.run();
@@ -181,6 +182,13 @@ public class ModelEditorController implements ToolController {
if (modelEditorSpinZ != null) modelEditorSpinZ.getValueFactory().setValue(bsZ);
}
// Align-Weapon-Ergebnis anzeigen
String alignResult = ctx.input().modelEditorAlignWeaponResult;
if (alignResult != null) {
ctx.input().modelEditorAlignWeaponResult = null;
ctx.setStatus().accept("Ausgerichtet: " + alignResult);
}
// Bounds-Aktualisierung
if (ctx.input().modelEditorBoundsReady) {
ctx.input().modelEditorBoundsReady = false;
@@ -811,7 +819,13 @@ public class ModelEditorController implements ToolController {
closeBtn.setMaxWidth(Double.MAX_VALUE);
closeBtn.setStyle("-fx-background-color:#555; -fx-text-fill:#ccc;");
saveBtn.setOnAction(e -> saveModelMeta(
saveBtn.setOnAction(e -> {
if (modelEditorSpinX != null) {
modelEditorSpinX.commitValue();
modelEditorSpinY.commitValue();
modelEditorSpinZ.commitValue();
}
saveModelMeta(
relPath, absolutePath,
nameTF.getText().trim(),
catCB.getValue() != null ? catCB.getValue().trim() : "",
@@ -843,7 +857,8 @@ public class ModelEditorController implements ToolController {
: "",
modelEditorCategoryCB != null
? modelEditorCategoryCB.getValue()
: de.blight.common.ModelCategory.NONE));
: de.blight.common.ModelCategory.NONE);
});
placeBtn.setOnAction(e -> {
ctx.input().modelEditorCloseRequest = true;
@@ -858,6 +873,35 @@ public class ModelEditorController implements ToolController {
ctx.switchToWorld().run();
});
// Waffe an Z-Achse ausrichten
Label toolsTitle = new Label("Werkzeuge:");
toolsTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;");
Button alignWeaponBtn = new Button("Waffe an Z-Achse ausrichten");
alignWeaponBtn.setMaxWidth(Double.MAX_VALUE);
alignWeaponBtn.setStyle("-fx-background-color:#3a3a5a; -fx-text-fill:#ddddff; -fx-cursor:hand;");
alignWeaponBtn.setTooltip(new Tooltip(
"Dreht das Modell so, dass die längste Achse auf +Z zeigt.\n" +
"Verschiebt die Mitte der Waffe in den Ursprung (0,0,0).\n" +
"Bäckt das Ergebnis in die Vertex-Positionen ein."));
alignWeaponBtn.setOnAction(e -> {
if (absolutePath == null) return;
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
confirm.initOwner(ctx.stageSupplier().get());
confirm.setTitle("Waffe ausrichten");
confirm.setHeaderText("Modell an Z-Achse ausrichten?");
confirm.setContentText(
"Die längste Achse der Waffe wird auf +Z ausgerichtet\n" +
"und die Mitte in den Ursprung gesetzt.\n\n" +
"Die Transformation wird direkt in die Vertex-\n" +
"Positionen eingebrannt. Nicht rückgängig machbar.");
confirm.showAndWait().ifPresent(btn -> {
if (btn == ButtonType.OK) {
ctx.input().modelEditorAlignWeaponPath = absolutePath;
ctx.input().modelEditorAlignWeaponRequest = true;
}
});
});
panel.getChildren().addAll(
title,
new Separator(),
@@ -889,6 +933,8 @@ public class ModelEditorController implements ToolController {
new Separator(),
footstepTitle, modelEditorFootstepSurfaceCB,
new Separator(),
toolsTitle, alignWeaponBtn,
new Separator(),
saveBtn, placeBtn, closeBtn
);
return panel;
@@ -972,6 +1018,7 @@ public class ModelEditorController implements ToolController {
ctx.setMainToolbar().accept(buildModelImportToolBar());
ctx.input().activeLayer = SharedInput.LAYER_MODEL_EDITOR;
if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(false);
ctx.setCenterView().accept(ctx.viewportSupplier().get());
openAssetOverlay.run();
@@ -1291,6 +1338,14 @@ public class ModelEditorController implements ToolController {
s.setEditable(true);
s.setMaxWidth(Double.MAX_VALUE);
s.setStyle("-fx-background-color:#3a3a4e;");
// Tab-Filter: feuert in der Filter-Phase, bevor FocusTraversalInputMap den Fokus verschiebt.
// Damit ist getText() noch der getippte Wert, nicht der bereits zurückgesetzte.
s.addEventFilter(javafx.scene.input.KeyEvent.KEY_PRESSED, ev -> {
if (ev.getCode() == javafx.scene.input.KeyCode.TAB) s.commitValue();
});
s.focusWithinProperty().addListener((obs, was, now) -> {
if (!now) s.commitValue();
});
return s;
}

View File

@@ -11,6 +11,7 @@ import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
import de.blight.common.model.Item;
import de.blight.common.model.ItemGrip;
import de.blight.common.model.ItemSubCategory;
import de.blight.game.animation.RetargetingSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -28,11 +29,13 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
private static final String BONE_HAND = "mixamorig:RightHand";
private static final String BONE_PALM = "mixamorig:RightHandMiddle1";
private static final String BONE_LEFT_FOREARM = "mixamorig:LeftHand";
private static final float DEG_TO_RAD = (float) Math.PI / 180f;
private static final float HALF_PI = (float) Math.PI * 0.5f;
private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.13f);
private static final Vector3f HAND_GRIP_OFFSET = new Vector3f(0.1f, 0.00f, 0.05f);
private static final Vector3f HAND_GRIP_OFFSET = new Vector3f(
ItemGrip.HAND_GRIP_OFFSET_X, ItemGrip.HAND_GRIP_OFFSET_Y, ItemGrip.HAND_GRIP_OFFSET_Z);
private static final Vector3f SHIELD_BACK_OFFSET = new Vector3f(0.12f, -0.10f, -0.10f);
private static final float SHIELD_ARM_ROT_X = -HALF_PI;
@@ -97,7 +100,6 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
} catch (Exception e) { log.warn("[NpcWeaponAttach] Bone '{}' nicht gefunden: {}", BONE_PALM, e.getMessage()); }
try { leftForeArmNode = skinCtrl.getAttachmentsNode(BONE_LEFT_FOREARM); }
catch (Exception e) { log.warn("[NpcWeaponAttach] Bone '{}' nicht gefunden: {}", BONE_LEFT_FOREARM, e.getMessage()); }
handBindRotInChar = extractJointBindRot(skinCtrl, BONE_HAND);
leftForeArmBindRotInChar = extractJointBindRot(skinCtrl, BONE_LEFT_FOREARM);
@@ -126,7 +128,6 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
palmOffsetLocal = computePalmOffset(handNode, palmNodeRef);
}
}
Quaternion charWorldRotInv = spatial.getWorldRotation().inverse();
Vector3f charWorldPos = spatial.getWorldTranslation();
@@ -137,11 +138,23 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
Vector3f boneInChar = charWorldRotInv.mult(boneNode.getWorldTranslation().subtract(charWorldPos));
Quaternion boneRotInChar = charWorldRotInv.mult(boneNode.getWorldRotation());
if (drawn) {
ItemGrip wGrip = weaponItem != null ? weaponItem.getGrip() : null;
boolean isTwoHand = weaponItem != null && weaponItem.getSubCategory() == ItemSubCategory.TWO_HANDED_SWORD;
Vector3f attachInChar = (palmOffsetLocal != null)
? boneInChar.add(boneRotInChar.mult(palmOffsetLocal.mult(0.5f)))
: boneInChar;
applyHandTransform(weaponSpatial, attachInChar, boneRotInChar,
weaponItem != null ? weaponItem.getGrip() : null);
boolean usesTwoHand = isTwoHand && leftForeArmNode != null
&& wGrip != null
&& (wGrip.getGrip2PosX() != 0f || wGrip.getGrip2PosY() != 0f || wGrip.getGrip2PosZ() != 0f);
if (usesTwoHand) {
Vector3f handballen = (palmOffsetLocal != null)
? boneInChar.subtract(boneRotInChar.mult(palmOffsetLocal.mult(0.5f)))
: boneInChar;
applyTwoHandTransform(weaponSpatial, handballen, boneRotInChar,
wGrip, charWorldRotInv, charWorldPos);
} else {
applyHandTransform(weaponSpatial, attachInChar, boneRotInChar, wGrip);
}
} else {
applyBackTransform(weaponSpatial, boneInChar, boneRotInChar,
weaponItem != null ? weaponItem.getGrip() : null);
@@ -195,22 +208,92 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
Quaternion animDelta = (handBindRotInChar != null)
? boneRotInChar.mult(handBindRotInChar.inverse())
: boneRotInChar;
Quaternion weaponRot = animDelta.mult(gripRot.inverse());
boolean bladeAtMinusZ = grip != null && grip.getGrip2PosZ() != 0f && grip.getGrip2PosZ() > grip.getPosZ();
Quaternion bladeFlip = bladeAtMinusZ
? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f)
: new Quaternion();
Quaternion weaponRot = animDelta.mult(bladeFlip).mult(gripRot.inverse());
Vector3f handOff = bladeAtMinusZ
? new Vector3f(HAND_GRIP_OFFSET.x, HAND_GRIP_OFFSET.y, -HAND_GRIP_OFFSET.z)
: HAND_GRIP_OFFSET;
Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(gripPos));
weaponPos = weaponPos.add(weaponRot.mult(HAND_GRIP_OFFSET));
weaponPos = weaponPos.add(weaponRot.mult(handOff));
s.setLocalRotation(weaponRot);
s.setLocalTranslation(weaponPos);
}
/**
* Zweihänder: Schwert dreht sich frei um Griff1 (rechte Handteller-Innenfläche) als Pivot;
* die Richtung wird so gewählt, dass Griff2 so nah wie möglich am linken Handteller liegt.
*/
private void applyTwoHandTransform(Spatial s, Vector3f bonePosInChar, Quaternion boneRotInChar,
ItemGrip grip, Quaternion charWorldRotInv, Vector3f charWorldPos) {
Quaternion gripRot = new Quaternion().fromAngles(
DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ());
Quaternion animDelta = (handBindRotInChar != null)
? boneRotInChar.mult(handBindRotInChar.inverse()) : boneRotInChar;
boolean bladeAtMinusZ3 = grip.getGrip2PosZ() > grip.getPosZ();
Quaternion bladeFlip3 = bladeAtMinusZ3
? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f)
: new Quaternion();
Quaternion weaponRot = animDelta.mult(bladeFlip3).mult(gripRot.inverse());
// Griff-Achse im Modell-Space: Griff1 → Griff2
Vector3f g1 = new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ());
Vector3f g2 = new Vector3f(grip.getGrip2PosX(), grip.getGrip2PosY(), grip.getGrip2PosZ());
Vector3f modelGripAxis = g2.subtract(g1).normalizeLocal();
Vector3f currentGripAxis = weaponRot.mult(modelGripAxis);
// Zielachse: rechtes Handgelenk → linkes Handgelenk im Char-Space
Vector3f rightWristInChar = charWorldRotInv.mult(handNode.getWorldTranslation().subtract(charWorldPos));
Vector3f leftWristInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos));
Vector3f targetGripAxis = leftWristInChar.subtract(rightWristInChar).normalizeLocal();
Quaternion swingCorr = swingFromTo(currentGripAxis, targetGripAxis);
weaponRot = swingCorr.mult(weaponRot);
// Grip1 direkt am Handteller platzieren (bonePosInChar enthält palmOffset bereits).
// Kein zusätzlicher HAND_GRIP_OFFSET – wir haben die exakten Greifpositionen.
Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(g1));
s.setLocalRotation(weaponRot);
s.setLocalTranslation(weaponPos);
}
private static Quaternion swingFromTo(Vector3f from, Vector3f to) {
float dot = com.jme3.math.FastMath.clamp(from.dot(to), -1f, 1f);
Vector3f axis = from.cross(to);
float axisLen = axis.length();
if (axisLen < 0.001f) {
if (dot > 0f) return new Quaternion();
Vector3f perp = Math.abs(from.x) <= Math.abs(from.y)
? from.cross(Vector3f.UNIT_X).normalizeLocal()
: from.cross(Vector3f.UNIT_Y).normalizeLocal();
return new Quaternion().fromAngleNormalAxis(com.jme3.math.FastMath.PI, perp);
}
return new Quaternion().fromAngleNormalAxis(com.jme3.math.FastMath.acos(dot), axis.divideLocal(axisLen));
}
private static final Quaternion BACK_ROT_NORM = new Quaternion(0f, -(float) Math.sqrt(0.5), (float) Math.sqrt(0.5), 0f);
private void applyBackTransform(Spatial s, Vector3f boneInChar,
Quaternion boneRotInChar, ItemGrip grip) {
Quaternion backRot = new Quaternion().fromAngles(0f, 0f, HALF_PI);
Quaternion weaponRot = boneRotInChar.mult(backRot);
Quaternion gripRot = (grip != null)
? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(),
DEG_TO_RAD * grip.getRotY(),
DEG_TO_RAD * grip.getRotZ())
: new Quaternion();
boolean bladeAtMinusZ2 = grip != null && grip.getGrip2PosZ() != 0f && grip.getGrip2PosZ() > grip.getPosZ();
Quaternion bladeFlip2 = bladeAtMinusZ2
? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f)
: new Quaternion();
Quaternion weaponRot = boneRotInChar.mult(BACK_ROT_NORM).mult(bladeFlip2).mult(gripRot.inverse());
Vector3f target = boneInChar.add(boneRotInChar.mult(BACK_GRIP_OFFSET));
Vector3f gripPos = grip != null
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO;
Vector3f backPos = target.subtract(weaponRot.mult(gripPos));
if (bladeAtMinusZ2) backPos = backPos.add(0.1f, 0f, 0f);
s.setLocalRotation(weaponRot);
s.setLocalTranslation(target.subtract(weaponRot.mult(gripPos)));
s.setLocalTranslation(backPos);
}
// ── Schild-Transforms (1:1 von WeaponAttachmentControl) ──────────────────────

View File

@@ -11,6 +11,7 @@ import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
import de.blight.common.model.Item;
import de.blight.common.model.ItemGrip;
import de.blight.common.model.ItemSubCategory;
import de.blight.common.model.MainCharacter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -31,12 +32,13 @@ public class WeaponAttachmentControl extends AbstractControl {
private static final String BONE_HAND = "mixamorig:RightHand";
private static final String BONE_PALM = "mixamorig:RightHandMiddle1";
private static final String BONE_LEFT_FOREARM = "mixamorig:LeftHand";
private static final float DEG_TO_RAD = (float) Math.PI / 180f;
private static final float HALF_PI = (float) Math.PI * 0.5f;
private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.13f);
/** Zusatz-Versatz beim gezückten Schwert in lokalem Waffen-Space (X=entlang Klinge, Y=entlang Griff, Z=senkrecht zur Schneide). */
private static final Vector3f HAND_GRIP_OFFSET = new Vector3f(0.1f, 0.0f, 0.05f);
private static final Vector3f HAND_GRIP_OFFSET = new Vector3f(
ItemGrip.HAND_GRIP_OFFSET_X, ItemGrip.HAND_GRIP_OFFSET_Y, ItemGrip.HAND_GRIP_OFFSET_Z);
/** Schild am Rücken: Griff unterhalb der Schultern, parallel zum Boden, Schild über dem Schwert. */
private static final Vector3f SHIELD_BACK_OFFSET = new Vector3f(0.12f, -0.10f, -0.10f);
@@ -126,7 +128,6 @@ public class WeaponAttachmentControl extends AbstractControl {
log.info("[WeaponAttach] palmOffsetLocal (from nodes)={}", palmOffsetLocal);
}
}
Item current = mc.getEquippedMeleeWeapon();
Item shield = mc.getEquippedShield();
boolean drawn = playerInput.isWeaponDrawn();
@@ -139,7 +140,7 @@ public class WeaponAttachmentControl extends AbstractControl {
if (weaponSpatial != null) {
lastDrawnState = drawn;
updateWeaponTransform(drawn, current != null ? current.getGrip() : null);
updateWeaponTransform(drawn, current);
}
if (shield != lastEquippedShield) {
@@ -173,7 +174,8 @@ public class WeaponAttachmentControl extends AbstractControl {
// ── Transform-Update (jedes Frame) ────────────────────────────────────────
private void updateWeaponTransform(boolean drawn, ItemGrip grip) {
private void updateWeaponTransform(boolean drawn, Item item) {
ItemGrip grip = item != null ? item.getGrip() : null;
Node boneNode = drawn ? handNode : backNode;
if (boneNode == null) return;
@@ -185,10 +187,22 @@ public class WeaponAttachmentControl extends AbstractControl {
Quaternion boneRotInChar = charWorldRotInv.mult(boneNode.getWorldRotation());
if (drawn) {
boolean isTwoHand = item != null && item.getSubCategory() == ItemSubCategory.TWO_HANDED_SWORD;
Vector3f attachInChar = (palmOffsetLocal != null)
? boneInChar.add(boneRotInChar.mult(palmOffsetLocal.mult(0.5f)))
: boneInChar;
boolean usesTwoHand = isTwoHand && leftForeArmNode != null
&& grip != null
&& (grip.getGrip2PosX() != 0f || grip.getGrip2PosY() != 0f || grip.getGrip2PosZ() != 0f);
if (usesTwoHand) {
// Handballen: entgegengesetzte Richtung von palmOffset (der Richtung Finger zeigt)
Vector3f handballen = (palmOffsetLocal != null)
? boneInChar.subtract(boneRotInChar.mult(palmOffsetLocal.mult(0.5f)))
: boneInChar;
applyTwoHandTransform(handballen, boneRotInChar, grip, charWorldRotInv, charWorldPos);
} else {
applyHandTransform(attachInChar, boneRotInChar, grip);
}
} else {
applyBackTransform(boneInChar, boneRotInChar, grip);
}
@@ -205,7 +219,11 @@ public class WeaponAttachmentControl extends AbstractControl {
Quaternion animDelta = (handBindRotInChar != null)
? boneRotInChar.mult(handBindRotInChar.inverse())
: boneRotInChar;
Quaternion weaponRot = animDelta.mult(gripRot.inverse());
boolean bladeAtMinusZ2 = grip != null && grip.getGrip2PosZ() != 0f && grip.getGrip2PosZ() > grip.getPosZ();
Quaternion bladeFlip2 = bladeAtMinusZ2
? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f)
: new Quaternion();
Quaternion weaponRot = animDelta.mult(bladeFlip2).mult(gripRot.inverse());
if (!gripDiagLogged && weaponSpatial != null) {
gripDiagLogged = true;
@@ -240,12 +258,74 @@ public class WeaponAttachmentControl extends AbstractControl {
}
}
Vector3f handOff2 = bladeAtMinusZ2
? new Vector3f(HAND_GRIP_OFFSET.x, HAND_GRIP_OFFSET.y, -HAND_GRIP_OFFSET.z)
: HAND_GRIP_OFFSET;
Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(gripPos));
weaponPos = weaponPos.add(weaponRot.mult(HAND_GRIP_OFFSET));
weaponPos = weaponPos.add(weaponRot.mult(handOff2));
if (grip != null && (grip.getHandOffsetX() != 0f || grip.getHandOffsetY() != 0f || grip.getHandOffsetZ() != 0f)) {
weaponPos = weaponPos.add(grip.getHandOffsetX(), grip.getHandOffsetY(), grip.getHandOffsetZ());
}
weaponSpatial.setLocalRotation(weaponRot);
weaponSpatial.setLocalTranslation(weaponPos);
}
/**
* Zweihänder-Constraint: orientiert die Waffe so, dass die Linie grip1→grip2 (Modell-Space)
* auf die Linie RightHand→LeftHand (Char-Space) zeigt. Roll bleibt aus gripRot erhalten.
*/
private void applyTwoHandTransform(Vector3f bonePosInChar, Quaternion boneRotInChar,
ItemGrip grip,
Quaternion charWorldRotInv, Vector3f charWorldPos) {
// Erst Standard-Einhand-Rotation berechnen (gibt Roll-Basis aus gripRot)
Quaternion gripRot = new Quaternion().fromAngles(
DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ());
Quaternion animDelta = (handBindRotInChar != null)
? boneRotInChar.mult(handBindRotInChar.inverse()) : boneRotInChar;
boolean bladeAtMinusZ3 = grip.getGrip2PosZ() > grip.getPosZ();
Quaternion bladeFlip3 = bladeAtMinusZ3
? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f)
: new Quaternion();
Quaternion weaponRot = animDelta.mult(bladeFlip3).mult(gripRot.inverse());
// Griff-Achse im Modell-Space: grip1 → grip2
Vector3f g1 = new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ());
Vector3f g2 = new Vector3f(grip.getGrip2PosX(), grip.getGrip2PosY(), grip.getGrip2PosZ());
Vector3f modelGripAxis = g2.subtract(g1).normalizeLocal();
// Aktuelle Griff-Achse im Char-Space (nach Einhand-Rotation)
Vector3f currentGripAxis = weaponRot.mult(modelGripAxis);
// Ziel-Achse: rechtes Handgelenk → linkes Handgelenk im Char-Space
Vector3f rightWristInChar = charWorldRotInv.mult(handNode.getWorldTranslation().subtract(charWorldPos));
Vector3f leftWristInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos));
Vector3f targetGripAxis = leftWristInChar.subtract(rightWristInChar).normalizeLocal();
// Minimale Swing-Rotation: dreht currentGripAxis → targetGripAxis, kein Roll-Einfluss
Quaternion swingCorr = swingFromTo(currentGripAxis, targetGripAxis);
weaponRot = swingCorr.mult(weaponRot);
// Grip1 direkt am Handteller platzieren (bonePosInChar enthält palmOffset bereits).
// Kein zusätzlicher HAND_GRIP_OFFSET – wir haben die exakten Greifpositionen.
Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(g1));
weaponSpatial.setLocalRotation(weaponRot);
weaponSpatial.setLocalTranslation(weaponPos);
}
private static Quaternion swingFromTo(Vector3f from, Vector3f to) {
float dot = com.jme3.math.FastMath.clamp(from.dot(to), -1f, 1f);
Vector3f axis = from.cross(to);
float axisLen = axis.length();
if (axisLen < 0.001f) {
if (dot > 0f) return new Quaternion();
Vector3f perp = Math.abs(from.x) <= Math.abs(from.y)
? from.cross(Vector3f.UNIT_X).normalizeLocal()
: from.cross(Vector3f.UNIT_Y).normalizeLocal();
return new Quaternion().fromAngleNormalAxis(com.jme3.math.FastMath.PI, perp);
}
return new Quaternion().fromAngleNormalAxis(com.jme3.math.FastMath.acos(dot), axis.divideLocal(axisLen));
}
private static float round1(float v) { return Math.round(v * 10f) / 10f; }
/**
@@ -273,15 +353,32 @@ public class WeaponAttachmentControl extends AbstractControl {
return bindMat.toRotationQuat().normalizeLocal();
}
// backRot normalisiert: fromAngles(0,0,PI/2) * kurzschwert_gripRot
// → Waffe wird zuerst per gripRot^-1 normalisiert, dann per backRot am Rücken ausgerichtet.
// Ergebnis für Kurzschwert (gripRot=fromAngles(0,-90°,90°)) identisch zur alten Formel.
private static final Quaternion BACK_ROT_NORM = new Quaternion(0f, -(float) Math.sqrt(0.5), (float) Math.sqrt(0.5), 0f);
private void applyBackTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
Quaternion backRot = new Quaternion().fromAngles(0f, 0f, HALF_PI);
Quaternion weaponRot = boneRotInChar.mult(backRot);
Quaternion gripRot = (grip != null)
? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(),
DEG_TO_RAD * grip.getRotY(),
DEG_TO_RAD * grip.getRotZ())
: new Quaternion();
// Klinge bei -Z erkannt (grip2PosZ > posZ) → 180° um X vorschalten, damit BACK_ROT_NORM
// dieselbe Wirkung hat wie bei korrekt ausgerichtetem Modell (Klinge bei +Z).
boolean bladeAtMinusZ = grip != null && grip.getGrip2PosZ() != 0f && grip.getGrip2PosZ() > grip.getPosZ();
Quaternion bladeFlip = bladeAtMinusZ
? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f)
: new Quaternion();
Quaternion weaponRot = boneRotInChar.mult(BACK_ROT_NORM).mult(bladeFlip).mult(gripRot.inverse());
Vector3f gripTarget = bonePosInChar.add(boneRotInChar.mult(BACK_GRIP_OFFSET));
Vector3f gripPos = (grip != null)
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
: Vector3f.ZERO;
Vector3f backPos = gripTarget.subtract(weaponRot.mult(gripPos));
if (bladeAtMinusZ) backPos = backPos.add(0.1f, 0f, 0f);
weaponSpatial.setLocalRotation(weaponRot);
weaponSpatial.setLocalTranslation(gripTarget.subtract(weaponRot.mult(gripPos)));
weaponSpatial.setLocalTranslation(backPos);
}
// ── Schild ────────────────────────────────────────────────────────────────

View File

@@ -285,3 +285,4 @@ npc.aggro.death.5=Das nächste Mal bleib, wo du hingehörst.
npc.bandit1.name=Bandit
npc.bjoernson.name=Bjoernson
npc.silas.name=Silas
rostiger_zweihaender.name=Rostiger Zweihaender

View File

@@ -140,3 +140,4 @@ npc.aggro.death.5=Next time, know your place.
npc.bandit1.name=Bandit
npc.bjoernson.name=Bjoernson
npc.silas.name=Silas
rostiger_zweihaender.name=Rostiger Zweihaender

View File

@@ -144,6 +144,8 @@ neues_item_1789920920760.description
neues_item_1789920920760.name
neues_item_1789920asdf920760.description
neues_item_1789920asdf920760.name
neues_item_1791316630215.description
neues_item_1791316630215.name
quest.blight_wuehler.description
quest.blight_wuehler.name
quest.blight_wuehler.success
@@ -153,6 +155,8 @@ quest.kuehles_bier.success
quest.magnetiterz.description
quest.magnetiterz.name
quest.magnetiterz.success
rostiger_zweihaender.description
rostiger_zweihaender.name
rostiges_kurzschwert.description
rostiges_kurzschwert.name
silas.name

View File

@@ -1,3 +1,4 @@
# uuid itemId x y z rotX rotY rotZ
fd832b8d-021a-4136-be0b-4ec1cb4f863c rostiges_kurzschwert -2.46281 2.00000 -2.27693 0.00000 0.00000 0.00000
feebed68-92b8-4458-9c7c-b1b781c1eba5 einfacher_schild -4.45856 1.72895 -3.67467 -0.14178 0.64812 0.00000
caa6cd73-4a2d-4112-9e22-b4d250ea41b0 rostiges_kurzschwert -2.46281 2.00000 -2.27693 0.00000 0.00000 0.00000
4fa26305-9741-4c8f-856d-20242b613486 einfacher_schild -4.45856 1.72895 -3.67467 -0.14178 0.64812 0.00000
89bd439c-b4d9-4fa9-8886-4ba51e19d244 rostiger_zweihaender 0.02061 2.00000 -10.13625 -1.58565 0.00000 0.00000