Kampfsystem, hier: Positionierung des Schwertes, weitere Features für die Griffe ergänzt
This commit is contained in:
BIN
assets/imported/models/rostiger+zweihaender.glb
Normal file
BIN
assets/imported/models/rostiger+zweihaender.glb
Normal file
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 |
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
@@ -242,12 +263,15 @@ public class ModelEditorState extends BaseAppState {
|
||||
// Grip-Gizmo aktualisieren
|
||||
if (input.gripEditorActive && input.gripEditorGizmoDirty) {
|
||||
input.gripEditorGizmoDirty = false;
|
||||
gripPosX = input.gripEditorPosX;
|
||||
gripPosY = input.gripEditorPosY;
|
||||
gripPosZ = input.gripEditorPosZ;
|
||||
gripRotX = input.gripEditorRotX;
|
||||
gripRotY = input.gripEditorRotY;
|
||||
gripRotZ = input.gripEditorRotZ;
|
||||
gripPosX = input.gripEditorPosX;
|
||||
gripPosY = input.gripEditorPosY;
|
||||
gripPosZ = input.gripEditorPosZ;
|
||||
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;
|
||||
@@ -728,14 +767,17 @@ public class ModelEditorState extends BaseAppState {
|
||||
gripGizmoNode.setCullHint(Spatial.CullHint.Always);
|
||||
previewRoot.attachChild(gripGizmoNode);
|
||||
|
||||
gripPosX = input.gripEditorPosX;
|
||||
gripPosY = input.gripEditorPosY;
|
||||
gripPosZ = input.gripEditorPosZ;
|
||||
gripRotX = input.gripEditorRotX;
|
||||
gripRotY = input.gripEditorRotY;
|
||||
gripRotZ = input.gripEditorRotZ;
|
||||
gripPosX = input.gripEditorPosX;
|
||||
gripPosY = input.gripEditorPosY;
|
||||
gripPosZ = input.gripEditorPosZ;
|
||||
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,15 +1260,20 @@ 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)) {
|
||||
try {
|
||||
java.nio.file.Files.copy(savedFile, mirror,
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
log.info("[ModelEditor] Spiegel aktualisiert: {}", mirror);
|
||||
} catch (Exception e) {
|
||||
log.warn("[ModelEditor] Spiegel-Update fehlgeschlagen ({}): {}", mirror, e.getMessage());
|
||||
}
|
||||
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);
|
||||
} catch (Exception e) {
|
||||
log.warn("[ModelEditor] Spiegel-Update fehlgeschlagen ({}): {}", mirror, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
float shaftLen = shieldMode ? 0.24f : 0.12f;
|
||||
float shaftRad = shieldMode ? 0.010f : 0.008f;
|
||||
float headRad = shieldMode ? 0.026f : 0.022f;
|
||||
float axisLen = shieldMode ? 0.08f : 0.06f;
|
||||
float markerSz = shieldMode ? 0.018f : 0.015f;
|
||||
float bladeDirZ = input.gripEditorBladeDirZ;
|
||||
|
||||
// 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;
|
||||
// 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));
|
||||
|
||||
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 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));
|
||||
}
|
||||
|
||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
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. */
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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.gripEditorRotX = g.getRotX();
|
||||
input.gripEditorRotY = g.getRotY();
|
||||
input.gripEditorRotZ = g.getRotZ();
|
||||
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");
|
||||
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, "°");
|
||||
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;
|
||||
@@ -141,12 +248,18 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
private void onSpinnerChanged() {
|
||||
if (suppressSpinnerEvents) return;
|
||||
SharedInput input = ctx.input();
|
||||
input.gripEditorPosX = posXSpin.getValue().floatValue();
|
||||
input.gripEditorPosY = posYSpin.getValue().floatValue();
|
||||
input.gripEditorPosZ = posZSpin.getValue().floatValue();
|
||||
input.gripEditorRotX = rotXSpin.getValue().floatValue();
|
||||
input.gripEditorRotY = rotYSpin.getValue().floatValue();
|
||||
input.gripEditorRotZ = rotZSpin.getValue().floatValue();
|
||||
input.gripEditorPosX = posXSpin.getValue().floatValue();
|
||||
input.gripEditorPosY = posYSpin.getValue().floatValue();
|
||||
input.gripEditorPosZ = posZSpin.getValue().floatValue();
|
||||
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;
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) ──────────────────────
|
||||
|
||||
@@ -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;
|
||||
applyHandTransform(attachInChar, boneRotInChar, grip);
|
||||
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)
|
||||
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 ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user