Compare commits

...

10 Commits

92 changed files with 2825 additions and 293 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -37,7 +37,30 @@
"Sword And Shield Idle boring 3", "Sword And Shield Idle boring 3",
"Sword And Shield Jump Running", "Sword And Shield Jump Running",
"Sword And Shield Jump", "Sword And Shield Jump",
"Sword And Shield Slash" "Sword And Shield Slash",
"Draw A Great Sword",
"Great Sword Attack 1",
"Great Sword Attack 2",
"Great Sword Attack 3",
"Great Sword Block Impact",
"Great Sword Blocking Idle",
"Great Sword Blocking to Idle",
"Great Sword Heavy Attack",
"Great Sword Idle",
"Great Sword Idle boring",
"Great Sword Idle boring 2",
"Great Sword Idle boring 3",
"Great Sword Idle to Blocking",
"Great Sword Impact",
"Great Sword Jump",
"Great Sword Parade",
"Great Sword Run",
"Great Sword Running Jump",
"Great Sword Strafe Right",
"Great Sword Strafe left",
"Great Sword Walk",
"Great Sword Walk backwards",
"Sheath A Great Sword"
], ],
"actionMap": { "actionMap": {
"DEFAULT": "Idle", "DEFAULT": "Idle",
@@ -82,7 +105,32 @@
"SWORD_IDLE_BORED": "Sword And Shield Idle boring", "SWORD_IDLE_BORED": "Sword And Shield Idle boring",
"SWORD_IDLE_BORED_2": "Sword And Shield Idle boring 2", "SWORD_IDLE_BORED_2": "Sword And Shield Idle boring 2",
"SWORD_IDLE_BORED_3": "Sword And Shield Idle boring 3", "SWORD_IDLE_BORED_3": "Sword And Shield Idle boring 3",
"STAND_TO_CROUCH": "Standing To Crouched" "STAND_TO_CROUCH": "Standing To Crouched",
"TWO_HAND_DRAW": "Draw A Great Sword",
"TWO_HAND_SHEATHE": "Sheath A Great Sword",
"TWO_HAND_BLOCK": "Great Sword Blocking Idle",
"TWO_HAND_ATTACK_1": "Great Sword Attack 1",
"TWO_HAND_ATTACK_2": "Great Sword Attack 2",
"TWO_HAND_ATTACK_3": "Great Sword Attack 3",
"TWO_HAND_HEAVY_ATTACK": "Great Sword Heavy Attack",
"TWO_HAND_IDLE_TO_BLOCK": "Great Sword Idle to Blocking",
"TWO_HAND_PARADE": "Great Sword Parade",
"TWO_HAND_BLOCK_IMPACT": "Great Sword Block Impact",
"TWO_HAND_IDLE_IMPACT": "Great Sword Impact",
"TWO_HAND_IDLE": "Great Sword Idle",
"TWO_HAND_WALK": "Great Sword Walk",
"TWO_HAND_RUN": "Great Sword Run",
"TWO_HAND_BLOCK_MOVE": "Great Sword Walk",
"TWO_HAND_BLOCK_MOVE_BACKWARD": "Great Sword Walk backwards",
"TWO_HAND_BLOCK_STRAFE_LEFT": "Great Sword Strafe left",
"TWO_HAND_BLOCK_STRAFE_RIGHT": "Great Sword Strafe Right",
"TWO_HAND_JUMP": "Great Sword Jump",
"TWO_HAND_IDLE_BORED": "Great Sword Idle boring",
"TWO_HAND_IDLE_BORED_2": "Great Sword Idle boring 2",
"TWO_HAND_IDLE_BORED_3": "Great Sword Idle boring 3",
"TWO_HAND_DEATH": "Sword And Shield Death",
"TWO_HAND_DEATH_2": "Sword And Shield Death 2",
"TWO_HAND_RUNNING_JUMP": "Great Sword Running Jump"
}, },
"previewModelPath": "Characters/Models/hero.j3o", "previewModelPath": "Characters/Models/hero.j3o",
"animOffsets": { "animOffsets": {
@@ -146,6 +194,14 @@
"Roll", "Roll",
"Sneaking Forward", "Sneaking Forward",
"Sword And Shield Jump Running", "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 19 14:39:04 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

@@ -1,4 +1,4 @@
#Sun Sep 20 15:31:32 CEST 2026 #Mon Oct 19 14:39:11 CEST 2026
attachedEmitters.count=0 attachedEmitters.count=0
attachedLights.count=0 attachedLights.count=0
attachedSounds.count=0 attachedSounds.count=0

View File

@@ -16,7 +16,13 @@
"posZ": -0.13, "posZ": -0.13,
"rotX": 90.0, "rotX": 90.0,
"rotY": 0.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": { "modelRef": {
"path": "Models/waffen/schilder/einfacher schild.j3o" "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.10473058,
"posZ": 0.4630257,
"rotX": 0.0,
"rotY": 0.0,
"rotZ": 0.0,
"grip2PosX": 0.0024840608,
"grip2PosY": 0.10473058,
"grip2PosZ": 0.6630257,
"handOffsetX": 0.0,
"handOffsetY": 0.0,
"handOffsetZ": 0.0
},
"modelRef": {
"path": "Models/waffen/schwerter/rostiger_zweihaender.j3o"
},
"consumable": false
}

View File

@@ -11,12 +11,18 @@
"worthGold": 0, "worthGold": 0,
"damage": 0, "damage": 0,
"grip": { "grip": {
"posX": 0.37, "posX": 0.1114975,
"posY": 0.08, "posY": 0.07795838,
"posZ": 0.0, "posZ": 0.36503196,
"rotX": 0.0, "rotX": 0.0,
"rotY": -90.0, "rotY": 0.0,
"rotZ": 90.0 "rotZ": 0.0,
"grip2PosX": 0.0,
"grip2PosY": 0.0,
"grip2PosZ": 0.0,
"handOffsetX": 0.0,
"handOffsetY": 0.0,
"handOffsetZ": 0.0
}, },
"modelRef": { "modelRef": {
"path": "Models/waffen/schwerter/rostiges_kurzschwert.j3o" "path": "Models/waffen/schwerter/rostiges_kurzschwert.j3o"

View File

@@ -0,0 +1,8 @@
{
"SHIELD": {
"rotX": -90.0,
"rotY": 0.0,
"rotZ": 0.0,
"elbowOffset": 0.25
}
}

View File

@@ -0,0 +1,18 @@
{
"SWORD": {
"handOffX": 0.085,
"handOffY": 0.115,
"handOffZ": -0.095,
"hand2OffX": 0.0,
"hand2OffY": 0.0,
"hand2OffZ": 0.0
},
"TWO_HANDED_SWORD": {
"handOffX": 0.08,
"handOffY": 0.15,
"handOffZ": -0.09,
"hand2OffX": -0.005,
"hand2OffY": -0.04,
"hand2OffZ": 0.065
}
}

View File

@@ -7,6 +7,15 @@ import lombok.Setter;
@Getter @Getter
@Setter @Setter
public class ItemGrip { 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 posX, posY, posZ;
private float rotX, rotY, rotZ; 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

@@ -0,0 +1,128 @@
package de.blight.common.model;
import com.google.gson.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
/**
* Lädt und speichert per-Kategorie Schild-Attachment-Konfiguration (shield_attachment.json).
* Siehe WeaponAttachmentIO: liest vom Dateisystem (Dev-Umgebung) und prüft bei jedem Zugriff
* die mtime der Datei, damit Editor-Änderungen ohne Neustart ankommen.
*/
public final class ShieldAttachmentIO {
private static final Logger log = LoggerFactory.getLogger(ShieldAttachmentIO.class);
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final String CLASSPATH_FILE = "shield_attachment.json";
private static final String[] ASSET_BASES = {
"blight-assets/src/main/resources",
"../blight-assets/src/main/resources",
"assets",
".",
};
private static final Map<String, ShieldCategoryAttachment> CONFIGS = new HashMap<>();
private static boolean loaded = false;
private static Path cachedFile = null;
private static long cachedMtime = -1;
private ShieldAttachmentIO() {}
public static ShieldCategoryAttachment get(ItemSubCategory cat) {
ensureLoaded();
if (cat == null) return new ShieldCategoryAttachment();
return CONFIGS.getOrDefault(cat.name(), new ShieldCategoryAttachment());
}
public static void set(ItemSubCategory cat, ShieldCategoryAttachment cfg) {
if (cat != null) CONFIGS.put(cat.name(), cfg);
}
public static synchronized void loadFromClasspath() {
try (InputStream is = ShieldAttachmentIO.class.getClassLoader()
.getResourceAsStream(CLASSPATH_FILE)) {
if (is == null) {
log.warn("[ShieldAttachmentIO] {} nicht auf Classpath – Defaults verwendet", CLASSPATH_FILE);
loaded = true;
return;
}
parse(new String(is.readAllBytes(), StandardCharsets.UTF_8));
loaded = true;
} catch (IOException e) {
log.warn("[ShieldAttachmentIO] Ladefehler: {}", e.getMessage());
loaded = true;
}
}
public static void load(Path file) {
if (!Files.exists(file)) return;
try {
parse(Files.readString(file, StandardCharsets.UTF_8));
loaded = true;
} catch (IOException e) {
log.warn("[ShieldAttachmentIO] Ladefehler: {}", e.getMessage());
}
}
public static void save(Path file) {
try {
Files.writeString(file, GSON.toJson(CONFIGS), StandardCharsets.UTF_8);
log.info("[ShieldAttachmentIO] Gespeichert: {}", file);
} catch (IOException e) {
log.warn("[ShieldAttachmentIO] Speicherfehler: {}", e.getMessage());
}
}
public static Map<String, ShieldCategoryAttachment> getAll() {
ensureLoaded();
return new HashMap<>(CONFIGS);
}
private static synchronized void ensureLoaded() {
Path file = resolveFile();
if (file != null) {
try {
long mtime = Files.getLastModifiedTime(file).toMillis();
if (!file.equals(cachedFile) || mtime != cachedMtime) {
parse(Files.readString(file, StandardCharsets.UTF_8));
cachedFile = file;
cachedMtime = mtime;
}
loaded = true;
return;
} catch (IOException e) {
log.warn("[ShieldAttachmentIO] Ladefehler: {}", e.getMessage());
}
}
if (!loaded) loadFromClasspath();
}
/** Sucht {@code shield_attachment.json} im Dateisystem der Dev-Umgebung (Editor/Gradle-Run). */
private static Path resolveFile() {
for (String base : ASSET_BASES) {
Path p = Paths.get(base, CLASSPATH_FILE);
if (Files.isRegularFile(p)) return p;
}
return null;
}
private static void parse(String json) {
CONFIGS.clear();
JsonObject obj = JsonParser.parseString(json).getAsJsonObject();
for (var entry : obj.entrySet()) {
CONFIGS.put(entry.getKey(),
GSON.fromJson(entry.getValue(), ShieldCategoryAttachment.class));
}
log.info("[ShieldAttachmentIO] {} Kategorien geladen", CONFIGS.size());
}
}

View File

@@ -0,0 +1,14 @@
package de.blight.common.model;
import lombok.Getter;
import lombok.Setter;
/** Attachment-Parameter für einen Schild-Typ (am linken Unterarm). */
@Getter
@Setter
public class ShieldCategoryAttachment {
private float rotX = -90f;
private float rotY = 0f;
private float rotZ = 0f;
private float elbowOffset = 0.25f;
}

View File

@@ -0,0 +1,138 @@
package de.blight.common.model;
import com.google.gson.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
/**
* Lädt und speichert per-Kategorie Waffen-Attachment-Konfiguration (weapon_attachment.json).
* Singleton-Map: wird vom Dateisystem geladen (Dev-Umgebung, siehe ASSET_BASES) und bei jedem
* Zugriff gegen die mtime der Datei geprüft, damit Änderungen aus dem Editor ohne Neustart
* ankommen. Nur wenn keine Quelldatei auffindbar ist (z.B. gepackter Build), wird einmalig
* vom Classpath geladen.
*/
public final class WeaponAttachmentIO {
private static final Logger log = LoggerFactory.getLogger(WeaponAttachmentIO.class);
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final String CLASSPATH_FILE = "weapon_attachment.json";
/** Siehe AnimationLibrary.ASSET_BASES (blight-game) – hier dupliziert, da blight-common
* keine Abhängigkeit auf blight-game haben darf. */
private static final String[] ASSET_BASES = {
"blight-assets/src/main/resources",
"../blight-assets/src/main/resources",
"assets",
".",
};
private static final Map<String, WeaponCategoryAttachment> CONFIGS = new HashMap<>();
private static boolean loaded = false;
private static Path cachedFile = null;
private static long cachedMtime = -1;
private WeaponAttachmentIO() {}
/** Gibt die Attachment-Konfiguration für die gegebene Kategorie zurück (lazy-load vom Classpath). */
public static WeaponCategoryAttachment get(ItemSubCategory cat) {
ensureLoaded();
if (cat == null) return new WeaponCategoryAttachment();
return CONFIGS.getOrDefault(cat.name(), new WeaponCategoryAttachment());
}
public static void set(ItemSubCategory cat, WeaponCategoryAttachment cfg) {
if (cat != null) CONFIGS.put(cat.name(), cfg);
}
/** Lädt weapon_attachment.json vom Classpath (für Runtime im Spiel). */
public static synchronized void loadFromClasspath() {
try (InputStream is = WeaponAttachmentIO.class.getClassLoader()
.getResourceAsStream(CLASSPATH_FILE)) {
if (is == null) {
log.warn("[WeaponAttachmentIO] {} nicht auf Classpath gefunden – Defaults verwendet",
CLASSPATH_FILE);
loaded = true;
return;
}
parse(new String(is.readAllBytes(), StandardCharsets.UTF_8));
loaded = true;
} catch (IOException e) {
log.warn("[WeaponAttachmentIO] Ladefehler: {}", e.getMessage());
loaded = true;
}
}
/** Lädt weapon_attachment.json vom Dateisystem (für Editor). */
public static void load(Path file) {
if (!Files.exists(file)) return;
try {
parse(Files.readString(file, StandardCharsets.UTF_8));
loaded = true;
} catch (IOException e) {
log.warn("[WeaponAttachmentIO] Ladefehler: {}", e.getMessage());
}
}
/** Speichert die aktuelle Konfiguration in eine JSON-Datei (nur im Editor). */
public static void save(Path file) {
try {
Files.writeString(file, GSON.toJson(CONFIGS), StandardCharsets.UTF_8);
log.info("[WeaponAttachmentIO] Gespeichert: {}", file);
} catch (IOException e) {
log.warn("[WeaponAttachmentIO] Speicherfehler: {}", e.getMessage());
}
}
/** Gibt eine Kopie der aktuellen Config-Map zurück (für Editor-Anzeige). */
public static Map<String, WeaponCategoryAttachment> getAll() {
ensureLoaded();
return new HashMap<>(CONFIGS);
}
private static synchronized void ensureLoaded() {
Path file = resolveFile();
if (file != null) {
try {
long mtime = Files.getLastModifiedTime(file).toMillis();
if (!file.equals(cachedFile) || mtime != cachedMtime) {
parse(Files.readString(file, StandardCharsets.UTF_8));
cachedFile = file;
cachedMtime = mtime;
}
loaded = true;
return;
} catch (IOException e) {
log.warn("[WeaponAttachmentIO] Ladefehler: {}", e.getMessage());
}
}
if (!loaded) loadFromClasspath();
}
/** Sucht {@code weapon_attachment.json} im Dateisystem der Dev-Umgebung (Editor/Gradle-Run). */
private static Path resolveFile() {
for (String base : ASSET_BASES) {
Path p = Paths.get(base, CLASSPATH_FILE);
if (Files.isRegularFile(p)) return p;
}
return null;
}
private static void parse(String json) {
CONFIGS.clear();
JsonObject obj = JsonParser.parseString(json).getAsJsonObject();
for (var entry : obj.entrySet()) {
CONFIGS.put(entry.getKey(),
GSON.fromJson(entry.getValue(), WeaponCategoryAttachment.class));
}
log.info("[WeaponAttachmentIO] {} Kategorien geladen", CONFIGS.size());
}
}

View File

@@ -0,0 +1,18 @@
package de.blight.common.model;
import lombok.Getter;
import lombok.Setter;
/** Attachment-Parameter für eine Waffen-Kategorie (ersetzt den hardcodierten HAND_GRIP_OFFSET). */
@Getter
@Setter
public class WeaponCategoryAttachment {
/** Rechte Hand: Offset in Waffen-Local-Space. Standardwert = bisheriger HAND_GRIP_OFFSET. */
private float handOffX = ItemGrip.HAND_GRIP_OFFSET_X;
private float handOffY = ItemGrip.HAND_GRIP_OFFSET_Y;
private float handOffZ = ItemGrip.HAND_GRIP_OFFSET_Z;
/** Linke Hand (Zweihänder): Korrektur auf g2 in Waffen-Local-Space. */
private float hand2OffX = 0f;
private float hand2OffY = 0f;
private float hand2OffZ = 0f;
}

View File

@@ -174,7 +174,7 @@ public class EditorApp extends Application {
animPreviewController = new de.blight.editor.toolset.anim.AnimPreviewController(toolCtx); animPreviewController = new de.blight.editor.toolset.anim.AnimPreviewController(toolCtx);
animSetController = new de.blight.editor.toolset.anim.AnimSetController(toolCtx); animSetController = new de.blight.editor.toolset.anim.AnimSetController(toolCtx);
objectController = new de.blight.editor.toolset.object.ObjectController(toolCtx); objectController = new de.blight.editor.toolset.object.ObjectController(toolCtx);
modelEditorController = new de.blight.editor.toolset.model.ModelEditorController(toolCtx, () -> assetController.openOverlay()); modelEditorController = new de.blight.editor.toolset.model.ModelEditorController(toolCtx, () -> { assetController.openOverlay(); assetController.expandModelsNode(); });
modelEditorController.setObjectController(objectController); modelEditorController.setObjectController(objectController);
atmosphereController = new de.blight.editor.toolset.atmosphere.AtmosphereController(toolCtx); atmosphereController = new de.blight.editor.toolset.atmosphere.AtmosphereController(toolCtx);
waterController = new de.blight.editor.toolset.water.WaterController(toolCtx); waterController = new de.blight.editor.toolset.water.WaterController(toolCtx);
@@ -197,7 +197,7 @@ public class EditorApp extends Application {
stoneController = new de.blight.editor.toolset.stone.StoneController(toolCtx); stoneController = new de.blight.editor.toolset.stone.StoneController(toolCtx);
contentController = new de.blight.editor.toolset.content.ContentController(toolCtx); contentController = new de.blight.editor.toolset.content.ContentController(toolCtx);
contentController.setOnEnterContentMode(() -> { contentController.setOnEnterContentMode(() -> {
if (assetController.isOverlayOpen()) assetController.closeOverlay(); assetController.closeOverlayImmediate();
assetController.hideTabBtn(); assetController.hideTabBtn();
}); });
tripoController = new de.blight.editor.toolset.tripo.TripoController(toolCtx); tripoController = new de.blight.editor.toolset.tripo.TripoController(toolCtx);
@@ -281,10 +281,13 @@ public class EditorApp extends Application {
return; 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) if (e.getCode() == KeyCode.TAB && "world".equals(currentTool)
&& !e.isAltDown() && !e.isControlDown() && !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(); e.consume();
cycleToolSubMode(); cycleToolSubMode();
return; return;
@@ -618,13 +621,14 @@ public class EditorApp extends Application {
private void switchToWorldEditor() { private void switchToWorldEditor() {
boolean wasObjEditor = "objecteditor".equals(registry.activeToolId()) || "objecteditor".equals(currentTool); boolean wasObjEditor = "objecteditor".equals(registry.activeToolId()) || "objecteditor".equals(currentTool);
boolean wasModelEditor = "modeleditor".equals(registry.activeToolId()) || "modeleditor".equals(currentTool); boolean wasModelEditor = "modeleditor".equals(registry.activeToolId()) || "modeleditor".equals(currentTool);
boolean wasGripEditor = "gripEditor".equals(currentTool);
boolean wasThumbnailManager = "thumbnailmanager".equals(currentTool); boolean wasThumbnailManager = "thumbnailmanager".equals(currentTool);
registry.deactivateAll(); registry.deactivateAll();
currentTool = "world"; currentTool = "world";
if (jmeApp != null) jmeApp.setRenderThrottled(false); if (jmeApp != null) jmeApp.setRenderThrottled(false);
root.setLeft(null); // ggf. gesetzten Content-Tree entfernen root.setLeft(null); // ggf. gesetzten Content-Tree entfernen
assetController.showTabBtn(); assetController.showTabBtn();
if (wasThumbnailManager) assetController.openOverlay(); assetController.openOverlay();
topBar.getChildren().set(1, topBarController.getWorldToolBar()); topBar.getChildren().set(1, topBarController.getWorldToolBar());
if (contentController.hasRoutineJmeSlot()) { if (contentController.hasRoutineJmeSlot()) {
contentController.detachRoutineJmeSlot(); contentController.detachRoutineJmeSlot();
@@ -647,9 +651,22 @@ public class EditorApp extends Application {
input.activeLayer = 0; input.activeLayer = 0;
input.modelEditorOpenPath = null; input.modelEditorOpenPath = null;
topBarController.getBaseBtn().setSelected(true); 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() { private void ensureNotContentEditor() {
if (contentController.isSingleWindowContentActive(currentTool)) { if (contentController.isSingleWindowContentActive(currentTool)) {
switchToWorldEditor(); switchToWorldEditor();
@@ -665,7 +682,7 @@ public class EditorApp extends Application {
thumbnailManagerView.refresh(); thumbnailManagerView.refresh();
} }
// Asset-Baum-Overlay ausblenden – wird in switchToWorldEditor() wiederhergestellt // Asset-Baum-Overlay ausblenden – wird in switchToWorldEditor() wiederhergestellt
if (assetController.isOverlayOpen()) assetController.closeOverlay(); assetController.closeOverlayImmediate();
assetController.hideTabBtn(); assetController.hideTabBtn();
topBar.getChildren().set(1, buildThumbnailManagerToolBar()); topBar.getChildren().set(1, buildThumbnailManagerToolBar());
root.setLeft(null); root.setLeft(null);
@@ -1067,16 +1084,18 @@ public class EditorApp extends Application {
input.topologyRequest = sel ? 1 : 2; input.topologyRequest = sel ? 1 : 2;
} }
} }
case F1 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getAuswahlBtn().fire()); } case F1 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getAuswahlBtn().fire()); }
case F2 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getBaseBtn().fire()); } case F2 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getBaseBtn().fire()); }
case F3 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getVoxelMergedBtn().fire()); } case F3 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getVoxelMergedBtn().fire()); }
case F4 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getTextureBtn().fire()); } case F4 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getTextureBtn().fire()); }
case F5 -> { case F5 -> {
if (pressed) { if (pressed) {
if ("world".equals(currentTool)) { Platform.runLater(() -> topBarController.getGrassBtn().fire()); } if ("world".equals(currentTool)) { Platform.runLater(() -> topBarController.getGrassBtn().fire()); }
else { else {
ToolController active = registry.getActive(); 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()); } if ("world".equals(currentTool)) { Platform.runLater(() -> topBarController.getStoneBtn().fire()); }
else { else {
ToolController active = registry.getActive(); 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 -> { case F7 -> {
if (pressed && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getObjPlaceBtn().fire()); } if (pressed && "objecteditor".equals(registry.activeToolId())) { Platform.runLater(objectController::activatePlaceMode); }
else if (pressed && "objecteditor".equals(registry.activeToolId())) { Platform.runLater(objectController::activatePlaceMode); } else if (pressed) { fireWorldToolBtn(() -> topBarController.getObjPlaceBtn().fire()); }
} }
case F8 -> { case F8 -> {
if (pressed) { if (pressed && input.ctrlHeld) { input.debugNoLightToggle = true; }
if ("world".equals(currentTool)) { Platform.runLater(() -> topBarController.getAtmosphaereBtn().fire()); } else if (!pressed && input.ctrlHeld) { input.debugNoLightToggle = true; }
else if (input.ctrlHeld) { input.debugNoLightToggle = true; } else if (pressed) { fireWorldToolBtn(() -> topBarController.getAtmosphaereBtn().fire()); }
} else if (input.ctrlHeld) { input.debugNoLightToggle = true; }
} }
case F9 -> { case F9 -> {
if (pressed && input.ctrlHeld) { Platform.runLater(() -> if (pressed && input.ctrlHeld) { Platform.runLater(() ->
contentController.activateContentSection(8, currentTool)); } 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 F10 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getWegeBtn().fire()); }
case F11 -> { if (pressed && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getZonenBtn().fire()); } } case F11 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getZonenBtn().fire()); }
case F12 -> { if (pressed && !input.ctrlHeld && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getPlayToolBtn().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 boolean modelEditorBakeScaleRequest = false;
public volatile java.nio.file.Path modelEditorBakeScalePath = null; 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. */ /** JME → JFX: j3o hatte eine gebakte Scale, die von der Meta abwich – Spinner aktualisieren. */
public volatile boolean modelEditorBakedScaleDetected = false; public volatile boolean modelEditorBakedScaleDetected = false;
@@ -934,6 +940,8 @@ public class SharedInput {
public volatile String bakeStatusMsg = null; public volatile String bakeStatusMsg = null;
/** JME → JFX: Anzahl dirty Chunks in der Session (0 = nichts zu backen). */ /** JME → JFX: Anzahl dirty Chunks in der Session (0 = nichts zu backen). */
public volatile int sessionDirtyVoxelCount = 0; 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). */ /** JFX → JME: Nur dirty Chunks backen (Bake-on-Exit). */
public volatile boolean bakeOnExitRequested = false; public volatile boolean bakeOnExitRequested = false;
/** JFX → JME: Laufenden Bake-on-Exit abbrechen. */ /** JFX → JME: Laufenden Bake-on-Exit abbrechen. */
@@ -1209,10 +1217,58 @@ public class SharedInput {
public volatile float gripEditorRotX = 0f; public volatile float gripEditorRotX = 0f;
public volatile float gripEditorRotY = 0f; public volatile float gripEditorRotY = 0f;
public volatile float gripEditorRotZ = 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. */ /** Gesetzt vom JFX-Thread wenn Spinner geändert → JME baut Gizmo neu. */
public volatile boolean gripEditorGizmoDirty = false; public volatile boolean gripEditorGizmoDirty = false;
/** true → Gizmo ist ein 28cm-Unterarm-Pfeil (Schild); false → 15cm-Waffen-Pfeil. */ /** true → Gizmo ist ein 28cm-Unterarm-Pfeil (Schild); false → 15cm-Waffen-Pfeil. */
public volatile boolean gripEditorIsShield = false; 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;
// ── Charakter-Preview im Grip-Editor ─────────────────────────────────────
/** JFX→JME: Charakter-Preview ein- (true) oder ausschalten (false). */
public volatile boolean gripEditorCharPreview = false;
/** JFX→JME: ItemSubCategory-Name der aktuell editierten Waffe. */
public volatile String gripEditorSubCategory = null;
/** JME→JFX: Charakter-Preview ist bereit / aktiv. */
public volatile boolean gripEditorCharPreviewReady = false;
/** JFX→JME: Kategorie-Offsets (rechte Hand) für Live-Update im Preview. */
public volatile float gripEditorCatHandOffX = 0.1f;
public volatile float gripEditorCatHandOffY = 0f;
public volatile float gripEditorCatHandOffZ = 0.05f;
/** JFX→JME: Kategorie-Offsets (linke Hand, nur Zweihänder). */
public volatile float gripEditorCatHand2OffX = 0f;
public volatile float gripEditorCatHand2OffY = 0f;
public volatile float gripEditorCatHand2OffZ = 0f;
/** JFX→JME: true wenn Kategorie-Offsets geändert wurden. */
public volatile boolean gripEditorCatOffsetsDirty = false;
// ── Attachment-Editor (eigenständige Ansicht für Hand-Anker pro Kategorie) ─
public volatile boolean attachmentEditorActive = false;
// ── Schild-Attachment-Editor ──────────────────────────────────────────────
public volatile boolean shieldEditorActive = false;
public volatile String shieldEditorSubCategory = null;
public volatile float shieldEditorRotX = -90f;
public volatile float shieldEditorRotY = 0f;
public volatile float shieldEditorRotZ = 0f;
public volatile float shieldEditorElbowOffset = 0.25f;
public volatile float shieldEditorGripX = 0f;
public volatile float shieldEditorGripY = 0f;
public volatile float shieldEditorGripZ = 0f;
public volatile boolean shieldEditorOffsetsDirty = false;
/** Klick/Drag im Viewport im Sculpt-Modus. action 0=links, 1=rechts. */ /** Klick/Drag im Viewport im Sculpt-Modus. action 0=links, 1=rechts. */
public record SculptEdit(float screenX, float screenY, int action) {} public record SculptEdit(float screenX, float screenY, int action) {}

View File

@@ -664,18 +664,10 @@ public class AnimPreviewState extends BaseAppState {
// snap passiert ausschließlich zur Laufzeit im Spiel via AnimationLibrary. // snap passiert ausschließlich zur Laufzeit im Spiel via AnimationLibrary.
boolean isJ3oSource = animAssetPath.matches("(?i).*\\.j3o$"); boolean isJ3oSource = animAssetPath.matches("(?i).*\\.j3o$");
// FBX-Import erfordert ein geladenes Charakter-Modell. // Kein Charakter-Modell geladen: Normalisierung gegen eigenes Skelett (Orphan-Fold + Scale-Strip).
// Ohne Ziel-Armatur kann normalizeAndRetargetClip die $AssimpFbx$_Translation-Orphan-Joints // Das erzeugte j3o ist später per redirectAnimClipByName auf jedes kompatible Rig anwendbar.
// nicht in den Hips-Track falten → Hips-Translation geht verloren → Charakter schrumpft. com.jme3.anim.Armature effectiveDst = dstArm != null ? dstArm : srcArm;
if (!isJ3oSource && srcArm != null && dstArm == null) { com.jme3.anim.Armature snapArm = effectiveDst;
input.animPreviewStatus =
"Fehler: FBX-Import erfordert ein geladenes Charakter-Modell. " +
"Bitte zuerst das Modell laden, dann die Animation importieren.";
LOG.warn("[AnimPreview] FBX-Import ohne Charakter-Modell abgebrochen: {}", animAssetPath);
return;
}
com.jme3.anim.Armature snapArm = dstArm != null ? dstArm : srcArm;
int saved = 0; int saved = 0;
for (AnimClip clip : sourceAC.getAnimClips()) { for (AnimClip clip : sourceAC.getAnimClips()) {
@@ -688,7 +680,7 @@ public class AnimPreviewState extends BaseAppState {
} }
} }
AnimClip result; AnimClip result;
if (!retarget) { if (!retarget && (isJ3oSource || srcArm == null)) {
result = clip; result = clip;
} else if (isJ3oSource) { } else if (isJ3oSource) {
// j3o-Clips wurden bereits beim FBX-Import normalisiert (in Modell-Raum gespeichert). // j3o-Clips wurden bereits beim FBX-Import normalisiert (in Modell-Raum gespeichert).
@@ -696,12 +688,15 @@ public class AnimPreviewState extends BaseAppState {
result = redirectAnimClipByName(clip, dstArm); result = redirectAnimClipByName(clip, dstArm);
} else { } else {
// FBX-Import: Normalisierung in Modell-Raum (Orphan-Fold + Bind-Pose-Delta). // FBX-Import: Normalisierung in Modell-Raum (Orphan-Fold + Bind-Pose-Delta).
// Das resultierende j3o ist dann mit plain redirectAnimClipByName kompatibel. // Kein Charakter-Modell → effectiveDst == srcArm → reine Normalisierung ohne Retarget.
result = normalizeAndRetargetClip(clip, dstArm); result = normalizeAndRetargetClip(clip, effectiveDst);
if (result == null) { if (result == null) {
LOG.warn("[AnimPreview] normalizeAndRetarget: keine Joints gemappt, versuche Retargeting"); LOG.warn("[AnimPreview] normalizeAndRetarget: keine Joints gemappt, versuche Retargeting");
result = de.blight.game.animation.RetargetingSystem.retarget(clip, srcArm, dstArm); if (dstArm != null) {
result = de.blight.game.animation.RetargetingSystem.retarget(clip, srcArm, dstArm);
}
} }
if (result == null) result = clip;
} }
if (result == null) continue; if (result == null) continue;

View File

@@ -279,6 +279,7 @@ public class ItemPlacementState extends BaseAppState {
try { try {
PlacedItemIO.save(items); PlacedItemIO.save(items);
syncBpiToSession();
log.info("[ItemPlacement] Item '{}' platziert bei ({}, {}, {})", pi.itemId(), pt.x, pt.y, pt.z); log.info("[ItemPlacement] Item '{}' platziert bei ({}, {}, {})", pi.itemId(), pt.x, pt.y, pt.z);
} catch (IOException e) { } catch (IOException e) {
log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage()); log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage());
@@ -664,6 +665,7 @@ public class ItemPlacementState extends BaseAppState {
nodes.remove(idx); nodes.remove(idx);
try { try {
PlacedItemIO.save(items); PlacedItemIO.save(items);
syncBpiToSession();
} catch (IOException e) { } catch (IOException e) {
log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage()); log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage());
} }
@@ -681,6 +683,7 @@ public class ItemPlacementState extends BaseAppState {
try { try {
PlacedItemIO.save(items); PlacedItemIO.save(items);
syncBpiToSession();
} catch (IOException e) { } catch (IOException e) {
log.warn("[ItemPlacement] Speichern nach Edit fehlgeschlagen: {}", e.getMessage()); log.warn("[ItemPlacement] Speichern nach Edit fehlgeschlagen: {}", e.getMessage());
} }
@@ -689,6 +692,7 @@ public class ItemPlacementState extends BaseAppState {
private void saveItems() { private void saveItems() {
try { try {
PlacedItemIO.save(items); PlacedItemIO.save(items);
syncBpiToSession();
} catch (IOException e) { } catch (IOException e) {
log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage()); log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage());
} }
@@ -696,6 +700,17 @@ public class ItemPlacementState extends BaseAppState {
// ── Hilfsmethoden ──────────────────────────────────────────────────────── // ── 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) { private static void applyRot(Node n, float rotX, float rotY, float rotZ) {
Quaternion q = new Quaternion(); Quaternion q = new Quaternion();
q.fromAngles(rotX, rotY, rotZ); q.fromAngles(rotX, rotY, rotZ);

View File

@@ -34,6 +34,12 @@ import com.jme3.effect.ParticleMesh;
import com.jme3.material.RenderState; import com.jme3.material.RenderState;
import com.jme3.texture.Texture; import com.jme3.texture.Texture;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import com.jme3.anim.AnimComposer;
import com.jme3.math.Matrix4f;
import de.blight.common.model.ItemGrip;
import de.blight.common.model.ItemSubCategory;
import de.blight.common.model.WeaponAttachmentIO;
import de.blight.common.model.WeaponCategoryAttachment;
import de.blight.editor.SharedInput; import de.blight.editor.SharedInput;
import de.blight.editor.util.ImpostorUtil; import de.blight.editor.util.ImpostorUtil;
import de.blight.editor.util.ModelExportUtil; import de.blight.editor.util.ModelExportUtil;
@@ -106,6 +112,18 @@ public class ModelEditorState extends BaseAppState {
private Node gripGizmoNode = null; private Node gripGizmoNode = null;
private float gripPosX = 0f, gripPosY = 0f, gripPosZ = 0f; private float gripPosX = 0f, gripPosY = 0f, gripPosZ = 0f;
private float gripRotX = 0f, gripRotY = 0f, gripRotZ = 0f; private float gripRotX = 0f, gripRotY = 0f, gripRotZ = 0f;
private float grip2PosX = 0f, grip2PosY = 0f, grip2PosZ = 0f;
// ── Charakter-Preview ────────────────────────────────────────────────────
private Node charPreviewNode = null;
private Spatial charWeaponSpatial = null;
private Node charHandNode = null; // RightHand-Attachment
private Node charLeftHandNode = null; // LeftHand-Attachment
private Node charPalmNode = null; // RightHandMiddle1-Attachment
private Quaternion charHandBindRot = null;
private Quaternion charLeftHandBindRot = null;
private Vector3f charPalmOffset = null;
private int charPalmDelayFrames = 0;
private float interactableOffsetZ = 0f; private float interactableOffsetZ = 0f;
private float interactableRotY = 0f; private float interactableRotY = 0f;
@@ -197,6 +215,10 @@ public class ModelEditorState extends BaseAppState {
app.getStateManager().detach(previewEnvCam); app.getStateManager().detach(previewEnvCam);
previewEnvCam = null; previewEnvCam = null;
} }
// Probe jetzt zur Szene hinzufügen – erst hier ist sie korrekt initialisiert
if (previewRoot != null) {
previewRoot.addLight(previewProbe);
}
previewProbeReady = true; previewProbeReady = true;
// Emissive-Fallback von bereits geladenen Materialien entfernen // Emissive-Fallback von bereits geladenen Materialien entfernen
for (Material m : pbrMaterials) { for (Material m : pbrMaterials) {
@@ -223,12 +245,23 @@ public class ModelEditorState extends BaseAppState {
} }
} }
if (input.modelEditorEmbedLodsRequest) { if (input.modelEditorAlignWeaponRequest) {
input.modelEditorEmbedLodsRequest = false; input.modelEditorAlignWeaponRequest = false;
embedLodsAndSave(input.modelEditorEmbedLodsPath); Path alignPath = input.modelEditorAlignWeaponPath;
if (alignPath != null) {
alignWeaponToZ(alignPath);
} else {
log.warn("[ModelEditor] AlignWeapon: kein Pfad gesetzt");
input.modelEditorAlignWeaponResult = "FEHLER: kein Pfad";
}
} }
// Modell laden if (input.gripEditorGuessRequest) {
input.gripEditorGuessRequest = false;
guessGripPosition();
}
// Modell laden – MUSS vor enableCharPreview stehen, damit currentPath aktuell ist
String openPath = input.modelEditorOpenPath; String openPath = input.modelEditorOpenPath;
if (openPath != null) { if (openPath != null) {
input.modelEditorOpenPath = null; input.modelEditorOpenPath = null;
@@ -239,15 +272,60 @@ public class ModelEditorState extends BaseAppState {
loadModel(openPath); loadModel(openPath);
} }
// Grip-Gizmo aktualisieren // Charakter-Preview ein-/ausschalten
if (input.gripEditorActive && input.gripEditorGizmoDirty) { boolean wantsPreview = (input.gripEditorCharPreview && input.gripEditorActive)
|| input.attachmentEditorActive
|| input.shieldEditorActive;
if (wantsPreview && charPreviewNode == null) {
enableCharPreview();
} else if (!wantsPreview && charPreviewNode != null) {
disableCharPreview();
}
// Palm-Offset nach 2 Frames berechnen
if (charPalmDelayFrames > 0 && charPalmNode != null && charHandNode != null) {
if (--charPalmDelayFrames == 0) {
Vector3f delta = charPalmNode.getWorldTranslation()
.subtract(charHandNode.getWorldTranslation());
charPalmOffset = charHandNode.getWorldRotation().inverse().mult(delta);
}
}
// Waffe/Schild im Charakter-Preview jedes Frame neu positionieren
if (charPreviewNode != null && charWeaponSpatial != null && charHandNode != null) {
updateCharPreviewWeapon();
}
// Orbit-Kamera im Charakter-Preview immer um den aktuellen Hand-Ankerpunkt drehen
// (statt um die Bounding-Box der isolierten Waffe, die am Boden/"Fuß" verankert ist)
if (charPreviewNode != null) {
Vector3f anchor = computeCharAnchorWorld();
if (anchor != null) orbitCenter = anchor;
}
if (input.modelEditorEmbedLodsRequest) {
input.modelEditorEmbedLodsRequest = false;
embedLodsAndSave(input.modelEditorEmbedLodsPath);
}
// Invariante: wenn Charakter-Preview aktiv, standalone-Modell immer ausblenden
if (charPreviewNode != null) {
if (modelWrapper != null) modelWrapper.setCullHint(Spatial.CullHint.Always);
if (gripGizmoNode != null) gripGizmoNode.setCullHint(Spatial.CullHint.Always);
}
// Grip-Gizmo aktualisieren (nicht im reinen Attachment-Editor-Modus)
if (input.gripEditorActive && !input.attachmentEditorActive && input.gripEditorGizmoDirty) {
input.gripEditorGizmoDirty = false; input.gripEditorGizmoDirty = false;
gripPosX = input.gripEditorPosX; gripPosX = input.gripEditorPosX;
gripPosY = input.gripEditorPosY; gripPosY = input.gripEditorPosY;
gripPosZ = input.gripEditorPosZ; gripPosZ = input.gripEditorPosZ;
gripRotX = input.gripEditorRotX; gripRotX = input.gripEditorRotX;
gripRotY = input.gripEditorRotY; gripRotY = input.gripEditorRotY;
gripRotZ = input.gripEditorRotZ; gripRotZ = input.gripEditorRotZ;
grip2PosX = input.gripEditorGrip2PosX;
grip2PosY = input.gripEditorGrip2PosY;
grip2PosZ = input.gripEditorGrip2PosZ;
rebuildGripGizmo(); rebuildGripGizmo();
} }
@@ -472,7 +550,22 @@ public class ModelEditorState extends BaseAppState {
// Rendering schwarz erscheinen lassen. // Rendering schwarz erscheinen lassen.
try { app.getAssetManager().deleteFromCache(new com.jme3.asset.ModelKey(assetPath)); } try { app.getAssetManager().deleteFromCache(new com.jme3.asset.ModelKey(assetPath)); }
catch (Exception ignored) {} 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); stripControls(model);
initImpostorControls(model); initImpostorControls(model);
recalcNormalsIfMissing(model); recalcNormalsIfMissing(model);
@@ -696,12 +789,12 @@ public class ModelEditorState extends BaseAppState {
new ColorRGBA(0.4f, 0.4f, 0.5f, 1f))); new ColorRGBA(0.4f, 0.4f, 0.5f, 1f)));
previewRoot.addLight(new AmbientLight(new ColorRGBA(0.65f, 0.65f, 0.7f, 1f))); previewRoot.addLight(new AmbientLight(new ColorRGBA(0.65f, 0.65f, 0.7f, 1f)));
// LightProbe: JME3-PBR braucht Probe damit g_AmbientLightColor verwendet wird. // LightProbe: erst nach dem Backen zur Szene hinzufügen – eine leere (uninitialisierte)
// EnvironmentCamera backt die Probe aus dem hellen Übergangshintergrund heraus; // Probe stört den PBR-Shader und macht das Modell dunkel, bevor der Bake fertig ist.
// danach wird der dunkle Display-Hintergrund wiederhergestellt. // Bis dahin sorgen DirectionalLights + AmbientLight für sichtbare Beleuchtung.
previewProbe = new LightProbe(); previewProbe = new LightProbe();
previewProbe.getArea().setRadius(1000f); previewProbe.getArea().setRadius(1000f);
previewRoot.addLight(previewProbe); // previewRoot.addLight(previewProbe) → wird in done()-Callback aufgerufen
previewEnvCam = new EnvironmentCamera(256, Vector3f.ZERO.clone()); previewEnvCam = new EnvironmentCamera(256, Vector3f.ZERO.clone());
app.getStateManager().attach(previewEnvCam); app.getStateManager().attach(previewEnvCam);
previewProbeFrame = 0; previewProbeFrame = 0;
@@ -728,14 +821,17 @@ public class ModelEditorState extends BaseAppState {
gripGizmoNode.setCullHint(Spatial.CullHint.Always); gripGizmoNode.setCullHint(Spatial.CullHint.Always);
previewRoot.attachChild(gripGizmoNode); previewRoot.attachChild(gripGizmoNode);
gripPosX = input.gripEditorPosX; gripPosX = input.gripEditorPosX;
gripPosY = input.gripEditorPosY; gripPosY = input.gripEditorPosY;
gripPosZ = input.gripEditorPosZ; gripPosZ = input.gripEditorPosZ;
gripRotX = input.gripEditorRotX; gripRotX = input.gripEditorRotX;
gripRotY = input.gripEditorRotY; gripRotY = input.gripEditorRotY;
gripRotZ = input.gripEditorRotZ; gripRotZ = input.gripEditorRotZ;
grip2PosX = input.gripEditorGrip2PosX;
grip2PosY = input.gripEditorGrip2PosY;
grip2PosZ = input.gripEditorGrip2PosZ;
orbitYaw = 30f; orbitYaw = input.gripEditorActive ? 90f : 30f;
orbitPitch = 25f; orbitPitch = 25f;
} }
@@ -757,6 +853,7 @@ public class ModelEditorState extends BaseAppState {
originalSpatial = null; originalSpatial = null;
interactableArrowNode = null; interactableArrowNode = null;
gripGizmoNode = null; gripGizmoNode = null;
disableCharPreview();
input.gripEditorActive = false; input.gripEditorActive = false;
camLight = null; camLight = null;
mainLight = null; mainLight = null;
@@ -1218,15 +1315,20 @@ public class ModelEditorState extends BaseAppState {
projectRoot.resolve("blight-assets").resolve("bin").resolve("main").resolve(rel), projectRoot.resolve("blight-assets").resolve("bin").resolve("main").resolve(rel),
projectRoot.resolve("blight-assets").resolve("build").resolve("resources").resolve("main").resolve(rel), projectRoot.resolve("blight-assets").resolve("build").resolve("resources").resolve("main").resolve(rel),
}; };
for (java.nio.file.Path mirror : mirrors) { java.nio.file.Path[] mirrorRoots = {
if (java.nio.file.Files.exists(mirror)) { projectRoot.resolve("blight-assets").resolve("bin").resolve("main"),
try { projectRoot.resolve("blight-assets").resolve("build").resolve("resources").resolve("main"),
java.nio.file.Files.copy(savedFile, mirror, };
java.nio.file.StandardCopyOption.REPLACE_EXISTING); for (int i = 0; i < mirrors.length; i++) {
log.info("[ModelEditor] Spiegel aktualisiert: {}", mirror); java.nio.file.Path mirror = mirrors[i];
} catch (Exception e) { if (!java.nio.file.Files.isDirectory(mirrorRoots[i])) continue;
log.warn("[ModelEditor] Spiegel-Update fehlgeschlagen ({}): {}", mirror, e.getMessage()); 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 +1526,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) { private static boolean hasSkinningControl(Spatial s) {
if (s.getControl(SkinningControl.class) != null) return true; if (s.getControl(SkinningControl.class) != null) return true;
if (s instanceof Node n) { if (s instanceof Node n) {
@@ -1533,66 +1925,77 @@ public class ModelEditorState extends BaseAppState {
input.modelInteractableActive ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); 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() { public void rebuildGripGizmo() {
if (gripGizmoNode == null) return; if (gripGizmoNode == null) return;
gripGizmoNode.detachAllChildren(); gripGizmoNode.detachAllChildren();
boolean shieldMode = input.gripEditorIsShield; 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 // Grip 1: orange (Waffe) oder cyan (Schild)
float shaftLen = shieldMode ? 0.24f : 0.12f; ColorRGBA c1s = shieldMode ? new ColorRGBA(0f, 0.75f, 0.9f, 1f) : new ColorRGBA(1f, 0.55f, 0f, 1f);
float shaftRad = shieldMode ? 0.010f : 0.008f; ColorRGBA c1h = shieldMode ? new ColorRGBA(0f, 0.5f, 0.75f, 1f) : new ColorRGBA(1f, 0.3f, 0f, 1f);
float headRad = shieldMode ? 0.026f : 0.022f; ColorRGBA c1a = shieldMode ? new ColorRGBA(0.6f, 1f, 0.6f, 1f) : new ColorRGBA(1f, 0.8f, 0.1f, 1f);
float headLen = shieldMode ? 0.04f : 0.03f; gripGizmoNode.attachChild(buildGripGroup("gripGroup",
gripPosX, gripPosY, gripPosZ, gripRotX, gripRotY, gripRotZ,
bladeDirZ, c1s, c1h, c1a, shaftLen, shaftRad, headRad, axisLen, markerSz));
ColorRGBA shaftColor = shieldMode // Grip 2: blau — nur für Waffen, nur wenn Position gesetzt
? new ColorRGBA(0f, 0.75f, 0.9f, 1f) if (!shieldMode && (grip2PosX != 0f || grip2PosY != 0f || grip2PosZ != 0f)) {
: new ColorRGBA(1f, 0.55f, 0f, 1f); gripGizmoNode.attachChild(buildGripGroup("grip2Group",
ColorRGBA headColor = shieldMode grip2PosX, grip2PosY, grip2PosZ, 0, 0, 0,
? new ColorRGBA(0f, 0.5f, 0.75f, 1f) bladeDirZ,
: new ColorRGBA(1f, 0.3f, 0f, 1f); new ColorRGBA(0.2f, 0.5f, 1f, 1f),
ColorRGBA axisColor = shieldMode new ColorRGBA(0.1f, 0.3f, 0.9f, 1f),
? new ColorRGBA(0.6f, 1f, 0.6f, 1f) new ColorRGBA(0.5f, 0.75f, 1f, 1f),
: new ColorRGBA(1f, 0.8f, 0.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); mat.setColor("Color", shaftColor);
Material matHead = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); Material matHead = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
matHead.setColor("Color", headColor); 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"); Material matAxis = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
matAxis.setColor("Color", axisColor); 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); gripAxis.setMaterial(matAxis);
Quaternion axisRot = new Quaternion(); Quaternion axisRot = new Quaternion();
axisRot.fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X); axisRot.fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X);
@@ -1603,8 +2006,7 @@ public class ModelEditorState extends BaseAppState {
group.attachChild(shaft); group.attachChild(shaft);
group.attachChild(gripAxis); group.attachChild(gripAxis);
group.attachChild(head); group.attachChild(head);
gripGizmoNode.attachChild(group); return group;
gripGizmoNode.setCullHint(Spatial.CullHint.Inherit);
} }
/** Blendet den Grip-Gizmo ein oder aus. */ /** Blendet den Grip-Gizmo ein oder aus. */
@@ -1677,4 +2079,305 @@ public class ModelEditorState extends BaseAppState {
float dz = -cosEle * (float) Math.cos(azi); float dz = -cosEle * (float) Math.cos(azi);
return new Vector3f(dx, dy, dz).normalizeLocal(); return new Vector3f(dx, dy, dz).normalizeLocal();
} }
// ── Charakter-Preview ────────────────────────────────────────────────────
private void enableCharPreview() {
if (previewRoot == null || currentPath == null) return;
try {
Spatial hero = app.getAssetManager().loadModel("Characters/Models/hero.j3o");
charPreviewNode = new Node("charPreview");
charPreviewNode.attachChild(hero);
previewRoot.attachChild(charPreviewNode);
SkinningControl skin = findSkinCtrl(hero);
if (skin != null) {
charHandBindRot = extractJointBindRot(skin, "mixamorig:RightHand");
charLeftHandBindRot = extractJointBindRot(skin, "mixamorig:LeftHand");
try { charHandNode = skin.getAttachmentsNode("mixamorig:RightHand"); } catch (Exception e) { log.warn("[CharPreview] RightHand: {}", e.getMessage()); }
try { charLeftHandNode = skin.getAttachmentsNode("mixamorig:LeftHand"); } catch (Exception e) { log.warn("[CharPreview] LeftHand: {}", e.getMessage()); }
try { charPalmNode = skin.getAttachmentsNode("mixamorig:RightHandMiddle1"); charPalmDelayFrames = 2; } catch (Exception e) { log.warn("[CharPreview] Palm: {}", e.getMessage()); }
AnimComposer anim = findAnimComposer(hero);
if (anim != null) {
String subCat = input.shieldEditorActive
? input.shieldEditorSubCategory : input.gripEditorSubCategory;
String clipName = previewAnimName(subCat);
loadClipToComposer(anim, skin, clipName);
}
}
charWeaponSpatial = app.getAssetManager().loadModel(currentPath);
charPreviewNode.attachChild(charWeaponSpatial);
// Standalone-Waffe ausblenden
if (modelWrapper != null) modelWrapper.setCullHint(Spatial.CullHint.Always);
if (gripGizmoNode != null) gripGizmoNode.setCullHint(Spatial.CullHint.Always);
orbitDist = 2.5f;
orbitYaw = 135f;
orbitPitch = 15f;
input.gripEditorCharPreviewReady = true;
log.info("[CharPreview] Aktiviert");
} catch (Exception e) {
log.warn("[CharPreview] Fehler beim Laden: {}", e.getMessage());
disableCharPreview();
}
}
private void disableCharPreview() {
if (charPreviewNode != null) {
charPreviewNode.removeFromParent();
charPreviewNode = null;
charWeaponSpatial = null;
charHandNode = null;
charLeftHandNode = null;
charPalmNode = null;
charHandBindRot = null;
charLeftHandBindRot = null;
charPalmOffset = null;
}
if (modelWrapper != null) modelWrapper.setCullHint(Spatial.CullHint.Inherit);
if (gripGizmoNode != null) gripGizmoNode.setCullHint(Spatial.CullHint.Always);
input.gripEditorCharPreviewReady = false;
updateBounds(); // Orbit-Zentrum zurück auf die Bounding-Box der isolierten Waffe
}
/**
* Liefert die aktuelle Position des Hand-Ankerpunkts (Waffen-Griff bzw. Schild-Anker)
* in Weltkoordinaten, oder null wenn kein Charakter-Preview aktiv ist.
*/
private Vector3f computeCharAnchorWorld() {
if (charPreviewNode == null) return null;
Quaternion charWorldRot = charPreviewNode.getWorldRotation();
Quaternion charWorldRotInv = charWorldRot.inverse();
Vector3f charWorldPos = charPreviewNode.getWorldTranslation();
if (input.shieldEditorActive) {
if (charLeftHandNode == null) return null;
Vector3f leftBoneInChar = charWorldRotInv.mult(
charLeftHandNode.getWorldTranslation().subtract(charWorldPos));
Quaternion leftBoneRotInChar = charWorldRotInv.mult(charLeftHandNode.getWorldRotation());
Vector3f anchorInChar = leftBoneInChar.add(
leftBoneRotInChar.mult(new Vector3f(0f, -input.shieldEditorElbowOffset, 0f)));
return charWorldPos.add(charWorldRot.mult(anchorInChar));
}
if (charHandNode == null) return null;
Vector3f boneInChar = charWorldRotInv.mult(
charHandNode.getWorldTranslation().subtract(charWorldPos));
Quaternion boneRotInChar = charWorldRotInv.mult(charHandNode.getWorldRotation());
Vector3f palmOff = charPalmOffset;
Vector3f attachInChar = (palmOff != null)
? boneInChar.add(boneRotInChar.mult(palmOff.mult(0.5f)))
: boneInChar;
return charWorldPos.add(charWorldRot.mult(attachInChar));
}
private void updateCharPreviewWeapon() {
if (input.shieldEditorActive) {
updateCharPreviewShield();
return;
}
// Kategorie-Offsets aus SharedInput lesen (live-update)
WeaponCategoryAttachment cfg = new WeaponCategoryAttachment();
cfg.setHandOffX(input.gripEditorCatHandOffX);
cfg.setHandOffY(input.gripEditorCatHandOffY);
cfg.setHandOffZ(input.gripEditorCatHandOffZ);
cfg.setHand2OffX(input.gripEditorCatHand2OffX);
cfg.setHand2OffY(input.gripEditorCatHand2OffY);
cfg.setHand2OffZ(input.gripEditorCatHand2OffZ);
// Grip-Werte aus SharedInput (Spinner = display-Werte, JSON-Werte = + HAND_GRIP_OFFSET)
float g1x = input.gripEditorPosX + ItemGrip.HAND_GRIP_OFFSET_X;
float g1y = input.gripEditorPosY + ItemGrip.HAND_GRIP_OFFSET_Y;
float g1z = input.gripEditorPosZ + ItemGrip.HAND_GRIP_OFFSET_Z;
float g2x = input.gripEditorGrip2PosX;
float g2y = input.gripEditorGrip2PosY;
float g2z = input.gripEditorGrip2PosZ;
boolean hasGrip2 = g2x != 0f || g2y != 0f || g2z != 0f;
boolean bladeAtMinusZ = hasGrip2 ? (g2z > g1z) : (g1z > 0f);
boolean isTwoHand = hasGrip2 && bladeAtMinusZ;
Quaternion charWorldRot = charPreviewNode.getWorldRotation();
Quaternion charWorldRotInv = charWorldRot.inverse();
Vector3f charWorldPos = charPreviewNode.getWorldTranslation();
Vector3f boneInChar = charWorldRotInv.mult(charHandNode.getWorldTranslation().subtract(charWorldPos));
Quaternion boneRotInChar = charWorldRotInv.mult(charHandNode.getWorldRotation());
Vector3f palmOff = charPalmOffset;
Vector3f attachInChar = (palmOff != null)
? boneInChar.add(boneRotInChar.mult(palmOff.mult(0.5f)))
: boneInChar;
float DEG_TO_RAD = (float) Math.PI / 180f;
Quaternion gripRot = new Quaternion().fromAngles(
DEG_TO_RAD * input.gripEditorRotX,
DEG_TO_RAD * input.gripEditorRotY,
DEG_TO_RAD * input.gripEditorRotZ);
Quaternion animDelta = (charHandBindRot != null)
? boneRotInChar.mult(charHandBindRot.inverse())
: boneRotInChar;
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(cfg.getHandOffX(), cfg.getHandOffY(), -cfg.getHandOffZ())
: new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), cfg.getHandOffZ());
Quaternion finalRot;
Vector3f finalPos;
if (isTwoHand && charLeftHandNode != null) {
Vector3f hand2Off = bladeAtMinusZ
? new Vector3f(cfg.getHand2OffX(), cfg.getHand2OffY(), -cfg.getHand2OffZ())
: new Vector3f(cfg.getHand2OffX(), cfg.getHand2OffY(), cfg.getHand2OffZ());
Vector3f g1 = new Vector3f(g1x, g1y, g1z);
Vector3f g2 = new Vector3f(g2x, g2y, g2z);
Vector3f g1eff = g1.subtract(handOff);
Vector3f g2eff = g2.add(hand2Off);
Vector3f leftHandInChar = charWorldRotInv.mult(
charLeftHandNode.getWorldTranslation().subtract(charWorldPos));
Vector3f currentGripDir = weaponRot.mult(g2eff.subtract(g1eff).normalizeLocal());
Vector3f targetDir = leftHandInChar.subtract(attachInChar).normalizeLocal();
finalRot = swingFromTo(currentGripDir, targetDir).mult(weaponRot);
finalPos = attachInChar.subtract(finalRot.mult(g1eff));
} else {
Vector3f gripPos = new Vector3f(g1x, g1y, g1z);
finalRot = weaponRot;
finalPos = attachInChar.subtract(finalRot.mult(gripPos)).addLocal(finalRot.mult(handOff));
}
charWeaponSpatial.setLocalRotation(finalRot);
charWeaponSpatial.setLocalTranslation(finalPos);
}
private void updateCharPreviewShield() {
if (charLeftHandNode == null) return;
float DEG_TO_RAD = (float) Math.PI / 180f;
Quaternion charWorldRotInv = charPreviewNode.getWorldRotation().inverse();
Vector3f charWorldPos = charPreviewNode.getWorldTranslation();
Vector3f leftBoneInChar = charWorldRotInv.mult(
charLeftHandNode.getWorldTranslation().subtract(charWorldPos));
Quaternion leftBoneRotInChar = charWorldRotInv.mult(charLeftHandNode.getWorldRotation());
Quaternion animDelta = (charLeftHandBindRot != null)
? leftBoneRotInChar.mult(charLeftHandBindRot.inverse())
: leftBoneRotInChar;
Quaternion armRot = new Quaternion().fromAngles(
DEG_TO_RAD * input.shieldEditorRotX,
DEG_TO_RAD * input.shieldEditorRotY,
DEG_TO_RAD * input.shieldEditorRotZ);
Quaternion shieldRot = animDelta.mult(armRot);
Vector3f anchor = leftBoneInChar.add(
leftBoneRotInChar.mult(new Vector3f(0f, -input.shieldEditorElbowOffset, 0f)));
Vector3f gripPos = new Vector3f(
input.shieldEditorGripX, input.shieldEditorGripY, input.shieldEditorGripZ);
charWeaponSpatial.setLocalRotation(shieldRot);
charWeaponSpatial.setLocalTranslation(anchor.subtract(shieldRot.mult(gripPos)));
}
private static String previewAnimName(String subCategory) {
if (subCategory == null) return "Idle";
return switch (subCategory) {
case "TWO_HANDED_SWORD" -> "Great Sword Idle";
case "SWORD", "SHIELD" -> "Sword And Shield Idle";
default -> "Idle";
};
}
private void loadClipToComposer(AnimComposer ac, SkinningControl dstSC, String clipName) {
if (ac.getAnimClipsNames().contains(clipName)) {
ac.setCurrentAction(clipName);
return;
}
try {
String assetKey = "Characters/clips/" + clipName + ".j3o";
Spatial clipModel = app.getAssetManager().loadModel(assetKey);
AnimComposer clipAC = findAnimComposer(clipModel);
SkinningControl clipSC = findSkinCtrl(clipModel);
if (clipAC == null || clipAC.getAnimClipsNames().isEmpty()) {
log.warn("[CharPreview] Clip-Datei leer oder kein AnimComposer: {}", assetKey);
return;
}
com.jme3.anim.Armature srcArm = clipSC != null ? clipSC.getArmature() : null;
com.jme3.anim.Armature dstArm = dstSC != null ? dstSC.getArmature() : null;
String firstName = clipAC.getAnimClipsNames().iterator().next();
com.jme3.anim.AnimClip src = clipAC.getAnimClip(firstName);
com.jme3.anim.AnimClip target;
if (srcArm != null && dstArm != null) {
target = de.blight.game.animation.RetargetingSystem.retarget(src, srcArm, dstArm);
if (target == null) target = src;
} else {
target = src;
}
if (!clipName.equals(target.getName())) {
com.jme3.anim.AnimClip renamed = new com.jme3.anim.AnimClip(clipName);
renamed.setTracks(target.getTracks());
target = renamed;
}
ac.addAnimClip(target);
ac.setCurrentAction(clipName);
log.info("[CharPreview] Clip '{}' geladen und gestartet", clipName);
} catch (Exception e) {
log.warn("[CharPreview] Clip '{}' konnte nicht geladen werden: {}", clipName, e.getMessage());
}
}
private static SkinningControl findSkinCtrl(Spatial s) {
SkinningControl sc = s.getControl(SkinningControl.class);
if (sc != null) return sc;
if (s instanceof Node n) {
for (Spatial c : n.getChildren()) {
sc = findSkinCtrl(c);
if (sc != null) return sc;
}
}
return null;
}
private static AnimComposer findAnimComposer(Spatial s) {
AnimComposer ac = s.getControl(AnimComposer.class);
if (ac != null) return ac;
if (s instanceof Node n) {
for (Spatial c : n.getChildren()) {
ac = findAnimComposer(c);
if (ac != null) return ac;
}
}
return null;
}
private static Quaternion extractJointBindRot(SkinningControl sc, String jointName) {
com.jme3.anim.Armature arm = sc.getArmature();
if (arm == null) return null;
com.jme3.anim.Joint joint = arm.getJoint(jointName);
if (joint == null) return null;
Matrix4f invBind = joint.getInverseModelBindMatrix();
if (invBind == null) return null;
Matrix4f bindMat = invBind.invert();
if (bindMat == null) return null;
return bindMat.toRotationQuat().normalizeLocal();
}
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 len = axis.length();
if (len < 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(len));
}
} }

View File

@@ -997,6 +997,11 @@ public class AnimSetController implements ToolController {
return; return;
} }
// ── Aktions-Combo mit Suchfeld ────────────────────────────────────────
javafx.scene.control.TextField actionSearch = new javafx.scene.control.TextField();
actionSearch.setPromptText("Aktion suchen…");
actionSearch.setMaxWidth(Double.MAX_VALUE);
ComboBox<de.blight.game.animation.AnimationAction> actionCombo = new ComboBox<>(); ComboBox<de.blight.game.animation.AnimationAction> actionCombo = new ComboBox<>();
actionCombo.getItems().addAll(available); actionCombo.getItems().addAll(available);
javafx.util.Callback<javafx.scene.control.ListView<de.blight.game.animation.AnimationAction>, javafx.util.Callback<javafx.scene.control.ListView<de.blight.game.animation.AnimationAction>,
@@ -1013,29 +1018,85 @@ public class AnimSetController implements ToolController {
actionCombo.setMaxWidth(Double.MAX_VALUE); actionCombo.setMaxWidth(Double.MAX_VALUE);
actionCombo.getSelectionModel().selectFirst(); actionCombo.getSelectionModel().selectFirst();
ComboBox<String> clipCombo = new ComboBox<>(); actionSearch.textProperty().addListener((obs, ov, nv) -> {
String lower = nv == null ? "" : nv.toLowerCase();
actionCombo.getItems().setAll(available.stream()
.filter(a -> lower.isEmpty()
|| a.displayName().toLowerCase().contains(lower)
|| a.name().toLowerCase().contains(lower))
.collect(java.util.stream.Collectors.toList()));
if (!actionCombo.getItems().isEmpty()) actionCombo.getSelectionModel().selectFirst();
});
// ── Clip-Combo mit Suchfeld ───────────────────────────────────────────
java.util.List<String> allClipNames = new java.util.ArrayList<>(animSetClipListView.getItems()); java.util.List<String> allClipNames = new java.util.ArrayList<>(animSetClipListView.getItems());
for (String subName : editableSubClips.keySet()) { for (String subName : editableSubClips.keySet()) {
if (!allClipNames.contains(subName)) allClipNames.add(subName); if (!allClipNames.contains(subName)) allClipNames.add(subName);
} }
javafx.scene.control.TextField clipSearch = new javafx.scene.control.TextField();
clipSearch.setPromptText("Clip suchen…");
clipSearch.setMaxWidth(Double.MAX_VALUE);
ComboBox<String> clipCombo = new ComboBox<>();
clipCombo.getItems().addAll(allClipNames); clipCombo.getItems().addAll(allClipNames);
clipCombo.setMaxWidth(Double.MAX_VALUE); clipCombo.setMaxWidth(Double.MAX_VALUE);
clipCombo.getSelectionModel().selectFirst(); clipCombo.getSelectionModel().selectFirst();
clipSearch.textProperty().addListener((obs, ov, nv) -> {
String lower = nv == null ? "" : nv.toLowerCase();
clipCombo.getItems().setAll(allClipNames.stream()
.filter(c -> lower.isEmpty() || c.toLowerCase().contains(lower))
.collect(java.util.stream.Collectors.toList()));
if (!clipCombo.getItems().isEmpty()) clipCombo.getSelectionModel().selectFirst();
});
// ── Layout ────────────────────────────────────────────────────────────
VBox actionBox = new VBox(4, actionSearch, actionCombo);
VBox clipBox = new VBox(4, clipSearch, clipCombo);
javafx.scene.layout.GridPane grid = new javafx.scene.layout.GridPane(); javafx.scene.layout.GridPane grid = new javafx.scene.layout.GridPane();
grid.setHgap(8); grid.setVgap(6); grid.setHgap(8); grid.setVgap(8);
grid.add(new Label("Aktion:"), 0, 0); grid.add(actionCombo, 1, 0); grid.add(new Label("Aktion:"), 0, 0); grid.add(actionBox, 1, 0);
grid.add(new Label("Clip:"), 0, 1); grid.add(clipCombo, 1, 1); grid.add(new Label("Clip:"), 0, 1); grid.add(clipBox, 1, 1);
javafx.scene.layout.ColumnConstraints cc = new javafx.scene.layout.ColumnConstraints(); javafx.scene.layout.ColumnConstraints cc = new javafx.scene.layout.ColumnConstraints();
cc.setHgrow(Priority.ALWAYS); cc.setHgrow(Priority.ALWAYS);
grid.getColumnConstraints().addAll(new javafx.scene.layout.ColumnConstraints(), cc); grid.getColumnConstraints().addAll(new javafx.scene.layout.ColumnConstraints(), cc);
CheckBox weitereCB = new CheckBox("Weitere zuordnen");
VBox content = new VBox(12, grid, weitereCB);
Dialog<ButtonType> dlg = new Dialog<>(); Dialog<ButtonType> dlg = new Dialog<>();
dlg.initOwner(ctx.stageSupplier().get()); dlg.initOwner(ctx.stageSupplier().get());
dlg.setTitle("Aktion zuordnen"); dlg.setTitle("Aktion zuordnen");
ButtonType ok = new ButtonType("Hinzufügen", ButtonBar.ButtonData.OK_DONE); ButtonType ok = new ButtonType("Hinzufügen", ButtonBar.ButtonData.OK_DONE);
dlg.getDialogPane().getButtonTypes().addAll(ok, ButtonType.CANCEL); dlg.getDialogPane().getButtonTypes().addAll(ok, ButtonType.CANCEL);
dlg.getDialogPane().setContent(grid); dlg.getDialogPane().setContent(content);
dlg.getDialogPane().setPrefWidth(480);
// Wenn "Weitere zuordnen" aktiv: Dialog offen lassen, nächste Aktion auto-selektieren.
javafx.scene.Node okNode = dlg.getDialogPane().lookupButton(ok);
okNode.addEventFilter(javafx.event.ActionEvent.ACTION, ev -> {
if (!weitereCB.isSelected()) return;
ev.consume();
var action = actionCombo.getValue();
var clip = clipCombo.getValue();
if (action == null || clip == null) return;
String entry = action.name() + " → " + clip;
if (!animSetActionListView.getItems().contains(entry)) {
animSetActionListView.getItems().add(entry);
autoSaveAnimSet();
}
available.remove(action);
String lower = actionSearch.getText() == null ? "" : actionSearch.getText().toLowerCase();
actionCombo.getItems().setAll(available.stream()
.filter(a -> lower.isEmpty()
|| a.displayName().toLowerCase().contains(lower)
|| a.name().toLowerCase().contains(lower))
.collect(java.util.stream.Collectors.toList()));
if (!actionCombo.getItems().isEmpty()) actionCombo.getSelectionModel().selectFirst();
});
dlg.showAndWait().ifPresent(bt -> { dlg.showAndWait().ifPresent(bt -> {
if (bt != ok) return; if (bt != ok) return;
var action = actionCombo.getValue(); var action = actionCombo.getValue();

View File

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

View File

@@ -0,0 +1,279 @@
package de.blight.editor.toolset.content;
import de.blight.common.model.ItemGrip;
import de.blight.common.model.ItemSubCategory;
import de.blight.common.model.WeaponAttachmentIO;
import de.blight.common.model.WeaponCategoryAttachment;
import de.blight.editor.SharedInput;
import de.blight.editor.ToolContext;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.*;
/**
* Eigenständige Ansicht zum Bearbeiten der per-Kategorie Hand-Anker-Offsets.
* Zeigt den Helden direkt mit Waffe in der Idle-Animation – keine Griff-Gizmos.
*/
public class AttachmentEditorController {
private final ToolContext ctx;
private Spinner<Double> handOffXSpin, handOffYSpin, handOffZSpin;
private Spinner<Double> hand2OffXSpin, hand2OffYSpin, hand2OffZSpin;
private boolean suppressEvents = false;
private ItemSubCategory itemSubCategory = null;
private Runnable onReturn = null;
public AttachmentEditorController(ToolContext ctx) {
this.ctx = ctx;
}
public void setOnReturn(Runnable r) { this.onReturn = r; }
// ── Öffnen ────────────────────────────────────────────────────────────────
public void open(String modelPath, ItemSubCategory subCategory, ItemGrip grip) {
itemSubCategory = subCategory;
SharedInput input = ctx.input();
// Grip-Editor-Modus nicht aktiv – reiner Attachment-Preview
input.gripEditorActive = false;
input.gripEditorIsShield = false;
input.gripEditorCharPreview = false;
input.attachmentEditorActive = true;
// Subkategorie + aktuelle Config in SharedInput laden
input.gripEditorSubCategory = subCategory != null ? subCategory.name() : null;
WeaponCategoryAttachment cfg = WeaponAttachmentIO.get(subCategory);
input.gripEditorCatHandOffX = cfg.getHandOffX();
input.gripEditorCatHandOffY = cfg.getHandOffY();
input.gripEditorCatHandOffZ = cfg.getHandOffZ();
input.gripEditorCatHand2OffX = cfg.getHand2OffX();
input.gripEditorCatHand2OffY = cfg.getHand2OffY();
input.gripEditorCatHand2OffZ = cfg.getHand2OffZ();
input.gripEditorCatOffsetsDirty = false;
// Grip-Werte des Items setzen, damit updateCharPreviewWeapon g1/g2 kennt
ItemGrip g = grip != null ? grip : new ItemGrip();
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.gripEditorGizmoDirty = false;
// Waffenmodell laden
if (modelPath != null && !modelPath.isBlank()) {
input.modelEditorOpenPath = modelPath;
}
input.activeLayer = SharedInput.LAYER_MODEL_EDITOR;
if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(false);
ctx.setCenterView().accept(ctx.viewportSupplier().get());
ctx.setCurrentTool().accept("attachmentEditor");
ctx.setMainToolbar().accept(buildToolbar());
VBox innerPanel = buildPanel(cfg, subCategory);
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);
}
// ── Toolbar ───────────────────────────────────────────────────────────────
private ToolBar buildToolbar() {
Button backBtn = new Button("<- Zurück");
backBtn.setOnAction(e -> onCancel());
Label title = new Label("Hand-Anker-Editor");
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
ToolBar tb = new ToolBar();
tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), title);
return tb;
}
// ── Panel ─────────────────────────────────────────────────────────────────
private VBox buildPanel(WeaponCategoryAttachment cfg, ItemSubCategory subCategory) {
handOffXSpin = doubleSpin(cfg.getHandOffX(), -2.0, 2.0, 0.005, "m");
handOffYSpin = doubleSpin(cfg.getHandOffY(), -2.0, 2.0, 0.005, "m");
handOffZSpin = doubleSpin(cfg.getHandOffZ(), -2.0, 2.0, 0.005, "m");
hand2OffXSpin = doubleSpin(cfg.getHand2OffX(), -2.0, 2.0, 0.005, "m");
hand2OffYSpin = doubleSpin(cfg.getHand2OffY(), -2.0, 2.0, 0.005, "m");
hand2OffZSpin = doubleSpin(cfg.getHand2OffZ(), -2.0, 2.0, 0.005, "m");
handOffXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
handOffYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
handOffZSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
hand2OffXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
hand2OffYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
hand2OffZSpin.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 -> saveAndReturn());
Button cancelBtn = new Button("Abbrechen");
cancelBtn.setMaxWidth(Double.MAX_VALUE);
cancelBtn.setStyle("-fx-background-color: #3a3a3a; -fx-text-fill: #cccccc; -fx-cursor: hand;");
cancelBtn.setOnAction(e -> onCancel());
String catLabel = subCategory != null ? subCategory.name() : "–";
VBox panel = new VBox(8);
panel.setPadding(new Insets(12));
panel.setStyle("-fx-background-color: #252535;");
panel.setPrefWidth(220);
panel.getChildren().addAll(
styledTitle("Kategorie: " + catLabel),
styledHint("Offsets gelten für alle Waffen dieser Kategorie.\nDer Held zeigt die Waffe in der Idle-Animation."),
new Separator(),
styledTitle("Rechte Hand – Anker-Offset (m)"),
styledHint("Verschiebt den Befestigungspunkt der rechten Hand\nim Knochenraum des Charakters."),
row("X:", handOffXSpin),
row("Y:", handOffYSpin),
row("Z:", handOffZSpin),
new Separator(),
styledTitle("Linke Hand – Anker-Offset (m)"),
styledHint("Zweiter Griffpunkt (linke Hand) – nur relevant bei\nZweihänder-Waffen."),
row("X:", hand2OffXSpin),
row("Y:", hand2OffYSpin),
row("Z:", hand2OffZSpin),
new Separator(),
saveBtn,
cancelBtn
);
return panel;
}
// ── Spinner-Events ────────────────────────────────────────────────────────
private void onSpinnerChanged() {
if (suppressEvents) return;
SharedInput input = ctx.input();
input.gripEditorCatHandOffX = handOffXSpin.getValue().floatValue();
input.gripEditorCatHandOffY = handOffYSpin.getValue().floatValue();
input.gripEditorCatHandOffZ = handOffZSpin.getValue().floatValue();
input.gripEditorCatHand2OffX = hand2OffXSpin.getValue().floatValue();
input.gripEditorCatHand2OffY = hand2OffYSpin.getValue().floatValue();
input.gripEditorCatHand2OffZ = hand2OffZSpin.getValue().floatValue();
input.gripEditorCatOffsetsDirty = true;
}
// ── Speichern / Abbrechen ─────────────────────────────────────────────────
private void saveAndReturn() {
if (itemSubCategory != null) {
WeaponCategoryAttachment cfg = new WeaponCategoryAttachment();
cfg.setHandOffX(handOffXSpin.getValue().floatValue());
cfg.setHandOffY(handOffYSpin.getValue().floatValue());
cfg.setHandOffZ(handOffZSpin.getValue().floatValue());
cfg.setHand2OffX(hand2OffXSpin.getValue().floatValue());
cfg.setHand2OffY(hand2OffYSpin.getValue().floatValue());
cfg.setHand2OffZ(hand2OffZSpin.getValue().floatValue());
WeaponAttachmentIO.set(itemSubCategory, cfg);
java.nio.file.Path dest = ctx.assetRoot().resolve("weapon_attachment.json");
WeaponAttachmentIO.save(dest);
syncToMirrors(dest);
}
closeEditor();
}
private void syncToMirrors(java.nio.file.Path savedFile) {
java.nio.file.Path projectRoot = de.blight.editor.ProjectRoot.PATH;
String[] mirrorRoots = {
"blight-assets/bin/main",
"blight-assets/build/resources/main"
};
for (String root : mirrorRoots) {
java.nio.file.Path mirrorRoot = projectRoot.resolve(root);
if (!java.nio.file.Files.isDirectory(mirrorRoot)) continue;
java.nio.file.Path mirror = mirrorRoot.resolve("weapon_attachment.json");
try {
java.nio.file.Files.copy(savedFile, mirror,
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
// Mirror nicht vorhanden oder nicht beschreibbar – ignorieren
}
}
}
private void onCancel() {
closeEditor();
}
private void closeEditor() {
SharedInput input = ctx.input();
input.attachmentEditorActive = false;
input.gripEditorCharPreview = false;
input.gripEditorCatOffsetsDirty = false;
Runnable ret = onReturn;
onReturn = null;
itemSubCategory = null;
if (ret != null) ret.run(); else ctx.switchToWorld().run();
}
// ── Helpers ───────────────────────────────────────────────────────────────
private static Spinner<Double> doubleSpin(float initial, double min, double max, double step, String unit) {
SpinnerValueFactory.DoubleSpinnerValueFactory vf =
new SpinnerValueFactory.DoubleSpinnerValueFactory(min, max, initial, step);
vf.setConverter(new javafx.util.StringConverter<>() {
@Override public String toString(Double v) { return v == null ? "0" : String.format("%.3f", v) + " " + unit; }
@Override public Double fromString(String s) {
try { return Double.parseDouble(s.replace(unit, "").replace(",", ".").trim()); }
catch (NumberFormatException e) { return vf.getValue(); }
}
});
Spinner<Double> sp = new Spinner<>(vf);
sp.setEditable(true);
sp.setMaxWidth(Double.MAX_VALUE);
return sp;
}
private static Label styledTitle(String text) {
Label l = new Label(text);
l.setStyle("-fx-font-weight: bold; -fx-font-size: 11; -fx-text-fill: #88aacc;");
l.setWrapText(true);
return l;
}
private static Label styledHint(String text) {
Label l = new Label(text);
l.setWrapText(true);
l.setStyle("-fx-text-fill: #888; -fx-font-size: 10;");
return l;
}
private static HBox row(String labelText, Spinner<Double> spinner) {
Label lbl = new Label(labelText);
lbl.setMinWidth(30);
lbl.setStyle("-fx-text-fill: #aaa;");
HBox.setHgrow(spinner, Priority.ALWAYS);
HBox box = new HBox(6, lbl, spinner);
box.setAlignment(Pos.CENTER_LEFT);
return box;
}
}

View File

@@ -88,11 +88,15 @@ public class ContentController {
// ────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────
private Runnable onEnterContentMode; private Runnable onEnterContentMode;
private GripEditorController gripEditorController; private GripEditorController gripEditorController;
private AttachmentEditorController attachmentEditorController;
private ShieldAttachmentEditorController shieldAttachmentEditorController;
public ContentController(ToolContext ctx) { public ContentController(ToolContext ctx) {
this.ctx = ctx; this.ctx = ctx;
this.gripEditorController = new GripEditorController(ctx); this.gripEditorController = new GripEditorController(ctx);
this.attachmentEditorController = new AttachmentEditorController(ctx);
this.shieldAttachmentEditorController = new ShieldAttachmentEditorController(ctx);
} }
// ── Öffentliche API ──────────────────────────────────────────────────────── // ── Öffentliche API ────────────────────────────────────────────────────────
@@ -113,6 +117,14 @@ public class ContentController {
/** Per-Frame-Polling: animEmbedStatus + animStripStatus → charEditorStatusLabel. */ /** Per-Frame-Polling: animEmbedStatus + animStripStatus → charEditorStatusLabel. */
public void onFrame() { 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; String embedStatus = ctx.input().animEmbedStatus;
if (embedStatus != null) { if (embedStatus != null) {
ctx.input().animEmbedStatus = null; ctx.input().animEmbedStatus = null;
@@ -349,24 +361,25 @@ public class ContentController {
showContentEditor(); showContentEditor();
contentLastSection = "item"; contentLastSection = "item";
if (contentItemBtn != null) contentItemBtn.setSelected(true); if (contentItemBtn != null) contentItemBtn.setSelected(true);
de.blight.editor.ui.ItemEditorView view = activeItemView = new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items")); wireItemView(activeItemView);
wireItemView(view); setContentCenter(activeItemView);
setContentCenter(view);
setContentRight(null); setContentRight(null);
if (selectItemId != null) view.selectItem(selectItemId); if (selectItemId != null) activeItemView.selectItem(selectItemId);
return; return;
} }
ctx.setCurrentTool().accept("itemEditor"); ctx.setCurrentTool().accept("itemEditor");
ToolBar tb = makeSingleWindowToolBar("Item-Manager"); ToolBar tb = makeSingleWindowToolBar("Item-Manager");
ctx.setMainToolbar().accept(tb); ctx.setMainToolbar().accept(tb);
ctx.setLeftPanel().accept(buildContentTreePanel()); ctx.setLeftPanel().accept(buildContentTreePanel());
de.blight.editor.ui.ItemEditorView view = activeItemView = new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items")); wireItemView(activeItemView);
wireItemView(view); ctx.setCenterView().accept(activeItemView);
ctx.setCenterView().accept(view);
ctx.setRightPanel().accept(null); ctx.setRightPanel().accept(null);
if (selectItemId != null) view.selectItem(selectItemId); if (selectItemId != null) {
activeItemView.selectItem(selectItemId);
selectItemInTree(selectItemId);
}
} }
/** Prefs-Restore: stellt Dual-Window-Position und Content-Section wieder her. */ /** Prefs-Restore: stellt Dual-Window-Position und Content-Section wieder her. */
@@ -989,7 +1002,7 @@ public class ContentController {
private void wireItemView(de.blight.editor.ui.ItemEditorView view) { private void wireItemView(de.blight.editor.ui.ItemEditorView view) {
view.setOnIdChanged(this::refreshContentItemTree); view.setOnIdChanged(this::refreshContentItemTree);
view.setGripEditorLauncher((modelPath, currentGrip, isShield, onSave) -> { view.setGripEditorLauncher((modelPath, currentGrip, isShield, subCategory, onSave) -> {
String itemId = view.getCurrentItemId(); String itemId = view.getCurrentItemId();
gripEditorController.setOnReturn(() -> { gripEditorController.setOnReturn(() -> {
if (dualWindowMode) { if (dualWindowMode) {
@@ -999,7 +1012,31 @@ public class ContentController {
switchToItemEditor(itemId); switchToItemEditor(itemId);
} }
}); });
gripEditorController.open(modelPath, currentGrip, isShield, onSave); gripEditorController.open(modelPath, currentGrip, isShield, subCategory, onSave);
});
view.setAttachmentEditorLauncher((modelPath, subCategory, grip) -> {
String itemId = view.getCurrentItemId();
attachmentEditorController.setOnReturn(() -> {
if (dualWindowMode) {
buildItemViewInContent();
if (activeItemView != null) activeItemView.selectItem(itemId);
} else {
switchToItemEditor(itemId);
}
});
attachmentEditorController.open(modelPath, subCategory, grip);
});
view.setShieldAttachmentEditorLauncher((modelPath, subCategory, grip) -> {
String itemId = view.getCurrentItemId();
shieldAttachmentEditorController.setOnReturn(() -> {
if (dualWindowMode) {
buildItemViewInContent();
if (activeItemView != null) activeItemView.selectItem(itemId);
} else {
switchToItemEditor(itemId);
}
});
shieldAttachmentEditorController.open(modelPath, subCategory, grip);
}); });
} }
@@ -1687,6 +1724,25 @@ public class ContentController {
} }
} }
private void selectItemInTree(String itemId) {
if (contentItemNode == null || contentTreeView == null || itemId == null) return;
contentItemNode.setExpanded(true);
for (TreeItem<String> leaf : contentItemNode.getChildren()) {
String val = leaf.getValue();
String leafId = val != null && val.contains(":") ? val.substring(val.indexOf(':') + 1) : val;
if (itemId.equals(leafId)) {
contentTreeUpdating = true;
try {
contentTreeView.getSelectionModel().select(leaf);
contentTreeView.scrollTo(contentTreeView.getRow(leaf));
} finally {
contentTreeUpdating = false;
}
return;
}
}
}
private void refreshContentFractionTree() { private void refreshContentFractionTree() {
if (contentFractionNode == null) return; if (contentFractionNode == null) return;
contentTreeUpdating = true; contentTreeUpdating = true;

View File

@@ -1,6 +1,7 @@
package de.blight.editor.toolset.content; package de.blight.editor.toolset.content;
import de.blight.common.model.ItemGrip; import de.blight.common.model.ItemGrip;
import de.blight.common.model.ItemSubCategory;
import de.blight.editor.SharedInput; import de.blight.editor.SharedInput;
import de.blight.editor.ToolContext; import de.blight.editor.ToolContext;
import de.blight.editor.state.ModelEditorState; import de.blight.editor.state.ModelEditorState;
@@ -23,14 +24,42 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
private Spinner<Double> posXSpin, posYSpin, posZSpin; private Spinner<Double> posXSpin, posYSpin, posZSpin;
private Spinner<Double> rotXSpin, rotYSpin, rotZSpin; private Spinner<Double> rotXSpin, rotYSpin, rotZSpin;
private Spinner<Double> grip2XSpin, grip2YSpin, grip2ZSpin;
private Spinner<Double> handOffXSpin, handOffYSpin, handOffZSpin;
private Consumer<ItemGrip> pendingOnSave; private Consumer<ItemGrip> pendingOnSave;
private Runnable onReturn; private Runnable onReturn;
private boolean suppressSpinnerEvents = false; private boolean suppressSpinnerEvents = false;
private boolean shieldMode = false; private boolean shieldMode = false;
private ItemSubCategory itemSubCategory = null;
public void setOnReturn(Runnable r) { this.onReturn = r; } 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;
float bladeDirZ = ctx.input().gripEditorBladeDirZ; // -1 = Klinge bei -Z, +1 = Klinge bei +Z
// 5cm Richtung Pommel von der erkannten Parierstange
float posZ = gZ + bladeDirZ * 0.05f;
posXSpin.getValueFactory().setValue((double) gX);
posYSpin.getValueFactory().setValue((double) gY);
posZSpin.getValueFactory().setValue((double) posZ);
// 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) nur für Zweihänder setzen
if (itemSubCategory == ItemSubCategory.TWO_HANDED_SWORD) {
float grip1JsonZ = posZ + ItemGrip.HAND_GRIP_OFFSET_Z;
grip2XSpin.getValueFactory().setValue((double) gX);
grip2YSpin.getValueFactory().setValue((double) gY);
grip2ZSpin.getValueFactory().setValue((double)(grip1JsonZ - bladeDirZ * 0.20f));
}
suppressSpinnerEvents = false;
onSpinnerChanged();
}
public GripEditorController(ToolContext ctx) { public GripEditorController(ToolContext ctx) {
this.ctx = ctx; this.ctx = ctx;
} }
@@ -38,22 +67,35 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
// ── GripEditorLauncher ──────────────────────────────────────────────────── // ── GripEditorLauncher ────────────────────────────────────────────────────
@Override @Override
public void open(String modelPath, ItemGrip currentGrip, boolean isShield, Consumer<ItemGrip> onSave) { public void open(String modelPath, ItemGrip currentGrip, boolean isShield,
pendingOnSave = onSave; ItemSubCategory subCategory, Consumer<ItemGrip> onSave) {
shieldMode = isShield; pendingOnSave = onSave;
shieldMode = isShield;
itemSubCategory = subCategory;
SharedInput input = ctx.input(); SharedInput input = ctx.input();
input.gripEditorActive = true; input.gripEditorActive = true;
input.gripEditorIsShield = isShield; input.gripEditorIsShield = isShield;
input.gripEditorSubCategory = subCategory != null ? subCategory.name() : null;
input.gripEditorCharPreview = false;
input.gripEditorCharPreviewReady = false;
input.attachmentEditorActive = false;
// Grip-Werte aus Item übernehmen (oder Defaults) // 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(); ItemGrip g = currentGrip != null ? currentGrip : new ItemGrip();
input.gripEditorPosX = g.getPosX(); input.gripEditorPosX = g.getPosX() - ItemGrip.HAND_GRIP_OFFSET_X;
input.gripEditorPosY = g.getPosY(); input.gripEditorPosY = g.getPosY() - ItemGrip.HAND_GRIP_OFFSET_Y;
input.gripEditorPosZ = g.getPosZ(); input.gripEditorPosZ = g.getPosZ() - ItemGrip.HAND_GRIP_OFFSET_Z;
input.gripEditorRotX = g.getRotX(); input.gripEditorRotX = g.getRotX();
input.gripEditorRotY = g.getRotY(); input.gripEditorRotY = g.getRotY();
input.gripEditorRotZ = g.getRotZ(); 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; input.gripEditorGizmoDirty = true;
// Waffenmodell im Modell-Editor laden // Waffenmodell im Modell-Editor laden
@@ -61,12 +103,30 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
input.modelEditorOpenPath = modelPath; input.modelEditorOpenPath = modelPath;
} }
input.activeLayer = SharedInput.LAYER_MODEL_EDITOR; 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 // Hauptfenster: JME-Viewport + Grip-Panel
ctx.setCenterView().accept(ctx.viewportSupplier().get()); ctx.setCenterView().accept(ctx.viewportSupplier().get());
ctx.setCurrentTool().accept("gripEditor"); ctx.setCurrentTool().accept("gripEditor");
ctx.setMainToolbar().accept(buildToolbar()); 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.setLeftPanel().accept(null);
ctx.setBottomPanel().accept(null); ctx.setBottomPanel().accept(null);
} }
@@ -74,7 +134,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
// ── Toolbar ─────────────────────────────────────────────────────────────── // ── Toolbar ───────────────────────────────────────────────────────────────
private ToolBar buildToolbar() { private ToolBar buildToolbar() {
Button cancelBtn = new Button("← Zurück"); Button cancelBtn = new Button("<- Zurück");
cancelBtn.setOnAction(e -> onCancel()); cancelBtn.setOnAction(e -> onCancel());
Label title = new Label("Griff-Editor"); Label title = new Label("Griff-Editor");
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
@@ -86,12 +146,18 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
// ── Panel ───────────────────────────────────────────────────────────────── // ── Panel ─────────────────────────────────────────────────────────────────
private VBox buildPanel(ItemGrip g, boolean isShield) { private VBox buildPanel(ItemGrip g, boolean isShield) {
posXSpin = doubleSpin(g.getPosX(), -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(), -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(), -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, "°"); rotXSpin = doubleSpin(g.getRotX(), -180.0, 180.0, 1.0, "°");
rotYSpin = doubleSpin(g.getRotY(), -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, "°"); 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()); posXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
posYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); posYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
@@ -99,12 +165,48 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
rotXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); rotXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
rotYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); rotYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
rotZSpin.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"); Button saveBtn = new Button("Speichern");
saveBtn.setMaxWidth(Double.MAX_VALUE); saveBtn.setMaxWidth(Double.MAX_VALUE);
saveBtn.setStyle("-fx-background-color: #2a5a2a; -fx-text-fill: #aaffaa; -fx-cursor: hand; -fx-font-weight: bold;"); saveBtn.setStyle("-fx-background-color: #2a5a2a; -fx-text-fill: #aaffaa; -fx-cursor: hand; -fx-font-weight: bold;");
saveBtn.setOnAction(e -> onSave()); 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"); Button cancelBtn = new Button("Abbrechen");
cancelBtn.setMaxWidth(Double.MAX_VALUE); cancelBtn.setMaxWidth(Double.MAX_VALUE);
cancelBtn.setStyle("-fx-background-color: #3a3a3a; -fx-text-fill: #cccccc; -fx-cursor: hand;"); cancelBtn.setStyle("-fx-background-color: #3a3a3a; -fx-text-fill: #cccccc; -fx-cursor: hand;");
@@ -116,21 +218,37 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
panel.setPrefWidth(220); panel.setPrefWidth(220);
panel.getChildren().addAll( panel.getChildren().addAll(
styledTitle("Position (m)"), styledTitle("1. Griff – Position (m)"),
styledHint("Handflächen-Kontaktpunkt im Modellraum (oranges Gizmo)."),
row("X:", posXSpin), row("X:", posXSpin),
row("Y:", posYSpin), row("Y:", posYSpin),
row("Z:", posZSpin), row("Z:", posZSpin),
new Separator(), new Separator(),
styledTitle("Rotation (°)"), styledTitle("Rotation Hand (°)"),
styledHint("Dreht den gesamten Griff-Gizmo. 0° = Hand oben (+Y), Klinge in -Z."),
row("X:", rotXSpin), row("X:", rotXSpin),
row("Y:", rotYSpin), row("Y:", rotYSpin),
row("Z:", rotZSpin), row("Z:", rotZSpin),
new Separator(), new Separator(),
styledTitle("2. Griff – Linke Hand (m)"),
styledHint("Zweiter Griffpunkt (blaues Gizmo) für Zweihänder.\n~20cm Richtung Knauf vom 1. Griff."),
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 styledHint(isShield
? "Pfeil zeigt vom Ellbogen zur Hand.\n28cm = Unterarmlänge." ? "Cyan-Pfeil zeigt vom Ellbogen zur Hand.\n28cm = Unterarmlänge."
: "Pfeil zeigt Klingenrichtung.\n15cm Länge = Maßstab."), : "Orange-Pfeil zeigt zur Klinge, gelber Stab nach oben (+Y).\nBlauer Pfeil = 2. Griff (linke Hand)."),
new Separator(), new Separator(),
saveBtn, saveBtn,
guessBtn,
resetBtn,
cancelBtn cancelBtn
); );
return panel; return panel;
@@ -141,12 +259,18 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
private void onSpinnerChanged() { private void onSpinnerChanged() {
if (suppressSpinnerEvents) return; if (suppressSpinnerEvents) return;
SharedInput input = ctx.input(); SharedInput input = ctx.input();
input.gripEditorPosX = posXSpin.getValue().floatValue(); input.gripEditorPosX = posXSpin.getValue().floatValue();
input.gripEditorPosY = posYSpin.getValue().floatValue(); input.gripEditorPosY = posYSpin.getValue().floatValue();
input.gripEditorPosZ = posZSpin.getValue().floatValue(); input.gripEditorPosZ = posZSpin.getValue().floatValue();
input.gripEditorRotX = rotXSpin.getValue().floatValue(); input.gripEditorRotX = rotXSpin.getValue().floatValue();
input.gripEditorRotY = rotYSpin.getValue().floatValue(); input.gripEditorRotY = rotYSpin.getValue().floatValue();
input.gripEditorRotZ = rotZSpin.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; input.gripEditorGizmoDirty = true;
} }
@@ -154,16 +278,24 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
private void onSave() { private void onSave() {
ItemGrip grip = new ItemGrip(); ItemGrip grip = new ItemGrip();
grip.setPosX(posXSpin.getValue().floatValue()); // Spinner zeigen Handflächen-Position → HAND_GRIP_OFFSET wieder addieren für JSON
grip.setPosY(posYSpin.getValue().floatValue()); grip.setPosX(posXSpin.getValue().floatValue() + ItemGrip.HAND_GRIP_OFFSET_X);
grip.setPosZ(posZSpin.getValue().floatValue()); 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.setRotX(rotXSpin.getValue().floatValue());
grip.setRotY(rotYSpin.getValue().floatValue()); grip.setRotY(rotYSpin.getValue().floatValue());
grip.setRotZ(rotZSpin.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(); SharedInput input = ctx.input();
input.gripEditorActive = false; input.gripEditorActive = false;
input.gripEditorIsShield = false; input.gripEditorIsShield = false;
input.gripEditorCharPreview = false;
Consumer<ItemGrip> save = pendingOnSave; Consumer<ItemGrip> save = pendingOnSave;
Runnable ret = onReturn; Runnable ret = onReturn;
@@ -175,8 +307,10 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
} }
private void onCancel() { private void onCancel() {
ctx.input().gripEditorActive = false; SharedInput input = ctx.input();
ctx.input().gripEditorIsShield = false; input.gripEditorActive = false;
input.gripEditorIsShield = false;
input.gripEditorCharPreview = false;
Runnable ret = onReturn; Runnable ret = onReturn;
pendingOnSave = null; pendingOnSave = null;
@@ -191,7 +325,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
SpinnerValueFactory.DoubleSpinnerValueFactory vf = SpinnerValueFactory.DoubleSpinnerValueFactory vf =
new SpinnerValueFactory.DoubleSpinnerValueFactory(min, max, initial, step); new SpinnerValueFactory.DoubleSpinnerValueFactory(min, max, initial, step);
vf.setConverter(new javafx.util.StringConverter<>() { vf.setConverter(new javafx.util.StringConverter<>() {
@Override public String toString(Double v) { return v == null ? "0" : String.format("%.2f", v) + " " + unit; } @Override public String toString(Double v) { return v == null ? "0" : String.format("%.3f", v) + " " + unit; }
@Override public Double fromString(String s) { @Override public Double fromString(String s) {
try { return Double.parseDouble(s.replace(unit, "").replace(",", ".").trim()); } try { return Double.parseDouble(s.replace(unit, "").replace(",", ".").trim()); }
catch (NumberFormatException e) { return vf.getValue(); } catch (NumberFormatException e) { return vf.getValue(); }
@@ -218,7 +352,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
private static HBox row(String labelText, Spinner<Double> spinner) { private static HBox row(String labelText, Spinner<Double> spinner) {
Label lbl = new Label(labelText); Label lbl = new Label(labelText);
lbl.setMinWidth(20); lbl.setMinWidth(30);
lbl.setStyle("-fx-text-fill: #aaa;"); lbl.setStyle("-fx-text-fill: #aaa;");
HBox.setHgrow(spinner, Priority.ALWAYS); HBox.setHgrow(spinner, Priority.ALWAYS);
HBox box = new HBox(6, lbl, spinner); HBox box = new HBox(6, lbl, spinner);

View File

@@ -0,0 +1,253 @@
package de.blight.editor.toolset.content;
import de.blight.common.model.ItemGrip;
import de.blight.common.model.ItemSubCategory;
import de.blight.common.model.ShieldAttachmentIO;
import de.blight.common.model.ShieldCategoryAttachment;
import de.blight.editor.SharedInput;
import de.blight.editor.ToolContext;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.*;
/**
* Eigenständige Ansicht zum Bearbeiten der Schild-Arm-Attachment-Parameter pro Kategorie.
* Zeigt den Helden in Sword-And-Shield Idle mit Schild am linken Arm.
*/
public class ShieldAttachmentEditorController {
private final ToolContext ctx;
private Spinner<Double> rotXSpin, rotYSpin, rotZSpin, elbowOffsetSpin;
private boolean suppressEvents = false;
private ItemSubCategory itemSubCategory = null;
private Runnable onReturn = null;
public ShieldAttachmentEditorController(ToolContext ctx) {
this.ctx = ctx;
}
public void setOnReturn(Runnable r) { this.onReturn = r; }
// ── Öffnen ────────────────────────────────────────────────────────────────
public void open(String modelPath, ItemSubCategory subCategory, ItemGrip grip) {
itemSubCategory = subCategory;
SharedInput input = ctx.input();
input.gripEditorActive = false;
input.gripEditorCharPreview = false;
input.attachmentEditorActive = false;
input.shieldEditorActive = true;
input.shieldEditorSubCategory = subCategory != null ? subCategory.name() : null;
ShieldCategoryAttachment cfg = ShieldAttachmentIO.get(subCategory);
input.shieldEditorRotX = cfg.getRotX();
input.shieldEditorRotY = cfg.getRotY();
input.shieldEditorRotZ = cfg.getRotZ();
input.shieldEditorElbowOffset = cfg.getElbowOffset();
input.shieldEditorOffsetsDirty = false;
// Grip des Schilds für Preview-Positionierung
ItemGrip g = grip != null ? grip : new ItemGrip();
input.shieldEditorGripX = g.getPosX();
input.shieldEditorGripY = g.getPosY();
input.shieldEditorGripZ = g.getPosZ();
if (modelPath != null && !modelPath.isBlank()) {
input.modelEditorOpenPath = modelPath;
}
input.activeLayer = SharedInput.LAYER_MODEL_EDITOR;
if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(false);
ctx.setCenterView().accept(ctx.viewportSupplier().get());
ctx.setCurrentTool().accept("shieldAttachmentEditor");
ctx.setMainToolbar().accept(buildToolbar());
VBox innerPanel = buildPanel(cfg, subCategory);
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);
}
// ── Toolbar ───────────────────────────────────────────────────────────────
private ToolBar buildToolbar() {
Button backBtn = new Button("<- Zurück");
backBtn.setOnAction(e -> onCancel());
Label title = new Label("Schild-Anker-Editor");
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
ToolBar tb = new ToolBar();
tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), title);
return tb;
}
// ── Panel ─────────────────────────────────────────────────────────────────
private VBox buildPanel(ShieldCategoryAttachment cfg, ItemSubCategory subCategory) {
rotXSpin = doubleSpin(cfg.getRotX(), -360.0, 360.0, 1.0, "°");
rotYSpin = doubleSpin(cfg.getRotY(), -360.0, 360.0, 1.0, "°");
rotZSpin = doubleSpin(cfg.getRotZ(), -360.0, 360.0, 1.0, "°");
elbowOffsetSpin = doubleSpin(cfg.getElbowOffset(), 0.0, 1.0, 0.005, "m");
rotXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
rotYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
rotZSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
elbowOffsetSpin.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 -> saveAndReturn());
Button cancelBtn = new Button("Abbrechen");
cancelBtn.setMaxWidth(Double.MAX_VALUE);
cancelBtn.setStyle("-fx-background-color: #3a3a3a; -fx-text-fill: #cccccc; -fx-cursor: hand;");
cancelBtn.setOnAction(e -> onCancel());
String catLabel = subCategory != null ? subCategory.name() : "–";
VBox panel = new VBox(8);
panel.setPadding(new Insets(12));
panel.setStyle("-fx-background-color: #252535;");
panel.setPrefWidth(220);
panel.getChildren().addAll(
styledTitle("Kategorie: " + catLabel),
styledHint("Steuert wie das Schild am linken Unterarm angebracht ist.\nDer Held zeigt die Pose in der Sword-And-Shield Idle-Animation."),
new Separator(),
styledTitle("Rotation am Arm (Grad)"),
styledHint("Dreht das Schild relativ zur Bind-Pose des LeftHand-Knochens."),
row("X:", rotXSpin),
row("Y:", rotYSpin),
row("Z:", rotZSpin),
new Separator(),
styledTitle("Ellbogen-Offset (m)"),
styledHint("Verschiebung in Richtung Ellbogen (lokale -Y des LeftHand-Knochens)."),
row("Off:", elbowOffsetSpin),
new Separator(),
saveBtn,
cancelBtn
);
return panel;
}
// ── Spinner-Events ────────────────────────────────────────────────────────
private void onSpinnerChanged() {
if (suppressEvents) return;
SharedInput input = ctx.input();
input.shieldEditorRotX = rotXSpin.getValue().floatValue();
input.shieldEditorRotY = rotYSpin.getValue().floatValue();
input.shieldEditorRotZ = rotZSpin.getValue().floatValue();
input.shieldEditorElbowOffset = elbowOffsetSpin.getValue().floatValue();
input.shieldEditorOffsetsDirty = true;
}
// ── Speichern / Abbrechen ─────────────────────────────────────────────────
private void saveAndReturn() {
if (itemSubCategory != null) {
ShieldCategoryAttachment cfg = new ShieldCategoryAttachment();
cfg.setRotX(rotXSpin.getValue().floatValue());
cfg.setRotY(rotYSpin.getValue().floatValue());
cfg.setRotZ(rotZSpin.getValue().floatValue());
cfg.setElbowOffset(elbowOffsetSpin.getValue().floatValue());
ShieldAttachmentIO.set(itemSubCategory, cfg);
java.nio.file.Path dest = ctx.assetRoot().resolve("shield_attachment.json");
ShieldAttachmentIO.save(dest);
syncToMirrors(dest);
}
closeEditor();
}
private void syncToMirrors(java.nio.file.Path savedFile) {
java.nio.file.Path projectRoot = de.blight.editor.ProjectRoot.PATH;
String[] mirrorRoots = {
"blight-assets/bin/main",
"blight-assets/build/resources/main"
};
for (String root : mirrorRoots) {
java.nio.file.Path mirror = projectRoot.resolve(root).resolve("shield_attachment.json");
try {
java.nio.file.Files.copy(savedFile, mirror,
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
// Mirror nicht vorhanden – ignorieren
}
}
}
private void onCancel() {
closeEditor();
}
private void closeEditor() {
SharedInput input = ctx.input();
input.shieldEditorActive = false;
input.shieldEditorOffsetsDirty = false;
Runnable ret = onReturn;
onReturn = null;
itemSubCategory = null;
if (ret != null) ret.run(); else ctx.switchToWorld().run();
}
// ── Helpers ───────────────────────────────────────────────────────────────
private static Spinner<Double> doubleSpin(float initial, double min, double max, double step, String unit) {
SpinnerValueFactory.DoubleSpinnerValueFactory vf =
new SpinnerValueFactory.DoubleSpinnerValueFactory(min, max, initial, step);
vf.setConverter(new javafx.util.StringConverter<>() {
@Override public String toString(Double v) { return v == null ? "0" : String.format("%.3f", v) + " " + unit; }
@Override public Double fromString(String s) {
try { return Double.parseDouble(s.replace(unit, "").replace(",", ".").trim()); }
catch (NumberFormatException e) { return vf.getValue(); }
}
});
Spinner<Double> sp = new Spinner<>(vf);
sp.setEditable(true);
sp.setMaxWidth(Double.MAX_VALUE);
return sp;
}
private static Label styledTitle(String text) {
Label l = new Label(text);
l.setStyle("-fx-font-weight: bold; -fx-font-size: 11; -fx-text-fill: #88aacc;");
l.setWrapText(true);
return l;
}
private static Label styledHint(String text) {
Label l = new Label(text);
l.setWrapText(true);
l.setStyle("-fx-text-fill: #888; -fx-font-size: 10;");
return l;
}
private static HBox row(String labelText, Spinner<Double> spinner) {
Label lbl = new Label(labelText);
lbl.setMinWidth(30);
lbl.setStyle("-fx-text-fill: #aaa;");
HBox.setHgrow(spinner, Priority.ALWAYS);
HBox box = new HBox(6, lbl, spinner);
box.setAlignment(Pos.CENTER_LEFT);
return box;
}
}

View File

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

View File

@@ -98,6 +98,7 @@ public class ModelEditorController implements ToolController {
modelEditorCurrentPath = relPath; modelEditorCurrentPath = relPath;
ctx.input().activeLayer = SharedInput.LAYER_MODEL_EDITOR; ctx.input().activeLayer = SharedInput.LAYER_MODEL_EDITOR;
if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(false);
ctx.setCenterView().accept(ctx.viewportSupplier().get()); ctx.setCenterView().accept(ctx.viewportSupplier().get());
openAssetOverlay.run(); openAssetOverlay.run();
@@ -181,6 +182,13 @@ public class ModelEditorController implements ToolController {
if (modelEditorSpinZ != null) modelEditorSpinZ.getValueFactory().setValue(bsZ); 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 // Bounds-Aktualisierung
if (ctx.input().modelEditorBoundsReady) { if (ctx.input().modelEditorBoundsReady) {
ctx.input().modelEditorBoundsReady = false; ctx.input().modelEditorBoundsReady = false;
@@ -269,11 +277,8 @@ public class ModelEditorController implements ToolController {
public void openModelEditor(String relPath, Path absolutePath) { public void openModelEditor(String relPath, Path absolutePath) {
this.pendingRelPath = relPath; this.pendingRelPath = relPath;
this.pendingAbsPath = absolutePath; this.pendingAbsPath = absolutePath;
if (ctx.registry().getActive() == this) { ctx.switchToWorld().run();
activate(); ctx.registry().activate(this);
} else {
ctx.registry().activate(this);
}
} }
/** Öffnet den Datei-Dialog für den Modell-Import. */ /** Öffnet den Datei-Dialog für den Modell-Import. */
@@ -811,7 +816,13 @@ public class ModelEditorController implements ToolController {
closeBtn.setMaxWidth(Double.MAX_VALUE); closeBtn.setMaxWidth(Double.MAX_VALUE);
closeBtn.setStyle("-fx-background-color:#555; -fx-text-fill:#ccc;"); 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, relPath, absolutePath,
nameTF.getText().trim(), nameTF.getText().trim(),
catCB.getValue() != null ? catCB.getValue().trim() : "", catCB.getValue() != null ? catCB.getValue().trim() : "",
@@ -843,7 +854,8 @@ public class ModelEditorController implements ToolController {
: "", : "",
modelEditorCategoryCB != null modelEditorCategoryCB != null
? modelEditorCategoryCB.getValue() ? modelEditorCategoryCB.getValue()
: de.blight.common.ModelCategory.NONE)); : de.blight.common.ModelCategory.NONE);
});
placeBtn.setOnAction(e -> { placeBtn.setOnAction(e -> {
ctx.input().modelEditorCloseRequest = true; ctx.input().modelEditorCloseRequest = true;
@@ -858,6 +870,35 @@ public class ModelEditorController implements ToolController {
ctx.switchToWorld().run(); 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( panel.getChildren().addAll(
title, title,
new Separator(), new Separator(),
@@ -889,6 +930,8 @@ public class ModelEditorController implements ToolController {
new Separator(), new Separator(),
footstepTitle, modelEditorFootstepSurfaceCB, footstepTitle, modelEditorFootstepSurfaceCB,
new Separator(), new Separator(),
toolsTitle, alignWeaponBtn,
new Separator(),
saveBtn, placeBtn, closeBtn saveBtn, placeBtn, closeBtn
); );
return panel; return panel;
@@ -972,6 +1015,7 @@ public class ModelEditorController implements ToolController {
ctx.setMainToolbar().accept(buildModelImportToolBar()); ctx.setMainToolbar().accept(buildModelImportToolBar());
ctx.input().activeLayer = SharedInput.LAYER_MODEL_EDITOR; ctx.input().activeLayer = SharedInput.LAYER_MODEL_EDITOR;
if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(false);
ctx.setCenterView().accept(ctx.viewportSupplier().get()); ctx.setCenterView().accept(ctx.viewportSupplier().get());
openAssetOverlay.run(); openAssetOverlay.run();
@@ -1291,6 +1335,14 @@ public class ModelEditorController implements ToolController {
s.setEditable(true); s.setEditable(true);
s.setMaxWidth(Double.MAX_VALUE); s.setMaxWidth(Double.MAX_VALUE);
s.setStyle("-fx-background-color:#3a3a4e;"); 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; return s;
} }

View File

@@ -56,11 +56,21 @@ public class ItemEditorView extends BorderPane {
private VBox effectsRows; private VBox effectsRows;
public interface GripEditorLauncher { public interface GripEditorLauncher {
void open(String modelPath, ItemGrip currentGrip, boolean isShield, java.util.function.Consumer<ItemGrip> onSave); void open(String modelPath, ItemGrip currentGrip, boolean isShield, ItemSubCategory subCategory, java.util.function.Consumer<ItemGrip> onSave);
}
public interface AttachmentEditorLauncher {
void open(String modelPath, ItemSubCategory subCategory, ItemGrip grip);
}
public interface ShieldAttachmentEditorLauncher {
void open(String modelPath, ItemSubCategory subCategory, ItemGrip grip);
} }
private Runnable onIdChanged; private Runnable onIdChanged;
private GripEditorLauncher gripEditorLauncher; private GripEditorLauncher gripEditorLauncher;
private AttachmentEditorLauncher attachmentEditorLauncher;
private ShieldAttachmentEditorLauncher shieldAttachmentEditorLauncher;
private final PauseTransition savePause = new PauseTransition(Duration.millis(600)); private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
@@ -90,6 +100,8 @@ public class ItemEditorView extends BorderPane {
public void setOnIdChanged(Runnable r) { this.onIdChanged = r; } public void setOnIdChanged(Runnable r) { this.onIdChanged = r; }
public void setGripEditorLauncher(GripEditorLauncher l) { this.gripEditorLauncher = l; } public void setGripEditorLauncher(GripEditorLauncher l) { this.gripEditorLauncher = l; }
public void setAttachmentEditorLauncher(AttachmentEditorLauncher l) { this.attachmentEditorLauncher = l; }
public void setShieldAttachmentEditorLauncher(ShieldAttachmentEditorLauncher l) { this.shieldAttachmentEditorLauncher = l; }
public String getCurrentItemId() { return current != null ? current.getItemId() : null; } public String getCurrentItemId() { return current != null ? current.getItemId() : null; }
public void selectItem(String itemId) { public void selectItem(String itemId) {
@@ -237,11 +249,17 @@ public class ItemEditorView extends BorderPane {
gripBtn.setStyle("-fx-background-color: #3a3a5a; -fx-text-fill: #ddddff; -fx-cursor: hand;"); gripBtn.setStyle("-fx-background-color: #3a3a5a; -fx-text-fill: #ddddff; -fx-cursor: hand;");
gripBtn.setOnAction(e -> openGripEditor()); gripBtn.setOnAction(e -> openGripEditor());
Button attachmentBtn = new Button("Hand-Anker bearbeiten...");
attachmentBtn.setMaxWidth(Double.MAX_VALUE);
attachmentBtn.setStyle("-fx-background-color: #1a3a3a; -fx-text-fill: #88ffcc; -fx-cursor: hand;");
attachmentBtn.setOnAction(e -> openAttachmentEditor());
weaponSection = new VBox(6, weaponSection = new VBox(6,
new Separator(), new Separator(),
sectionTitle("Kampfwerte"), sectionTitle("Kampfwerte"),
row("Schaden:", damageSpinner), row("Schaden:", damageSpinner),
gripBtn gripBtn,
attachmentBtn
); );
weaponSection.setVisible(false); weaponSection.setVisible(false);
weaponSection.setManaged(false); weaponSection.setManaged(false);
@@ -252,10 +270,16 @@ public class ItemEditorView extends BorderPane {
shieldGripBtn.setStyle("-fx-background-color: #2a4a5a; -fx-text-fill: #aaddff; -fx-cursor: hand;"); shieldGripBtn.setStyle("-fx-background-color: #2a4a5a; -fx-text-fill: #aaddff; -fx-cursor: hand;");
shieldGripBtn.setOnAction(e -> openShieldGripEditor()); shieldGripBtn.setOnAction(e -> openShieldGripEditor());
Button shieldAttachmentBtn = new Button("Arm-Anker bearbeiten...");
shieldAttachmentBtn.setMaxWidth(Double.MAX_VALUE);
shieldAttachmentBtn.setStyle("-fx-background-color: #1a4a4a; -fx-text-fill: #aaffee; -fx-cursor: hand;");
shieldAttachmentBtn.setOnAction(e -> openShieldAttachmentEditor());
shieldSection = new VBox(6, shieldSection = new VBox(6,
new Separator(), new Separator(),
sectionTitle("Schild-Griff"), sectionTitle("Schild-Griff"),
shieldGripBtn shieldGripBtn,
shieldAttachmentBtn
); );
shieldSection.setVisible(false); shieldSection.setVisible(false);
shieldSection.setManaged(false); shieldSection.setManaged(false);
@@ -325,21 +349,34 @@ public class ItemEditorView extends BorderPane {
private void openGripEditor() { private void openGripEditor() {
if (current == null || gripEditorLauncher == null) return; if (current == null || gripEditorLauncher == null) return;
String modelPath = modelRefField.getText().trim(); String modelPath = modelRefField.getText().trim();
gripEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getGrip(), false, grip -> { gripEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getGrip(), false, current.getSubCategory(), grip -> {
current.setGrip(grip); current.setGrip(grip);
persistCurrent(); persistCurrent();
}); });
} }
private void openAttachmentEditor() {
if (current == null || attachmentEditorLauncher == null) return;
String modelPath = modelRefField.getText().trim();
attachmentEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getSubCategory(), current.getGrip());
}
private void openShieldGripEditor() { private void openShieldGripEditor() {
if (current == null || gripEditorLauncher == null) return; if (current == null || gripEditorLauncher == null) return;
String modelPath = modelRefField.getText().trim(); String modelPath = modelRefField.getText().trim();
gripEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getGrip(), true, grip -> { gripEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getGrip(), true, current.getSubCategory(), grip -> {
current.setGrip(grip); current.setGrip(grip);
persistCurrent(); persistCurrent();
}); });
} }
private void openShieldAttachmentEditor() {
if (current == null || shieldAttachmentEditorLauncher == null) return;
String modelPath = modelRefField.getText().trim();
shieldAttachmentEditorLauncher.open(modelPath.isBlank() ? null : modelPath,
current.getSubCategory(), current.getGrip());
}
private void updateWeaponSectionVisibility() { private void updateWeaponSectionVisibility() {
ItemCategory cat = catCombo.getValue(); ItemCategory cat = catCombo.getValue();
ItemSubCategory sub = subCatCombo.getValue(); ItemSubCategory sub = subCatCombo.getValue();

View File

@@ -53,10 +53,13 @@ public enum AnimationAction {
AXE_IDLE, AXE_WALK, AXE_RUN, AXE_IDLE, AXE_WALK, AXE_RUN,
// ── Zweihänder ──────────────────────────────────────────────────────────── // ── Zweihänder ────────────────────────────────────────────────────────────
TWO_HAND_DRAW, TWO_HAND_SHEATHE, TWO_HAND_BLOCK, TWO_HAND_DRAW, TWO_HAND_SHEATHE, TWO_HAND_BLOCK,
TWO_HAND_ATTACK_1, TWO_HAND_ATTACK_2, TWO_HAND_ATTACK_3, TWO_HAND_HEAVY_ATTACK, TWO_HAND_ATTACK_1, TWO_HAND_ATTACK_2, TWO_HAND_ATTACK_3, TWO_HAND_HEAVY_ATTACK,
TWO_HAND_IDLE_TO_BLOCK, TWO_HAND_PARADE, TWO_HAND_BLOCK_IMPACT, TWO_HAND_IDLE_IMPACT, TWO_HAND_IDLE_TO_BLOCK, TWO_HAND_PARADE, TWO_HAND_BLOCK_IMPACT, TWO_HAND_IDLE_IMPACT,
TWO_HAND_IDLE, TWO_HAND_WALK, TWO_HAND_RUN, TWO_HAND_IDLE, TWO_HAND_WALK, TWO_HAND_RUN, TWO_HAND_JUMP, TWO_HAND_RUNNING_JUMP,
TWO_HAND_IDLE_BORED, TWO_HAND_IDLE_BORED_2, TWO_HAND_IDLE_BORED_3,
TWO_HAND_DEATH, TWO_HAND_DEATH_2,
TWO_HAND_BLOCK_MOVE, TWO_HAND_BLOCK_MOVE_BACKWARD, TWO_HAND_BLOCK_STRAFE_LEFT, TWO_HAND_BLOCK_STRAFE_RIGHT,
// ── Bogen ───────────────────────────────────────────────────────────────── // ── Bogen ─────────────────────────────────────────────────────────────────
BOW_DRAW, BOW_SHEATHE, BOW_HIT_TAKEN, BOW_DRAW, BOW_SHEATHE, BOW_HIT_TAKEN,
@@ -157,8 +160,19 @@ public enum AnimationAction {
case TWO_HAND_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Zweihänder)"); case TWO_HAND_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Zweihänder)");
case TWO_HAND_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Zweihänder)"); case TWO_HAND_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Zweihänder)");
case TWO_HAND_IDLE -> TextRegistry.resolve(null, key, "Stehen (Zweihänder)"); case TWO_HAND_IDLE -> TextRegistry.resolve(null, key, "Stehen (Zweihänder)");
case TWO_HAND_WALK -> TextRegistry.resolve(null, key, "Gehen (Zweihänder)"); case TWO_HAND_WALK -> TextRegistry.resolve(null, key, "Gehen (Zweihänder)");
case TWO_HAND_RUN -> TextRegistry.resolve(null, key, "Laufen (Zweihänder)"); case TWO_HAND_RUN -> TextRegistry.resolve(null, key, "Laufen (Zweihänder)");
case TWO_HAND_JUMP -> TextRegistry.resolve(null, key, "Sprung (Zweihänder)");
case TWO_HAND_RUNNING_JUMP -> TextRegistry.resolve(null, key, "Sprung Laufend (Zweihänder)");
case TWO_HAND_IDLE_BORED -> TextRegistry.resolve(null, key, "Idle Gelangweilt (Zweihänder)");
case TWO_HAND_IDLE_BORED_2 -> TextRegistry.resolve(null, key, "Idle Gelangweilt 2 (Zweihänder)");
case TWO_HAND_IDLE_BORED_3 -> TextRegistry.resolve(null, key, "Idle Gelangweilt 3 (Zweihänder)");
case TWO_HAND_DEATH -> TextRegistry.resolve(null, key, "Tod (Zweihänder)");
case TWO_HAND_DEATH_2 -> TextRegistry.resolve(null, key, "Tod 2 (Zweihänder)");
case TWO_HAND_BLOCK_MOVE -> TextRegistry.resolve(null, key, "Block Vorwärts (Zweihänder)");
case TWO_HAND_BLOCK_MOVE_BACKWARD -> TextRegistry.resolve(null, key, "Block Rückwärts (Zweihänder)");
case TWO_HAND_BLOCK_STRAFE_LEFT -> TextRegistry.resolve(null, key, "Block Strafe Links (Zweihänder)");
case TWO_HAND_BLOCK_STRAFE_RIGHT -> TextRegistry.resolve(null, key, "Block Strafe Rechts (Zweihänder)");
case BOW_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Bogen)"); case BOW_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Bogen)");
case BOW_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Bogen)"); case BOW_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Bogen)");

View File

@@ -31,9 +31,8 @@ public final class SwordShieldCombat extends CombatStyleBase {
public static AnimationAction resolveIdleImpactAction(ItemSubCategory weaponType) { public static AnimationAction resolveIdleImpactAction(ItemSubCategory weaponType) {
if (weaponType == null) { return AnimationAction.SWORD_IDLE_IMPACT; } if (weaponType == null) { return AnimationAction.SWORD_IDLE_IMPACT; }
return switch (weaponType) { return switch (weaponType) {
case AXE -> AnimationAction.AXE_IDLE_IMPACT; case AXE -> AnimationAction.AXE_IDLE_IMPACT;
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE_IMPACT; default -> AnimationAction.SWORD_IDLE_IMPACT;
default -> AnimationAction.SWORD_IDLE_IMPACT;
}; };
} }
@@ -44,9 +43,8 @@ public final class SwordShieldCombat extends CombatStyleBase {
public static AnimationAction resolveBlockImpactAction(ItemSubCategory weaponType) { public static AnimationAction resolveBlockImpactAction(ItemSubCategory weaponType) {
if (weaponType == null) { return AnimationAction.SWORD_BLOCK_IMPACT; } if (weaponType == null) { return AnimationAction.SWORD_BLOCK_IMPACT; }
return switch (weaponType) { return switch (weaponType) {
case AXE -> AnimationAction.AXE_BLOCK_IMPACT; case AXE -> AnimationAction.AXE_BLOCK_IMPACT;
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_BLOCK_IMPACT; default -> AnimationAction.SWORD_BLOCK_IMPACT;
default -> AnimationAction.SWORD_BLOCK_IMPACT;
}; };
} }
@@ -57,9 +55,8 @@ public final class SwordShieldCombat extends CombatStyleBase {
public static AnimationAction resolveParadeAction(ItemSubCategory weaponType) { public static AnimationAction resolveParadeAction(ItemSubCategory weaponType) {
if (weaponType == null) { return AnimationAction.SWORD_PARADE; } if (weaponType == null) { return AnimationAction.SWORD_PARADE; }
return switch (weaponType) { return switch (weaponType) {
case AXE -> AnimationAction.AXE_PARADE; case AXE -> AnimationAction.AXE_PARADE;
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_PARADE; default -> AnimationAction.SWORD_PARADE;
default -> AnimationAction.SWORD_PARADE;
}; };
} }

View File

@@ -0,0 +1,68 @@
package de.blight.game.combat.twohanded;
import de.blight.game.animation.AnimationAction;
import de.blight.game.combat.CombatStyleBase;
/**
* Gemeinsame Kampflogik für den Zweihänder-Stil.
*
* Wird von beiden Seiten genutzt:
* – {@code PlayerInputControl} (Held)
* – {@code WorldNpcsState} (Gegner-KI)
*
* Alle Animations-Auflösungen (welche Animation für welches Ereignis?) und
* alle Reaktions-Anwendungen (Impact, Stagger, Konterparade) sind hier
* zentralisiert. Bug-Fixes gelten damit automatisch für Held und NPC.
*
* Neuer Kampfstil: erbt von {@link CombatStyleBase} und folgt demselben
* Muster in einem eigenen Unter-Package.
*/
public final class TwoHandedCombat extends CombatStyleBase {
private TwoHandedCombat() {}
// ── Animations-Auflösung ──────────────────────────────────────────────────
/**
* Impact-Animation in Idle/Kampf-Haltung (Entität wurde getroffen).
* Identisch für Spieler und NPC.
*/
public static AnimationAction resolveIdleImpactAction() {
return AnimationAction.TWO_HAND_IDLE_IMPACT;
}
/**
* Block-Impact-Animation (Entität hat einen Treffer abgeblockt).
* Identisch für Spieler und NPC.
*/
public static AnimationAction resolveBlockImpactAction() {
return AnimationAction.TWO_HAND_BLOCK_IMPACT;
}
/**
* Konterangriff-Animation nach erfolgreicher Parade.
* Identisch für Spieler und NPC.
*/
public static AnimationAction resolveParadeAction() {
return AnimationAction.TWO_HAND_PARADE;
}
// ── Reaktionen anwenden ───────────────────────────────────────────────────
/**
* Normaler Treffer-Impact (Entität wurde getroffen, kein Block).
* Für Held: {@code PlayerInputControl} implementiert {@link AnimReceiver}.
* Für NPC: {@code WorldNpcsState.npcAnimReceiver(s)} liefert den Adapter.
*/
public static void applyNormalImpact(AnimReceiver receiver) {
receiver.receiveBlockingAnim(resolveIdleImpactAction(), IMPACT_SPEED_NORMAL, null);
}
/**
* Konterparade-Impact (Angreifer wird durch Parade gestoppt – Slow Motion).
* Wird aufgerufen auf dem ANGREIFER, nicht auf dem Parierenden.
*/
public static void applyParriedImpact(AnimReceiver receiver) {
receiver.receiveBlockingAnim(resolveIdleImpactAction(), IMPACT_SPEED_PARRIED, null);
}
}

View File

@@ -11,6 +11,11 @@ import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl; import com.jme3.scene.control.AbstractControl;
import de.blight.common.model.Item; import de.blight.common.model.Item;
import de.blight.common.model.ItemGrip; import de.blight.common.model.ItemGrip;
import de.blight.common.model.ItemSubCategory;
import de.blight.common.model.ShieldAttachmentIO;
import de.blight.common.model.ShieldCategoryAttachment;
import de.blight.common.model.WeaponAttachmentIO;
import de.blight.common.model.WeaponCategoryAttachment;
import de.blight.game.animation.RetargetingSystem; import de.blight.game.animation.RetargetingSystem;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -28,11 +33,14 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
private static final String BONE_HAND = "mixamorig:RightHand"; private static final String BONE_HAND = "mixamorig:RightHand";
private static final String BONE_PALM = "mixamorig:RightHandMiddle1"; private static final String BONE_PALM = "mixamorig:RightHandMiddle1";
private static final String BONE_LEFT_FOREARM = "mixamorig:LeftHand"; private static final String BONE_LEFT_FOREARM = "mixamorig:LeftHand";
private static final float DEG_TO_RAD = (float) Math.PI / 180f; 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 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 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 TWO_HAND_BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.03f);
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 Vector3f SHIELD_BACK_OFFSET = new Vector3f(0.12f, -0.10f, -0.10f);
private static final float SHIELD_ARM_ROT_X = -HALF_PI; private static final float SHIELD_ARM_ROT_X = -HALF_PI;
@@ -97,7 +105,6 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
} catch (Exception e) { log.warn("[NpcWeaponAttach] Bone '{}' nicht gefunden: {}", BONE_PALM, e.getMessage()); } } catch (Exception e) { log.warn("[NpcWeaponAttach] Bone '{}' nicht gefunden: {}", BONE_PALM, e.getMessage()); }
try { leftForeArmNode = skinCtrl.getAttachmentsNode(BONE_LEFT_FOREARM); } try { leftForeArmNode = skinCtrl.getAttachmentsNode(BONE_LEFT_FOREARM); }
catch (Exception e) { log.warn("[NpcWeaponAttach] Bone '{}' nicht gefunden: {}", BONE_LEFT_FOREARM, e.getMessage()); } catch (Exception e) { log.warn("[NpcWeaponAttach] Bone '{}' nicht gefunden: {}", BONE_LEFT_FOREARM, e.getMessage()); }
handBindRotInChar = extractJointBindRot(skinCtrl, BONE_HAND); handBindRotInChar = extractJointBindRot(skinCtrl, BONE_HAND);
leftForeArmBindRotInChar = extractJointBindRot(skinCtrl, BONE_LEFT_FOREARM); leftForeArmBindRotInChar = extractJointBindRot(skinCtrl, BONE_LEFT_FOREARM);
@@ -126,7 +133,6 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
palmOffsetLocal = computePalmOffset(handNode, palmNodeRef); palmOffsetLocal = computePalmOffset(handNode, palmNodeRef);
} }
} }
Quaternion charWorldRotInv = spatial.getWorldRotation().inverse(); Quaternion charWorldRotInv = spatial.getWorldRotation().inverse();
Vector3f charWorldPos = spatial.getWorldTranslation(); Vector3f charWorldPos = spatial.getWorldTranslation();
@@ -137,14 +143,27 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
Vector3f boneInChar = charWorldRotInv.mult(boneNode.getWorldTranslation().subtract(charWorldPos)); Vector3f boneInChar = charWorldRotInv.mult(boneNode.getWorldTranslation().subtract(charWorldPos));
Quaternion boneRotInChar = charWorldRotInv.mult(boneNode.getWorldRotation()); Quaternion boneRotInChar = charWorldRotInv.mult(boneNode.getWorldRotation());
if (drawn) { if (drawn) {
ItemGrip wGrip = weaponItem != null ? weaponItem.getGrip() : null;
boolean isTwoHand = weaponItem != null && weaponItem.getSubCategory() == ItemSubCategory.TWO_HANDED_SWORD;
Vector3f attachInChar = (palmOffsetLocal != null) Vector3f attachInChar = (palmOffsetLocal != null)
? boneInChar.add(boneRotInChar.mult(palmOffsetLocal.mult(0.5f))) ? boneInChar.add(boneRotInChar.mult(palmOffsetLocal.mult(0.5f)))
: boneInChar; : boneInChar;
applyHandTransform(weaponSpatial, attachInChar, boneRotInChar, boolean usesTwoHand = isTwoHand && leftForeArmNode != null
weaponItem != null ? weaponItem.getGrip() : null); && wGrip != null
&& (wGrip.getGrip2PosX() != 0f || wGrip.getGrip2PosY() != 0f || wGrip.getGrip2PosZ() != 0f);
WeaponCategoryAttachment cfg = WeaponAttachmentIO.get(
weaponItem != null ? weaponItem.getSubCategory() : null);
if (usesTwoHand) {
applyTwoHandTransform(weaponSpatial, attachInChar, boneRotInChar,
wGrip, charWorldRotInv, charWorldPos, cfg);
} else {
applyHandTransform(weaponSpatial, attachInChar, boneRotInChar, wGrip, cfg);
}
} else { } else {
boolean twoHanded = weaponItem != null
&& weaponItem.getSubCategory() == ItemSubCategory.TWO_HANDED_SWORD;
applyBackTransform(weaponSpatial, boneInChar, boneRotInChar, applyBackTransform(weaponSpatial, boneInChar, boneRotInChar,
weaponItem != null ? weaponItem.getGrip() : null); weaponItem != null ? weaponItem.getGrip() : null, twoHanded);
} }
} }
} }
@@ -155,7 +174,9 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
if (drawn && leftForeArmNode != null) { if (drawn && leftForeArmNode != null) {
Vector3f boneInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos)); Vector3f boneInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos));
Quaternion boneRotInChar = charWorldRotInv.mult(leftForeArmNode.getWorldRotation()); Quaternion boneRotInChar = charWorldRotInv.mult(leftForeArmNode.getWorldRotation());
applyShieldArmTransform(shieldSpatial, boneInChar, boneRotInChar, grip); ShieldCategoryAttachment shieldCfg = ShieldAttachmentIO.get(
shieldItem != null ? shieldItem.getSubCategory() : null);
applyShieldArmTransform(shieldSpatial, boneInChar, boneRotInChar, grip, shieldCfg);
shieldSpatial.setCullHint(Spatial.CullHint.Inherit); shieldSpatial.setCullHint(Spatial.CullHint.Inherit);
} else if (!drawn && backNode != null) { } else if (!drawn && backNode != null) {
Vector3f boneInChar = charWorldRotInv.mult(backNode.getWorldTranslation().subtract(charWorldPos)); Vector3f boneInChar = charWorldRotInv.mult(backNode.getWorldTranslation().subtract(charWorldPos));
@@ -183,7 +204,8 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
// ── Hand-Transform (1:1 von WeaponAttachmentControl) ───────────────────────── // ── Hand-Transform (1:1 von WeaponAttachmentControl) ─────────────────────────
private void applyHandTransform(Spatial s, Vector3f bonePosInChar, private void applyHandTransform(Spatial s, Vector3f bonePosInChar,
Quaternion boneRotInChar, ItemGrip grip) { Quaternion boneRotInChar, ItemGrip grip,
WeaponCategoryAttachment cfg) {
Quaternion gripRot = (grip != null) Quaternion gripRot = (grip != null)
? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(), ? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(),
DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotY(),
@@ -195,36 +217,115 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
Quaternion animDelta = (handBindRotInChar != null) Quaternion animDelta = (handBindRotInChar != null)
? boneRotInChar.mult(handBindRotInChar.inverse()) ? boneRotInChar.mult(handBindRotInChar.inverse())
: boneRotInChar; : boneRotInChar;
Quaternion weaponRot = animDelta.mult(gripRot.inverse()); boolean bladeAtMinusZ = grip != null && (
(grip.getGrip2PosX() != 0f || grip.getGrip2PosY() != 0f || grip.getGrip2PosZ() != 0f)
? grip.getGrip2PosZ() > grip.getPosZ()
: grip.getPosZ() > 0f);
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(cfg.getHandOffX(), cfg.getHandOffY(), -cfg.getHandOffZ())
: new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), cfg.getHandOffZ());
Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(gripPos)); Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(gripPos));
weaponPos = weaponPos.add(weaponRot.mult(HAND_GRIP_OFFSET)); weaponPos = weaponPos.add(weaponRot.mult(handOff));
if (grip != null && (grip.getHandOffsetX() != 0f || grip.getHandOffsetY() != 0f || grip.getHandOffsetZ() != 0f)) {
weaponPos = weaponPos.add(grip.getHandOffsetX(), grip.getHandOffsetY(), grip.getHandOffsetZ());
}
s.setLocalRotation(weaponRot); s.setLocalRotation(weaponRot);
s.setLocalTranslation(weaponPos); s.setLocalTranslation(weaponPos);
} }
private void applyBackTransform(Spatial s, Vector3f boneInChar, private void applyTwoHandTransform(Spatial s, Vector3f bonePosInChar, Quaternion boneRotInChar,
Quaternion boneRotInChar, ItemGrip grip) { ItemGrip grip, Quaternion charWorldRotInv, Vector3f charWorldPos,
Quaternion backRot = new Quaternion().fromAngles(0f, 0f, HALF_PI); WeaponCategoryAttachment cfg) {
Quaternion weaponRot = boneRotInChar.mult(backRot); Quaternion gripRot = new Quaternion().fromAngles(
Vector3f target = boneInChar.add(boneRotInChar.mult(BACK_GRIP_OFFSET)); DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ());
Vector3f gripPos = grip != null Quaternion animDelta = (handBindRotInChar != null)
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO; ? boneRotInChar.mult(handBindRotInChar.inverse()) : boneRotInChar;
boolean bladeAtMinusZ = 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 g1 = new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ());
Vector3f g2 = new Vector3f(grip.getGrip2PosX(), grip.getGrip2PosY(), grip.getGrip2PosZ());
Vector3f handOff = bladeAtMinusZ
? new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), -cfg.getHandOffZ())
: new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), cfg.getHandOffZ());
Vector3f hand2Off = bladeAtMinusZ
? new Vector3f(cfg.getHand2OffX(), cfg.getHand2OffY(), -cfg.getHand2OffZ())
: new Vector3f(cfg.getHand2OffX(), cfg.getHand2OffY(), cfg.getHand2OffZ());
Vector3f g1eff = g1.subtract(handOff);
Vector3f g2eff = g2.add(hand2Off);
Vector3f leftHandInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos));
Vector3f currentGripDir = weaponRot.mult(g2eff.subtract(g1eff).normalizeLocal());
Vector3f targetDir = leftHandInChar.subtract(bonePosInChar).normalizeLocal();
weaponRot = swingFromTo(currentGripDir, targetDir).mult(weaponRot);
Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(g1eff));
s.setLocalRotation(weaponRot); s.setLocalRotation(weaponRot);
s.setLocalTranslation(target.subtract(weaponRot.mult(gripPos))); 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 len = axis.length();
if (len < 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(len));
}
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, boolean twoHanded) {
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.getGrip2PosX() != 0f || grip.getGrip2PosY() != 0f || grip.getGrip2PosZ() != 0f)
? grip.getGrip2PosZ() > grip.getPosZ()
: grip.getPosZ() > 0f);
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(
twoHanded ? TWO_HAND_BACK_GRIP_OFFSET : BACK_GRIP_OFFSET));
// X=0: Waffenmitte (nicht handspezifischer Griffpunkt) auf Wirbelsäule zentrieren
Vector3f gripPos = grip != null
? new Vector3f(0f, grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO;
Vector3f backPos = target.subtract(weaponRot.mult(gripPos));
s.setLocalRotation(weaponRot);
s.setLocalTranslation(backPos);
} }
// ── Schild-Transforms (1:1 von WeaponAttachmentControl) ────────────────────── // ── Schild-Transforms (1:1 von WeaponAttachmentControl) ──────────────────────
private void applyShieldArmTransform(Spatial s, Vector3f bonePosInChar, private void applyShieldArmTransform(Spatial s, Vector3f bonePosInChar,
Quaternion boneRotInChar, ItemGrip grip) { Quaternion boneRotInChar, ItemGrip grip,
ShieldCategoryAttachment cfg) {
Quaternion animDelta = (leftForeArmBindRotInChar != null) Quaternion animDelta = (leftForeArmBindRotInChar != null)
? boneRotInChar.mult(leftForeArmBindRotInChar.inverse()) ? boneRotInChar.mult(leftForeArmBindRotInChar.inverse())
: boneRotInChar; : boneRotInChar;
Quaternion armRot = new Quaternion().fromAngles(SHIELD_ARM_ROT_X, SHIELD_ARM_ROT_Y, SHIELD_ARM_ROT_Z); Quaternion armRot = new Quaternion().fromAngles(
DEG_TO_RAD * cfg.getRotX(), DEG_TO_RAD * cfg.getRotY(), DEG_TO_RAD * cfg.getRotZ());
Quaternion shieldRot = animDelta.mult(armRot); Quaternion shieldRot = animDelta.mult(armRot);
Vector3f gripPos = (grip != null) Vector3f gripPos = (grip != null)
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO; ? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO;
Vector3f anchor = bonePosInChar.add(boneRotInChar.mult(new Vector3f(0f, -SHIELD_ARM_ELBOW_OFFSET, 0f))); Vector3f anchor = bonePosInChar.add(boneRotInChar.mult(new Vector3f(0f, -cfg.getElbowOffset(), 0f)));
s.setLocalRotation(shieldRot); s.setLocalRotation(shieldRot);
s.setLocalTranslation(anchor.subtract(shieldRot.mult(gripPos))); s.setLocalTranslation(anchor.subtract(shieldRot.mult(gripPos)));
} }

View File

@@ -6,6 +6,7 @@ import com.jme3.anim.tween.Tween;
import com.jme3.anim.tween.action.BaseAction; import com.jme3.anim.tween.action.BaseAction;
import com.jme3.bullet.control.CharacterControl; import com.jme3.bullet.control.CharacterControl;
import com.jme3.input.InputManager; import com.jme3.input.InputManager;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput; import com.jme3.input.MouseInput;
import com.jme3.input.RawInputListener; import com.jme3.input.RawInputListener;
import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.ActionListener;
@@ -34,6 +35,7 @@ import de.blight.game.combat.CombatStyleBase;
import de.blight.game.combat.MeleeAttackCallback; import de.blight.game.combat.MeleeAttackCallback;
import de.blight.game.combat.WeaponCombatState; import de.blight.game.combat.WeaponCombatState;
import de.blight.game.combat.swordshield.SwordShieldCombat; import de.blight.game.combat.swordshield.SwordShieldCombat;
import de.blight.game.combat.twohanded.TwoHandedCombat;
import de.blight.game.navigation.CharacterNavigator; import de.blight.game.navigation.CharacterNavigator;
import de.blight.game.state.TerrainChunkState; import de.blight.game.state.TerrainChunkState;
@@ -109,11 +111,14 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
private static final String ACT_COMBAT_ALT = "CombatAltAction"; private static final String ACT_COMBAT_ALT = "CombatAltAction";
private static final String ACT_ROLL = "Roll"; private static final String ACT_ROLL = "Roll";
private static final String ACT_SNEAK = "Sneak"; private static final String ACT_SNEAK = "Sneak";
private static final String ACT_BORED_1 = "BoredAnim1";
private static final String ACT_BORED_2 = "BoredAnim2";
private static final String ACT_BORED_3 = "BoredAnim3";
private static final String[] ACTION_NAMES = { private static final String[] ACTION_NAMES = {
"Forward", "Backward", "Left", "Right", "Jump", "Sprint", "Walk", "Forward", "Backward", "Left", "Right", "Jump", "Sprint", "Walk",
ACT_DRAW_MELEE, ACT_DRAW_RANGED, ACT_SHEATH, ACT_COMBAT_ATK, ACT_COMBAT_ALT, ACT_DRAW_MELEE, ACT_DRAW_RANGED, ACT_SHEATH, ACT_COMBAT_ATK, ACT_COMBAT_ALT,
ACT_ROLL, ACT_SNEAK ACT_ROLL, ACT_SNEAK, ACT_BORED_1, ACT_BORED_2, ACT_BORED_3
}; };
private final InputManager inputManager; private final InputManager inputManager;
@@ -380,8 +385,11 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
} }
} }
case ACT_COMBAT_ALT -> handleCombatAlt(isPressed); case ACT_COMBAT_ALT -> handleCombatAlt(isPressed);
case ACT_ROLL -> { if (isPressed) triggerRollFromInput(); } case ACT_ROLL -> { if (isPressed) triggerRollFromInput(); }
case ACT_SNEAK -> { if (!weaponDrawn) handleSneakKey(isPressed); } case ACT_SNEAK -> { if (!weaponDrawn) handleSneakKey(isPressed); }
case ACT_BORED_1 -> { if (isPressed) triggerBoredAnim(0); }
case ACT_BORED_2 -> { if (isPressed) triggerBoredAnim(1); }
case ACT_BORED_3 -> { if (isPressed) triggerBoredAnim(2); }
} }
}; };
@@ -399,6 +407,8 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
this.visual = visual; this.visual = visual;
} }
public Spatial getVisual() { return visual; }
/** /**
* Setzt die Navigationsquellen (PathFinder + Terrain). Muss vor oder beim * Setzt die Navigationsquellen (PathFinder + Terrain). Muss vor oder beim
* ersten setAnimationContext gesetzt sein, damit der CharacterNavigator * ersten setAnimationContext gesetzt sein, damit der CharacterNavigator
@@ -467,6 +477,7 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
public void setMainCharacter(MainCharacter mc) { this.mainCharacter = mc; } public void setMainCharacter(MainCharacter mc) { this.mainCharacter = mc; }
public boolean isWeaponDrawn() { return weaponDrawn; } public boolean isWeaponDrawn() { return weaponDrawn; }
public boolean isDrawAnimActive() { return drawAnimActive; }
public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; } public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; }
public boolean isLastAttackCritical() { return lastAttackCritical; } public boolean isLastAttackCritical() { return lastAttackCritical; }
public com.jme3.anim.SkinningControl getSkinningControl() { return skinningControl; } public com.jme3.anim.SkinningControl getSkinningControl() { return skinningControl; }
@@ -831,6 +842,9 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
inputManager.addMapping(ACT_COMBAT_ALT, new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)); inputManager.addMapping(ACT_COMBAT_ALT, new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
inputManager.addMapping(ACT_ROLL, new KeyTrigger(kb.roll)); inputManager.addMapping(ACT_ROLL, new KeyTrigger(kb.roll));
inputManager.addMapping(ACT_SNEAK, new KeyTrigger(kb.sneak)); inputManager.addMapping(ACT_SNEAK, new KeyTrigger(kb.sneak));
inputManager.addMapping(ACT_BORED_1, new KeyTrigger(KeyInput.KEY_F1));
inputManager.addMapping(ACT_BORED_2, new KeyTrigger(KeyInput.KEY_F2));
inputManager.addMapping(ACT_BORED_3, new KeyTrigger(KeyInput.KEY_F3));
inputManager.addListener(actionListener, ACTION_NAMES); inputManager.addListener(actionListener, ACTION_NAMES);
} }
@@ -1221,7 +1235,8 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
} else { } else {
boolean swordShield = drawnWeaponType == de.blight.common.model.ItemSubCategory.SWORD boolean swordShield = drawnWeaponType == de.blight.common.model.ItemSubCategory.SWORD
&& hasShieldEquipped(); && hasShieldEquipped();
if (swordShield) { boolean twoHand = drawnWeaponType == ItemSubCategory.TWO_HANDED_SWORD;
if (swordShield || twoHand) {
if (boredAnimRemaining > 0f) { if (boredAnimRemaining > 0f) {
boredAnimRemaining -= tpf; boredAnimRemaining -= tpf;
target = boredVariant; target = boredVariant;
@@ -1230,9 +1245,15 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
if (boredIdleTimer >= BORED_IDLE_DELAY) { if (boredIdleTimer >= BORED_IDLE_DELAY) {
boredIdleTimer = 0f; boredIdleTimer = 0f;
int pick = playerRng.nextInt(3); int pick = playerRng.nextInt(3);
boredVariant = pick == 0 ? AnimationAction.SWORD_IDLE_BORED if (swordShield) {
: pick == 1 ? AnimationAction.SWORD_IDLE_BORED_2 boredVariant = pick == 0 ? AnimationAction.SWORD_IDLE_BORED
: AnimationAction.SWORD_IDLE_BORED_3; : pick == 1 ? AnimationAction.SWORD_IDLE_BORED_2
: AnimationAction.SWORD_IDLE_BORED_3;
} else {
boredVariant = pick == 0 ? AnimationAction.TWO_HAND_IDLE_BORED
: pick == 1 ? AnimationAction.TWO_HAND_IDLE_BORED_2
: AnimationAction.TWO_HAND_IDLE_BORED_3;
}
boredAnimRemaining = resolveClipLength(boredVariant, 3.0f); boredAnimRemaining = resolveClipLength(boredVariant, 3.0f);
target = boredVariant; target = boredVariant;
} else { } else {
@@ -1254,10 +1275,11 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
} else if (blockPhase == BlockPhase.HOLDING && !combatAnimActive) { } else if (blockPhase == BlockPhase.HOLDING && !combatAnimActive) {
// Beim Blocken: Bewegungsanimation nach Taste, Strafe hat Vorrang // Beim Blocken: Bewegungsanimation nach Taste, Strafe hat Vorrang
AnimationAction blockMoveAnim; AnimationAction blockMoveAnim;
if (right) blockMoveAnim = AnimationAction.SWORD_BLOCK_STRAFE_RIGHT; boolean twoHand = drawnWeaponType == ItemSubCategory.TWO_HANDED_SWORD;
else if (left) blockMoveAnim = AnimationAction.SWORD_BLOCK_STRAFE_LEFT; if (right) blockMoveAnim = twoHand ? AnimationAction.TWO_HAND_BLOCK_STRAFE_RIGHT : AnimationAction.SWORD_BLOCK_STRAFE_RIGHT;
else if (forward) blockMoveAnim = AnimationAction.SWORD_BLOCK_MOVE; else if (left) blockMoveAnim = twoHand ? AnimationAction.TWO_HAND_BLOCK_STRAFE_LEFT : AnimationAction.SWORD_BLOCK_STRAFE_LEFT;
else if (backward) blockMoveAnim = AnimationAction.SWORD_BLOCK_MOVE_BACKWARD; else if (forward) blockMoveAnim = twoHand ? AnimationAction.TWO_HAND_BLOCK_MOVE : AnimationAction.SWORD_BLOCK_MOVE;
else if (backward) blockMoveAnim = twoHand ? AnimationAction.TWO_HAND_BLOCK_MOVE_BACKWARD : AnimationAction.SWORD_BLOCK_MOVE_BACKWARD;
else blockMoveAnim = getMeleeBlockAction(drawnWeaponType); else blockMoveAnim = getMeleeBlockAction(drawnWeaponType);
if (blockMoveAnim != currentAnim) { if (blockMoveAnim != currentAnim) {
@@ -1407,6 +1429,12 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
private void handleCombatAlt(boolean isPressed) { private void handleCombatAlt(boolean isPressed) {
if (!weaponDrawn) return; if (!weaponDrawn) return;
if (isMeleeType(drawnWeaponType)) { if (isMeleeType(drawnWeaponType)) {
// Schwert blockt per Schild – ohne Schild ist Blocken nicht möglich.
// Zweihänder/Axt blocken mit der Waffe selbst, brauchen keinen Schild.
if (drawnWeaponType == ItemSubCategory.SWORD && !hasShieldEquipped()) {
rightMouseHeld = false;
return;
}
if (isPressed) { if (isPressed) {
rightMouseHeld = true; rightMouseHeld = true;
// Sofort blocken nur wenn keine andere Animation läuft // Sofort blocken nur wenn keine andere Animation läuft
@@ -1674,6 +1702,25 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
currentAnim = AnimationAction.CROUCH_TO_STAND; currentAnim = AnimationAction.CROUCH_TO_STAND;
} }
private void triggerBoredAnim(int variant) {
if (!weaponDrawn || forward || backward || left || right || combatAnimActive || drawAnimActive) return;
if (physicsChar != null && !physicsChar.onGround()) return;
boolean swordShield = drawnWeaponType == de.blight.common.model.ItemSubCategory.SWORD && hasShieldEquipped();
boolean twoHand = drawnWeaponType == ItemSubCategory.TWO_HANDED_SWORD;
if (!swordShield && !twoHand) return;
if (swordShield) {
boredVariant = variant == 0 ? AnimationAction.SWORD_IDLE_BORED
: variant == 1 ? AnimationAction.SWORD_IDLE_BORED_2
: AnimationAction.SWORD_IDLE_BORED_3;
} else {
boredVariant = variant == 0 ? AnimationAction.TWO_HAND_IDLE_BORED
: variant == 1 ? AnimationAction.TWO_HAND_IDLE_BORED_2
: AnimationAction.TWO_HAND_IDLE_BORED_3;
}
boredAnimRemaining = resolveClipLength(boredVariant, 3.0f);
boredIdleTimer = 0f;
}
private void triggerRollFromInput() { private void triggerRollFromInput() {
if (!weaponDrawn) return; if (!weaponDrawn) return;
if (rollActive || pickupActive || blockingAnimActive || turnActive if (rollActive || pickupActive || blockingAnimActive || turnActive
@@ -1861,10 +1908,12 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
} }
public static AnimationAction getMeleeBlockImpactAction(ItemSubCategory s) { public static AnimationAction getMeleeBlockImpactAction(ItemSubCategory s) {
if (s == ItemSubCategory.TWO_HANDED_SWORD) { return TwoHandedCombat.resolveBlockImpactAction(); }
return SwordShieldCombat.resolveBlockImpactAction(s); return SwordShieldCombat.resolveBlockImpactAction(s);
} }
public static AnimationAction getMeleeIdleImpactAction(ItemSubCategory s) { public static AnimationAction getMeleeIdleImpactAction(ItemSubCategory s) {
if (s == ItemSubCategory.TWO_HANDED_SWORD) { return TwoHandedCombat.resolveIdleImpactAction(); }
return SwordShieldCombat.resolveIdleImpactAction(s); return SwordShieldCombat.resolveIdleImpactAction(s);
} }
@@ -2143,7 +2192,9 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
SWORD_WALK, AXE_WALK, TWO_HAND_WALK, SWORD_WALK, AXE_WALK, TWO_HAND_WALK,
BOW_WALK, CROSSBOW_WALK, CRYSTAL_WALK, BOW_WALK, CROSSBOW_WALK, CRYSTAL_WALK,
SWORD_BLOCK_MOVE, SWORD_BLOCK_MOVE_BACKWARD, SWORD_BLOCK_MOVE, SWORD_BLOCK_MOVE_BACKWARD,
SWORD_BLOCK_STRAFE_LEFT, SWORD_BLOCK_STRAFE_RIGHT -> "walking"; SWORD_BLOCK_STRAFE_LEFT, SWORD_BLOCK_STRAFE_RIGHT,
TWO_HAND_BLOCK_MOVE, TWO_HAND_BLOCK_MOVE_BACKWARD,
TWO_HAND_BLOCK_STRAFE_LEFT, TWO_HAND_BLOCK_STRAFE_RIGHT -> "walking";
case RUN, case RUN,
SWORD_RUN, AXE_RUN, TWO_HAND_RUN, SWORD_RUN, AXE_RUN, TWO_HAND_RUN,
BOW_RUN, CROSSBOW_RUN, CRYSTAL_RUN -> "running"; BOW_RUN, CROSSBOW_RUN, CRYSTAL_RUN -> "running";

View File

@@ -11,7 +11,12 @@ import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl; import com.jme3.scene.control.AbstractControl;
import de.blight.common.model.Item; import de.blight.common.model.Item;
import de.blight.common.model.ItemGrip; import de.blight.common.model.ItemGrip;
import de.blight.common.model.ItemSubCategory;
import de.blight.common.model.MainCharacter; import de.blight.common.model.MainCharacter;
import de.blight.common.model.ShieldAttachmentIO;
import de.blight.common.model.ShieldCategoryAttachment;
import de.blight.common.model.WeaponAttachmentIO;
import de.blight.common.model.WeaponCategoryAttachment;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -31,12 +36,14 @@ public class WeaponAttachmentControl extends AbstractControl {
private static final String BONE_HAND = "mixamorig:RightHand"; private static final String BONE_HAND = "mixamorig:RightHand";
private static final String BONE_PALM = "mixamorig:RightHandMiddle1"; private static final String BONE_PALM = "mixamorig:RightHandMiddle1";
private static final String BONE_LEFT_FOREARM = "mixamorig:LeftHand"; private static final String BONE_LEFT_FOREARM = "mixamorig:LeftHand";
private static final float DEG_TO_RAD = (float) Math.PI / 180f; 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 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 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 TWO_HAND_BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.03f);
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. */ /** 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); private static final Vector3f SHIELD_BACK_OFFSET = new Vector3f(0.12f, -0.10f, -0.10f);
@@ -65,10 +72,11 @@ public class WeaponAttachmentControl extends AbstractControl {
// Wird im 2. Frame aus den Runtime-Node-Positionen berechnet (skalierungs-unabhängig). // Wird im 2. Frame aus den Runtime-Node-Positionen berechnet (skalierungs-unabhängig).
private Vector3f palmOffsetLocal; private Vector3f palmOffsetLocal;
private Item lastEquippedWeapon; private Item lastEquippedWeapon;
private boolean lastDrawnState = false; private boolean lastDrawnState = false;
private Spatial weaponSpatial; private Spatial weaponSpatial;
private boolean gripDiagLogged = false; private boolean gripDiagLogged = false;
private Item lastEquippedShield; private Item lastEquippedShield;
private Spatial shieldSpatial; private Spatial shieldSpatial;
@@ -126,7 +134,6 @@ public class WeaponAttachmentControl extends AbstractControl {
log.info("[WeaponAttach] palmOffsetLocal (from nodes)={}", palmOffsetLocal); log.info("[WeaponAttach] palmOffsetLocal (from nodes)={}", palmOffsetLocal);
} }
} }
Item current = mc.getEquippedMeleeWeapon(); Item current = mc.getEquippedMeleeWeapon();
Item shield = mc.getEquippedShield(); Item shield = mc.getEquippedShield();
boolean drawn = playerInput.isWeaponDrawn(); boolean drawn = playerInput.isWeaponDrawn();
@@ -139,7 +146,7 @@ public class WeaponAttachmentControl extends AbstractControl {
if (weaponSpatial != null) { if (weaponSpatial != null) {
lastDrawnState = drawn; lastDrawnState = drawn;
updateWeaponTransform(drawn, current != null ? current.getGrip() : null); updateWeaponTransform(drawn, current);
} }
if (shield != lastEquippedShield) { if (shield != lastEquippedShield) {
@@ -173,7 +180,8 @@ public class WeaponAttachmentControl extends AbstractControl {
// ── Transform-Update (jedes Frame) ──────────────────────────────────────── // ── 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; Node boneNode = drawn ? handNode : backNode;
if (boneNode == null) return; if (boneNode == null) return;
@@ -185,16 +193,28 @@ public class WeaponAttachmentControl extends AbstractControl {
Quaternion boneRotInChar = charWorldRotInv.mult(boneNode.getWorldRotation()); Quaternion boneRotInChar = charWorldRotInv.mult(boneNode.getWorldRotation());
if (drawn) { if (drawn) {
boolean isTwoHand = item != null && item.getSubCategory() == ItemSubCategory.TWO_HANDED_SWORD;
WeaponCategoryAttachment cfg = WeaponAttachmentIO.get(item != null ? item.getSubCategory() : null);
Vector3f attachInChar = (palmOffsetLocal != null) Vector3f attachInChar = (palmOffsetLocal != null)
? boneInChar.add(boneRotInChar.mult(palmOffsetLocal.mult(0.5f))) ? boneInChar.add(boneRotInChar.mult(palmOffsetLocal.mult(0.5f)))
: boneInChar; : boneInChar;
applyHandTransform(attachInChar, boneRotInChar, grip); boolean usesTwoHand = isTwoHand && leftForeArmNode != null
&& grip != null
&& (grip.getGrip2PosX() != 0f || grip.getGrip2PosY() != 0f || grip.getGrip2PosZ() != 0f)
&& !playerInput.isDrawAnimActive();
if (usesTwoHand) {
applyTwoHandTransform(attachInChar, boneRotInChar, grip, charWorldRotInv, charWorldPos, cfg);
} else {
applyHandTransform(attachInChar, boneRotInChar, grip, cfg);
}
} else { } else {
applyBackTransform(boneInChar, boneRotInChar, grip); boolean twoHanded = item != null && item.getSubCategory() == ItemSubCategory.TWO_HANDED_SWORD;
applyBackTransform(boneInChar, boneRotInChar, grip, twoHanded);
} }
} }
private void applyHandTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) { private void applyHandTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip,
WeaponCategoryAttachment cfg) {
Quaternion gripRot = (grip != null) Quaternion gripRot = (grip != null)
? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ()) ? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ())
: new Quaternion(); : new Quaternion();
@@ -205,7 +225,14 @@ public class WeaponAttachmentControl extends AbstractControl {
Quaternion animDelta = (handBindRotInChar != null) Quaternion animDelta = (handBindRotInChar != null)
? boneRotInChar.mult(handBindRotInChar.inverse()) ? boneRotInChar.mult(handBindRotInChar.inverse())
: boneRotInChar; : boneRotInChar;
Quaternion weaponRot = animDelta.mult(gripRot.inverse()); boolean bladeAtMinusZ2 = grip != null && (
(grip.getGrip2PosX() != 0f || grip.getGrip2PosY() != 0f || grip.getGrip2PosZ() != 0f)
? grip.getGrip2PosZ() > grip.getPosZ()
: grip.getPosZ() > 0f);
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) { if (!gripDiagLogged && weaponSpatial != null) {
gripDiagLogged = true; gripDiagLogged = true;
@@ -240,12 +267,72 @@ public class WeaponAttachmentControl extends AbstractControl {
} }
} }
Vector3f handOff2 = bladeAtMinusZ2
? new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), -cfg.getHandOffZ())
: new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), cfg.getHandOffZ());
Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(gripPos)); 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.setLocalRotation(weaponRot);
weaponSpatial.setLocalTranslation(weaponPos); weaponSpatial.setLocalTranslation(weaponPos);
} }
private void applyTwoHandTransform(Vector3f bonePosInChar, Quaternion boneRotInChar,
ItemGrip grip,
Quaternion charWorldRotInv, Vector3f charWorldPos,
WeaponCategoryAttachment cfg) {
// Basis-Rotation aus rechter Hand (gibt Roll/Handflächenausrichtung)
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 bladeAtMinusZ = 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 g1 = new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ());
Vector3f g2 = new Vector3f(grip.getGrip2PosX(), grip.getGrip2PosY(), grip.getGrip2PosZ());
// g1 hat handOff eingebaut (Kategorie-Config), g2 nicht → g1eff bereinigt den Unterschied
Vector3f handOff = bladeAtMinusZ
? new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), -cfg.getHandOffZ())
: new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), cfg.getHandOffZ());
// Korrektur g2 durch hand2Offset der Kategorie
Vector3f hand2Off = bladeAtMinusZ
? new Vector3f(cfg.getHand2OffX(), cfg.getHand2OffY(), -cfg.getHand2OffZ())
: new Vector3f(cfg.getHand2OffX(), cfg.getHand2OffY(), cfg.getHand2OffZ());
Vector3f g1eff = g1.subtract(handOff);
Vector3f g2eff = g2.add(hand2Off);
// Swing: g1eff→g2eff-Richtung auf rechteHand→linkeHand ausrichten
Vector3f leftHandInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos));
Vector3f currentGripDir = weaponRot.mult(g2eff.subtract(g1eff).normalizeLocal());
Vector3f targetDir = leftHandInChar.subtract(bonePosInChar).normalizeLocal();
weaponRot = swingFromTo(currentGripDir, targetDir).mult(weaponRot);
// g1eff an bonePosInChar pinnen
Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(g1eff));
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 len = axis.length();
if (len < 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(len));
}
private static float round1(float v) { return Math.round(v * 10f) / 10f; } private static float round1(float v) { return Math.round(v * 10f) / 10f; }
/** /**
@@ -273,15 +360,36 @@ public class WeaponAttachmentControl extends AbstractControl {
return bindMat.toRotationQuat().normalizeLocal(); return bindMat.toRotationQuat().normalizeLocal();
} }
private void applyBackTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) { // backRot normalisiert: fromAngles(0,0,PI/2) * kurzschwert_gripRot
Quaternion backRot = new Quaternion().fromAngles(0f, 0f, HALF_PI); // → Waffe wird zuerst per gripRot^-1 normalisiert, dann per backRot am Rücken ausgerichtet.
Quaternion weaponRot = boneRotInChar.mult(backRot); // Ergebnis für Kurzschwert (gripRot=fromAngles(0,-90°,90°)) identisch zur alten Formel.
Vector3f gripTarget = bonePosInChar.add(boneRotInChar.mult(BACK_GRIP_OFFSET)); private static final Quaternion BACK_ROT_NORM = new Quaternion(0f, -(float) Math.sqrt(0.5), (float) Math.sqrt(0.5), 0f);
Vector3f gripPos = (grip != null)
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) private void applyBackTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip,
boolean twoHanded) {
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 bladeAtMinusZ = grip != null && (
(grip.getGrip2PosX() != 0f || grip.getGrip2PosY() != 0f || grip.getGrip2PosZ() != 0f)
? grip.getGrip2PosZ() > grip.getPosZ()
: grip.getPosZ() > 0f);
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 offset = twoHanded ? TWO_HAND_BACK_GRIP_OFFSET : BACK_GRIP_OFFSET;
Vector3f gripTarget = bonePosInChar.add(boneRotInChar.mult(offset));
// X=0: Waffenmitte (nicht handspezifischer Griffpunkt) auf Wirbelsäule zentrieren
Vector3f gripPos = (grip != null)
? new Vector3f(0f, grip.getPosY(), grip.getPosZ())
: Vector3f.ZERO; : Vector3f.ZERO;
Vector3f backPos = gripTarget.subtract(weaponRot.mult(gripPos));
weaponSpatial.setLocalRotation(weaponRot); weaponSpatial.setLocalRotation(weaponRot);
weaponSpatial.setLocalTranslation(gripTarget.subtract(weaponRot.mult(gripPos))); weaponSpatial.setLocalTranslation(backPos);
} }
// ── Schild ──────────────────────────────────────────────────────────────── // ── Schild ────────────────────────────────────────────────────────────────
@@ -311,7 +419,9 @@ public class WeaponAttachmentControl extends AbstractControl {
Vector3f boneInChar = charWorldRotInv.mult( Vector3f boneInChar = charWorldRotInv.mult(
leftForeArmNode.getWorldTranslation().subtract(charWorldPos)); leftForeArmNode.getWorldTranslation().subtract(charWorldPos));
Quaternion boneRotInChar = charWorldRotInv.mult(leftForeArmNode.getWorldRotation()); Quaternion boneRotInChar = charWorldRotInv.mult(leftForeArmNode.getWorldRotation());
applyShieldArmTransform(boneInChar, boneRotInChar, grip); ShieldCategoryAttachment shieldCfg = ShieldAttachmentIO.get(
shield != null ? shield.getSubCategory() : null);
applyShieldArmTransform(boneInChar, boneRotInChar, grip, shieldCfg);
shieldSpatial.setCullHint(Spatial.CullHint.Inherit); shieldSpatial.setCullHint(Spatial.CullHint.Inherit);
} else if (!drawn && backNode != null) { } else if (!drawn && backNode != null) {
Vector3f boneInChar = charWorldRotInv.mult( Vector3f boneInChar = charWorldRotInv.mult(
@@ -324,17 +434,18 @@ public class WeaponAttachmentControl extends AbstractControl {
} }
} }
private void applyShieldArmTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) { private void applyShieldArmTransform(Vector3f bonePosInChar, Quaternion boneRotInChar,
ItemGrip grip, ShieldCategoryAttachment cfg) {
Quaternion animDelta = (leftForeArmBindRotInChar != null) Quaternion animDelta = (leftForeArmBindRotInChar != null)
? boneRotInChar.mult(leftForeArmBindRotInChar.inverse()) ? boneRotInChar.mult(leftForeArmBindRotInChar.inverse())
: boneRotInChar; : boneRotInChar;
Quaternion armRot = new Quaternion().fromAngles(SHIELD_ARM_ROT_X, SHIELD_ARM_ROT_Y, SHIELD_ARM_ROT_Z); Quaternion armRot = new Quaternion().fromAngles(
DEG_TO_RAD * cfg.getRotX(), DEG_TO_RAD * cfg.getRotY(), DEG_TO_RAD * cfg.getRotZ());
Quaternion shieldRot = animDelta.mult(armRot); Quaternion shieldRot = animDelta.mult(armRot);
Vector3f gripPos = (grip != null) Vector3f gripPos = (grip != null)
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) ? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
: Vector3f.ZERO; : Vector3f.ZERO;
// Ankerpunkt Richtung Ellbogen verschieben (lokale −Y des LeftHand-Bones) Vector3f anchor = bonePosInChar.add(boneRotInChar.mult(new Vector3f(0f, -cfg.getElbowOffset(), 0f)));
Vector3f anchor = bonePosInChar.add(boneRotInChar.mult(new Vector3f(0f, -SHIELD_ARM_ELBOW_OFFSET, 0f)));
shieldSpatial.setLocalRotation(shieldRot); shieldSpatial.setLocalRotation(shieldRot);
shieldSpatial.setLocalTranslation(anchor.subtract(shieldRot.mult(gripPos))); shieldSpatial.setLocalTranslation(anchor.subtract(shieldRot.mult(gripPos)));
} }

View File

@@ -246,7 +246,7 @@ public class WorldItemsState extends BaseAppState {
boolean doLog = hoverLogTimer >= 1f && !items.isEmpty(); boolean doLog = hoverLogTimer >= 1f && !items.isEmpty();
if (hoverLogTimer >= 1f) hoverLogTimer = 0f; if (hoverLogTimer >= 1f) hoverLogTimer = 0f;
if (doLog) { if (doLog) {
log.info("[Hover] items={} screenW={} screenH={} centerX={} hMargin={}", log.debug("[Hover] items={} screenW={} screenH={} centerX={} hMargin={}",
items.size(), cam.getWidth(), cam.getHeight(), centerX, hMargin); items.size(), cam.getWidth(), cam.getHeight(), centerX, hMargin);
} }
@@ -261,39 +261,39 @@ public class WorldItemsState extends BaseAppState {
// Distanz vom Charakter (nicht von der Kamera) // Distanz vom Charakter (nicht von der Kamera)
float charDist = worldPos.distance(charPos); float charDist = worldPos.distance(charPos);
if (doLog) log.info("[Hover] item[{}] id={} charDist={}", i, pi.itemId(), charDist); if (doLog) log.debug("[Hover] item[{}] id={} charDist={}", i, pi.itemId(), charDist);
if (charDist > INTERACT_RANGE) { if (charDist > INTERACT_RANGE) {
if (doLog) log.info("[Hover] → skip: außerhalb Reichweite ({} > {})", charDist, INTERACT_RANGE); if (doLog) log.debug("[Hover] → skip: außerhalb Reichweite ({} > {})", charDist, INTERACT_RANGE);
continue; continue;
} }
// Muss vor der Kamera liegen // Muss vor der Kamera liegen
Vector3f camToItem = worldPos.subtract(cam.getLocation()); Vector3f camToItem = worldPos.subtract(cam.getLocation());
if (camToItem.dot(cam.getDirection()) <= 0f) { if (camToItem.dot(cam.getDirection()) <= 0f) {
if (doLog) log.info("[Hover] → skip: hinter der Kamera"); if (doLog) log.debug("[Hover] → skip: hinter der Kamera");
continue; continue;
} }
// Bildschirmprojektion (von der Kamera aus) // Bildschirmprojektion (von der Kamera aus)
Vector3f screen = cam.getScreenCoordinates(worldPos); Vector3f screen = cam.getScreenCoordinates(worldPos);
float hDiff = Math.abs(screen.x - centerX); float hDiff = Math.abs(screen.x - centerX);
if (doLog) log.info("[Hover] screen=({},{},{}) hDiff={} hMargin={}", if (doLog) log.debug("[Hover] screen=({},{},{}) hDiff={} hMargin={}",
(int)screen.x, (int)screen.y, screen.z, (int)hDiff, (int)hMargin); (int)screen.x, (int)screen.y, screen.z, (int)hDiff, (int)hMargin);
if (hDiff > hMargin) { if (hDiff > hMargin) {
if (doLog) log.info("[Hover] → skip: horizontal außerhalb Mitte"); if (doLog) log.debug("[Hover] → skip: horizontal außerhalb Mitte");
continue; continue;
} }
if (screen.y < 0f || screen.y > screenH) { if (screen.y < 0f || screen.y > screenH) {
if (doLog) log.info("[Hover] → skip: vertikal außerhalb Bildschirm"); if (doLog) log.debug("[Hover] → skip: vertikal außerhalb Bildschirm");
continue; continue;
} }
// Verdeckungsprüfung: Strahl von Kamera zum Item // Verdeckungsprüfung: Strahl von Kamera zum Item
float camDist = camToItem.length(); float camDist = camToItem.length();
if (isOccluded(camToItem.normalize(), camDist, i, doLog)) { if (isOccluded(camToItem.normalize(), camDist, i, doLog)) {
if (doLog) log.info("[Hover] → skip: verdeckt"); if (doLog) log.debug("[Hover] → skip: verdeckt");
continue; continue;
} }
@@ -303,7 +303,7 @@ public class WorldItemsState extends BaseAppState {
} }
} }
if (doLog) log.info("[Hover] → hoveredIdx={}", bestIdx); if (doLog) log.debug("[Hover] → hoveredIdx={}", bestIdx);
setHovered(bestIdx); setHovered(bestIdx);
} }
@@ -312,25 +312,25 @@ public class WorldItemsState extends BaseAppState {
CollisionResults results = new CollisionResults(); CollisionResults results = new CollisionResults();
rootNode.collideWith(ray, results); rootNode.collideWith(ray, results);
if (doLog) log.info("[Hover] occlusion: {} Treffer, itemDist={:.2f}", results.size(), itemDist); if (doLog) log.debug("[Hover] occlusion: {} Treffer, itemDist={:.2f}", results.size(), itemDist);
for (int j = 0; j < results.size(); j++) { for (int j = 0; j < results.size(); j++) {
CollisionResult cr = results.getCollision(j); CollisionResult cr = results.getCollision(j);
float d = cr.getDistance(); float d = cr.getDistance();
if (d < 1.5f) { if (d < 1.5f) {
if (doLog) log.info("[Hover] [{}] d={:.2f} → Nah-Clip skip ({})", j, d, cr.getGeometry().getName()); if (doLog) log.debug("[Hover] [{}] d={:.2f} → Nah-Clip skip ({})", j, d, cr.getGeometry().getName());
continue; continue;
} }
if (d > itemDist - 0.15f) { if (d > itemDist - 0.15f) {
if (doLog) log.info("[Hover] [{}] d={:.2f} → am/hinter Item, kein Blocker ({})", j, d, cr.getGeometry().getName()); if (doLog) log.debug("[Hover] [{}] d={:.2f} → am/hinter Item, kein Blocker ({})", j, d, cr.getGeometry().getName());
continue; continue;
} }
int vidx = findVisualIndex(cr.getGeometry()); int vidx = findVisualIndex(cr.getGeometry());
if (vidx == itemIdx) { if (vidx == itemIdx) {
if (doLog) log.info("[Hover] [{}] d={:.2f} → Item selbst ({})", j, d, cr.getGeometry().getName()); if (doLog) log.debug("[Hover] [{}] d={:.2f} → Item selbst ({})", j, d, cr.getGeometry().getName());
continue; continue;
} }
if (doLog) log.info("[Hover] [{}] d={:.2f} → BLOCKIERT durch '{}'", j, d, cr.getGeometry().getName()); if (doLog) log.debug("[Hover] [{}] d={:.2f} → BLOCKIERT durch '{}'", j, d, cr.getGeometry().getName());
return true; return true;
} }
return false; return false;

View File

@@ -55,6 +55,7 @@ import de.blight.game.combat.DamageCalculator;
import de.blight.game.combat.MeleeAttackCallback; import de.blight.game.combat.MeleeAttackCallback;
import de.blight.game.combat.WeaponCombatState; import de.blight.game.combat.WeaponCombatState;
import de.blight.game.combat.swordshield.SwordShieldCombat; import de.blight.game.combat.swordshield.SwordShieldCombat;
import de.blight.game.combat.twohanded.TwoHandedCombat;
import de.blight.game.npc.AggroEvent; import de.blight.game.npc.AggroEvent;
import de.blight.game.npc.NpcAggroBehavior; import de.blight.game.npc.NpcAggroBehavior;
import de.blight.lang.TextResolver; import de.blight.lang.TextResolver;
@@ -2036,7 +2037,12 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
s.combat.phase = WeaponCombatState.Phase.COUNTER_WINDOW; s.combat.phase = WeaponCombatState.Phase.COUNTER_WINDOW;
s.combat.timer = dur * 2f; s.combat.timer = dur * 2f;
s.paradeFromParry = true; s.paradeFromParry = true;
SwordShieldCombat.applyParriedImpact(playerInput, playerInput.getDrawnWeaponType()); ItemSubCategory pwt = playerInput.getDrawnWeaponType();
if (pwt == ItemSubCategory.TWO_HANDED_SWORD) {
TwoHandedCombat.applyParriedImpact(playerInput);
} else {
SwordShieldCombat.applyParriedImpact(playerInput, pwt);
}
return; return;
} }
s.konterParadeAttackId = -1; // Entscheidung war kein Konter → zurücksetzen s.konterParadeAttackId = -1; // Entscheidung war kein Konter → zurücksetzen
@@ -2501,11 +2507,19 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
} }
private void staggerNpc(SpawnedNpc s) { private void staggerNpc(SpawnedNpc s) {
SwordShieldCombat.applyNormalImpact(npcAnimReceiver(s), s.combatWeaponType); if (s.combatWeaponType == ItemSubCategory.TWO_HANDED_SWORD) {
TwoHandedCombat.applyNormalImpact(npcAnimReceiver(s));
} else {
SwordShieldCombat.applyNormalImpact(npcAnimReceiver(s), s.combatWeaponType);
}
} }
private void staggerNpcSlow(SpawnedNpc s) { private void staggerNpcSlow(SpawnedNpc s) {
SwordShieldCombat.applyParriedImpact(npcAnimReceiver(s), s.combatWeaponType); if (s.combatWeaponType == ItemSubCategory.TWO_HANDED_SWORD) {
TwoHandedCombat.applyParriedImpact(npcAnimReceiver(s));
} else {
SwordShieldCombat.applyParriedImpact(npcAnimReceiver(s), s.combatWeaponType);
}
} }
private void setNpcCurrentActionSpeed(SpawnedNpc s, float speed) { private void setNpcCurrentActionSpeed(SpawnedNpc s, float speed) {
@@ -2575,15 +2589,18 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
s.paradeFromParry, playerInput.isBlocking()); s.paradeFromParry, playerInput.isBlocking());
if (isHeavy) { if (isHeavy) {
ItemSubCategory pwtH = playerInput.getDrawnWeaponType();
if (playerInput.isBlocking()) { if (playerInput.isBlocking()) {
// Schwerer Angriff bricht Block — kein HP-Verlust, aber Stagger // Schwerer Angriff bricht Block — kein HP-Verlust, aber Stagger
if (weaponSoundSystem != null) { weaponSoundSystem.play("impact"); } if (weaponSoundSystem != null) { weaponSoundSystem.play("impact"); }
playerInput.triggerBlockImpact(); playerInput.triggerBlockImpact();
SwordShieldCombat.applyNormalImpact(playerInput, playerInput.getDrawnWeaponType()); if (pwtH == ItemSubCategory.TWO_HANDED_SWORD) { TwoHandedCombat.applyNormalImpact(playerInput); }
else { SwordShieldCombat.applyNormalImpact(playerInput, pwtH); }
} else { } else {
if (weaponSoundSystem != null) { weaponSoundSystem.play("impact"); } if (weaponSoundSystem != null) { weaponSoundSystem.play("impact"); }
applyPlayerDamage(s, Math.max(1, base * 3)); applyPlayerDamage(s, Math.max(1, base * 3));
SwordShieldCombat.applyNormalImpact(playerInput, playerInput.getDrawnWeaponType()); if (pwtH == ItemSubCategory.TWO_HANDED_SWORD) { TwoHandedCombat.applyNormalImpact(playerInput); }
else { SwordShieldCombat.applyNormalImpact(playerInput, pwtH); }
} }
return; return;
} }
@@ -2600,7 +2617,9 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
// Konterparade: Spieler wurde bereits per applyParriedImpact gestaggert → keine zweite Animation. // Konterparade: Spieler wurde bereits per applyParriedImpact gestaggert → keine zweite Animation.
// Block+Konter: Spieler war NICHT gestaggert → normale Impact-Animation abspielen. // Block+Konter: Spieler war NICHT gestaggert → normale Impact-Animation abspielen.
if (!s.paradeFromParry) { if (!s.paradeFromParry) {
SwordShieldCombat.applyNormalImpact(playerInput, playerInput.getDrawnWeaponType()); ItemSubCategory pwtN = playerInput.getDrawnWeaponType();
if (pwtN == ItemSubCategory.TWO_HANDED_SWORD) { TwoHandedCombat.applyNormalImpact(playerInput); }
else { SwordShieldCombat.applyNormalImpact(playerInput, pwtN); }
} }
s.paradeFromParry = false; s.paradeFromParry = false;
} }
@@ -2686,14 +2705,17 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
} }
private AnimationAction resolveNpcBlockImpactAction(SpawnedNpc s) { private AnimationAction resolveNpcBlockImpactAction(SpawnedNpc s) {
if (s.combatWeaponType == ItemSubCategory.TWO_HANDED_SWORD) { return TwoHandedCombat.resolveBlockImpactAction(); }
return SwordShieldCombat.resolveBlockImpactAction(s.combatWeaponType); return SwordShieldCombat.resolveBlockImpactAction(s.combatWeaponType);
} }
private AnimationAction resolveNpcParadeAction(SpawnedNpc s) { private AnimationAction resolveNpcParadeAction(SpawnedNpc s) {
if (s.combatWeaponType == ItemSubCategory.TWO_HANDED_SWORD) { return TwoHandedCombat.resolveParadeAction(); }
return SwordShieldCombat.resolveParadeAction(s.combatWeaponType); return SwordShieldCombat.resolveParadeAction(s.combatWeaponType);
} }
private AnimationAction resolveNpcStaggerAction(SpawnedNpc s) { private AnimationAction resolveNpcStaggerAction(SpawnedNpc s) {
if (s.combatWeaponType == ItemSubCategory.TWO_HANDED_SWORD) { return TwoHandedCombat.resolveIdleImpactAction(); }
return SwordShieldCombat.resolveIdleImpactAction(s.combatWeaponType); return SwordShieldCombat.resolveIdleImpactAction(s.combatWeaponType);
} }

View File

@@ -285,3 +285,4 @@ npc.aggro.death.5=Das nächste Mal bleib, wo du hingehörst.
npc.bandit1.name=Bandit npc.bandit1.name=Bandit
npc.bjoernson.name=Bjoernson npc.bjoernson.name=Bjoernson
npc.silas.name=Silas 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.bandit1.name=Bandit
npc.bjoernson.name=Bjoernson npc.bjoernson.name=Bjoernson
npc.silas.name=Silas 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_1789920920760.name
neues_item_1789920asdf920760.description neues_item_1789920asdf920760.description
neues_item_1789920asdf920760.name neues_item_1789920asdf920760.name
neues_item_1791316630215.description
neues_item_1791316630215.name
quest.blight_wuehler.description quest.blight_wuehler.description
quest.blight_wuehler.name quest.blight_wuehler.name
quest.blight_wuehler.success quest.blight_wuehler.success
@@ -153,6 +155,8 @@ quest.kuehles_bier.success
quest.magnetiterz.description quest.magnetiterz.description
quest.magnetiterz.name quest.magnetiterz.name
quest.magnetiterz.success quest.magnetiterz.success
rostiger_zweihaender.description
rostiger_zweihaender.name
rostiges_kurzschwert.description rostiges_kurzschwert.description
rostiges_kurzschwert.name rostiges_kurzschwert.name
silas.name silas.name

View File

@@ -1,3 +1,4 @@
# uuid itemId x y z rotX rotY rotZ # 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 caa6cd73-4a2d-4112-9e22-b4d250ea41b0 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 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