Kampfsystem - hier: Anker beim Kurzschwert, sowohl für Helden als auch NPC

This commit is contained in:
2026-10-26 12:09:37 +01:00
parent 238776cfde
commit f189e6d81f
6 changed files with 139 additions and 10 deletions

View File

@@ -1,8 +1,8 @@
{ {
"SWORD": { "SWORD": {
"handOffX": 0.095, "handOffX": 0.085,
"handOffY": 0.045, "handOffY": 0.115,
"handOffZ": 0.645, "handOffZ": -0.095,
"hand2OffX": 0.0, "hand2OffX": 0.0,
"hand2OffY": 0.0, "hand2OffY": 0.0,
"hand2OffZ": 0.0 "hand2OffZ": 0.0

View File

@@ -9,11 +9,14 @@ import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
* Lädt und speichert per-Kategorie Schild-Attachment-Konfiguration (shield_attachment.json). * 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 { public final class ShieldAttachmentIO {
@@ -21,8 +24,17 @@ public final class ShieldAttachmentIO {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final String CLASSPATH_FILE = "shield_attachment.json"; 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 final Map<String, ShieldCategoryAttachment> CONFIGS = new HashMap<>();
private static boolean loaded = false; private static boolean loaded = false;
private static Path cachedFile = null;
private static long cachedMtime = -1;
private ShieldAttachmentIO() {} private ShieldAttachmentIO() {}
@@ -77,9 +89,33 @@ public final class ShieldAttachmentIO {
} }
private static synchronized void ensureLoaded() { 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(); 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) { private static void parse(String json) {
CONFIGS.clear(); CONFIGS.clear();
JsonObject obj = JsonParser.parseString(json).getAsJsonObject(); JsonObject obj = JsonParser.parseString(json).getAsJsonObject();

View File

@@ -9,12 +9,16 @@ import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
* Lädt und speichert per-Kategorie Waffen-Attachment-Konfiguration (weapon_attachment.json). * Lädt und speichert per-Kategorie Waffen-Attachment-Konfiguration (weapon_attachment.json).
* Singleton-Map: wird beim ersten Zugriff automatisch vom Classpath geladen. * 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 { public final class WeaponAttachmentIO {
@@ -22,8 +26,19 @@ public final class WeaponAttachmentIO {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final String CLASSPATH_FILE = "weapon_attachment.json"; 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 final Map<String, WeaponCategoryAttachment> CONFIGS = new HashMap<>();
private static boolean loaded = false; private static boolean loaded = false;
private static Path cachedFile = null;
private static long cachedMtime = -1;
private WeaponAttachmentIO() {} private WeaponAttachmentIO() {}
@@ -84,9 +99,33 @@ public final class WeaponAttachmentIO {
} }
private static synchronized void ensureLoaded() { 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(); 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) { private static void parse(String json) {
CONFIGS.clear(); CONFIGS.clear();
JsonObject obj = JsonParser.parseString(json).getAsJsonObject(); JsonObject obj = JsonParser.parseString(json).getAsJsonObject();

View File

@@ -296,6 +296,13 @@ public class ModelEditorState extends BaseAppState {
updateCharPreviewWeapon(); 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) { if (input.modelEditorEmbedLodsRequest) {
input.modelEditorEmbedLodsRequest = false; input.modelEditorEmbedLodsRequest = false;
embedLodsAndSave(input.modelEditorEmbedLodsPath); embedLodsAndSave(input.modelEditorEmbedLodsPath);
@@ -2133,6 +2140,38 @@ public class ModelEditorState extends BaseAppState {
if (modelWrapper != null) modelWrapper.setCullHint(Spatial.CullHint.Inherit); if (modelWrapper != null) modelWrapper.setCullHint(Spatial.CullHint.Inherit);
if (gripGizmoNode != null) gripGizmoNode.setCullHint(Spatial.CullHint.Always); if (gripGizmoNode != null) gripGizmoNode.setCullHint(Spatial.CullHint.Always);
input.gripEditorCharPreviewReady = false; 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() { private void updateCharPreviewWeapon() {

View File

@@ -217,7 +217,10 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
Quaternion animDelta = (handBindRotInChar != null) Quaternion animDelta = (handBindRotInChar != null)
? boneRotInChar.mult(handBindRotInChar.inverse()) ? boneRotInChar.mult(handBindRotInChar.inverse())
: boneRotInChar; : boneRotInChar;
boolean bladeAtMinusZ = grip != null && grip.getGrip2PosZ() != 0f && grip.getGrip2PosZ() > grip.getPosZ(); boolean bladeAtMinusZ = grip != null && (
(grip.getGrip2PosX() != 0f || grip.getGrip2PosY() != 0f || grip.getGrip2PosZ() != 0f)
? grip.getGrip2PosZ() > grip.getPosZ()
: grip.getPosZ() > 0f);
Quaternion bladeFlip = bladeAtMinusZ Quaternion bladeFlip = bladeAtMinusZ
? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f) ? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f)
: new Quaternion(); : new Quaternion();
@@ -227,6 +230,9 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
: new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), cfg.getHandOffZ()); : new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), cfg.getHandOffZ());
Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(gripPos)); Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(gripPos));
weaponPos = weaponPos.add(weaponRot.mult(handOff)); weaponPos = weaponPos.add(weaponRot.mult(handOff));
if (grip != null && (grip.getHandOffsetX() != 0f || grip.getHandOffsetY() != 0f || grip.getHandOffsetZ() != 0f)) {
weaponPos = weaponPos.add(grip.getHandOffsetX(), grip.getHandOffsetY(), grip.getHandOffsetZ());
}
s.setLocalRotation(weaponRot); s.setLocalRotation(weaponRot);
s.setLocalTranslation(weaponPos); s.setLocalTranslation(weaponPos);
} }
@@ -288,17 +294,20 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotY(),
DEG_TO_RAD * grip.getRotZ()) DEG_TO_RAD * grip.getRotZ())
: new Quaternion(); : new Quaternion();
boolean bladeAtMinusZ2 = grip != null && grip.getGrip2PosZ() != 0f && grip.getGrip2PosZ() > grip.getPosZ(); boolean bladeAtMinusZ2 = grip != null && (
(grip.getGrip2PosX() != 0f || grip.getGrip2PosY() != 0f || grip.getGrip2PosZ() != 0f)
? grip.getGrip2PosZ() > grip.getPosZ()
: grip.getPosZ() > 0f);
Quaternion bladeFlip2 = bladeAtMinusZ2 Quaternion bladeFlip2 = bladeAtMinusZ2
? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f) ? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f)
: new Quaternion(); : new Quaternion();
Quaternion weaponRot = boneRotInChar.mult(BACK_ROT_NORM).mult(bladeFlip2).mult(gripRot.inverse()); Quaternion weaponRot = boneRotInChar.mult(BACK_ROT_NORM).mult(bladeFlip2).mult(gripRot.inverse());
Vector3f target = boneInChar.add(boneRotInChar.mult( Vector3f target = boneInChar.add(boneRotInChar.mult(
twoHanded ? TWO_HAND_BACK_GRIP_OFFSET : BACK_GRIP_OFFSET)); twoHanded ? TWO_HAND_BACK_GRIP_OFFSET : BACK_GRIP_OFFSET));
// X=0: Waffenmitte (nicht handspezifischer Griffpunkt) auf Wirbelsäule zentrieren
Vector3f gripPos = grip != null Vector3f gripPos = grip != null
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO; ? new Vector3f(0f, grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO;
Vector3f backPos = target.subtract(weaponRot.mult(gripPos)); Vector3f backPos = target.subtract(weaponRot.mult(gripPos));
if (bladeAtMinusZ2) backPos = backPos.add(0.1f, 0f, 0f);
s.setLocalRotation(weaponRot); s.setLocalRotation(weaponRot);
s.setLocalTranslation(backPos); s.setLocalTranslation(backPos);
} }

View File

@@ -1429,6 +1429,12 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver {
private void handleCombatAlt(boolean isPressed) { private void handleCombatAlt(boolean isPressed) {
if (!weaponDrawn) return; if (!weaponDrawn) return;
if (isMeleeType(drawnWeaponType)) { if (isMeleeType(drawnWeaponType)) {
// Schwert blockt per Schild – ohne Schild ist Blocken nicht möglich.
// Zweihänder/Axt blocken mit der Waffe selbst, brauchen keinen Schild.
if (drawnWeaponType == ItemSubCategory.SWORD && !hasShieldEquipped()) {
rightMouseHeld = false;
return;
}
if (isPressed) { if (isPressed) {
rightMouseHeld = true; rightMouseHeld = true;
// Sofort blocken nur wenn keine andere Animation läuft // Sofort blocken nur wenn keine andere Animation läuft