Compare commits
10 Commits
62931e3273
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| f189e6d81f | |||
| 238776cfde | |||
| 60741d3116 | |||
| 93a3c266e1 | |||
| d19538bb63 | |||
| 6bb195da90 | |||
| b498d0fd5b | |||
| 911b512208 | |||
| 21c8af0105 | |||
| 9ff911eaa6 |
BIN
assets/imported/animations/Draw A Great Sword.fbx
Normal file
BIN
assets/imported/animations/Draw A Great Sword.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Attack 1.fbx
Normal file
BIN
assets/imported/animations/Great Sword Attack 1.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Attack 2.fbx
Normal file
BIN
assets/imported/animations/Great Sword Attack 2.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Attack 3.fbx
Normal file
BIN
assets/imported/animations/Great Sword Attack 3.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Block Impact.fbx
Normal file
BIN
assets/imported/animations/Great Sword Block Impact.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Blocking Idle.fbx
Normal file
BIN
assets/imported/animations/Great Sword Blocking Idle.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Blocking to Idle.fbx
Normal file
BIN
assets/imported/animations/Great Sword Blocking to Idle.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Heavy Attack.fbx
Normal file
BIN
assets/imported/animations/Great Sword Heavy Attack.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Idle boring 2.fbx
Normal file
BIN
assets/imported/animations/Great Sword Idle boring 2.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Idle boring 3.fbx
Normal file
BIN
assets/imported/animations/Great Sword Idle boring 3.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Idle boring.fbx
Normal file
BIN
assets/imported/animations/Great Sword Idle boring.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Idle to Blocking.fbx
Normal file
BIN
assets/imported/animations/Great Sword Idle to Blocking.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Idle.fbx
Normal file
BIN
assets/imported/animations/Great Sword Idle.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Impact.fbx
Normal file
BIN
assets/imported/animations/Great Sword Impact.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Jump.fbx
Normal file
BIN
assets/imported/animations/Great Sword Jump.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Parade.fbx
Normal file
BIN
assets/imported/animations/Great Sword Parade.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Run.fbx
Normal file
BIN
assets/imported/animations/Great Sword Run.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Running Jump.fbx
Normal file
BIN
assets/imported/animations/Great Sword Running Jump.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Strafe Right.fbx
Normal file
BIN
assets/imported/animations/Great Sword Strafe Right.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Strafe left.fbx
Normal file
BIN
assets/imported/animations/Great Sword Strafe left.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Walk backwards.fbx
Normal file
BIN
assets/imported/animations/Great Sword Walk backwards.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Great Sword Walk.fbx
Normal file
BIN
assets/imported/animations/Great Sword Walk.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sheath A Great Sword.fbx
Normal file
BIN
assets/imported/animations/Sheath A Great Sword.fbx
Normal file
Binary file not shown.
BIN
assets/imported/models/rostiger+zweihaender.glb
Normal file
BIN
assets/imported/models/rostiger+zweihaender.glb
Normal file
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -37,7 +37,30 @@
|
||||
"Sword And Shield Idle boring 3",
|
||||
"Sword And Shield Jump Running",
|
||||
"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": {
|
||||
"DEFAULT": "Idle",
|
||||
@@ -82,7 +105,32 @@
|
||||
"SWORD_IDLE_BORED": "Sword And Shield Idle boring",
|
||||
"SWORD_IDLE_BORED_2": "Sword And Shield Idle boring 2",
|
||||
"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",
|
||||
"animOffsets": {
|
||||
@@ -146,6 +194,14 @@
|
||||
"Roll",
|
||||
"Sneaking Forward",
|
||||
"Sword And Shield Jump Running",
|
||||
"Sword And Shield Jump"
|
||||
"Sword And Shield Jump",
|
||||
"Great Sword Walk backwards",
|
||||
"Great Sword Walk",
|
||||
"Great Sword Strafe left",
|
||||
"Great Sword Strafe Right",
|
||||
"Great Sword Running Jump",
|
||||
"Great Sword Run",
|
||||
"Great Sword Heavy Attack",
|
||||
"Great Sword Attack 3"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#Tue Oct 06 21:54:42 CEST 2026
|
||||
attachedEmitters.count=0
|
||||
attachedLights.count=0
|
||||
attachedSounds.count=0
|
||||
castShadow=true
|
||||
category=
|
||||
cullDistance=120.0
|
||||
footstepSurface=
|
||||
interactableOffsetX=0.0
|
||||
interactableOffsetY=0.5
|
||||
interactableOffsetZ=0.0
|
||||
interactableRotY=0.0
|
||||
interactableType=NONE
|
||||
lod1Distance=30.0
|
||||
lod1Path=
|
||||
lod2Distance=80.0
|
||||
lod2Path=
|
||||
modelCategory=NONE
|
||||
name=rostiger+zweihaender
|
||||
pivotOffsetY=0.0
|
||||
placementOffsetY=0.0
|
||||
randomScaleMax=1.0
|
||||
randomScaleMin=1.0
|
||||
receiveShadow=true
|
||||
scaleX=1.0
|
||||
scaleY=1.0
|
||||
scaleZ=1.0
|
||||
solid=true
|
||||
tags=
|
||||
uniformScale=true
|
||||
Binary file not shown.
@@ -0,0 +1,30 @@
|
||||
#Mon Oct 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
|
||||
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
#Sun Sep 20 15:31:32 CEST 2026
|
||||
#Mon Oct 19 14:39:11 CEST 2026
|
||||
attachedEmitters.count=0
|
||||
attachedLights.count=0
|
||||
attachedSounds.count=0
|
||||
|
||||
@@ -16,7 +16,13 @@
|
||||
"posZ": -0.13,
|
||||
"rotX": 90.0,
|
||||
"rotY": 0.0,
|
||||
"rotZ": 90.0
|
||||
"rotZ": 90.0,
|
||||
"grip2PosX": 0.0,
|
||||
"grip2PosY": 0.0,
|
||||
"grip2PosZ": 0.0,
|
||||
"handOffsetX": 0.0,
|
||||
"handOffsetY": 0.0,
|
||||
"handOffsetZ": 0.0
|
||||
},
|
||||
"modelRef": {
|
||||
"path": "Models/waffen/schilder/einfacher schild.j3o"
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"itemId": "rostiger_zweihaender",
|
||||
"category": "WEAPON",
|
||||
"subCategory": "TWO_HANDED_SWORD",
|
||||
"name": {
|
||||
"id": "rostiger_zweihaender.name"
|
||||
},
|
||||
"description": {
|
||||
"id": "rostiger_zweihaender.description"
|
||||
},
|
||||
"worthGold": 0,
|
||||
"damage": 50,
|
||||
"grip": {
|
||||
"posX": 0.10248406,
|
||||
"posY": 0.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
|
||||
}
|
||||
@@ -11,12 +11,18 @@
|
||||
"worthGold": 0,
|
||||
"damage": 0,
|
||||
"grip": {
|
||||
"posX": 0.37,
|
||||
"posY": 0.08,
|
||||
"posZ": 0.0,
|
||||
"posX": 0.1114975,
|
||||
"posY": 0.07795838,
|
||||
"posZ": 0.36503196,
|
||||
"rotX": 0.0,
|
||||
"rotY": -90.0,
|
||||
"rotZ": 90.0
|
||||
"rotY": 0.0,
|
||||
"rotZ": 0.0,
|
||||
"grip2PosX": 0.0,
|
||||
"grip2PosY": 0.0,
|
||||
"grip2PosZ": 0.0,
|
||||
"handOffsetX": 0.0,
|
||||
"handOffsetY": 0.0,
|
||||
"handOffsetZ": 0.0
|
||||
},
|
||||
"modelRef": {
|
||||
"path": "Models/waffen/schwerter/rostiges_kurzschwert.j3o"
|
||||
|
||||
8
blight-assets/src/main/resources/shield_attachment.json
Normal file
8
blight-assets/src/main/resources/shield_attachment.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"SHIELD": {
|
||||
"rotX": -90.0,
|
||||
"rotY": 0.0,
|
||||
"rotZ": 0.0,
|
||||
"elbowOffset": 0.25
|
||||
}
|
||||
}
|
||||
18
blight-assets/src/main/resources/weapon_attachment.json
Normal file
18
blight-assets/src/main/resources/weapon_attachment.json
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,15 @@ import lombok.Setter;
|
||||
@Getter
|
||||
@Setter
|
||||
public class ItemGrip {
|
||||
/** Offset (Modell-local), den das Runtime-System beim Anbringen der Waffe addiert. */
|
||||
public static final float HAND_GRIP_OFFSET_X = 0.1f;
|
||||
public static final float HAND_GRIP_OFFSET_Y = 0.0f;
|
||||
public static final float HAND_GRIP_OFFSET_Z = 0.05f;
|
||||
|
||||
private float posX, posY, posZ;
|
||||
private float rotX, rotY, rotZ;
|
||||
/** Position des linken Hand-Griffs im Modell-Space (nur Zweihänder). */
|
||||
private float grip2PosX, grip2PosY, grip2PosZ;
|
||||
/** Zusätzlicher Offset in Character-Space (X=rechts, Y=oben, Z=vorwärts). */
|
||||
private float handOffsetX, handOffsetY, handOffsetZ;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -174,7 +174,7 @@ public class EditorApp extends Application {
|
||||
animPreviewController = new de.blight.editor.toolset.anim.AnimPreviewController(toolCtx);
|
||||
animSetController = new de.blight.editor.toolset.anim.AnimSetController(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);
|
||||
atmosphereController = new de.blight.editor.toolset.atmosphere.AtmosphereController(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);
|
||||
contentController = new de.blight.editor.toolset.content.ContentController(toolCtx);
|
||||
contentController.setOnEnterContentMode(() -> {
|
||||
if (assetController.isOverlayOpen()) assetController.closeOverlay();
|
||||
assetController.closeOverlayImmediate();
|
||||
assetController.hideTabBtn();
|
||||
});
|
||||
tripoController = new de.blight.editor.toolset.tripo.TripoController(toolCtx);
|
||||
@@ -281,10 +281,13 @@ public class EditorApp extends Application {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Tab: Sub-Modus innerhalb des aktiven Tools cyclen
|
||||
// Tab: Sub-Modus innerhalb des aktiven Tools cyclen.
|
||||
// Nicht auslösen wenn Fokus auf einem Eingabefeld liegt (TextInputControl oder Spinner –
|
||||
// Spinner.getFocusOwner() gibt den Spinner selbst zurück, nicht das interne TextField).
|
||||
if (e.getCode() == KeyCode.TAB && "world".equals(currentTool)
|
||||
&& !e.isAltDown() && !e.isControlDown()
|
||||
&& !(scene.getFocusOwner() instanceof javafx.scene.control.TextInputControl)) {
|
||||
&& !(scene.getFocusOwner() instanceof javafx.scene.control.TextInputControl)
|
||||
&& !(scene.getFocusOwner() instanceof javafx.scene.control.Spinner)) {
|
||||
e.consume();
|
||||
cycleToolSubMode();
|
||||
return;
|
||||
@@ -618,13 +621,14 @@ public class EditorApp extends Application {
|
||||
private void switchToWorldEditor() {
|
||||
boolean wasObjEditor = "objecteditor".equals(registry.activeToolId()) || "objecteditor".equals(currentTool);
|
||||
boolean wasModelEditor = "modeleditor".equals(registry.activeToolId()) || "modeleditor".equals(currentTool);
|
||||
boolean wasGripEditor = "gripEditor".equals(currentTool);
|
||||
boolean wasThumbnailManager = "thumbnailmanager".equals(currentTool);
|
||||
registry.deactivateAll();
|
||||
currentTool = "world";
|
||||
if (jmeApp != null) jmeApp.setRenderThrottled(false);
|
||||
root.setLeft(null); // ggf. gesetzten Content-Tree entfernen
|
||||
assetController.showTabBtn();
|
||||
if (wasThumbnailManager) assetController.openOverlay();
|
||||
assetController.openOverlay();
|
||||
topBar.getChildren().set(1, topBarController.getWorldToolBar());
|
||||
if (contentController.hasRoutineJmeSlot()) {
|
||||
contentController.detachRoutineJmeSlot();
|
||||
@@ -647,9 +651,22 @@ public class EditorApp extends Application {
|
||||
input.activeLayer = 0;
|
||||
input.modelEditorOpenPath = null;
|
||||
topBarController.getBaseBtn().setSelected(true);
|
||||
} else if (wasGripEditor) {
|
||||
input.activeLayer = 0;
|
||||
input.gripEditorActive = false;
|
||||
input.gripEditorIsShield = false;
|
||||
Platform.runLater(() -> topBarController.getAuswahlBtn().fire());
|
||||
}
|
||||
}
|
||||
|
||||
/** Wechselt ggf. zum Welteditor und feuert dann den gewünschten Toolbar-Button. */
|
||||
private void fireWorldToolBtn(Runnable btn) {
|
||||
Platform.runLater(() -> {
|
||||
if (!"world".equals(currentTool)) switchToWorldEditor();
|
||||
btn.run();
|
||||
});
|
||||
}
|
||||
|
||||
private void ensureNotContentEditor() {
|
||||
if (contentController.isSingleWindowContentActive(currentTool)) {
|
||||
switchToWorldEditor();
|
||||
@@ -665,7 +682,7 @@ public class EditorApp extends Application {
|
||||
thumbnailManagerView.refresh();
|
||||
}
|
||||
// Asset-Baum-Overlay ausblenden – wird in switchToWorldEditor() wiederhergestellt
|
||||
if (assetController.isOverlayOpen()) assetController.closeOverlay();
|
||||
assetController.closeOverlayImmediate();
|
||||
assetController.hideTabBtn();
|
||||
topBar.getChildren().set(1, buildThumbnailManagerToolBar());
|
||||
root.setLeft(null);
|
||||
@@ -1067,16 +1084,18 @@ public class EditorApp extends Application {
|
||||
input.topologyRequest = sel ? 1 : 2;
|
||||
}
|
||||
}
|
||||
case F1 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getAuswahlBtn().fire()); }
|
||||
case F2 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getBaseBtn().fire()); }
|
||||
case F3 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getVoxelMergedBtn().fire()); }
|
||||
case F4 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getTextureBtn().fire()); }
|
||||
case F1 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getAuswahlBtn().fire()); }
|
||||
case F2 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getBaseBtn().fire()); }
|
||||
case F3 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getVoxelMergedBtn().fire()); }
|
||||
case F4 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getTextureBtn().fire()); }
|
||||
case F5 -> {
|
||||
if (pressed) {
|
||||
if ("world".equals(currentTool)) { Platform.runLater(() -> topBarController.getGrassBtn().fire()); }
|
||||
else {
|
||||
ToolController active = registry.getActive();
|
||||
if (active != null) { active.onPreview(); } else if (onF5 != null) { onF5.run(); }
|
||||
if (active != null) { active.onPreview(); }
|
||||
else if (onF5 != null) { onF5.run(); }
|
||||
else { fireWorldToolBtn(() -> topBarController.getGrassBtn().fire()); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1085,28 +1104,29 @@ public class EditorApp extends Application {
|
||||
if ("world".equals(currentTool)) { Platform.runLater(() -> topBarController.getStoneBtn().fire()); }
|
||||
else {
|
||||
ToolController active = registry.getActive();
|
||||
if (active != null) { active.onRandomSeed(); } else if (onF6 != null) { onF6.run(); }
|
||||
if (active != null) { active.onRandomSeed(); }
|
||||
else if (onF6 != null) { onF6.run(); }
|
||||
else { fireWorldToolBtn(() -> topBarController.getStoneBtn().fire()); }
|
||||
}
|
||||
}
|
||||
}
|
||||
case F7 -> {
|
||||
if (pressed && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getObjPlaceBtn().fire()); }
|
||||
else if (pressed && "objecteditor".equals(registry.activeToolId())) { Platform.runLater(objectController::activatePlaceMode); }
|
||||
if (pressed && "objecteditor".equals(registry.activeToolId())) { Platform.runLater(objectController::activatePlaceMode); }
|
||||
else if (pressed) { fireWorldToolBtn(() -> topBarController.getObjPlaceBtn().fire()); }
|
||||
}
|
||||
case F8 -> {
|
||||
if (pressed) {
|
||||
if ("world".equals(currentTool)) { Platform.runLater(() -> topBarController.getAtmosphaereBtn().fire()); }
|
||||
else if (input.ctrlHeld) { input.debugNoLightToggle = true; }
|
||||
} else if (input.ctrlHeld) { input.debugNoLightToggle = true; }
|
||||
if (pressed && input.ctrlHeld) { input.debugNoLightToggle = true; }
|
||||
else if (!pressed && input.ctrlHeld) { input.debugNoLightToggle = true; }
|
||||
else if (pressed) { fireWorldToolBtn(() -> topBarController.getAtmosphaereBtn().fire()); }
|
||||
}
|
||||
case F9 -> {
|
||||
if (pressed && input.ctrlHeld) { Platform.runLater(() ->
|
||||
contentController.activateContentSection(8, currentTool)); }
|
||||
else if (pressed && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getWasserMergedBtn().fire()); }
|
||||
else if (pressed) { fireWorldToolBtn(() -> topBarController.getWasserMergedBtn().fire()); }
|
||||
}
|
||||
case F10 -> { if (pressed && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getWegeBtn().fire()); } }
|
||||
case F11 -> { if (pressed && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getZonenBtn().fire()); } }
|
||||
case F12 -> { if (pressed && !input.ctrlHeld && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getPlayToolBtn().fire()); } }
|
||||
case F10 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getWegeBtn().fire()); }
|
||||
case F11 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getZonenBtn().fire()); }
|
||||
case F12 -> { if (pressed && !input.ctrlHeld) fireWorldToolBtn(() -> topBarController.getPlayToolBtn().fire()); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -878,6 +878,12 @@ public class SharedInput {
|
||||
public volatile boolean modelEditorBakeScaleRequest = false;
|
||||
public volatile java.nio.file.Path modelEditorBakeScalePath = null;
|
||||
|
||||
/** JFX → JME: Waffe an Z-Achse ausrichten und Mitte in Ursprung setzen. */
|
||||
public volatile boolean modelEditorAlignWeaponRequest = false;
|
||||
public volatile java.nio.file.Path modelEditorAlignWeaponPath = null;
|
||||
/** JME → JFX: Ergebnis der letzten Ausrichte-Operation (null = kein Ergebnis). */
|
||||
public volatile String modelEditorAlignWeaponResult = null;
|
||||
|
||||
/** JME → JFX: j3o hatte eine gebakte Scale, die von der Meta abwich – Spinner aktualisieren. */
|
||||
public volatile boolean modelEditorBakedScaleDetected = false;
|
||||
|
||||
@@ -934,6 +940,8 @@ public class SharedInput {
|
||||
public volatile String bakeStatusMsg = null;
|
||||
/** JME → JFX: Anzahl dirty Chunks in der Session (0 = nichts zu backen). */
|
||||
public volatile int sessionDirtyVoxelCount = 0;
|
||||
/** JFX → JME: Pfad zum aktiven Session-Verzeichnis (gesetzt von GameController beim Start). */
|
||||
public volatile java.nio.file.Path activeSessionDir = null;
|
||||
/** JFX → JME: Nur dirty Chunks backen (Bake-on-Exit). */
|
||||
public volatile boolean bakeOnExitRequested = false;
|
||||
/** JFX → JME: Laufenden Bake-on-Exit abbrechen. */
|
||||
@@ -1209,10 +1217,58 @@ public class SharedInput {
|
||||
public volatile float gripEditorRotX = 0f;
|
||||
public volatile float gripEditorRotY = 0f;
|
||||
public volatile float gripEditorRotZ = 0f;
|
||||
public volatile float gripEditorGrip2PosX = 0f;
|
||||
public volatile float gripEditorGrip2PosY = 0f;
|
||||
public volatile float gripEditorGrip2PosZ = 0f;
|
||||
public volatile float gripEditorHandOffX = 0f;
|
||||
public volatile float gripEditorHandOffY = 0f;
|
||||
public volatile float gripEditorHandOffZ = 0f;
|
||||
/** Gesetzt vom JFX-Thread wenn Spinner geändert → JME baut Gizmo neu. */
|
||||
public volatile boolean gripEditorGizmoDirty = false;
|
||||
/** true → Gizmo ist ein 28cm-Unterarm-Pfeil (Schild); false → 15cm-Waffen-Pfeil. */
|
||||
public volatile boolean gripEditorIsShield = false;
|
||||
/** JFX → JME: Griff-Position aus Modell-Geometrie schätzen (Parierstange erkennen). */
|
||||
public volatile boolean gripEditorGuessRequest = false;
|
||||
/** JME → JFX: Ergebnis der Griff-Schätzung — X/Y = Querschnittsmitte, Z = Griff-Z mit Puffer.
|
||||
* gripEditorGuessPosZ = Float.NaN bedeutet: kein Ergebnis vorhanden. */
|
||||
public volatile float gripEditorGuessPosX = 0f;
|
||||
public volatile float gripEditorGuessPosY = 0f;
|
||||
public volatile float gripEditorGuessPosZ = Float.NaN;
|
||||
/** Klingenrichtung im Modellraum: -1 = Klinge bei -Z, +1 = Klinge bei +Z. Wird von guessGripPosition gesetzt. */
|
||||
public volatile float gripEditorBladeDirZ = -1f;
|
||||
|
||||
// ── 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. */
|
||||
public record SculptEdit(float screenX, float screenY, int action) {}
|
||||
|
||||
@@ -664,18 +664,10 @@ public class AnimPreviewState extends BaseAppState {
|
||||
// snap passiert ausschließlich zur Laufzeit im Spiel via AnimationLibrary.
|
||||
boolean isJ3oSource = animAssetPath.matches("(?i).*\\.j3o$");
|
||||
|
||||
// FBX-Import erfordert ein geladenes Charakter-Modell.
|
||||
// Ohne Ziel-Armatur kann normalizeAndRetargetClip die $AssimpFbx$_Translation-Orphan-Joints
|
||||
// nicht in den Hips-Track falten → Hips-Translation geht verloren → Charakter schrumpft.
|
||||
if (!isJ3oSource && srcArm != null && dstArm == null) {
|
||||
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;
|
||||
// Kein Charakter-Modell geladen: Normalisierung gegen eigenes Skelett (Orphan-Fold + Scale-Strip).
|
||||
// Das erzeugte j3o ist später per redirectAnimClipByName auf jedes kompatible Rig anwendbar.
|
||||
com.jme3.anim.Armature effectiveDst = dstArm != null ? dstArm : srcArm;
|
||||
com.jme3.anim.Armature snapArm = effectiveDst;
|
||||
|
||||
int saved = 0;
|
||||
for (AnimClip clip : sourceAC.getAnimClips()) {
|
||||
@@ -688,7 +680,7 @@ public class AnimPreviewState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
AnimClip result;
|
||||
if (!retarget) {
|
||||
if (!retarget && (isJ3oSource || srcArm == null)) {
|
||||
result = clip;
|
||||
} else if (isJ3oSource) {
|
||||
// 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);
|
||||
} else {
|
||||
// FBX-Import: Normalisierung in Modell-Raum (Orphan-Fold + Bind-Pose-Delta).
|
||||
// Das resultierende j3o ist dann mit plain redirectAnimClipByName kompatibel.
|
||||
result = normalizeAndRetargetClip(clip, dstArm);
|
||||
// Kein Charakter-Modell → effectiveDst == srcArm → reine Normalisierung ohne Retarget.
|
||||
result = normalizeAndRetargetClip(clip, effectiveDst);
|
||||
if (result == null) {
|
||||
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;
|
||||
|
||||
|
||||
@@ -279,6 +279,7 @@ public class ItemPlacementState extends BaseAppState {
|
||||
|
||||
try {
|
||||
PlacedItemIO.save(items);
|
||||
syncBpiToSession();
|
||||
log.info("[ItemPlacement] Item '{}' platziert bei ({}, {}, {})", pi.itemId(), pt.x, pt.y, pt.z);
|
||||
} catch (IOException e) {
|
||||
log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage());
|
||||
@@ -664,6 +665,7 @@ public class ItemPlacementState extends BaseAppState {
|
||||
nodes.remove(idx);
|
||||
try {
|
||||
PlacedItemIO.save(items);
|
||||
syncBpiToSession();
|
||||
} catch (IOException e) {
|
||||
log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
@@ -681,6 +683,7 @@ public class ItemPlacementState extends BaseAppState {
|
||||
|
||||
try {
|
||||
PlacedItemIO.save(items);
|
||||
syncBpiToSession();
|
||||
} catch (IOException e) {
|
||||
log.warn("[ItemPlacement] Speichern nach Edit fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
@@ -689,6 +692,7 @@ public class ItemPlacementState extends BaseAppState {
|
||||
private void saveItems() {
|
||||
try {
|
||||
PlacedItemIO.save(items);
|
||||
syncBpiToSession();
|
||||
} catch (IOException e) {
|
||||
log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
@@ -696,6 +700,17 @@ public class ItemPlacementState extends BaseAppState {
|
||||
|
||||
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
||||
|
||||
private void syncBpiToSession() {
|
||||
Path sd = input.activeSessionDir;
|
||||
if (sd == null) return;
|
||||
try {
|
||||
java.nio.file.Files.copy(PlacedItemIO.getPath(), sd.resolve("blight_placed_items.bpi"),
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException ex) {
|
||||
log.warn("[ItemPlacement] Session-Sync fehlgeschlagen: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyRot(Node n, float rotX, float rotY, float rotZ) {
|
||||
Quaternion q = new Quaternion();
|
||||
q.fromAngles(rotX, rotY, rotZ);
|
||||
|
||||
@@ -34,6 +34,12 @@ import com.jme3.effect.ParticleMesh;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.texture.Texture;
|
||||
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.util.ImpostorUtil;
|
||||
import de.blight.editor.util.ModelExportUtil;
|
||||
@@ -106,6 +112,18 @@ public class ModelEditorState extends BaseAppState {
|
||||
private Node gripGizmoNode = null;
|
||||
private float gripPosX = 0f, gripPosY = 0f, gripPosZ = 0f;
|
||||
private float gripRotX = 0f, gripRotY = 0f, gripRotZ = 0f;
|
||||
private float grip2PosX = 0f, grip2PosY = 0f, grip2PosZ = 0f;
|
||||
|
||||
// ── 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 interactableRotY = 0f;
|
||||
|
||||
@@ -197,6 +215,10 @@ public class ModelEditorState extends BaseAppState {
|
||||
app.getStateManager().detach(previewEnvCam);
|
||||
previewEnvCam = null;
|
||||
}
|
||||
// Probe jetzt zur Szene hinzufügen – erst hier ist sie korrekt initialisiert
|
||||
if (previewRoot != null) {
|
||||
previewRoot.addLight(previewProbe);
|
||||
}
|
||||
previewProbeReady = true;
|
||||
// Emissive-Fallback von bereits geladenen Materialien entfernen
|
||||
for (Material m : pbrMaterials) {
|
||||
@@ -223,12 +245,23 @@ public class ModelEditorState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
if (input.modelEditorEmbedLodsRequest) {
|
||||
input.modelEditorEmbedLodsRequest = false;
|
||||
embedLodsAndSave(input.modelEditorEmbedLodsPath);
|
||||
if (input.modelEditorAlignWeaponRequest) {
|
||||
input.modelEditorAlignWeaponRequest = false;
|
||||
Path alignPath = input.modelEditorAlignWeaponPath;
|
||||
if (alignPath != null) {
|
||||
alignWeaponToZ(alignPath);
|
||||
} else {
|
||||
log.warn("[ModelEditor] AlignWeapon: kein Pfad gesetzt");
|
||||
input.modelEditorAlignWeaponResult = "FEHLER: kein Pfad";
|
||||
}
|
||||
}
|
||||
|
||||
// Modell laden
|
||||
if (input.gripEditorGuessRequest) {
|
||||
input.gripEditorGuessRequest = false;
|
||||
guessGripPosition();
|
||||
}
|
||||
|
||||
// Modell laden – MUSS vor enableCharPreview stehen, damit currentPath aktuell ist
|
||||
String openPath = input.modelEditorOpenPath;
|
||||
if (openPath != null) {
|
||||
input.modelEditorOpenPath = null;
|
||||
@@ -239,15 +272,60 @@ public class ModelEditorState extends BaseAppState {
|
||||
loadModel(openPath);
|
||||
}
|
||||
|
||||
// Grip-Gizmo aktualisieren
|
||||
if (input.gripEditorActive && input.gripEditorGizmoDirty) {
|
||||
// Charakter-Preview ein-/ausschalten
|
||||
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;
|
||||
gripPosX = input.gripEditorPosX;
|
||||
gripPosY = input.gripEditorPosY;
|
||||
gripPosZ = input.gripEditorPosZ;
|
||||
gripRotX = input.gripEditorRotX;
|
||||
gripRotY = input.gripEditorRotY;
|
||||
gripRotZ = input.gripEditorRotZ;
|
||||
gripPosX = input.gripEditorPosX;
|
||||
gripPosY = input.gripEditorPosY;
|
||||
gripPosZ = input.gripEditorPosZ;
|
||||
gripRotX = input.gripEditorRotX;
|
||||
gripRotY = input.gripEditorRotY;
|
||||
gripRotZ = input.gripEditorRotZ;
|
||||
grip2PosX = input.gripEditorGrip2PosX;
|
||||
grip2PosY = input.gripEditorGrip2PosY;
|
||||
grip2PosZ = input.gripEditorGrip2PosZ;
|
||||
rebuildGripGizmo();
|
||||
}
|
||||
|
||||
@@ -472,7 +550,22 @@ public class ModelEditorState extends BaseAppState {
|
||||
// Rendering schwarz erscheinen lassen.
|
||||
try { app.getAssetManager().deleteFromCache(new com.jme3.asset.ModelKey(assetPath)); }
|
||||
catch (Exception ignored) {}
|
||||
Spatial model = app.getAssetManager().loadModel(assetPath);
|
||||
// Direkt aus ASSET_ROOT (src/main/resources) laden – umgeht den ClasspathLocator,
|
||||
// der bin/main bevorzugt und nach einem Bake möglicherweise die veraltete Kopie liefert.
|
||||
java.nio.file.Path absFile = ASSET_ROOT.resolve(
|
||||
java.nio.file.Paths.get(assetPath.replace('/', java.io.File.separatorChar)));
|
||||
Spatial model;
|
||||
if (absFile.toFile().exists()) {
|
||||
BinaryImporter directImp = BinaryImporter.getInstance();
|
||||
directImp.setAssetManager(app.getAssetManager());
|
||||
Savable directLoaded = directImp.load(absFile.toFile());
|
||||
log.info("[ModelEditor] Direkt geladen: {} ({}B)", absFile.getFileName(), absFile.toFile().length());
|
||||
model = (directLoaded instanceof Spatial) ? (Spatial) directLoaded
|
||||
: app.getAssetManager().loadModel(assetPath);
|
||||
} else {
|
||||
log.warn("[ModelEditor] Nicht in ASSET_ROOT, lade via AM: {}", assetPath);
|
||||
model = app.getAssetManager().loadModel(assetPath);
|
||||
}
|
||||
stripControls(model);
|
||||
initImpostorControls(model);
|
||||
recalcNormalsIfMissing(model);
|
||||
@@ -696,12 +789,12 @@ public class ModelEditorState extends BaseAppState {
|
||||
new ColorRGBA(0.4f, 0.4f, 0.5f, 1f)));
|
||||
previewRoot.addLight(new AmbientLight(new ColorRGBA(0.65f, 0.65f, 0.7f, 1f)));
|
||||
|
||||
// LightProbe: JME3-PBR braucht Probe damit g_AmbientLightColor verwendet wird.
|
||||
// EnvironmentCamera backt die Probe aus dem hellen Übergangshintergrund heraus;
|
||||
// danach wird der dunkle Display-Hintergrund wiederhergestellt.
|
||||
// LightProbe: erst nach dem Backen zur Szene hinzufügen – eine leere (uninitialisierte)
|
||||
// Probe stört den PBR-Shader und macht das Modell dunkel, bevor der Bake fertig ist.
|
||||
// Bis dahin sorgen DirectionalLights + AmbientLight für sichtbare Beleuchtung.
|
||||
previewProbe = new LightProbe();
|
||||
previewProbe.getArea().setRadius(1000f);
|
||||
previewRoot.addLight(previewProbe);
|
||||
// previewRoot.addLight(previewProbe) → wird in done()-Callback aufgerufen
|
||||
previewEnvCam = new EnvironmentCamera(256, Vector3f.ZERO.clone());
|
||||
app.getStateManager().attach(previewEnvCam);
|
||||
previewProbeFrame = 0;
|
||||
@@ -728,14 +821,17 @@ public class ModelEditorState extends BaseAppState {
|
||||
gripGizmoNode.setCullHint(Spatial.CullHint.Always);
|
||||
previewRoot.attachChild(gripGizmoNode);
|
||||
|
||||
gripPosX = input.gripEditorPosX;
|
||||
gripPosY = input.gripEditorPosY;
|
||||
gripPosZ = input.gripEditorPosZ;
|
||||
gripRotX = input.gripEditorRotX;
|
||||
gripRotY = input.gripEditorRotY;
|
||||
gripRotZ = input.gripEditorRotZ;
|
||||
gripPosX = input.gripEditorPosX;
|
||||
gripPosY = input.gripEditorPosY;
|
||||
gripPosZ = input.gripEditorPosZ;
|
||||
gripRotX = input.gripEditorRotX;
|
||||
gripRotY = input.gripEditorRotY;
|
||||
gripRotZ = input.gripEditorRotZ;
|
||||
grip2PosX = input.gripEditorGrip2PosX;
|
||||
grip2PosY = input.gripEditorGrip2PosY;
|
||||
grip2PosZ = input.gripEditorGrip2PosZ;
|
||||
|
||||
orbitYaw = 30f;
|
||||
orbitYaw = input.gripEditorActive ? 90f : 30f;
|
||||
orbitPitch = 25f;
|
||||
}
|
||||
|
||||
@@ -757,6 +853,7 @@ public class ModelEditorState extends BaseAppState {
|
||||
originalSpatial = null;
|
||||
interactableArrowNode = null;
|
||||
gripGizmoNode = null;
|
||||
disableCharPreview();
|
||||
input.gripEditorActive = false;
|
||||
camLight = 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("build").resolve("resources").resolve("main").resolve(rel),
|
||||
};
|
||||
for (java.nio.file.Path mirror : mirrors) {
|
||||
if (java.nio.file.Files.exists(mirror)) {
|
||||
try {
|
||||
java.nio.file.Files.copy(savedFile, mirror,
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
log.info("[ModelEditor] Spiegel aktualisiert: {}", mirror);
|
||||
} catch (Exception e) {
|
||||
log.warn("[ModelEditor] Spiegel-Update fehlgeschlagen ({}): {}", mirror, e.getMessage());
|
||||
}
|
||||
java.nio.file.Path[] mirrorRoots = {
|
||||
projectRoot.resolve("blight-assets").resolve("bin").resolve("main"),
|
||||
projectRoot.resolve("blight-assets").resolve("build").resolve("resources").resolve("main"),
|
||||
};
|
||||
for (int i = 0; i < mirrors.length; i++) {
|
||||
java.nio.file.Path mirror = mirrors[i];
|
||||
if (!java.nio.file.Files.isDirectory(mirrorRoots[i])) continue;
|
||||
try {
|
||||
java.nio.file.Files.createDirectories(mirror.getParent());
|
||||
java.nio.file.Files.copy(savedFile, mirror,
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
log.info("[ModelEditor] Spiegel aktualisiert: {}", mirror);
|
||||
} catch (Exception e) {
|
||||
log.warn("[ModelEditor] Spiegel-Update fehlgeschlagen ({}): {}", mirror, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1424,6 +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) {
|
||||
if (s.getControl(SkinningControl.class) != null) return true;
|
||||
if (s instanceof Node n) {
|
||||
@@ -1533,66 +1925,77 @@ public class ModelEditorState extends BaseAppState {
|
||||
input.modelInteractableActive ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
}
|
||||
|
||||
/** Zeichnet / aktualisiert den Grip-Gizmo: 15cm orange (Waffe) oder 28cm cyan (Schild/Unterarm). */
|
||||
/** Zeichnet / aktualisiert den Grip-Gizmo: 15cm orange (Waffe/Grip1), blau (Grip2), cyan (Schild). */
|
||||
public void rebuildGripGizmo() {
|
||||
if (gripGizmoNode == null) return;
|
||||
gripGizmoNode.detachAllChildren();
|
||||
|
||||
boolean shieldMode = input.gripEditorIsShield;
|
||||
float shaftLen = shieldMode ? 0.24f : 0.12f;
|
||||
float shaftRad = shieldMode ? 0.010f : 0.008f;
|
||||
float headRad = shieldMode ? 0.026f : 0.022f;
|
||||
float axisLen = shieldMode ? 0.08f : 0.06f;
|
||||
float markerSz = shieldMode ? 0.018f : 0.015f;
|
||||
float bladeDirZ = input.gripEditorBladeDirZ;
|
||||
|
||||
// Waffe: 15cm orange | Schild: 28cm cyan
|
||||
float shaftLen = shieldMode ? 0.24f : 0.12f;
|
||||
float shaftRad = shieldMode ? 0.010f : 0.008f;
|
||||
float headRad = shieldMode ? 0.026f : 0.022f;
|
||||
float headLen = shieldMode ? 0.04f : 0.03f;
|
||||
// Grip 1: orange (Waffe) oder cyan (Schild)
|
||||
ColorRGBA c1s = shieldMode ? new ColorRGBA(0f, 0.75f, 0.9f, 1f) : new ColorRGBA(1f, 0.55f, 0f, 1f);
|
||||
ColorRGBA c1h = shieldMode ? new ColorRGBA(0f, 0.5f, 0.75f, 1f) : new ColorRGBA(1f, 0.3f, 0f, 1f);
|
||||
ColorRGBA c1a = shieldMode ? new ColorRGBA(0.6f, 1f, 0.6f, 1f) : new ColorRGBA(1f, 0.8f, 0.1f, 1f);
|
||||
gripGizmoNode.attachChild(buildGripGroup("gripGroup",
|
||||
gripPosX, gripPosY, gripPosZ, gripRotX, gripRotY, gripRotZ,
|
||||
bladeDirZ, c1s, c1h, c1a, shaftLen, shaftRad, headRad, axisLen, markerSz));
|
||||
|
||||
ColorRGBA shaftColor = shieldMode
|
||||
? new ColorRGBA(0f, 0.75f, 0.9f, 1f)
|
||||
: new ColorRGBA(1f, 0.55f, 0f, 1f);
|
||||
ColorRGBA headColor = shieldMode
|
||||
? new ColorRGBA(0f, 0.5f, 0.75f, 1f)
|
||||
: new ColorRGBA(1f, 0.3f, 0f, 1f);
|
||||
ColorRGBA axisColor = shieldMode
|
||||
? new ColorRGBA(0.6f, 1f, 0.6f, 1f)
|
||||
: new ColorRGBA(1f, 0.8f, 0.1f, 1f);
|
||||
// Grip 2: blau — nur für Waffen, nur wenn Position gesetzt
|
||||
if (!shieldMode && (grip2PosX != 0f || grip2PosY != 0f || grip2PosZ != 0f)) {
|
||||
gripGizmoNode.attachChild(buildGripGroup("grip2Group",
|
||||
grip2PosX, grip2PosY, grip2PosZ, 0, 0, 0,
|
||||
bladeDirZ,
|
||||
new ColorRGBA(0.2f, 0.5f, 1f, 1f),
|
||||
new ColorRGBA(0.1f, 0.3f, 0.9f, 1f),
|
||||
new ColorRGBA(0.5f, 0.75f, 1f, 1f),
|
||||
shaftLen, shaftRad, headRad, axisLen, markerSz));
|
||||
}
|
||||
|
||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
gripGizmoNode.setCullHint(Spatial.CullHint.Inherit);
|
||||
}
|
||||
|
||||
private Node buildGripGroup(String name,
|
||||
float posX, float posY, float posZ, float rotXDeg, float rotYDeg, float rotZDeg,
|
||||
float bladeDirZ, ColorRGBA shaftColor, ColorRGBA headColor, ColorRGBA axisColor,
|
||||
float shaftLen, float shaftRad, float headRad, float axisLen, float markerSz) {
|
||||
|
||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", shaftColor);
|
||||
Material matHead = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
matHead.setColor("Color", headColor);
|
||||
|
||||
Node group = new Node("gripGroup");
|
||||
group.setLocalTranslation(gripPosX, gripPosY, gripPosZ);
|
||||
|
||||
Quaternion rot = new Quaternion();
|
||||
rot.fromAngles(
|
||||
com.jme3.math.FastMath.DEG_TO_RAD * gripRotX,
|
||||
com.jme3.math.FastMath.DEG_TO_RAD * gripRotY,
|
||||
com.jme3.math.FastMath.DEG_TO_RAD * gripRotZ);
|
||||
group.setLocalRotation(rot);
|
||||
|
||||
float markerSize = shieldMode ? 0.018f : 0.015f;
|
||||
Geometry marker = new Geometry("gMarker", new Box(markerSize, markerSize, markerSize));
|
||||
marker.setMaterial(mat);
|
||||
|
||||
// Schaft entlang +Z
|
||||
Geometry shaft = new Geometry("gShaft", new Cylinder(4, 8, shaftRad, shaftLen, true));
|
||||
shaft.setMaterial(mat);
|
||||
shaft.setLocalTranslation(0f, 0f, shaftLen * 0.5f);
|
||||
|
||||
// Kegelspitze: Dome zeigt per Default nach +Y → 90° um X → zeigt nach +Z
|
||||
Geometry head = new Geometry("gHead", new Dome(Vector3f.ZERO, 2, 8, headRad, false));
|
||||
head.setMaterial(matHead);
|
||||
head.setLocalTranslation(0f, 0f, shaftLen);
|
||||
Quaternion headRot = new Quaternion();
|
||||
headRot.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X);
|
||||
head.setLocalRotation(headRot);
|
||||
|
||||
// Querachsen-Zylinder entlang +Y: zeigt Roll-/Orientierungsachse
|
||||
Material matAxis = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
matAxis.setColor("Color", axisColor);
|
||||
float axisLen = shieldMode ? 0.08f : 0.06f;
|
||||
Geometry gripAxis = new Geometry("gGripAxis", new Cylinder(4, 8, 0.006f, axisLen, true));
|
||||
|
||||
Node group = new Node(name);
|
||||
group.setLocalTranslation(posX, posY, posZ);
|
||||
if (rotXDeg != 0 || rotYDeg != 0 || rotZDeg != 0) {
|
||||
Quaternion rot = new Quaternion();
|
||||
rot.fromAngles(FastMath.DEG_TO_RAD * rotXDeg, FastMath.DEG_TO_RAD * rotYDeg, FastMath.DEG_TO_RAD * rotZDeg);
|
||||
group.setLocalRotation(rot);
|
||||
}
|
||||
|
||||
Geometry marker = new Geometry(name + "M", new Box(markerSz, markerSz, markerSz));
|
||||
marker.setMaterial(mat);
|
||||
|
||||
Geometry shaft = new Geometry(name + "S", new Cylinder(4, 8, shaftRad, shaftLen, true));
|
||||
shaft.setMaterial(mat);
|
||||
shaft.setLocalTranslation(0f, 0f, bladeDirZ * shaftLen * 0.5f);
|
||||
|
||||
Geometry head = new Geometry(name + "H", new Dome(Vector3f.ZERO, 2, 8, headRad, false));
|
||||
head.setMaterial(matHead);
|
||||
head.setLocalTranslation(0f, 0f, bladeDirZ * shaftLen);
|
||||
Quaternion headRot = new Quaternion();
|
||||
headRot.fromAngleAxis(bladeDirZ * FastMath.HALF_PI, Vector3f.UNIT_X);
|
||||
head.setLocalRotation(headRot);
|
||||
|
||||
// Orientierungsstab +Y (oben): JME-Cylinder liegt auf Z, Rx(-90°) → +Y
|
||||
Geometry gripAxis = new Geometry(name + "A", new Cylinder(4, 8, 0.006f, axisLen, true));
|
||||
gripAxis.setMaterial(matAxis);
|
||||
Quaternion axisRot = new Quaternion();
|
||||
axisRot.fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X);
|
||||
@@ -1603,8 +2006,7 @@ public class ModelEditorState extends BaseAppState {
|
||||
group.attachChild(shaft);
|
||||
group.attachChild(gripAxis);
|
||||
group.attachChild(head);
|
||||
gripGizmoNode.attachChild(group);
|
||||
gripGizmoNode.setCullHint(Spatial.CullHint.Inherit);
|
||||
return group;
|
||||
}
|
||||
|
||||
/** Blendet den Grip-Gizmo ein oder aus. */
|
||||
@@ -1677,4 +2079,305 @@ public class ModelEditorState extends BaseAppState {
|
||||
float dz = -cosEle * (float) Math.cos(azi);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -997,6 +997,11 @@ public class AnimSetController implements ToolController {
|
||||
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<>();
|
||||
actionCombo.getItems().addAll(available);
|
||||
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.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());
|
||||
for (String subName : editableSubClips.keySet()) {
|
||||
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.setMaxWidth(Double.MAX_VALUE);
|
||||
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();
|
||||
grid.setHgap(8); grid.setVgap(6);
|
||||
grid.add(new Label("Aktion:"), 0, 0); grid.add(actionCombo, 1, 0);
|
||||
grid.add(new Label("Clip:"), 0, 1); grid.add(clipCombo, 1, 1);
|
||||
grid.setHgap(8); grid.setVgap(8);
|
||||
grid.add(new Label("Aktion:"), 0, 0); grid.add(actionBox, 1, 0);
|
||||
grid.add(new Label("Clip:"), 0, 1); grid.add(clipBox, 1, 1);
|
||||
javafx.scene.layout.ColumnConstraints cc = new javafx.scene.layout.ColumnConstraints();
|
||||
cc.setHgrow(Priority.ALWAYS);
|
||||
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<>();
|
||||
dlg.initOwner(ctx.stageSupplier().get());
|
||||
dlg.setTitle("Aktion zuordnen");
|
||||
ButtonType ok = new ButtonType("Hinzufügen", ButtonBar.ButtonData.OK_DONE);
|
||||
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 -> {
|
||||
if (bt != ok) return;
|
||||
var action = actionCombo.getValue();
|
||||
|
||||
@@ -646,6 +646,12 @@ public class AssetTreeController {
|
||||
tt.play();
|
||||
}
|
||||
|
||||
public void closeOverlayImmediate() {
|
||||
assetOverlayOpen = false;
|
||||
assetTabBtn.setText("›");
|
||||
assetPanel.setTranslateX(-420);
|
||||
}
|
||||
|
||||
// ── Asset-Tree-Cell (DnD + Kontextmenü) ──────────────────────────────────
|
||||
|
||||
private TreeCell<String> buildAssetCell() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -88,11 +88,15 @@ public class ContentController {
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private Runnable onEnterContentMode;
|
||||
private GripEditorController gripEditorController;
|
||||
private GripEditorController gripEditorController;
|
||||
private AttachmentEditorController attachmentEditorController;
|
||||
private ShieldAttachmentEditorController shieldAttachmentEditorController;
|
||||
|
||||
public ContentController(ToolContext 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 ────────────────────────────────────────────────────────
|
||||
@@ -113,6 +117,14 @@ public class ContentController {
|
||||
|
||||
/** Per-Frame-Polling: animEmbedStatus + animStripStatus → charEditorStatusLabel. */
|
||||
public void onFrame() {
|
||||
float guessZ = ctx.input().gripEditorGuessPosZ;
|
||||
if (!Float.isNaN(guessZ)) {
|
||||
float guessX = ctx.input().gripEditorGuessPosX;
|
||||
float guessY = ctx.input().gripEditorGuessPosY;
|
||||
ctx.input().gripEditorGuessPosZ = Float.NaN;
|
||||
gripEditorController.applyGripGuess(guessX, guessY, guessZ);
|
||||
}
|
||||
|
||||
String embedStatus = ctx.input().animEmbedStatus;
|
||||
if (embedStatus != null) {
|
||||
ctx.input().animEmbedStatus = null;
|
||||
@@ -349,24 +361,25 @@ public class ContentController {
|
||||
showContentEditor();
|
||||
contentLastSection = "item";
|
||||
if (contentItemBtn != null) contentItemBtn.setSelected(true);
|
||||
de.blight.editor.ui.ItemEditorView view =
|
||||
new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
|
||||
wireItemView(view);
|
||||
setContentCenter(view);
|
||||
activeItemView = new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
|
||||
wireItemView(activeItemView);
|
||||
setContentCenter(activeItemView);
|
||||
setContentRight(null);
|
||||
if (selectItemId != null) view.selectItem(selectItemId);
|
||||
if (selectItemId != null) activeItemView.selectItem(selectItemId);
|
||||
return;
|
||||
}
|
||||
ctx.setCurrentTool().accept("itemEditor");
|
||||
ToolBar tb = makeSingleWindowToolBar("Item-Manager");
|
||||
ctx.setMainToolbar().accept(tb);
|
||||
ctx.setLeftPanel().accept(buildContentTreePanel());
|
||||
de.blight.editor.ui.ItemEditorView view =
|
||||
new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
|
||||
wireItemView(view);
|
||||
ctx.setCenterView().accept(view);
|
||||
activeItemView = new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
|
||||
wireItemView(activeItemView);
|
||||
ctx.setCenterView().accept(activeItemView);
|
||||
ctx.setRightPanel().accept(null);
|
||||
if (selectItemId != null) view.selectItem(selectItemId);
|
||||
if (selectItemId != null) {
|
||||
activeItemView.selectItem(selectItemId);
|
||||
selectItemInTree(selectItemId);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
view.setOnIdChanged(this::refreshContentItemTree);
|
||||
view.setGripEditorLauncher((modelPath, currentGrip, isShield, onSave) -> {
|
||||
view.setGripEditorLauncher((modelPath, currentGrip, isShield, subCategory, onSave) -> {
|
||||
String itemId = view.getCurrentItemId();
|
||||
gripEditorController.setOnReturn(() -> {
|
||||
if (dualWindowMode) {
|
||||
@@ -999,7 +1012,31 @@ public class ContentController {
|
||||
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() {
|
||||
if (contentFractionNode == null) return;
|
||||
contentTreeUpdating = true;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.blight.editor.toolset.content;
|
||||
|
||||
import de.blight.common.model.ItemGrip;
|
||||
import de.blight.common.model.ItemSubCategory;
|
||||
import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.ToolContext;
|
||||
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> rotXSpin, rotYSpin, rotZSpin;
|
||||
private Spinner<Double> grip2XSpin, grip2YSpin, grip2ZSpin;
|
||||
private Spinner<Double> handOffXSpin, handOffYSpin, handOffZSpin;
|
||||
|
||||
private Consumer<ItemGrip> pendingOnSave;
|
||||
private Runnable onReturn;
|
||||
private boolean suppressSpinnerEvents = false;
|
||||
private boolean shieldMode = false;
|
||||
private ItemSubCategory itemSubCategory = null;
|
||||
|
||||
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) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
@@ -38,22 +67,35 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
// ── GripEditorLauncher ────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void open(String modelPath, ItemGrip currentGrip, boolean isShield, Consumer<ItemGrip> onSave) {
|
||||
pendingOnSave = onSave;
|
||||
shieldMode = isShield;
|
||||
public void open(String modelPath, ItemGrip currentGrip, boolean isShield,
|
||||
ItemSubCategory subCategory, Consumer<ItemGrip> onSave) {
|
||||
pendingOnSave = onSave;
|
||||
shieldMode = isShield;
|
||||
itemSubCategory = subCategory;
|
||||
|
||||
SharedInput input = ctx.input();
|
||||
input.gripEditorActive = true;
|
||||
input.gripEditorIsShield = isShield;
|
||||
input.gripEditorActive = true;
|
||||
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)
|
||||
// Spinner/Gizmo zeigen den Handflächen-Kontaktpunkt (= JSON-pos minus HAND_GRIP_OFFSET)
|
||||
ItemGrip g = currentGrip != null ? currentGrip : new ItemGrip();
|
||||
input.gripEditorPosX = g.getPosX();
|
||||
input.gripEditorPosY = g.getPosY();
|
||||
input.gripEditorPosZ = g.getPosZ();
|
||||
input.gripEditorRotX = g.getRotX();
|
||||
input.gripEditorRotY = g.getRotY();
|
||||
input.gripEditorRotZ = g.getRotZ();
|
||||
input.gripEditorPosX = g.getPosX() - ItemGrip.HAND_GRIP_OFFSET_X;
|
||||
input.gripEditorPosY = g.getPosY() - ItemGrip.HAND_GRIP_OFFSET_Y;
|
||||
input.gripEditorPosZ = g.getPosZ() - ItemGrip.HAND_GRIP_OFFSET_Z;
|
||||
input.gripEditorRotX = g.getRotX();
|
||||
input.gripEditorRotY = g.getRotY();
|
||||
input.gripEditorRotZ = g.getRotZ();
|
||||
input.gripEditorGrip2PosX = g.getGrip2PosX();
|
||||
input.gripEditorGrip2PosY = g.getGrip2PosY();
|
||||
input.gripEditorGrip2PosZ = g.getGrip2PosZ();
|
||||
input.gripEditorHandOffX = g.getHandOffsetX();
|
||||
input.gripEditorHandOffY = g.getHandOffsetY();
|
||||
input.gripEditorHandOffZ = g.getHandOffsetZ();
|
||||
input.gripEditorGizmoDirty = true;
|
||||
|
||||
// Waffenmodell im Modell-Editor laden
|
||||
@@ -61,12 +103,30 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
input.modelEditorOpenPath = modelPath;
|
||||
}
|
||||
input.activeLayer = SharedInput.LAYER_MODEL_EDITOR;
|
||||
// JME vollständig aktivieren, damit LightProbe-Bake (8 Frames) nicht gedrosselt läuft
|
||||
if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(false);
|
||||
|
||||
// Hauptfenster: JME-Viewport + Grip-Panel
|
||||
ctx.setCenterView().accept(ctx.viewportSupplier().get());
|
||||
ctx.setCurrentTool().accept("gripEditor");
|
||||
ctx.setMainToolbar().accept(buildToolbar());
|
||||
ctx.setRightPanel().accept(buildPanel(g, isShield));
|
||||
|
||||
VBox innerPanel = buildPanel(g, isShield);
|
||||
ScrollPane scroll = new ScrollPane(innerPanel);
|
||||
scroll.setFitToWidth(true);
|
||||
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scroll.setStyle("-fx-background-color:transparent;-fx-background:transparent;");
|
||||
scroll.skinProperty().addListener((obs, o, n) -> {
|
||||
javafx.scene.Node vp = scroll.lookup(".viewport");
|
||||
if (vp != null) vp.setStyle("-fx-background-color: #252535;");
|
||||
});
|
||||
VBox outerPanel = new VBox(scroll);
|
||||
VBox.setVgrow(scroll, Priority.ALWAYS);
|
||||
outerPanel.setPrefWidth(220);
|
||||
outerPanel.setMaxWidth(220);
|
||||
outerPanel.setStyle("-fx-background-color: #252535;");
|
||||
ctx.setRightPanel().accept(outerPanel);
|
||||
|
||||
ctx.setLeftPanel().accept(null);
|
||||
ctx.setBottomPanel().accept(null);
|
||||
}
|
||||
@@ -74,7 +134,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
// ── Toolbar ───────────────────────────────────────────────────────────────
|
||||
|
||||
private ToolBar buildToolbar() {
|
||||
Button cancelBtn = new Button("← Zurück");
|
||||
Button cancelBtn = new Button("<- Zurück");
|
||||
cancelBtn.setOnAction(e -> onCancel());
|
||||
Label title = new Label("Griff-Editor");
|
||||
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||
@@ -86,12 +146,18 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
// ── Panel ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private VBox buildPanel(ItemGrip g, boolean isShield) {
|
||||
posXSpin = doubleSpin(g.getPosX(), -2.0, 2.0, 0.01, "m");
|
||||
posYSpin = doubleSpin(g.getPosY(), -2.0, 2.0, 0.01, "m");
|
||||
posZSpin = doubleSpin(g.getPosZ(), -2.0, 2.0, 0.01, "m");
|
||||
rotXSpin = doubleSpin(g.getRotX(), -180.0, 180.0, 1.0, "°");
|
||||
rotYSpin = doubleSpin(g.getRotY(), -180.0, 180.0, 1.0, "°");
|
||||
rotZSpin = doubleSpin(g.getRotZ(), -180.0, 180.0, 1.0, "°");
|
||||
posXSpin = doubleSpin(g.getPosX() - ItemGrip.HAND_GRIP_OFFSET_X, -2.0, 2.0, 0.01, "m");
|
||||
posYSpin = doubleSpin(g.getPosY() - ItemGrip.HAND_GRIP_OFFSET_Y, -2.0, 2.0, 0.01, "m");
|
||||
posZSpin = doubleSpin(g.getPosZ() - ItemGrip.HAND_GRIP_OFFSET_Z, -2.0, 2.0, 0.01, "m");
|
||||
rotXSpin = doubleSpin(g.getRotX(), -180.0, 180.0, 1.0, "°");
|
||||
rotYSpin = doubleSpin(g.getRotY(), -180.0, 180.0, 1.0, "°");
|
||||
rotZSpin = doubleSpin(g.getRotZ(), -180.0, 180.0, 1.0, "°");
|
||||
grip2XSpin = doubleSpin(g.getGrip2PosX(), -2.0, 2.0, 0.01, "m");
|
||||
grip2YSpin = doubleSpin(g.getGrip2PosY(), -2.0, 2.0, 0.01, "m");
|
||||
grip2ZSpin = doubleSpin(g.getGrip2PosZ(), -2.0, 2.0, 0.01, "m");
|
||||
handOffXSpin = doubleSpin(g.getHandOffsetX(), -1.0, 1.0, 0.01, "m");
|
||||
handOffYSpin = doubleSpin(g.getHandOffsetY(), -1.0, 1.0, 0.01, "m");
|
||||
handOffZSpin = doubleSpin(g.getHandOffsetZ(), -1.0, 1.0, 0.01, "m");
|
||||
|
||||
posXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
|
||||
posYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
|
||||
@@ -99,12 +165,48 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
rotXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
|
||||
rotYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
|
||||
rotZSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
|
||||
grip2XSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
|
||||
grip2YSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
|
||||
grip2ZSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
|
||||
handOffXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
|
||||
handOffYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
|
||||
handOffZSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
|
||||
|
||||
Button saveBtn = new Button("Speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-background-color: #2a5a2a; -fx-text-fill: #aaffaa; -fx-cursor: hand; -fx-font-weight: bold;");
|
||||
saveBtn.setOnAction(e -> onSave());
|
||||
|
||||
Button guessBtn = new Button("Griff raten");
|
||||
guessBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
guessBtn.setStyle("-fx-background-color: #1a3a5a; -fx-text-fill: #88ccff; -fx-cursor: hand;");
|
||||
guessBtn.setTooltip(new javafx.scene.control.Tooltip(
|
||||
"Erkennt die Parierstange im Modell und setzt\n" +
|
||||
"den Griff-Kontaktpunkt an diese Position.\n" +
|
||||
"Voraussetzung: Klinge liegt auf der Z-Achse."));
|
||||
guessBtn.setOnAction(e -> ctx.input().gripEditorGuessRequest = true);
|
||||
|
||||
Button resetBtn = new Button("Griff zurücksetzen");
|
||||
resetBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
resetBtn.setStyle("-fx-background-color: #5a3a1a; -fx-text-fill: #ffcc88; -fx-cursor: hand;");
|
||||
resetBtn.setOnAction(e -> {
|
||||
suppressSpinnerEvents = true;
|
||||
posXSpin.getValueFactory().setValue(0.0);
|
||||
posYSpin.getValueFactory().setValue(0.0);
|
||||
posZSpin.getValueFactory().setValue(0.0);
|
||||
rotXSpin.getValueFactory().setValue(0.0);
|
||||
rotYSpin.getValueFactory().setValue(0.0);
|
||||
rotZSpin.getValueFactory().setValue(0.0);
|
||||
grip2XSpin.getValueFactory().setValue(0.0);
|
||||
grip2YSpin.getValueFactory().setValue(0.0);
|
||||
grip2ZSpin.getValueFactory().setValue(0.0);
|
||||
handOffXSpin.getValueFactory().setValue(0.0);
|
||||
handOffYSpin.getValueFactory().setValue(0.0);
|
||||
handOffZSpin.getValueFactory().setValue(0.0);
|
||||
suppressSpinnerEvents = false;
|
||||
onSpinnerChanged();
|
||||
});
|
||||
|
||||
Button cancelBtn = new Button("Abbrechen");
|
||||
cancelBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
cancelBtn.setStyle("-fx-background-color: #3a3a3a; -fx-text-fill: #cccccc; -fx-cursor: hand;");
|
||||
@@ -116,21 +218,37 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
panel.setPrefWidth(220);
|
||||
|
||||
panel.getChildren().addAll(
|
||||
styledTitle("Position (m)"),
|
||||
styledTitle("1. Griff – Position (m)"),
|
||||
styledHint("Handflächen-Kontaktpunkt im Modellraum (oranges Gizmo)."),
|
||||
row("X:", posXSpin),
|
||||
row("Y:", posYSpin),
|
||||
row("Z:", posZSpin),
|
||||
new Separator(),
|
||||
styledTitle("Rotation (°)"),
|
||||
styledTitle("Rotation Hand (°)"),
|
||||
styledHint("Dreht den gesamten Griff-Gizmo. 0° = Hand oben (+Y), Klinge in -Z."),
|
||||
row("X:", rotXSpin),
|
||||
row("Y:", rotYSpin),
|
||||
row("Z:", rotZSpin),
|
||||
new Separator(),
|
||||
styledTitle("2. Griff – Linke Hand (m)"),
|
||||
styledHint("Zweiter Griffpunkt (blaues Gizmo) für Zweihänder.\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
|
||||
? "Pfeil zeigt vom Ellbogen zur Hand.\n28cm = Unterarmlänge."
|
||||
: "Pfeil zeigt Klingenrichtung.\n15cm Länge = Maßstab."),
|
||||
? "Cyan-Pfeil zeigt vom Ellbogen zur Hand.\n28cm = Unterarmlänge."
|
||||
: "Orange-Pfeil zeigt zur Klinge, gelber Stab nach oben (+Y).\nBlauer Pfeil = 2. Griff (linke Hand)."),
|
||||
new Separator(),
|
||||
saveBtn,
|
||||
guessBtn,
|
||||
resetBtn,
|
||||
cancelBtn
|
||||
);
|
||||
return panel;
|
||||
@@ -141,12 +259,18 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
private void onSpinnerChanged() {
|
||||
if (suppressSpinnerEvents) return;
|
||||
SharedInput input = ctx.input();
|
||||
input.gripEditorPosX = posXSpin.getValue().floatValue();
|
||||
input.gripEditorPosY = posYSpin.getValue().floatValue();
|
||||
input.gripEditorPosZ = posZSpin.getValue().floatValue();
|
||||
input.gripEditorRotX = rotXSpin.getValue().floatValue();
|
||||
input.gripEditorRotY = rotYSpin.getValue().floatValue();
|
||||
input.gripEditorRotZ = rotZSpin.getValue().floatValue();
|
||||
input.gripEditorPosX = posXSpin.getValue().floatValue();
|
||||
input.gripEditorPosY = posYSpin.getValue().floatValue();
|
||||
input.gripEditorPosZ = posZSpin.getValue().floatValue();
|
||||
input.gripEditorRotX = rotXSpin.getValue().floatValue();
|
||||
input.gripEditorRotY = rotYSpin.getValue().floatValue();
|
||||
input.gripEditorRotZ = rotZSpin.getValue().floatValue();
|
||||
input.gripEditorGrip2PosX = grip2XSpin.getValue().floatValue();
|
||||
input.gripEditorGrip2PosY = grip2YSpin.getValue().floatValue();
|
||||
input.gripEditorGrip2PosZ = grip2ZSpin.getValue().floatValue();
|
||||
input.gripEditorHandOffX = handOffXSpin.getValue().floatValue();
|
||||
input.gripEditorHandOffY = handOffYSpin.getValue().floatValue();
|
||||
input.gripEditorHandOffZ = handOffZSpin.getValue().floatValue();
|
||||
input.gripEditorGizmoDirty = true;
|
||||
}
|
||||
|
||||
@@ -154,16 +278,24 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
|
||||
private void onSave() {
|
||||
ItemGrip grip = new ItemGrip();
|
||||
grip.setPosX(posXSpin.getValue().floatValue());
|
||||
grip.setPosY(posYSpin.getValue().floatValue());
|
||||
grip.setPosZ(posZSpin.getValue().floatValue());
|
||||
// Spinner zeigen Handflächen-Position → HAND_GRIP_OFFSET wieder addieren für JSON
|
||||
grip.setPosX(posXSpin.getValue().floatValue() + ItemGrip.HAND_GRIP_OFFSET_X);
|
||||
grip.setPosY(posYSpin.getValue().floatValue() + ItemGrip.HAND_GRIP_OFFSET_Y);
|
||||
grip.setPosZ(posZSpin.getValue().floatValue() + ItemGrip.HAND_GRIP_OFFSET_Z);
|
||||
grip.setRotX(rotXSpin.getValue().floatValue());
|
||||
grip.setRotY(rotYSpin.getValue().floatValue());
|
||||
grip.setRotZ(rotZSpin.getValue().floatValue());
|
||||
grip.setGrip2PosX(grip2XSpin.getValue().floatValue());
|
||||
grip.setGrip2PosY(grip2YSpin.getValue().floatValue());
|
||||
grip.setGrip2PosZ(grip2ZSpin.getValue().floatValue());
|
||||
grip.setHandOffsetX(handOffXSpin.getValue().floatValue());
|
||||
grip.setHandOffsetY(handOffYSpin.getValue().floatValue());
|
||||
grip.setHandOffsetZ(handOffZSpin.getValue().floatValue());
|
||||
|
||||
SharedInput input = ctx.input();
|
||||
input.gripEditorActive = false;
|
||||
input.gripEditorIsShield = false;
|
||||
input.gripEditorActive = false;
|
||||
input.gripEditorIsShield = false;
|
||||
input.gripEditorCharPreview = false;
|
||||
|
||||
Consumer<ItemGrip> save = pendingOnSave;
|
||||
Runnable ret = onReturn;
|
||||
@@ -175,8 +307,10 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
}
|
||||
|
||||
private void onCancel() {
|
||||
ctx.input().gripEditorActive = false;
|
||||
ctx.input().gripEditorIsShield = false;
|
||||
SharedInput input = ctx.input();
|
||||
input.gripEditorActive = false;
|
||||
input.gripEditorIsShield = false;
|
||||
input.gripEditorCharPreview = false;
|
||||
|
||||
Runnable ret = onReturn;
|
||||
pendingOnSave = null;
|
||||
@@ -191,7 +325,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
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("%.2f", v) + " " + unit; }
|
||||
@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(); }
|
||||
@@ -218,7 +352,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
|
||||
private static HBox row(String labelText, Spinner<Double> spinner) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(20);
|
||||
lbl.setMinWidth(30);
|
||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||
HBox.setHgrow(spinner, Priority.ALWAYS);
|
||||
HBox box = new HBox(6, lbl, spinner);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -247,6 +247,7 @@ public class GameController {
|
||||
Path runDir = ProjectRoot.PATH.resolve("run");
|
||||
Path sessionDir = runDir.resolve("session-" + System.currentTimeMillis());
|
||||
copyDirectory(srcMapDir, sessionDir);
|
||||
in.activeSessionDir = sessionDir;
|
||||
|
||||
try {
|
||||
java.util.List<Path> sessions;
|
||||
@@ -310,6 +311,7 @@ public class GameController {
|
||||
}
|
||||
}
|
||||
consoleBuffer.offer("--- Spiel beendet ---");
|
||||
in.activeSessionDir = null;
|
||||
Platform.runLater(() -> {
|
||||
if (primary != null) primary.setIconified(false);
|
||||
if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); }
|
||||
|
||||
@@ -98,6 +98,7 @@ public class ModelEditorController implements ToolController {
|
||||
|
||||
modelEditorCurrentPath = relPath;
|
||||
ctx.input().activeLayer = SharedInput.LAYER_MODEL_EDITOR;
|
||||
if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(false);
|
||||
ctx.setCenterView().accept(ctx.viewportSupplier().get());
|
||||
openAssetOverlay.run();
|
||||
|
||||
@@ -181,6 +182,13 @@ public class ModelEditorController implements ToolController {
|
||||
if (modelEditorSpinZ != null) modelEditorSpinZ.getValueFactory().setValue(bsZ);
|
||||
}
|
||||
|
||||
// Align-Weapon-Ergebnis anzeigen
|
||||
String alignResult = ctx.input().modelEditorAlignWeaponResult;
|
||||
if (alignResult != null) {
|
||||
ctx.input().modelEditorAlignWeaponResult = null;
|
||||
ctx.setStatus().accept("Ausgerichtet: " + alignResult);
|
||||
}
|
||||
|
||||
// Bounds-Aktualisierung
|
||||
if (ctx.input().modelEditorBoundsReady) {
|
||||
ctx.input().modelEditorBoundsReady = false;
|
||||
@@ -269,11 +277,8 @@ public class ModelEditorController implements ToolController {
|
||||
public void openModelEditor(String relPath, Path absolutePath) {
|
||||
this.pendingRelPath = relPath;
|
||||
this.pendingAbsPath = absolutePath;
|
||||
if (ctx.registry().getActive() == this) {
|
||||
activate();
|
||||
} else {
|
||||
ctx.registry().activate(this);
|
||||
}
|
||||
ctx.switchToWorld().run();
|
||||
ctx.registry().activate(this);
|
||||
}
|
||||
|
||||
/** Öffnet den Datei-Dialog für den Modell-Import. */
|
||||
@@ -811,7 +816,13 @@ public class ModelEditorController implements ToolController {
|
||||
closeBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
closeBtn.setStyle("-fx-background-color:#555; -fx-text-fill:#ccc;");
|
||||
|
||||
saveBtn.setOnAction(e -> saveModelMeta(
|
||||
saveBtn.setOnAction(e -> {
|
||||
if (modelEditorSpinX != null) {
|
||||
modelEditorSpinX.commitValue();
|
||||
modelEditorSpinY.commitValue();
|
||||
modelEditorSpinZ.commitValue();
|
||||
}
|
||||
saveModelMeta(
|
||||
relPath, absolutePath,
|
||||
nameTF.getText().trim(),
|
||||
catCB.getValue() != null ? catCB.getValue().trim() : "",
|
||||
@@ -843,7 +854,8 @@ public class ModelEditorController implements ToolController {
|
||||
: "",
|
||||
modelEditorCategoryCB != null
|
||||
? modelEditorCategoryCB.getValue()
|
||||
: de.blight.common.ModelCategory.NONE));
|
||||
: de.blight.common.ModelCategory.NONE);
|
||||
});
|
||||
|
||||
placeBtn.setOnAction(e -> {
|
||||
ctx.input().modelEditorCloseRequest = true;
|
||||
@@ -858,6 +870,35 @@ public class ModelEditorController implements ToolController {
|
||||
ctx.switchToWorld().run();
|
||||
});
|
||||
|
||||
// Waffe an Z-Achse ausrichten
|
||||
Label toolsTitle = new Label("Werkzeuge:");
|
||||
toolsTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;");
|
||||
Button alignWeaponBtn = new Button("Waffe an Z-Achse ausrichten");
|
||||
alignWeaponBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
alignWeaponBtn.setStyle("-fx-background-color:#3a3a5a; -fx-text-fill:#ddddff; -fx-cursor:hand;");
|
||||
alignWeaponBtn.setTooltip(new Tooltip(
|
||||
"Dreht das Modell so, dass die längste Achse auf +Z zeigt.\n" +
|
||||
"Verschiebt die Mitte der Waffe in den Ursprung (0,0,0).\n" +
|
||||
"Bäckt das Ergebnis in die Vertex-Positionen ein."));
|
||||
alignWeaponBtn.setOnAction(e -> {
|
||||
if (absolutePath == null) return;
|
||||
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
|
||||
confirm.initOwner(ctx.stageSupplier().get());
|
||||
confirm.setTitle("Waffe ausrichten");
|
||||
confirm.setHeaderText("Modell an Z-Achse ausrichten?");
|
||||
confirm.setContentText(
|
||||
"Die längste Achse der Waffe wird auf +Z ausgerichtet\n" +
|
||||
"und die Mitte in den Ursprung gesetzt.\n\n" +
|
||||
"Die Transformation wird direkt in die Vertex-\n" +
|
||||
"Positionen eingebrannt. Nicht rückgängig machbar.");
|
||||
confirm.showAndWait().ifPresent(btn -> {
|
||||
if (btn == ButtonType.OK) {
|
||||
ctx.input().modelEditorAlignWeaponPath = absolutePath;
|
||||
ctx.input().modelEditorAlignWeaponRequest = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
panel.getChildren().addAll(
|
||||
title,
|
||||
new Separator(),
|
||||
@@ -889,6 +930,8 @@ public class ModelEditorController implements ToolController {
|
||||
new Separator(),
|
||||
footstepTitle, modelEditorFootstepSurfaceCB,
|
||||
new Separator(),
|
||||
toolsTitle, alignWeaponBtn,
|
||||
new Separator(),
|
||||
saveBtn, placeBtn, closeBtn
|
||||
);
|
||||
return panel;
|
||||
@@ -972,6 +1015,7 @@ public class ModelEditorController implements ToolController {
|
||||
|
||||
ctx.setMainToolbar().accept(buildModelImportToolBar());
|
||||
ctx.input().activeLayer = SharedInput.LAYER_MODEL_EDITOR;
|
||||
if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(false);
|
||||
ctx.setCenterView().accept(ctx.viewportSupplier().get());
|
||||
openAssetOverlay.run();
|
||||
|
||||
@@ -1291,6 +1335,14 @@ public class ModelEditorController implements ToolController {
|
||||
s.setEditable(true);
|
||||
s.setMaxWidth(Double.MAX_VALUE);
|
||||
s.setStyle("-fx-background-color:#3a3a4e;");
|
||||
// Tab-Filter: feuert in der Filter-Phase, bevor FocusTraversalInputMap den Fokus verschiebt.
|
||||
// Damit ist getText() noch der getippte Wert, nicht der bereits zurückgesetzte.
|
||||
s.addEventFilter(javafx.scene.input.KeyEvent.KEY_PRESSED, ev -> {
|
||||
if (ev.getCode() == javafx.scene.input.KeyCode.TAB) s.commitValue();
|
||||
});
|
||||
s.focusWithinProperty().addListener((obs, was, now) -> {
|
||||
if (!now) s.commitValue();
|
||||
});
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,11 +56,21 @@ public class ItemEditorView extends BorderPane {
|
||||
private VBox effectsRows;
|
||||
|
||||
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 GripEditorLauncher gripEditorLauncher;
|
||||
private GripEditorLauncher gripEditorLauncher;
|
||||
private AttachmentEditorLauncher attachmentEditorLauncher;
|
||||
private ShieldAttachmentEditorLauncher shieldAttachmentEditorLauncher;
|
||||
|
||||
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 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 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.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,
|
||||
new Separator(),
|
||||
sectionTitle("Kampfwerte"),
|
||||
row("Schaden:", damageSpinner),
|
||||
gripBtn
|
||||
gripBtn,
|
||||
attachmentBtn
|
||||
);
|
||||
weaponSection.setVisible(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.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,
|
||||
new Separator(),
|
||||
sectionTitle("Schild-Griff"),
|
||||
shieldGripBtn
|
||||
shieldGripBtn,
|
||||
shieldAttachmentBtn
|
||||
);
|
||||
shieldSection.setVisible(false);
|
||||
shieldSection.setManaged(false);
|
||||
@@ -325,21 +349,34 @@ public class ItemEditorView extends BorderPane {
|
||||
private void openGripEditor() {
|
||||
if (current == null || gripEditorLauncher == null) return;
|
||||
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);
|
||||
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() {
|
||||
if (current == null || gripEditorLauncher == null) return;
|
||||
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);
|
||||
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() {
|
||||
ItemCategory cat = catCombo.getValue();
|
||||
ItemSubCategory sub = subCatCombo.getValue();
|
||||
|
||||
@@ -53,10 +53,13 @@ public enum AnimationAction {
|
||||
AXE_IDLE, AXE_WALK, AXE_RUN,
|
||||
|
||||
// ── 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_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 ─────────────────────────────────────────────────────────────────
|
||||
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_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (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_RUN -> TextRegistry.resolve(null, key, "Laufen (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_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_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Bogen)");
|
||||
|
||||
@@ -31,9 +31,8 @@ public final class SwordShieldCombat extends CombatStyleBase {
|
||||
public static AnimationAction resolveIdleImpactAction(ItemSubCategory weaponType) {
|
||||
if (weaponType == null) { return AnimationAction.SWORD_IDLE_IMPACT; }
|
||||
return switch (weaponType) {
|
||||
case AXE -> AnimationAction.AXE_IDLE_IMPACT;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE_IMPACT;
|
||||
default -> AnimationAction.SWORD_IDLE_IMPACT;
|
||||
case AXE -> AnimationAction.AXE_IDLE_IMPACT;
|
||||
default -> AnimationAction.SWORD_IDLE_IMPACT;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,9 +43,8 @@ public final class SwordShieldCombat extends CombatStyleBase {
|
||||
public static AnimationAction resolveBlockImpactAction(ItemSubCategory weaponType) {
|
||||
if (weaponType == null) { return AnimationAction.SWORD_BLOCK_IMPACT; }
|
||||
return switch (weaponType) {
|
||||
case AXE -> AnimationAction.AXE_BLOCK_IMPACT;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_BLOCK_IMPACT;
|
||||
default -> AnimationAction.SWORD_BLOCK_IMPACT;
|
||||
case AXE -> AnimationAction.AXE_BLOCK_IMPACT;
|
||||
default -> AnimationAction.SWORD_BLOCK_IMPACT;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -57,9 +55,8 @@ public final class SwordShieldCombat extends CombatStyleBase {
|
||||
public static AnimationAction resolveParadeAction(ItemSubCategory weaponType) {
|
||||
if (weaponType == null) { return AnimationAction.SWORD_PARADE; }
|
||||
return switch (weaponType) {
|
||||
case AXE -> AnimationAction.AXE_PARADE;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_PARADE;
|
||||
default -> AnimationAction.SWORD_PARADE;
|
||||
case AXE -> AnimationAction.AXE_PARADE;
|
||||
default -> AnimationAction.SWORD_PARADE;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,11 @@ import com.jme3.scene.Spatial;
|
||||
import com.jme3.scene.control.AbstractControl;
|
||||
import de.blight.common.model.Item;
|
||||
import de.blight.common.model.ItemGrip;
|
||||
import de.blight.common.model.ItemSubCategory;
|
||||
import de.blight.common.model.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 org.slf4j.Logger;
|
||||
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_PALM = "mixamorig:RightHandMiddle1";
|
||||
private static final String BONE_LEFT_FOREARM = "mixamorig:LeftHand";
|
||||
|
||||
private static final float DEG_TO_RAD = (float) Math.PI / 180f;
|
||||
private static final float HALF_PI = (float) Math.PI * 0.5f;
|
||||
|
||||
private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.13f);
|
||||
private static final Vector3f HAND_GRIP_OFFSET = new Vector3f(0.1f, 0.00f, 0.05f);
|
||||
private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.13f);
|
||||
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 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()); }
|
||||
try { leftForeArmNode = skinCtrl.getAttachmentsNode(BONE_LEFT_FOREARM); }
|
||||
catch (Exception e) { log.warn("[NpcWeaponAttach] Bone '{}' nicht gefunden: {}", BONE_LEFT_FOREARM, e.getMessage()); }
|
||||
|
||||
handBindRotInChar = extractJointBindRot(skinCtrl, BONE_HAND);
|
||||
leftForeArmBindRotInChar = extractJointBindRot(skinCtrl, BONE_LEFT_FOREARM);
|
||||
|
||||
@@ -126,7 +133,6 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
|
||||
palmOffsetLocal = computePalmOffset(handNode, palmNodeRef);
|
||||
}
|
||||
}
|
||||
|
||||
Quaternion charWorldRotInv = spatial.getWorldRotation().inverse();
|
||||
Vector3f charWorldPos = spatial.getWorldTranslation();
|
||||
|
||||
@@ -137,14 +143,27 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
|
||||
Vector3f boneInChar = charWorldRotInv.mult(boneNode.getWorldTranslation().subtract(charWorldPos));
|
||||
Quaternion boneRotInChar = charWorldRotInv.mult(boneNode.getWorldRotation());
|
||||
if (drawn) {
|
||||
ItemGrip wGrip = weaponItem != null ? weaponItem.getGrip() : null;
|
||||
boolean isTwoHand = weaponItem != null && weaponItem.getSubCategory() == ItemSubCategory.TWO_HANDED_SWORD;
|
||||
Vector3f attachInChar = (palmOffsetLocal != null)
|
||||
? boneInChar.add(boneRotInChar.mult(palmOffsetLocal.mult(0.5f)))
|
||||
: boneInChar;
|
||||
applyHandTransform(weaponSpatial, attachInChar, boneRotInChar,
|
||||
weaponItem != null ? weaponItem.getGrip() : null);
|
||||
boolean usesTwoHand = isTwoHand && leftForeArmNode != null
|
||||
&& wGrip != null
|
||||
&& (wGrip.getGrip2PosX() != 0f || wGrip.getGrip2PosY() != 0f || wGrip.getGrip2PosZ() != 0f);
|
||||
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 {
|
||||
boolean twoHanded = weaponItem != null
|
||||
&& weaponItem.getSubCategory() == ItemSubCategory.TWO_HANDED_SWORD;
|
||||
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) {
|
||||
Vector3f boneInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos));
|
||||
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);
|
||||
} else if (!drawn && backNode != null) {
|
||||
Vector3f boneInChar = charWorldRotInv.mult(backNode.getWorldTranslation().subtract(charWorldPos));
|
||||
@@ -183,7 +204,8 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
|
||||
// ── Hand-Transform (1:1 von WeaponAttachmentControl) ─────────────────────────
|
||||
|
||||
private void applyHandTransform(Spatial s, Vector3f bonePosInChar,
|
||||
Quaternion boneRotInChar, ItemGrip grip) {
|
||||
Quaternion boneRotInChar, ItemGrip grip,
|
||||
WeaponCategoryAttachment cfg) {
|
||||
Quaternion gripRot = (grip != null)
|
||||
? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(),
|
||||
DEG_TO_RAD * grip.getRotY(),
|
||||
@@ -195,36 +217,115 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
|
||||
Quaternion animDelta = (handBindRotInChar != null)
|
||||
? boneRotInChar.mult(handBindRotInChar.inverse())
|
||||
: boneRotInChar;
|
||||
Quaternion weaponRot = animDelta.mult(gripRot.inverse());
|
||||
boolean bladeAtMinusZ = grip != null && (
|
||||
(grip.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));
|
||||
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.setLocalTranslation(weaponPos);
|
||||
}
|
||||
|
||||
private void applyBackTransform(Spatial s, Vector3f boneInChar,
|
||||
Quaternion boneRotInChar, ItemGrip grip) {
|
||||
Quaternion backRot = new Quaternion().fromAngles(0f, 0f, HALF_PI);
|
||||
Quaternion weaponRot = boneRotInChar.mult(backRot);
|
||||
Vector3f target = boneInChar.add(boneRotInChar.mult(BACK_GRIP_OFFSET));
|
||||
Vector3f gripPos = grip != null
|
||||
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO;
|
||||
private void applyTwoHandTransform(Spatial s, Vector3f bonePosInChar, Quaternion boneRotInChar,
|
||||
ItemGrip grip, Quaternion charWorldRotInv, Vector3f charWorldPos,
|
||||
WeaponCategoryAttachment cfg) {
|
||||
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());
|
||||
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.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) ──────────────────────
|
||||
|
||||
private void applyShieldArmTransform(Spatial s, Vector3f bonePosInChar,
|
||||
Quaternion boneRotInChar, ItemGrip grip) {
|
||||
Quaternion boneRotInChar, ItemGrip grip,
|
||||
ShieldCategoryAttachment cfg) {
|
||||
Quaternion animDelta = (leftForeArmBindRotInChar != null)
|
||||
? boneRotInChar.mult(leftForeArmBindRotInChar.inverse())
|
||||
: 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);
|
||||
Vector3f gripPos = (grip != null)
|
||||
? 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.setLocalTranslation(anchor.subtract(shieldRot.mult(gripPos)));
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.jme3.anim.tween.Tween;
|
||||
import com.jme3.anim.tween.action.BaseAction;
|
||||
import com.jme3.bullet.control.CharacterControl;
|
||||
import com.jme3.input.InputManager;
|
||||
import com.jme3.input.KeyInput;
|
||||
import com.jme3.input.MouseInput;
|
||||
import com.jme3.input.RawInputListener;
|
||||
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.WeaponCombatState;
|
||||
import de.blight.game.combat.swordshield.SwordShieldCombat;
|
||||
import de.blight.game.combat.twohanded.TwoHandedCombat;
|
||||
import de.blight.game.navigation.CharacterNavigator;
|
||||
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_ROLL = "Roll";
|
||||
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 = {
|
||||
"Forward", "Backward", "Left", "Right", "Jump", "Sprint", "Walk",
|
||||
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;
|
||||
@@ -380,8 +385,11 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
|
||||
}
|
||||
}
|
||||
case ACT_COMBAT_ALT -> handleCombatAlt(isPressed);
|
||||
case ACT_ROLL -> { if (isPressed) triggerRollFromInput(); }
|
||||
case ACT_SNEAK -> { if (!weaponDrawn) handleSneakKey(isPressed); }
|
||||
case ACT_ROLL -> { if (isPressed) triggerRollFromInput(); }
|
||||
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;
|
||||
}
|
||||
|
||||
public Spatial getVisual() { return visual; }
|
||||
|
||||
/**
|
||||
* Setzt die Navigationsquellen (PathFinder + Terrain). Muss vor oder beim
|
||||
* 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 boolean isWeaponDrawn() { return weaponDrawn; }
|
||||
public boolean isDrawAnimActive() { return drawAnimActive; }
|
||||
public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; }
|
||||
public boolean isLastAttackCritical() { return lastAttackCritical; }
|
||||
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_ROLL, new KeyTrigger(kb.roll));
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1221,7 +1235,8 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
|
||||
} else {
|
||||
boolean swordShield = drawnWeaponType == de.blight.common.model.ItemSubCategory.SWORD
|
||||
&& hasShieldEquipped();
|
||||
if (swordShield) {
|
||||
boolean twoHand = drawnWeaponType == ItemSubCategory.TWO_HANDED_SWORD;
|
||||
if (swordShield || twoHand) {
|
||||
if (boredAnimRemaining > 0f) {
|
||||
boredAnimRemaining -= tpf;
|
||||
target = boredVariant;
|
||||
@@ -1230,9 +1245,15 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
|
||||
if (boredIdleTimer >= BORED_IDLE_DELAY) {
|
||||
boredIdleTimer = 0f;
|
||||
int pick = playerRng.nextInt(3);
|
||||
boredVariant = pick == 0 ? AnimationAction.SWORD_IDLE_BORED
|
||||
: pick == 1 ? AnimationAction.SWORD_IDLE_BORED_2
|
||||
: AnimationAction.SWORD_IDLE_BORED_3;
|
||||
if (swordShield) {
|
||||
boredVariant = pick == 0 ? AnimationAction.SWORD_IDLE_BORED
|
||||
: 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);
|
||||
target = boredVariant;
|
||||
} else {
|
||||
@@ -1254,10 +1275,11 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
|
||||
} else if (blockPhase == BlockPhase.HOLDING && !combatAnimActive) {
|
||||
// Beim Blocken: Bewegungsanimation nach Taste, Strafe hat Vorrang
|
||||
AnimationAction blockMoveAnim;
|
||||
if (right) blockMoveAnim = AnimationAction.SWORD_BLOCK_STRAFE_RIGHT;
|
||||
else if (left) blockMoveAnim = AnimationAction.SWORD_BLOCK_STRAFE_LEFT;
|
||||
else if (forward) blockMoveAnim = AnimationAction.SWORD_BLOCK_MOVE;
|
||||
else if (backward) blockMoveAnim = AnimationAction.SWORD_BLOCK_MOVE_BACKWARD;
|
||||
boolean twoHand = drawnWeaponType == ItemSubCategory.TWO_HANDED_SWORD;
|
||||
if (right) blockMoveAnim = twoHand ? AnimationAction.TWO_HAND_BLOCK_STRAFE_RIGHT : AnimationAction.SWORD_BLOCK_STRAFE_RIGHT;
|
||||
else if (left) blockMoveAnim = twoHand ? AnimationAction.TWO_HAND_BLOCK_STRAFE_LEFT : AnimationAction.SWORD_BLOCK_STRAFE_LEFT;
|
||||
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);
|
||||
|
||||
if (blockMoveAnim != currentAnim) {
|
||||
@@ -1407,6 +1429,12 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
|
||||
private void handleCombatAlt(boolean isPressed) {
|
||||
if (!weaponDrawn) return;
|
||||
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) {
|
||||
rightMouseHeld = true;
|
||||
// Sofort blocken nur wenn keine andere Animation läuft
|
||||
@@ -1674,6 +1702,25 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
|
||||
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() {
|
||||
if (!weaponDrawn) return;
|
||||
if (rollActive || pickupActive || blockingAnimActive || turnActive
|
||||
@@ -1861,10 +1908,12 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
|
||||
}
|
||||
|
||||
public static AnimationAction getMeleeBlockImpactAction(ItemSubCategory s) {
|
||||
if (s == ItemSubCategory.TWO_HANDED_SWORD) { return TwoHandedCombat.resolveBlockImpactAction(); }
|
||||
return SwordShieldCombat.resolveBlockImpactAction(s);
|
||||
}
|
||||
|
||||
public static AnimationAction getMeleeIdleImpactAction(ItemSubCategory s) {
|
||||
if (s == ItemSubCategory.TWO_HANDED_SWORD) { return TwoHandedCombat.resolveIdleImpactAction(); }
|
||||
return SwordShieldCombat.resolveIdleImpactAction(s);
|
||||
}
|
||||
|
||||
@@ -2143,7 +2192,9 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
|
||||
SWORD_WALK, AXE_WALK, TWO_HAND_WALK,
|
||||
BOW_WALK, CROSSBOW_WALK, CRYSTAL_WALK,
|
||||
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,
|
||||
SWORD_RUN, AXE_RUN, TWO_HAND_RUN,
|
||||
BOW_RUN, CROSSBOW_RUN, CRYSTAL_RUN -> "running";
|
||||
|
||||
@@ -11,7 +11,12 @@ import com.jme3.scene.Spatial;
|
||||
import com.jme3.scene.control.AbstractControl;
|
||||
import de.blight.common.model.Item;
|
||||
import de.blight.common.model.ItemGrip;
|
||||
import de.blight.common.model.ItemSubCategory;
|
||||
import de.blight.common.model.MainCharacter;
|
||||
import 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.LoggerFactory;
|
||||
|
||||
@@ -31,12 +36,14 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
private static final String BONE_HAND = "mixamorig:RightHand";
|
||||
private static final String BONE_PALM = "mixamorig:RightHandMiddle1";
|
||||
private static final String BONE_LEFT_FOREARM = "mixamorig:LeftHand";
|
||||
|
||||
private static final float DEG_TO_RAD = (float) Math.PI / 180f;
|
||||
private static final float HALF_PI = (float) Math.PI * 0.5f;
|
||||
|
||||
private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.13f);
|
||||
/** Zusatz-Versatz beim gezückten Schwert in lokalem Waffen-Space (X=entlang Klinge, Y=entlang Griff, Z=senkrecht zur Schneide). */
|
||||
private static final Vector3f HAND_GRIP_OFFSET = new Vector3f(0.1f, 0.0f, 0.05f);
|
||||
private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.13f);
|
||||
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);
|
||||
|
||||
/** 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);
|
||||
@@ -65,10 +72,11 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
// Wird im 2. Frame aus den Runtime-Node-Positionen berechnet (skalierungs-unabhängig).
|
||||
private Vector3f palmOffsetLocal;
|
||||
|
||||
|
||||
private Item lastEquippedWeapon;
|
||||
private boolean lastDrawnState = false;
|
||||
private Spatial weaponSpatial;
|
||||
private boolean gripDiagLogged = false;
|
||||
private boolean gripDiagLogged = false;
|
||||
|
||||
private Item lastEquippedShield;
|
||||
private Spatial shieldSpatial;
|
||||
@@ -126,7 +134,6 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
log.info("[WeaponAttach] palmOffsetLocal (from nodes)={}", palmOffsetLocal);
|
||||
}
|
||||
}
|
||||
|
||||
Item current = mc.getEquippedMeleeWeapon();
|
||||
Item shield = mc.getEquippedShield();
|
||||
boolean drawn = playerInput.isWeaponDrawn();
|
||||
@@ -139,7 +146,7 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
|
||||
if (weaponSpatial != null) {
|
||||
lastDrawnState = drawn;
|
||||
updateWeaponTransform(drawn, current != null ? current.getGrip() : null);
|
||||
updateWeaponTransform(drawn, current);
|
||||
}
|
||||
|
||||
if (shield != lastEquippedShield) {
|
||||
@@ -173,7 +180,8 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
|
||||
// ── Transform-Update (jedes Frame) ────────────────────────────────────────
|
||||
|
||||
private void updateWeaponTransform(boolean drawn, ItemGrip grip) {
|
||||
private void updateWeaponTransform(boolean drawn, Item item) {
|
||||
ItemGrip grip = item != null ? item.getGrip() : null;
|
||||
Node boneNode = drawn ? handNode : backNode;
|
||||
if (boneNode == null) return;
|
||||
|
||||
@@ -185,16 +193,28 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
Quaternion boneRotInChar = charWorldRotInv.mult(boneNode.getWorldRotation());
|
||||
|
||||
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)
|
||||
? boneInChar.add(boneRotInChar.mult(palmOffsetLocal.mult(0.5f)))
|
||||
: boneInChar;
|
||||
applyHandTransform(attachInChar, boneRotInChar, grip);
|
||||
boolean usesTwoHand = isTwoHand && leftForeArmNode != null
|
||||
&& grip != null
|
||||
&& (grip.getGrip2PosX() != 0f || grip.getGrip2PosY() != 0f || grip.getGrip2PosZ() != 0f)
|
||||
&& !playerInput.isDrawAnimActive();
|
||||
if (usesTwoHand) {
|
||||
applyTwoHandTransform(attachInChar, boneRotInChar, grip, charWorldRotInv, charWorldPos, cfg);
|
||||
} else {
|
||||
applyHandTransform(attachInChar, boneRotInChar, grip, cfg);
|
||||
}
|
||||
} 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)
|
||||
? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ())
|
||||
: new Quaternion();
|
||||
@@ -205,7 +225,14 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
Quaternion animDelta = (handBindRotInChar != null)
|
||||
? boneRotInChar.mult(handBindRotInChar.inverse())
|
||||
: boneRotInChar;
|
||||
Quaternion weaponRot = animDelta.mult(gripRot.inverse());
|
||||
boolean bladeAtMinusZ2 = grip != null && (
|
||||
(grip.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) {
|
||||
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));
|
||||
weaponPos = weaponPos.add(weaponRot.mult(HAND_GRIP_OFFSET));
|
||||
weaponPos = weaponPos.add(weaponRot.mult(handOff2));
|
||||
if (grip != null && (grip.getHandOffsetX() != 0f || grip.getHandOffsetY() != 0f || grip.getHandOffsetZ() != 0f)) {
|
||||
weaponPos = weaponPos.add(grip.getHandOffsetX(), grip.getHandOffsetY(), grip.getHandOffsetZ());
|
||||
}
|
||||
weaponSpatial.setLocalRotation(weaponRot);
|
||||
weaponSpatial.setLocalTranslation(weaponPos);
|
||||
}
|
||||
|
||||
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; }
|
||||
|
||||
/**
|
||||
@@ -273,15 +360,36 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
return bindMat.toRotationQuat().normalizeLocal();
|
||||
}
|
||||
|
||||
private void applyBackTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
|
||||
Quaternion backRot = new Quaternion().fromAngles(0f, 0f, HALF_PI);
|
||||
Quaternion weaponRot = boneRotInChar.mult(backRot);
|
||||
Vector3f gripTarget = bonePosInChar.add(boneRotInChar.mult(BACK_GRIP_OFFSET));
|
||||
Vector3f gripPos = (grip != null)
|
||||
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
|
||||
// backRot normalisiert: fromAngles(0,0,PI/2) * kurzschwert_gripRot
|
||||
// → Waffe wird zuerst per gripRot^-1 normalisiert, dann per backRot am Rücken ausgerichtet.
|
||||
// Ergebnis für Kurzschwert (gripRot=fromAngles(0,-90°,90°)) identisch zur alten Formel.
|
||||
private static final Quaternion BACK_ROT_NORM = new Quaternion(0f, -(float) Math.sqrt(0.5), (float) Math.sqrt(0.5), 0f);
|
||||
|
||||
private void applyBackTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip,
|
||||
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 backPos = gripTarget.subtract(weaponRot.mult(gripPos));
|
||||
weaponSpatial.setLocalRotation(weaponRot);
|
||||
weaponSpatial.setLocalTranslation(gripTarget.subtract(weaponRot.mult(gripPos)));
|
||||
weaponSpatial.setLocalTranslation(backPos);
|
||||
}
|
||||
|
||||
// ── Schild ────────────────────────────────────────────────────────────────
|
||||
@@ -311,7 +419,9 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
Vector3f boneInChar = charWorldRotInv.mult(
|
||||
leftForeArmNode.getWorldTranslation().subtract(charWorldPos));
|
||||
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);
|
||||
} else if (!drawn && backNode != null) {
|
||||
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)
|
||||
? boneRotInChar.mult(leftForeArmBindRotInChar.inverse())
|
||||
: 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);
|
||||
Vector3f gripPos = (grip != null)
|
||||
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
|
||||
: Vector3f.ZERO;
|
||||
// Ankerpunkt Richtung Ellbogen verschieben (lokale −Y des LeftHand-Bones)
|
||||
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)));
|
||||
shieldSpatial.setLocalRotation(shieldRot);
|
||||
shieldSpatial.setLocalTranslation(anchor.subtract(shieldRot.mult(gripPos)));
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ public class WorldItemsState extends BaseAppState {
|
||||
boolean doLog = hoverLogTimer >= 1f && !items.isEmpty();
|
||||
if (hoverLogTimer >= 1f) hoverLogTimer = 0f;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -261,39 +261,39 @@ public class WorldItemsState extends BaseAppState {
|
||||
|
||||
// Distanz vom Charakter (nicht von der Kamera)
|
||||
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 (doLog) log.info("[Hover] → skip: außerhalb Reichweite ({} > {})", charDist, INTERACT_RANGE);
|
||||
if (doLog) log.debug("[Hover] → skip: außerhalb Reichweite ({} > {})", charDist, INTERACT_RANGE);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Muss vor der Kamera liegen
|
||||
Vector3f camToItem = worldPos.subtract(cam.getLocation());
|
||||
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;
|
||||
}
|
||||
|
||||
// Bildschirmprojektion (von der Kamera aus)
|
||||
Vector3f screen = cam.getScreenCoordinates(worldPos);
|
||||
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);
|
||||
|
||||
if (hDiff > hMargin) {
|
||||
if (doLog) log.info("[Hover] → skip: horizontal außerhalb Mitte");
|
||||
if (doLog) log.debug("[Hover] → skip: horizontal außerhalb Mitte");
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// Verdeckungsprüfung: Strahl von Kamera zum Item
|
||||
float camDist = camToItem.length();
|
||||
if (isOccluded(camToItem.normalize(), camDist, i, doLog)) {
|
||||
if (doLog) log.info("[Hover] → skip: verdeckt");
|
||||
if (doLog) log.debug("[Hover] → skip: verdeckt");
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -312,25 +312,25 @@ public class WorldItemsState extends BaseAppState {
|
||||
CollisionResults results = new CollisionResults();
|
||||
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++) {
|
||||
CollisionResult cr = results.getCollision(j);
|
||||
float d = cr.getDistance();
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
int vidx = findVisualIndex(cr.getGeometry());
|
||||
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;
|
||||
}
|
||||
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 false;
|
||||
|
||||
@@ -55,6 +55,7 @@ import de.blight.game.combat.DamageCalculator;
|
||||
import de.blight.game.combat.MeleeAttackCallback;
|
||||
import de.blight.game.combat.WeaponCombatState;
|
||||
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.NpcAggroBehavior;
|
||||
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.timer = dur * 2f;
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
@@ -2575,15 +2589,18 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
s.paradeFromParry, playerInput.isBlocking());
|
||||
|
||||
if (isHeavy) {
|
||||
ItemSubCategory pwtH = playerInput.getDrawnWeaponType();
|
||||
if (playerInput.isBlocking()) {
|
||||
// Schwerer Angriff bricht Block — kein HP-Verlust, aber Stagger
|
||||
if (weaponSoundSystem != null) { weaponSoundSystem.play("impact"); }
|
||||
playerInput.triggerBlockImpact();
|
||||
SwordShieldCombat.applyNormalImpact(playerInput, playerInput.getDrawnWeaponType());
|
||||
if (pwtH == ItemSubCategory.TWO_HANDED_SWORD) { TwoHandedCombat.applyNormalImpact(playerInput); }
|
||||
else { SwordShieldCombat.applyNormalImpact(playerInput, pwtH); }
|
||||
} else {
|
||||
if (weaponSoundSystem != null) { weaponSoundSystem.play("impact"); }
|
||||
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;
|
||||
}
|
||||
@@ -2600,7 +2617,9 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
// Konterparade: Spieler wurde bereits per applyParriedImpact gestaggert → keine zweite Animation.
|
||||
// Block+Konter: Spieler war NICHT gestaggert → normale Impact-Animation abspielen.
|
||||
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;
|
||||
}
|
||||
@@ -2686,14 +2705,17 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
}
|
||||
|
||||
private AnimationAction resolveNpcBlockImpactAction(SpawnedNpc s) {
|
||||
if (s.combatWeaponType == ItemSubCategory.TWO_HANDED_SWORD) { return TwoHandedCombat.resolveBlockImpactAction(); }
|
||||
return SwordShieldCombat.resolveBlockImpactAction(s.combatWeaponType);
|
||||
}
|
||||
|
||||
private AnimationAction resolveNpcParadeAction(SpawnedNpc s) {
|
||||
if (s.combatWeaponType == ItemSubCategory.TWO_HANDED_SWORD) { return TwoHandedCombat.resolveParadeAction(); }
|
||||
return SwordShieldCombat.resolveParadeAction(s.combatWeaponType);
|
||||
}
|
||||
|
||||
private AnimationAction resolveNpcStaggerAction(SpawnedNpc s) {
|
||||
if (s.combatWeaponType == ItemSubCategory.TWO_HANDED_SWORD) { return TwoHandedCombat.resolveIdleImpactAction(); }
|
||||
return SwordShieldCombat.resolveIdleImpactAction(s.combatWeaponType);
|
||||
}
|
||||
|
||||
|
||||
@@ -285,3 +285,4 @@ npc.aggro.death.5=Das nächste Mal bleib, wo du hingehörst.
|
||||
npc.bandit1.name=Bandit
|
||||
npc.bjoernson.name=Bjoernson
|
||||
npc.silas.name=Silas
|
||||
rostiger_zweihaender.name=Rostiger Zweihaender
|
||||
|
||||
@@ -140,3 +140,4 @@ npc.aggro.death.5=Next time, know your place.
|
||||
npc.bandit1.name=Bandit
|
||||
npc.bjoernson.name=Bjoernson
|
||||
npc.silas.name=Silas
|
||||
rostiger_zweihaender.name=Rostiger Zweihaender
|
||||
|
||||
@@ -144,6 +144,8 @@ neues_item_1789920920760.description
|
||||
neues_item_1789920920760.name
|
||||
neues_item_1789920asdf920760.description
|
||||
neues_item_1789920asdf920760.name
|
||||
neues_item_1791316630215.description
|
||||
neues_item_1791316630215.name
|
||||
quest.blight_wuehler.description
|
||||
quest.blight_wuehler.name
|
||||
quest.blight_wuehler.success
|
||||
@@ -153,6 +155,8 @@ quest.kuehles_bier.success
|
||||
quest.magnetiterz.description
|
||||
quest.magnetiterz.name
|
||||
quest.magnetiterz.success
|
||||
rostiger_zweihaender.description
|
||||
rostiger_zweihaender.name
|
||||
rostiges_kurzschwert.description
|
||||
rostiges_kurzschwert.name
|
||||
silas.name
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# uuid itemId x y z rotX rotY rotZ
|
||||
fd832b8d-021a-4136-be0b-4ec1cb4f863c rostiges_kurzschwert -2.46281 2.00000 -2.27693 0.00000 0.00000 0.00000
|
||||
feebed68-92b8-4458-9c7c-b1b781c1eba5 einfacher_schild -4.45856 1.72895 -3.67467 -0.14178 0.64812 0.00000
|
||||
caa6cd73-4a2d-4112-9e22-b4d250ea41b0 rostiges_kurzschwert -2.46281 2.00000 -2.27693 0.00000 0.00000 0.00000
|
||||
4fa26305-9741-4c8f-856d-20242b613486 einfacher_schild -4.45856 1.72895 -3.67467 -0.14178 0.64812 0.00000
|
||||
89bd439c-b4d9-4fa9-8886-4ba51e19d244 rostiger_zweihaender 0.02061 2.00000 -10.13625 -1.58565 0.00000 0.00000
|
||||
|
||||
Reference in New Issue
Block a user