Atmosphäre-Tools, EZ-Tree-Fixes, i18n, AnimSet, Baum-Export
- blight-lang: TextResolver + EN/DE Sprachpakete (TextReference i18n) - AnimSet: Clips + ActionMap in .animset.json zusammengeführt - EZ-Tree: Branch-Parameter-Fixes (length/radius/children/force nicht senden, twist Grad→Radiant, leaves.size ×5); Ordner-ComboBox mit Auto-Refresh - Logging beim Baum-Export in allen drei Generatoren (EZ-Tree, Blight, Palme) - Atmosphäre-Tools: Emitter, Licht, Wasser, Sound-/Musikbereiche, Spiel-Starten - AnimPreviewState, RetargetingSystem, AnimationLibrary (Animations-Editor) - Terrain-Transparenz-Fix, Schatten-Fix, ThirdPersonCamera-Fix - DayNightState, WeatherState, CloudsNode, JmeConsole - MapIO v6, neue blight-common Modell-Klassen (GameCharacter, NPC, Quests…) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
98
ANIMATIONEN_BLENDER_WORKFLOW.txt
Normal file
98
ANIMATIONEN_BLENDER_WORKFLOW.txt
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
ANIMATIONEN – BLENDER RETARGETING WORKFLOW
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
WARUM DIESER WORKFLOW?
|
||||||
|
----------------------
|
||||||
|
Das Mixamo-Skelett (stand.glb etc.) und das Tripo3D-Skelett (Charakter-Modell) haben
|
||||||
|
unterschiedliche Bone-Orientierungs-Konventionen. Das Runtime-Retargeting in Java kann
|
||||||
|
diesen Mismatch nicht zuverlässig beheben. Lösung: Animationen einmalig in Blender auf
|
||||||
|
das Tripo-Skelett "umrechnen" (baken) und als neue GLBs exportieren.
|
||||||
|
|
||||||
|
Diese Arbeit ist EINMALIG pro Animations-Clip. Danach funktioniert jedes weitere
|
||||||
|
Tripo3D-Modell mit denselben Bone-Namen automatisch über AnimationLibrary.
|
||||||
|
|
||||||
|
|
||||||
|
VORAUSSETZUNGEN
|
||||||
|
---------------
|
||||||
|
- Charakter-Modell als GLB-Datei (die Quelldatei, aus der die .j3o erzeugt wurde)
|
||||||
|
- Mixamo-Animations-GLB (z.B. stand.glb, twohand_idle.glb)
|
||||||
|
- Blender 3.x oder 4.x
|
||||||
|
|
||||||
|
|
||||||
|
WORKFLOW (für jede Animation einmal wiederholen)
|
||||||
|
------------------------------------------------
|
||||||
|
|
||||||
|
Schritt 1: Beide Armatures in Blender laden
|
||||||
|
File → Import → glTF 2.0 → Charakter-Modell .glb (= Custom-Armature)
|
||||||
|
File → Import → glTF 2.0 → stand.glb (= Mixamo-Armature + Animation)
|
||||||
|
|
||||||
|
Schritt 2: Custom-Armature vorbereiten
|
||||||
|
- Custom-Armature selektieren
|
||||||
|
- Pose Mode (Ctrl+Tab)
|
||||||
|
- Alle Bones selektieren (A)
|
||||||
|
|
||||||
|
Schritt 3: Copy-Rotation-Constraints setzen
|
||||||
|
Für jeden Bone in der Custom-Armature:
|
||||||
|
Bone-Properties → Constraints → Add Bone Constraint → Copy Rotation
|
||||||
|
Target: Mixamo-Armature
|
||||||
|
Bone: entsprechender Mixamo-Bone (siehe Mapping unten)
|
||||||
|
|
||||||
|
BONE-MAPPING (Custom → Mixamo):
|
||||||
|
┌─────────────────┬─────────────────────────┐
|
||||||
|
│ Custom │ Mixamo │
|
||||||
|
├─────────────────┼─────────────────────────┤
|
||||||
|
│ Pelvis │ mixamorig:Hips │
|
||||||
|
│ Waist │ mixamorig:Spine │
|
||||||
|
│ Spine01 │ mixamorig:Spine1 │
|
||||||
|
│ Spine02 │ mixamorig:Spine2 │
|
||||||
|
│ NeckTwist01 │ mixamorig:Neck │
|
||||||
|
│ L_Clavicle │ mixamorig:LeftShoulder │
|
||||||
|
│ L_Upperarm │ mixamorig:LeftArm │
|
||||||
|
│ L_Forearm │ mixamorig:LeftForeArm │
|
||||||
|
│ L_Hand │ mixamorig:LeftHand │
|
||||||
|
│ R_Clavicle │ mixamorig:RightShoulder │
|
||||||
|
│ R_Upperarm │ mixamorig:RightArm │
|
||||||
|
│ R_Forearm │ mixamorig:RightForeArm │
|
||||||
|
│ R_Hand │ mixamorig:RightHand │
|
||||||
|
│ L_Thigh │ mixamorig:LeftUpLeg │
|
||||||
|
│ L_Calf │ mixamorig:LeftLeg │
|
||||||
|
│ L_Foot │ mixamorig:LeftFoot │
|
||||||
|
│ R_Thigh │ mixamorig:RightUpLeg │
|
||||||
|
│ R_Calf │ mixamorig:RightLeg │
|
||||||
|
│ R_Foot │ mixamorig:RightFoot │
|
||||||
|
└─────────────────┴─────────────────────────┘
|
||||||
|
|
||||||
|
Schritt 4: Animation baken
|
||||||
|
Pose → Animation → Bake Action
|
||||||
|
✓ Only Selected Bones
|
||||||
|
✓ Visual Keying
|
||||||
|
✓ Clear Constraints
|
||||||
|
Bake Data: Pose
|
||||||
|
|
||||||
|
→ Erzeugt eine neue Action auf dem Custom-Skelett.
|
||||||
|
|
||||||
|
Schritt 5: Exportieren
|
||||||
|
- Mixamo-Armature aus der Szene löschen (X → Delete)
|
||||||
|
- Custom-Armature selektieren
|
||||||
|
- File → Export → glTF 2.0
|
||||||
|
Format: GLB
|
||||||
|
Include: Selected Objects only
|
||||||
|
Animation: ✓ (Export animations)
|
||||||
|
- Speichern als: blight-assets/src/main/resources/animations/stand.glb
|
||||||
|
(überschreibt das alte, fehlerhafte GLB)
|
||||||
|
|
||||||
|
→ Workflow für jede weitere Animation (twohand_idle.glb etc.) wiederholen.
|
||||||
|
|
||||||
|
|
||||||
|
WIE ES DANACH FUNKTIONIERT
|
||||||
|
---------------------------
|
||||||
|
Die neuen GLBs haben das Tripo-Skelett mit den korrekten vorgerechneten Animationen.
|
||||||
|
AnimationLibrary lädt sie beim Start automatisch und verteilt sie per applyAllTo()
|
||||||
|
auf alle Charaktere im Spiel.
|
||||||
|
|
||||||
|
Neue Tripo3D-Charaktere (gleiche Bone-Namen) bekommen alle Animationen automatisch —
|
||||||
|
kein weiteres Blender-Mapping nötig, weil das RetargetingSystem Bind-Pose-Unterschiede
|
||||||
|
(z.B. unterschiedliche Proportionen) selbst korrigiert.
|
||||||
|
|
||||||
|
Neuer Animations-Clip von Mixamo:
|
||||||
|
→ Einmal durch diesen Workflow → GLB in animations/ ablegen → fertig.
|
||||||
@@ -3,3 +3,12 @@
|
|||||||
// geteilten Assets (MatDefs, Shaders, Textures) auf dem Classpath zu erhalten.
|
// geteilten Assets (MatDefs, Shaders, Textures) auf dem Classpath zu erhalten.
|
||||||
//
|
//
|
||||||
// Editor-spezifische Assets (Tool-Icons etc.) verbleiben in blight-editor.
|
// Editor-spezifische Assets (Tool-Icons etc.) verbleiben in blight-editor.
|
||||||
|
|
||||||
|
apply plugin: 'java'
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
java { srcDirs = [] }
|
||||||
|
resources { srcDirs = ['src/main/resources'] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
BIN
blight-assets/src/main/resources/Textures/gras/gras1.png
Normal file
BIN
blight-assets/src/main/resources/Textures/gras/gras1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
BIN
blight-assets/src/main/resources/Textures/gras/gras2.png
Normal file
BIN
blight-assets/src/main/resources/Textures/gras/gras2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 468 KiB |
BIN
blight-assets/src/main/resources/animations/running.glb
Normal file
BIN
blight-assets/src/main/resources/animations/running.glb
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/animations/walking.glb
Normal file
BIN
blight-assets/src/main/resources/animations/walking.glb
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-assets/src/main/resources/trees/oak/medium/EzBaum1.j3o
Normal file
BIN
blight-assets/src/main/resources/trees/oak/medium/EzBaum1.j3o
Normal file
Binary file not shown.
@@ -21,3 +21,10 @@ compileJava.options.encoding = 'UTF-8'
|
|||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly 'org.projectlombok:lombok:1.18.38'
|
||||||
|
annotationProcessor 'org.projectlombok:lombok:1.18.38'
|
||||||
|
implementation 'org.slf4j:slf4j-api:2.0.17'
|
||||||
|
implementation 'com.google.code.gson:gson:2.11.0'
|
||||||
|
}
|
||||||
|
|||||||
86
blight-common/src/main/java/de/blight/common/EmitterIO.java
Normal file
86
blight-common/src/main/java/de/blight/common/EmitterIO.java
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package de.blight.common;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest und schreibt platzierte Partikel-Emitter als tab-separierte Textdatei
|
||||||
|
* ({@code blight_emitters.bpe}) neben der Kartendatei.
|
||||||
|
*/
|
||||||
|
public final class EmitterIO {
|
||||||
|
|
||||||
|
private EmitterIO() {}
|
||||||
|
|
||||||
|
public static Path getPath() {
|
||||||
|
return MapIO.getMapPath().resolveSibling("blight_emitters.bpe");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void save(List<PlacedEmitter> emitters) throws IOException {
|
||||||
|
Path p = getPath();
|
||||||
|
Files.createDirectories(p.getParent());
|
||||||
|
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
||||||
|
w.write("# x\ty\tz\tactivationRadius\ttexturePath\timagesX\timagesY"
|
||||||
|
+ "\tstartR\tstartG\tstartB\tstartA"
|
||||||
|
+ "\tendR\tendG\tendB\tendA"
|
||||||
|
+ "\tstartSize\tendSize"
|
||||||
|
+ "\tvelX\tvelY\tvelZ\tvelVariation"
|
||||||
|
+ "\tgravX\tgravY\tgravZ"
|
||||||
|
+ "\tlowLife\thighLife\tmaxParticles\temitRate");
|
||||||
|
w.newLine();
|
||||||
|
for (PlacedEmitter e : emitters) {
|
||||||
|
w.write(String.format(Locale.ROOT,
|
||||||
|
"%.5f\t%.5f\t%.5f\t%.5f\t%s\t%d\t%d"
|
||||||
|
+ "\t%.4f\t%.4f\t%.4f\t%.4f"
|
||||||
|
+ "\t%.4f\t%.4f\t%.4f\t%.4f"
|
||||||
|
+ "\t%.4f\t%.4f"
|
||||||
|
+ "\t%.4f\t%.4f\t%.4f\t%.4f"
|
||||||
|
+ "\t%.4f\t%.4f\t%.4f"
|
||||||
|
+ "\t%.4f\t%.4f\t%d\t%.4f%n",
|
||||||
|
e.x(), e.y(), e.z(), e.activationRadius(),
|
||||||
|
e.texturePath(), e.imagesX(), e.imagesY(),
|
||||||
|
e.startR(), e.startG(), e.startB(), e.startA(),
|
||||||
|
e.endR(), e.endG(), e.endB(), e.endA(),
|
||||||
|
e.startSize(), e.endSize(),
|
||||||
|
e.velX(), e.velY(), e.velZ(), e.velocityVariation(),
|
||||||
|
e.gravX(), e.gravY(), e.gravZ(),
|
||||||
|
e.lowLife(), e.highLife(), e.maxParticles(), e.emitRate()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<PlacedEmitter> load() throws IOException {
|
||||||
|
Path p = getPath();
|
||||||
|
if (!Files.exists(p)) return List.of();
|
||||||
|
List<PlacedEmitter> list = new ArrayList<>();
|
||||||
|
for (String line : Files.readAllLines(p)) {
|
||||||
|
line = line.strip();
|
||||||
|
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||||
|
String[] f = line.split("\t", -1);
|
||||||
|
if (f.length < 28) continue;
|
||||||
|
try {
|
||||||
|
list.add(new PlacedEmitter(
|
||||||
|
Float.parseFloat(f[0]), // x
|
||||||
|
Float.parseFloat(f[1]), // y
|
||||||
|
Float.parseFloat(f[2]), // z
|
||||||
|
Float.parseFloat(f[3]), // activationRadius
|
||||||
|
f[4], // texturePath
|
||||||
|
Integer.parseInt(f[5]), // imagesX
|
||||||
|
Integer.parseInt(f[6]), // imagesY
|
||||||
|
Float.parseFloat(f[7]), Float.parseFloat(f[8]),
|
||||||
|
Float.parseFloat(f[9]), Float.parseFloat(f[10]), // startRGBA
|
||||||
|
Float.parseFloat(f[11]), Float.parseFloat(f[12]),
|
||||||
|
Float.parseFloat(f[13]), Float.parseFloat(f[14]), // endRGBA
|
||||||
|
Float.parseFloat(f[15]), Float.parseFloat(f[16]), // startSize, endSize
|
||||||
|
Float.parseFloat(f[17]), Float.parseFloat(f[18]),
|
||||||
|
Float.parseFloat(f[19]), Float.parseFloat(f[20]), // vel XYZ + var
|
||||||
|
Float.parseFloat(f[21]), Float.parseFloat(f[22]),
|
||||||
|
Float.parseFloat(f[23]), // grav XYZ
|
||||||
|
Float.parseFloat(f[24]), Float.parseFloat(f[25]), // lowLife, highLife
|
||||||
|
Integer.parseInt(f[26]), Float.parseFloat(f[27]) // maxParticles, emitRate
|
||||||
|
));
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
blight-common/src/main/java/de/blight/common/LightIO.java
Normal file
60
blight-common/src/main/java/de/blight/common/LightIO.java
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package de.blight.common;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest und schreibt platzierte Lichtquellen als tab-separierte Textdatei
|
||||||
|
* ({@code blight_lights.bll}) neben der Kartendatei.
|
||||||
|
*
|
||||||
|
* Spalten: x y z r g b intensity radius
|
||||||
|
*/
|
||||||
|
public final class LightIO {
|
||||||
|
|
||||||
|
private LightIO() {}
|
||||||
|
|
||||||
|
public static Path getPath() {
|
||||||
|
return MapIO.getMapPath().resolveSibling("blight_lights.bll");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void save(List<PlacedLight> lights) throws IOException {
|
||||||
|
Path p = getPath();
|
||||||
|
Files.createDirectories(p.getParent());
|
||||||
|
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
||||||
|
w.write("# x\ty\tz\tr\tg\tb\tintensity\tradius");
|
||||||
|
w.newLine();
|
||||||
|
for (PlacedLight l : lights) {
|
||||||
|
w.write(String.format(Locale.ROOT,
|
||||||
|
"%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f%n",
|
||||||
|
l.x(), l.y(), l.z(),
|
||||||
|
l.r(), l.g(), l.b(),
|
||||||
|
l.intensity(), l.radius()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<PlacedLight> load() throws IOException {
|
||||||
|
Path p = getPath();
|
||||||
|
if (!Files.exists(p)) return List.of();
|
||||||
|
List<PlacedLight> list = new ArrayList<>();
|
||||||
|
for (String line : Files.readAllLines(p)) {
|
||||||
|
line = line.strip();
|
||||||
|
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||||
|
String[] f = line.split("\t", -1);
|
||||||
|
if (f.length < 8) continue;
|
||||||
|
try {
|
||||||
|
float x = Float.parseFloat(f[0]);
|
||||||
|
float y = Float.parseFloat(f[1]);
|
||||||
|
float z = Float.parseFloat(f[2]);
|
||||||
|
float r = Float.parseFloat(f[3]);
|
||||||
|
float g = Float.parseFloat(f[4]);
|
||||||
|
float b = Float.parseFloat(f[5]);
|
||||||
|
float intensity = Float.parseFloat(f[6]);
|
||||||
|
float radius = Float.parseFloat(f[7]);
|
||||||
|
list.add(new PlacedLight(x, y, z, r, g, b, intensity, radius));
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@ package de.blight.common;
|
|||||||
* Basis-Terrain : 4097 × 4097 Vertices (= 4096 × 4096 Zellen),
|
* Basis-Terrain : 4097 × 4097 Vertices (= 4096 × 4096 Zellen),
|
||||||
* 8 Welteinheiten pro Zelle → Welt −2048 .. +2048.
|
* 8 Welteinheiten pro Zelle → Welt −2048 .. +2048.
|
||||||
* Obere Schicht : 513 × 513 Vertices (= 512 × 512 Zellen), gleiche Weltausdehnung.
|
* Obere Schicht : 513 × 513 Vertices (= 512 × 512 Zellen), gleiche Weltausdehnung.
|
||||||
* Splatmap : 513 × 513 Pixel (passt auf Spiel-Terrain 1:1).
|
* Splatmap : 513 × 513 Pixel (1:1 zu beiden Terrain-Grids).
|
||||||
* Kanäle R/G/B = Gewicht für Tex2/Tex3/Tex4; Tex1 füllt den Rest.
|
* Kanäle R/G/B/A = Gewicht für Tex1-Helligkeit / Tex2 / Tex3 / Tex4.
|
||||||
*/
|
*/
|
||||||
public final class MapData {
|
public final class MapData {
|
||||||
|
|
||||||
@@ -29,6 +29,9 @@ public final class MapData {
|
|||||||
/** Pixel pro Achse der Splatmap (entspricht UPPER_VERTS = Spiel-Terrain-Auflösung). */
|
/** Pixel pro Achse der Splatmap (entspricht UPPER_VERTS = Spiel-Terrain-Auflösung). */
|
||||||
public static final int SPLAT_SIZE = 513;
|
public static final int SPLAT_SIZE = 513;
|
||||||
|
|
||||||
|
/** Anzahl konfigurierbarer Textur-Slots pro Terrain-Layer. */
|
||||||
|
public static final int TEXTURE_SLOTS = 4;
|
||||||
|
|
||||||
// ── Daten ─────────────────────────────────────────────────────────────────
|
// ── Daten ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Y-Höhe jedes Vertex im Basis-Terrain [TERRAIN_VERTS²]. */
|
/** Y-Höhe jedes Vertex im Basis-Terrain [TERRAIN_VERTS²]. */
|
||||||
@@ -40,29 +43,55 @@ public final class MapData {
|
|||||||
/** Y der Höhlendecke [UPPER_VERTS²]. */
|
/** Y der Höhlendecke [UPPER_VERTS²]. */
|
||||||
public final float[] upperBottom;
|
public final float[] upperBottom;
|
||||||
|
|
||||||
/** 1 = Loch (offen), 0 = massiv [UPPER_CELLS²]. */
|
|
||||||
public final byte[] upperHole;
|
|
||||||
|
|
||||||
/** Splatmap Rot-Kanal: Tex1-Helligkeit (Alpha.R), immer 255 [SPLAT_SIZE²]. */
|
/** Splatmap Rot-Kanal: Tex1-Helligkeit (Alpha.R), immer 255 [SPLAT_SIZE²]. */
|
||||||
public final byte[] splatR;
|
public final byte[] splatR;
|
||||||
|
/** Splatmap Grün-Kanal: Tex2-Blend (Alpha.G) [SPLAT_SIZE²]. */
|
||||||
/** Splatmap Grün-Kanal: Tex2 (Fels) mix-Faktor (Alpha.G) [SPLAT_SIZE²], Bytes 0–255. */
|
|
||||||
public final byte[] splatG;
|
public final byte[] splatG;
|
||||||
|
/** Splatmap Blau-Kanal: Tex3-Blend (Alpha.B) [SPLAT_SIZE²]. */
|
||||||
/** Splatmap Blau-Kanal: Tex3 (Erde) mix-Faktor (Alpha.B) [SPLAT_SIZE²], Bytes 0–255. */
|
|
||||||
public final byte[] splatB;
|
public final byte[] splatB;
|
||||||
|
/** Splatmap Alpha-Kanal: Tex4-Blend (Alpha.A) [SPLAT_SIZE²]. */
|
||||||
|
public final byte[] splatA;
|
||||||
|
|
||||||
|
/** Texturpfade für Basis-Terrain (4 Slots, "" = Standard-Textur). */
|
||||||
|
public final String[] terrainTextures = new String[]{"", "", "", ""};
|
||||||
|
|
||||||
|
/** Splatmap Rot-Kanal Gebirge: Tex1-Helligkeit, immer 255 [SPLAT_SIZE²]. */
|
||||||
|
public final byte[] upperSplatR;
|
||||||
|
/** Splatmap Grün-Kanal Gebirge: Tex2-Blend [SPLAT_SIZE²]. */
|
||||||
|
public final byte[] upperSplatG;
|
||||||
|
/** Splatmap Blau-Kanal Gebirge: Tex3-Blend [SPLAT_SIZE²]. */
|
||||||
|
public final byte[] upperSplatB;
|
||||||
|
/** Splatmap Alpha-Kanal Gebirge: Tex4-Blend [SPLAT_SIZE²]. */
|
||||||
|
public final byte[] upperSplatA;
|
||||||
|
|
||||||
|
/** Texturpfade für Gebirge (4 Slots, "" = Standard-Textur). */
|
||||||
|
public final String[] upperTextures = new String[]{"", "", "", ""};
|
||||||
|
|
||||||
/** Gras-Dichte [SPLAT_SIZE²], Bytes 0–255 (0=kein Gras, 255=max Dichte). */
|
/** Gras-Dichte [SPLAT_SIZE²], Bytes 0–255 (0=kein Gras, 255=max Dichte). */
|
||||||
public final byte[] grassDensity;
|
public final byte[] grassDensity;
|
||||||
|
|
||||||
|
/** Loch-Maske der oberen Schicht [UPPER_CELLS²], != 0 = Loch (Zelle ausgeblendet). */
|
||||||
|
public final byte[] upperHole;
|
||||||
|
|
||||||
|
/** Spawnpunkt X-Koordinate in Welteinheiten (default: 0 = Kartenmitte). */
|
||||||
|
public float spawnX = 0f;
|
||||||
|
|
||||||
|
/** Spawnpunkt Z-Koordinate in Welteinheiten (default: 0 = Kartenmitte). */
|
||||||
|
public float spawnZ = 0f;
|
||||||
|
|
||||||
public MapData() {
|
public MapData() {
|
||||||
terrainHeight = new float[TERRAIN_VERTS * TERRAIN_VERTS];
|
terrainHeight = new float[TERRAIN_VERTS * TERRAIN_VERTS];
|
||||||
upperTop = new float[UPPER_VERTS * UPPER_VERTS];
|
upperTop = new float[UPPER_VERTS * UPPER_VERTS];
|
||||||
upperBottom = new float[UPPER_VERTS * UPPER_VERTS];
|
upperBottom = new float[UPPER_VERTS * UPPER_VERTS];
|
||||||
upperHole = new byte [UPPER_CELLS * UPPER_CELLS];
|
|
||||||
splatR = new byte [SPLAT_SIZE * SPLAT_SIZE];
|
splatR = new byte [SPLAT_SIZE * SPLAT_SIZE];
|
||||||
splatG = new byte [SPLAT_SIZE * SPLAT_SIZE];
|
splatG = new byte [SPLAT_SIZE * SPLAT_SIZE];
|
||||||
splatB = new byte [SPLAT_SIZE * SPLAT_SIZE];
|
splatB = new byte [SPLAT_SIZE * SPLAT_SIZE];
|
||||||
|
splatA = new byte [SPLAT_SIZE * SPLAT_SIZE];
|
||||||
|
upperSplatR = new byte [SPLAT_SIZE * SPLAT_SIZE];
|
||||||
|
upperSplatG = new byte [SPLAT_SIZE * SPLAT_SIZE];
|
||||||
|
upperSplatB = new byte [SPLAT_SIZE * SPLAT_SIZE];
|
||||||
|
upperSplatA = new byte [SPLAT_SIZE * SPLAT_SIZE];
|
||||||
grassDensity = new byte [SPLAT_SIZE * SPLAT_SIZE];
|
grassDensity = new byte [SPLAT_SIZE * SPLAT_SIZE];
|
||||||
|
upperHole = new byte [UPPER_CELLS * UPPER_CELLS];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import java.util.zip.*;
|
|||||||
* 1 – Basis-Terrain + Obere Schicht (kein Splatmap)
|
* 1 – Basis-Terrain + Obere Schicht (kein Splatmap)
|
||||||
* 2 – wie 1 + Splatmap (R/G/B je 513×513 Bytes)
|
* 2 – wie 1 + Splatmap (R/G/B je 513×513 Bytes)
|
||||||
* 3 – wie 2 + Gras-Dichte (513×513 Bytes)
|
* 3 – wie 2 + Gras-Dichte (513×513 Bytes)
|
||||||
|
* 4 – wie 3 + Spawnpunkt (2× float)
|
||||||
|
* 5 – wie 4 + Splatmap-Alpha + Texturpfade + Gebirge-Splatmap (RGBA + Pfade)
|
||||||
|
* 6 – wie 5 ohne upperHole; upperTop/Bottom behalten (zukünftige Höhlen-Architektur)
|
||||||
*/
|
*/
|
||||||
public final class MapIO {
|
public final class MapIO {
|
||||||
|
|
||||||
@@ -47,7 +50,7 @@ public final class MapIO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final int MAGIC = 0x424C4947; // "BLIG"
|
private static final int MAGIC = 0x424C4947; // "BLIG"
|
||||||
private static final int VERSION = 3;
|
private static final int VERSION = 6;
|
||||||
|
|
||||||
private MapIO() {}
|
private MapIO() {}
|
||||||
|
|
||||||
@@ -84,13 +87,23 @@ public final class MapIO {
|
|||||||
writeFloats(out, data.terrainHeight);
|
writeFloats(out, data.terrainHeight);
|
||||||
writeFloats(out, data.upperTop);
|
writeFloats(out, data.upperTop);
|
||||||
writeFloats(out, data.upperBottom);
|
writeFloats(out, data.upperBottom);
|
||||||
out.write(data.upperHole);
|
|
||||||
// v2: splatmap
|
// v2: splatmap
|
||||||
out.write(data.splatR);
|
out.write(data.splatR);
|
||||||
out.write(data.splatG);
|
out.write(data.splatG);
|
||||||
out.write(data.splatB);
|
out.write(data.splatB);
|
||||||
// v3: gras-dichte
|
// v3: gras-dichte
|
||||||
out.write(data.grassDensity);
|
out.write(data.grassDensity);
|
||||||
|
// v4: spawnpunkt
|
||||||
|
out.writeFloat(data.spawnX);
|
||||||
|
out.writeFloat(data.spawnZ);
|
||||||
|
// v5: splatA + texturpfade + gebirge-splatmap
|
||||||
|
out.write(data.splatA);
|
||||||
|
writeStrings(out, data.terrainTextures);
|
||||||
|
out.write(data.upperSplatR);
|
||||||
|
out.write(data.upperSplatG);
|
||||||
|
out.write(data.upperSplatB);
|
||||||
|
out.write(data.upperSplatA);
|
||||||
|
writeStrings(out, data.upperTextures);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +121,10 @@ public final class MapIO {
|
|||||||
readFloats(in, data.terrainHeight);
|
readFloats(in, data.terrainHeight);
|
||||||
readFloats(in, data.upperTop);
|
readFloats(in, data.upperTop);
|
||||||
readFloats(in, data.upperBottom);
|
readFloats(in, data.upperBottom);
|
||||||
in.readFully(data.upperHole);
|
if (version <= 5) {
|
||||||
|
// v5 had upperHole[UPPER_CELLS²]; read and discard
|
||||||
|
in.skip((long) MapData.UPPER_CELLS * MapData.UPPER_CELLS);
|
||||||
|
}
|
||||||
|
|
||||||
if (version >= 2) {
|
if (version >= 2) {
|
||||||
in.readFully(data.splatR);
|
in.readFully(data.splatR);
|
||||||
@@ -118,7 +134,22 @@ public final class MapIO {
|
|||||||
if (version >= 3) {
|
if (version >= 3) {
|
||||||
in.readFully(data.grassDensity);
|
in.readFully(data.grassDensity);
|
||||||
}
|
}
|
||||||
// version 1/2: grassDensity stays all-zeros (= kein Gras)
|
if (version >= 4) {
|
||||||
|
data.spawnX = in.readFloat();
|
||||||
|
data.spawnZ = in.readFloat();
|
||||||
|
}
|
||||||
|
if (version >= 5) {
|
||||||
|
in.readFully(data.splatA);
|
||||||
|
readStrings(in, data.terrainTextures);
|
||||||
|
in.readFully(data.upperSplatR);
|
||||||
|
in.readFully(data.upperSplatG);
|
||||||
|
in.readFully(data.upperSplatB);
|
||||||
|
in.readFully(data.upperSplatA);
|
||||||
|
readStrings(in, data.upperTextures);
|
||||||
|
} else {
|
||||||
|
// Ältere Maps: upperSplatR auf 255 initialisieren (Tex1 immer sichtbar)
|
||||||
|
java.util.Arrays.fill(data.upperSplatR, (byte) 255);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -137,4 +168,14 @@ public final class MapIO {
|
|||||||
in.readFully(bytes);
|
in.readFully(bytes);
|
||||||
ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).asFloatBuffer().get(arr);
|
ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).asFloatBuffer().get(arr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void writeStrings(DataOutputStream out, String[] arr) throws IOException {
|
||||||
|
out.writeInt(arr.length);
|
||||||
|
for (String s : arr) out.writeUTF(s != null ? s : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void readStrings(DataInputStream in, String[] arr) throws IOException {
|
||||||
|
int len = in.readInt();
|
||||||
|
for (int i = 0; i < len && i < arr.length; i++) arr[i] = in.readUTF();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package de.blight.common;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public final class MusicAreaIO {
|
||||||
|
|
||||||
|
private MusicAreaIO() {}
|
||||||
|
|
||||||
|
public static Path getPath() {
|
||||||
|
return MapIO.getMapPath().resolveSibling("blight_music_areas.bma");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void save(List<PlacedMusicArea> areas) throws IOException {
|
||||||
|
Path p = getPath();
|
||||||
|
Files.createDirectories(p.getParent());
|
||||||
|
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
||||||
|
w.write("# polygon\tdayTrack\tnightTrack\tcombatTrack");
|
||||||
|
w.newLine();
|
||||||
|
for (PlacedMusicArea a : areas) {
|
||||||
|
w.write(SoundAreaIO.encodePolygon(a.pointsX(), a.pointsZ()));
|
||||||
|
w.write('\t');
|
||||||
|
w.write(a.dayTrack());
|
||||||
|
w.write('\t');
|
||||||
|
w.write(a.nightTrack());
|
||||||
|
w.write('\t');
|
||||||
|
w.write(a.combatTrack());
|
||||||
|
w.newLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<PlacedMusicArea> load() throws IOException {
|
||||||
|
Path p = getPath();
|
||||||
|
if (!Files.exists(p)) return List.of();
|
||||||
|
List<PlacedMusicArea> list = new ArrayList<>();
|
||||||
|
for (String line : Files.readAllLines(p)) {
|
||||||
|
line = line.strip();
|
||||||
|
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||||
|
String[] f = line.split("\t", -1);
|
||||||
|
if (f.length < 4) continue;
|
||||||
|
try {
|
||||||
|
float[][] pts = SoundAreaIO.decodePolygon(f[0]);
|
||||||
|
if (pts[0].length < 3) continue;
|
||||||
|
list.add(new PlacedMusicArea(pts[0], pts[1], f[1], f[2], f[3]));
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package de.blight.common;
|
||||||
|
|
||||||
|
public record PlacedEmitter(
|
||||||
|
float x, float y, float z,
|
||||||
|
float activationRadius,
|
||||||
|
String texturePath,
|
||||||
|
int imagesX, int imagesY,
|
||||||
|
float startR, float startG, float startB, float startA,
|
||||||
|
float endR, float endG, float endB, float endA,
|
||||||
|
float startSize, float endSize,
|
||||||
|
float velX, float velY, float velZ, float velocityVariation,
|
||||||
|
float gravX, float gravY, float gravZ,
|
||||||
|
float lowLife, float highLife,
|
||||||
|
int maxParticles, float emitRate
|
||||||
|
) {
|
||||||
|
|
||||||
|
public static PlacedEmitter fire(float x, float y, float z) {
|
||||||
|
return new PlacedEmitter(x, y + 0.5f, z, 20f,
|
||||||
|
"Effects/Explosion/flame.png", 2, 2,
|
||||||
|
1f, 0.7f, 0.1f, 1f,
|
||||||
|
1f, 0.1f, 0f, 0f,
|
||||||
|
0.5f, 1.2f,
|
||||||
|
0f, 2.5f, 0f, 0.5f,
|
||||||
|
0f, -0.1f, 0f,
|
||||||
|
1f, 3f,
|
||||||
|
60, 25f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlacedEmitter smoke(float x, float y, float z) {
|
||||||
|
return new PlacedEmitter(x, y + 1.5f, z, 20f,
|
||||||
|
"Effects/Smoke/Smoke.png", 1, 1,
|
||||||
|
0.5f, 0.5f, 0.5f, 0.4f,
|
||||||
|
0.2f, 0.2f, 0.2f, 0f,
|
||||||
|
0.8f, 2.5f,
|
||||||
|
0f, 1.2f, 0f, 0.3f,
|
||||||
|
0f, 0.05f, 0f,
|
||||||
|
3f, 6f,
|
||||||
|
24, 6f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlacedEmitter sparks(float x, float y, float z) {
|
||||||
|
return new PlacedEmitter(x, y + 0.3f, z, 20f,
|
||||||
|
"Effects/Explosion/spark.png", 1, 1,
|
||||||
|
1f, 0.9f, 0.2f, 1f,
|
||||||
|
1f, 0.3f, 0f, 0f,
|
||||||
|
0.05f, 0.02f,
|
||||||
|
0f, 4f, 0f, 1.0f,
|
||||||
|
0f, -5f, 0f,
|
||||||
|
0.5f, 2f,
|
||||||
|
50, 20f);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package de.blight.common;
|
||||||
|
|
||||||
|
/** Unveränderliche Snapshot-Daten einer platzierten Lichtquelle auf der Karte. */
|
||||||
|
public record PlacedLight(
|
||||||
|
float x, float y, float z,
|
||||||
|
float r, float g, float b,
|
||||||
|
float intensity,
|
||||||
|
float radius
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package de.blight.common;
|
||||||
|
|
||||||
|
/** Unveränderliche Snapshot-Daten eines platzierten 3D-Modells auf der Karte. */
|
||||||
|
public record PlacedModel(
|
||||||
|
String modelPath, // "@box" / "@sphere" / ... / "models/foo.j3o"
|
||||||
|
float x, float y, float z,
|
||||||
|
float rotY, float rotX, float rotZ,
|
||||||
|
float scale,
|
||||||
|
boolean solid,
|
||||||
|
String texturePath, String normalMapPath, String materialPath,
|
||||||
|
/** Relativer Asset-Pfad zur exportierten j3o-Datei des Custom Meshes; "" wenn nicht verwendet. */
|
||||||
|
String meshFile,
|
||||||
|
/** AnimationLibrary-Clip-Key (z. B. "walk/Run"); "" = keine Animation. */
|
||||||
|
String animClip
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package de.blight.common;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest und schreibt platzierte Modelle als tab-separierte Textdatei
|
||||||
|
* ({@code blight_objects.blo}) neben der Kartendatei.
|
||||||
|
*
|
||||||
|
* Spalten (seit v2):
|
||||||
|
* modelPath x y z rotY scale rotX rotZ solid texPath nmPath matPath meshFile
|
||||||
|
*
|
||||||
|
* Alte Dateien mit 6 Spalten (v1) werden gelesen; fehlende Felder erhalten Standardwerte.
|
||||||
|
*/
|
||||||
|
public final class PlacedModelIO {
|
||||||
|
|
||||||
|
private PlacedModelIO() {}
|
||||||
|
|
||||||
|
public static Path getPath() {
|
||||||
|
return MapIO.getMapPath().resolveSibling("blight_objects.blo");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void save(List<PlacedModel> models) throws IOException {
|
||||||
|
Path p = getPath();
|
||||||
|
Files.createDirectories(p.getParent());
|
||||||
|
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
||||||
|
w.write("# modelPath\tx\ty\tz\trotY\tscale\trotX\trotZ\tsolid\ttexPath\tnmPath\tmatPath\tmeshFile\tanimClip");
|
||||||
|
w.newLine();
|
||||||
|
for (PlacedModel m : models) {
|
||||||
|
w.write(String.format(Locale.ROOT,
|
||||||
|
"%s\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%b\t%s\t%s\t%s\t%s\t%s%n",
|
||||||
|
m.modelPath(),
|
||||||
|
m.x(), m.y(), m.z(),
|
||||||
|
m.rotY(), m.scale(),
|
||||||
|
m.rotX(), m.rotZ(),
|
||||||
|
m.solid(),
|
||||||
|
nvl(m.texturePath()), nvl(m.normalMapPath()), nvl(m.materialPath()),
|
||||||
|
nvl(m.meshFile()), nvl(m.animClip())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<PlacedModel> load() throws IOException {
|
||||||
|
Path p = getPath();
|
||||||
|
if (!Files.exists(p)) return List.of();
|
||||||
|
List<PlacedModel> list = new ArrayList<>();
|
||||||
|
for (String line : Files.readAllLines(p)) {
|
||||||
|
line = line.strip();
|
||||||
|
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||||
|
String[] f = line.split("\t", -1);
|
||||||
|
if (f.length < 6) continue;
|
||||||
|
try {
|
||||||
|
String modelPath = f[0];
|
||||||
|
float x = Float.parseFloat(f[1]);
|
||||||
|
float y = Float.parseFloat(f[2]);
|
||||||
|
float z = Float.parseFloat(f[3]);
|
||||||
|
float rotY = Float.parseFloat(f[4]);
|
||||||
|
float scale = Float.parseFloat(f[5]);
|
||||||
|
float rotX = f.length > 6 ? Float.parseFloat(f[6]) : 0f;
|
||||||
|
float rotZ = f.length > 7 ? Float.parseFloat(f[7]) : 0f;
|
||||||
|
boolean solid = f.length > 8 ? Boolean.parseBoolean(f[8]) : false;
|
||||||
|
String texPath = f.length > 9 ? f[9] : "";
|
||||||
|
String nmPath = f.length > 10 ? f[10] : "";
|
||||||
|
String matPath = f.length > 11 ? f[11] : "";
|
||||||
|
String meshFile = f.length > 12 ? f[12] : "";
|
||||||
|
String animClip = f.length > 13 ? f[13] : "";
|
||||||
|
list.add(new PlacedModel(modelPath, x, y, z,
|
||||||
|
rotY, rotX, rotZ, scale, solid,
|
||||||
|
texPath, nmPath, matPath, meshFile, animClip));
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String nvl(String s) { return s != null ? s : ""; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package de.blight.common;
|
||||||
|
|
||||||
|
public record PlacedMusicArea(
|
||||||
|
float[] pointsX,
|
||||||
|
float[] pointsZ,
|
||||||
|
String dayTrack,
|
||||||
|
String nightTrack,
|
||||||
|
String combatTrack
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package de.blight.common;
|
||||||
|
|
||||||
|
public record PlacedSoundArea(
|
||||||
|
float[] pointsX,
|
||||||
|
float[] pointsZ,
|
||||||
|
String soundPath,
|
||||||
|
float volume,
|
||||||
|
boolean crossfade
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package de.blight.common;
|
||||||
|
|
||||||
|
/** Unveränderliche Daten einer platzierten Wasseroberfläche (Teich, See). */
|
||||||
|
public record PlacedWater(
|
||||||
|
float x, float y, float z,
|
||||||
|
float width, float depth
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package de.blight.common;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public final class SoundAreaIO {
|
||||||
|
|
||||||
|
private SoundAreaIO() {}
|
||||||
|
|
||||||
|
public static Path getPath() {
|
||||||
|
return MapIO.getMapPath().resolveSibling("blight_sound_areas.bsa");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void save(List<PlacedSoundArea> areas) throws IOException {
|
||||||
|
Path p = getPath();
|
||||||
|
Files.createDirectories(p.getParent());
|
||||||
|
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
||||||
|
w.write("# polygon\tsoundPath\tvolume\tcrossfade");
|
||||||
|
w.newLine();
|
||||||
|
for (PlacedSoundArea a : areas) {
|
||||||
|
w.write(encodePolygon(a.pointsX(), a.pointsZ()));
|
||||||
|
w.write('\t');
|
||||||
|
w.write(a.soundPath());
|
||||||
|
w.write('\t');
|
||||||
|
w.write(String.format(Locale.ROOT, "%.4f\t%b%n", a.volume(), a.crossfade()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<PlacedSoundArea> load() throws IOException {
|
||||||
|
Path p = getPath();
|
||||||
|
if (!Files.exists(p)) return List.of();
|
||||||
|
List<PlacedSoundArea> list = new ArrayList<>();
|
||||||
|
for (String line : Files.readAllLines(p)) {
|
||||||
|
line = line.strip();
|
||||||
|
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||||
|
String[] f = line.split("\t", -1);
|
||||||
|
if (f.length < 4) continue;
|
||||||
|
try {
|
||||||
|
float[][] pts = decodePolygon(f[0]);
|
||||||
|
if (pts[0].length < 3) continue;
|
||||||
|
list.add(new PlacedSoundArea(pts[0], pts[1], f[1],
|
||||||
|
Float.parseFloat(f[2]), Boolean.parseBoolean(f[3])));
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String encodePolygon(float[] xs, float[] zs) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < xs.length; i++) {
|
||||||
|
if (i > 0) sb.append(';');
|
||||||
|
sb.append(String.format(Locale.ROOT, "%.3f,%.3f", xs[i], zs[i]));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static float[][] decodePolygon(String encoded) {
|
||||||
|
String[] pts = encoded.split(";", -1);
|
||||||
|
float[] xs = new float[pts.length];
|
||||||
|
float[] zs = new float[pts.length];
|
||||||
|
for (int i = 0; i < pts.length; i++) {
|
||||||
|
String[] xz = pts[i].split(",", -1);
|
||||||
|
xs[i] = Float.parseFloat(xz[0]);
|
||||||
|
zs[i] = Float.parseFloat(xz[1]);
|
||||||
|
}
|
||||||
|
return new float[][]{xs, zs};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package de.blight.common;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest und schreibt platzierte Wasseroberflächen als tab-separierte Textdatei
|
||||||
|
* ({@code blight_water.blw}) neben der Kartendatei.
|
||||||
|
*
|
||||||
|
* Spalten: x y z width depth
|
||||||
|
*/
|
||||||
|
public final class WaterBodyIO {
|
||||||
|
|
||||||
|
private WaterBodyIO() {}
|
||||||
|
|
||||||
|
public static Path getPath() {
|
||||||
|
return MapIO.getMapPath().resolveSibling("blight_water.blw");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void save(List<PlacedWater> bodies) throws IOException {
|
||||||
|
Path p = getPath();
|
||||||
|
Files.createDirectories(p.getParent());
|
||||||
|
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
||||||
|
w.write("# x\ty\tz\twidth\tdepth");
|
||||||
|
w.newLine();
|
||||||
|
for (PlacedWater b : bodies) {
|
||||||
|
w.write(String.format(Locale.ROOT,
|
||||||
|
"%.5f\t%.5f\t%.5f\t%.5f\t%.5f%n",
|
||||||
|
b.x(), b.y(), b.z(), b.width(), b.depth()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<PlacedWater> load() throws IOException {
|
||||||
|
Path p = getPath();
|
||||||
|
if (!Files.exists(p)) return List.of();
|
||||||
|
List<PlacedWater> list = new ArrayList<>();
|
||||||
|
for (String line : Files.readAllLines(p)) {
|
||||||
|
line = line.strip();
|
||||||
|
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||||
|
String[] f = line.split("\t", -1);
|
||||||
|
if (f.length < 5) continue;
|
||||||
|
try {
|
||||||
|
list.add(new PlacedWater(
|
||||||
|
Float.parseFloat(f[0]),
|
||||||
|
Float.parseFloat(f[1]),
|
||||||
|
Float.parseFloat(f[2]),
|
||||||
|
Float.parseFloat(f[3]),
|
||||||
|
Float.parseFloat(f[4])));
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
public interface AudioReference {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
import com.google.gson.*;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt und speichert {@link GameCharacter}-Instanzen als JSON.
|
||||||
|
* Dateiformat: <id>.character im Verzeichnis character/
|
||||||
|
*
|
||||||
|
* JSON-Struktur:
|
||||||
|
* {
|
||||||
|
* "type": "MAIN_CHARACTER" | "NPC",
|
||||||
|
* "characterId": "hero",
|
||||||
|
* "name": "Der Held",
|
||||||
|
* "modelPath": "Models/hero.j3o",
|
||||||
|
* "animSetPath": "animations/sets/hero.j3o",
|
||||||
|
* ... (subclass fields via Gson)
|
||||||
|
* }
|
||||||
|
* Die Aktions-Zuweisung (IDLE → Clip-Name usw.) ist im AnimSet gespeichert
|
||||||
|
* (animSetPath.replaceAll(".j3o", "") + ".animset.json").
|
||||||
|
*/
|
||||||
|
public final class CharacterIO {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(CharacterIO.class);
|
||||||
|
private static final String EXTENSION = ".character";
|
||||||
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||||
|
|
||||||
|
private CharacterIO() {}
|
||||||
|
|
||||||
|
// ── Save ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static void save(GameCharacter character, Path directory) throws IOException {
|
||||||
|
String id = character.getCharacterId();
|
||||||
|
if (id == null || id.isBlank()) throw new IllegalArgumentException("characterId must be set");
|
||||||
|
Files.createDirectories(directory);
|
||||||
|
|
||||||
|
JsonObject obj = (JsonObject) GSON.toJsonTree(character);
|
||||||
|
obj.addProperty("type", character instanceof MainCharacter ? "MAIN_CHARACTER" : "NPC");
|
||||||
|
Files.writeString(directory.resolve(id + EXTENSION),
|
||||||
|
GSON.toJson(obj), StandardCharsets.UTF_8);
|
||||||
|
log.debug("[CharacterIO] Gespeichert: {}", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Load ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static GameCharacter load(Path file) throws IOException {
|
||||||
|
String json = Files.readString(file, StandardCharsets.UTF_8);
|
||||||
|
JsonObject obj = JsonParser.parseString(json).getAsJsonObject();
|
||||||
|
String type = obj.has("type") ? obj.get("type").getAsString() : "NPC";
|
||||||
|
GameCharacter c = "MAIN_CHARACTER".equals(type)
|
||||||
|
? GSON.fromJson(obj, MainCharacter.class)
|
||||||
|
: GSON.fromJson(obj, NPC.class);
|
||||||
|
log.debug("[CharacterIO] Geladen: {}", file.getFileName());
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lädt alle .character-Dateien aus dem angegebenen Verzeichnis. */
|
||||||
|
public static List<GameCharacter> loadAll(Path directory) {
|
||||||
|
if (!Files.isDirectory(directory)) return List.of();
|
||||||
|
List<GameCharacter> result = new ArrayList<>();
|
||||||
|
try (Stream<Path> walk = Files.list(directory)) {
|
||||||
|
walk.filter(p -> p.toString().endsWith(EXTENSION))
|
||||||
|
.sorted()
|
||||||
|
.forEach(p -> {
|
||||||
|
try { result.add(load(p)); } catch (IOException e) {
|
||||||
|
log.warn("[CharacterIO] Fehler beim Laden von {}: {}", p, e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("[CharacterIO] Fehler beim Scannen von {}: {}", directory, e.getMessage());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt true zurück wenn bereits eine MainCharacter-Datei im Verzeichnis existiert (außer der mit excludeId). */
|
||||||
|
public static boolean mainCharacterExists(Path directory, String excludeId) {
|
||||||
|
for (GameCharacter c : loadAll(directory)) {
|
||||||
|
if (c instanceof MainCharacter
|
||||||
|
&& !Objects.equals(c.getCharacterId(), excludeId)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
import de.blight.common.model.quests.Quest;
|
||||||
|
|
||||||
|
public interface CharacterListener {
|
||||||
|
|
||||||
|
public void questFulfilled(Quest quest);
|
||||||
|
|
||||||
|
public void questAborted(Quest quest);
|
||||||
|
|
||||||
|
public void levelUp();
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class Collectable implements Interactable {
|
||||||
|
|
||||||
|
private Item item;
|
||||||
|
private ObjectReference objectReference;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AlchemyTable
|
||||||
|
* EnchantmentTable
|
||||||
|
* Smithy
|
||||||
|
* Goldsmiths
|
||||||
|
* Workshop
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class CraftingTable {
|
||||||
|
|
||||||
|
private TextReference name;
|
||||||
|
private ObjectReference object;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import de.blight.common.model.quests.Quest;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class DialogOption {
|
||||||
|
|
||||||
|
private int requiresChapter;
|
||||||
|
private Quest requiresQuestOpen;
|
||||||
|
private Quest requiresQuestComplete;
|
||||||
|
private Status requiresStatus;
|
||||||
|
|
||||||
|
private TextReference textHero;
|
||||||
|
private AudioReference audioHero;
|
||||||
|
private TextReference textNpc;
|
||||||
|
private AudioReference audioNpc;
|
||||||
|
|
||||||
|
private List<DialogOption> nextOptions;
|
||||||
|
private List<DialogOption> disablesOptions;
|
||||||
|
|
||||||
|
private RequiredItem requiredItem;
|
||||||
|
private RecievesItem recievesItem;
|
||||||
|
|
||||||
|
private Quest recievesQuest;
|
||||||
|
private Quest fulfillsQuest;
|
||||||
|
private List<Quest> abortsQuests = new ArrayList<Quest>();
|
||||||
|
|
||||||
|
private boolean enablesTrade;
|
||||||
|
|
||||||
|
public Status getRequiredStatus() {
|
||||||
|
if (requiresStatus == null) {
|
||||||
|
return Status.ENEMY;
|
||||||
|
}
|
||||||
|
return requiresStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public abstract class GameCharacter {
|
||||||
|
/** Eindeutige ID — entspricht dem Dateinamen ohne Extension. */
|
||||||
|
private String characterId;
|
||||||
|
/** Anzeigename des Charakters — wird zur Laufzeit per TextReference aufgelöst. */
|
||||||
|
private TextReference name;
|
||||||
|
/** Relativer Pfad zum .j3o-Modell (relativ zu blight-assets/src/main/resources). */
|
||||||
|
private String modelPath;
|
||||||
|
/** Relativer Pfad zum .j3o-Animations-Set (in animations/sets/). */
|
||||||
|
private String animSetPath;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
public interface Interactable {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
|
public class Inventar {
|
||||||
|
|
||||||
|
private HashMap<Item, Integer> items = new HashMap<Item, Integer>();
|
||||||
|
|
||||||
|
public void collect(Item item) {
|
||||||
|
add(item, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean remove(Item item, int count) {
|
||||||
|
if (items.containsKey(item) && items.get(item) >= count) {
|
||||||
|
items.merge(item, count * -1, Integer::sum);
|
||||||
|
if (items.get(item) <= 0) {
|
||||||
|
items.remove(item);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean remove(Item item) {
|
||||||
|
return remove(item, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean remove(ItemCount itemCount) {
|
||||||
|
return remove(itemCount.getItem(), itemCount.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean use(Item item, MainCharacter character) {
|
||||||
|
if (remove(item)) {
|
||||||
|
item.use(character);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasItem(ItemCount itemCount) {
|
||||||
|
return hasItem(itemCount.getItem(), itemCount.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasItem(Item item, Integer count) {
|
||||||
|
return items.containsKey(item) && items.get(item) >= count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recieveItem(ItemCount itemCount) {
|
||||||
|
add(itemCount.getItem(), itemCount.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void add(Item item) {
|
||||||
|
add(item, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void add(Item item, int count) {
|
||||||
|
if (items.containsKey(item)) {
|
||||||
|
items.merge(item, count, Integer::sum);
|
||||||
|
} else {
|
||||||
|
items.put(item, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canCraft(Recipe recipe) {
|
||||||
|
for (Entry<Item, Integer> entry : recipe.getComponents().entrySet()) {
|
||||||
|
if (!hasItem(entry.getKey(), entry.getValue())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void craft(Recipe recipe) {
|
||||||
|
for (Entry<Item, Integer> entry : recipe.getComponents().entrySet()) {
|
||||||
|
remove(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
add(recipe.getCreates());
|
||||||
|
}
|
||||||
|
}
|
||||||
20
blight-common/src/main/java/de/blight/common/model/Item.java
Normal file
20
blight-common/src/main/java/de/blight/common/model/Item.java
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class Item {
|
||||||
|
|
||||||
|
private String itemId;
|
||||||
|
private ItemCategory category;
|
||||||
|
private TextReference name;
|
||||||
|
private TextReference description;
|
||||||
|
private int worthGold;
|
||||||
|
|
||||||
|
|
||||||
|
public void use(MainCharacter character) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
public enum ItemCategory {
|
||||||
|
|
||||||
|
WEAPON,
|
||||||
|
GEAR,
|
||||||
|
CONSUMABLES,
|
||||||
|
QUEST_ITEMS,
|
||||||
|
USABLES,
|
||||||
|
MISC;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
public interface ItemCount {
|
||||||
|
|
||||||
|
public Item getItem();
|
||||||
|
public int getCount();
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
public interface Location {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import de.blight.common.model.quests.Quest;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class MainCharacter extends GameCharacter {
|
||||||
|
|
||||||
|
private int chapter;
|
||||||
|
private Inventar inventar;
|
||||||
|
private int level;
|
||||||
|
private int xp;
|
||||||
|
|
||||||
|
private int currentHp;
|
||||||
|
private int maxHp;
|
||||||
|
|
||||||
|
private int currentStamina;
|
||||||
|
private int maxStamina;
|
||||||
|
|
||||||
|
private int currentMana;
|
||||||
|
private int myMana;
|
||||||
|
|
||||||
|
private List<Quest> openQuests;
|
||||||
|
private List<Quest> completedQuests;
|
||||||
|
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
@Setter(AccessLevel.NONE)
|
||||||
|
private List<CharacterListener> listeners = new ArrayList<CharacterListener>();
|
||||||
|
|
||||||
|
public void handleDialogOption(DialogOption option) {
|
||||||
|
if (option.getRequiredItem() != null) {
|
||||||
|
getInventar().remove(option.getRequiredItem());
|
||||||
|
}
|
||||||
|
if (option.getRecievesItem() != null) {
|
||||||
|
getInventar().recieveItem(option.getRecievesItem());
|
||||||
|
}
|
||||||
|
if (option.getFulfillsQuest() != null ) {
|
||||||
|
fullfillQuest(option.getFulfillsQuest());
|
||||||
|
}
|
||||||
|
option.getAbortsQuests().forEach(this::abortQuest);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fullfillQuest(Quest quest) {
|
||||||
|
openQuests.remove(quest);
|
||||||
|
completedQuests.add(quest);
|
||||||
|
checkLevelUp(quest.getXp());
|
||||||
|
listeners.forEach(listener -> listener.questFulfilled(quest));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkLevelUp(int questXp) {
|
||||||
|
var xpRequired = XPHelper.getXpRequired(level);
|
||||||
|
var newXp = xp + questXp;
|
||||||
|
if (newXp > xpRequired) {
|
||||||
|
this.level++;
|
||||||
|
this.xp = newXp - xpRequired;
|
||||||
|
listeners.forEach(CharacterListener::levelUp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void abortQuest(Quest quest) {
|
||||||
|
openQuests.remove(quest);
|
||||||
|
listeners.forEach(listener -> listener.questAborted(quest));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeListener(CharacterListener listener) {
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addListener(CharacterListener listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
blight-common/src/main/java/de/blight/common/model/NPC.java
Normal file
65
blight-common/src/main/java/de/blight/common/model/NPC.java
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class NPC extends GameCharacter {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(NPC.class);
|
||||||
|
|
||||||
|
private Status status;
|
||||||
|
private boolean trader;
|
||||||
|
private List<Item> items;
|
||||||
|
|
||||||
|
private List<DialogOption> currentOptions;
|
||||||
|
|
||||||
|
public List<DialogOption> getAvailableOptions(MainCharacter character) {
|
||||||
|
return currentOptions.stream().filter(option ->
|
||||||
|
option.getRequiresChapter() < character.getChapter() &&
|
||||||
|
option.getRequiresStatus().requirementFulfilled(status) &&
|
||||||
|
(option.getRequiresQuestOpen() == null || character.getOpenQuests().contains(option.getRequiresQuestOpen())) &&
|
||||||
|
(option.getRequiredItem() == null || character.getInventar().hasItem(option.getRequiredItem())) &&
|
||||||
|
(option.getRequiresQuestComplete() == null || character.getCompletedQuests().contains(option.getRequiresQuestComplete()))).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean chooseDialogOption(DialogOption option, MainCharacter character) {
|
||||||
|
if (!currentOptions.contains(option)) {
|
||||||
|
LOG.warn("Dialog Option was choosen but is not available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (option.getRequiredItem() != null && !character.getInventar().hasItem(option.getRequiredItem())) {
|
||||||
|
LOG.warn("Dialog Option was choosen but required item is not in Inventar");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (option.getRequiresQuestOpen() != null && !character.getOpenQuests().contains(option.getRequiresQuestOpen())) {
|
||||||
|
LOG.warn("Dialog Option was choosen but required quest is not open");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (option.getRequiredStatus().requirementFulfilled(status)) {
|
||||||
|
LOG.warn("Dialog Option was choosen but required Status is not fulfilled");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (option.getRequiresQuestComplete() != null && !character.getCompletedQuests().contains(option.getRequiresQuestComplete())) {
|
||||||
|
LOG.warn("Dialog Option was choosen but required Quest is not complete");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
currentOptions.remove(option);
|
||||||
|
currentOptions.removeAll(option.getDisablesOptions());
|
||||||
|
currentOptions.addAll(option.getNextOptions());
|
||||||
|
|
||||||
|
character.handleDialogOption(option);
|
||||||
|
|
||||||
|
if (option.isEnablesTrade()) {
|
||||||
|
this.trader = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
public interface ObjectReference {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class RecievesItem implements ItemCount {
|
||||||
|
|
||||||
|
private Item item;
|
||||||
|
private int count = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class Recipe {
|
||||||
|
|
||||||
|
private Item creates;
|
||||||
|
private Map<Item, Integer> components;
|
||||||
|
private Interactable requires;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class RequiredItem implements ItemCount {
|
||||||
|
|
||||||
|
private Item item;
|
||||||
|
private int count = 1;
|
||||||
|
private boolean gives;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
public enum Status {
|
||||||
|
|
||||||
|
FRIENDLY(3), NEUTRAL(2), ENRAGED(1), ENEMY(0);
|
||||||
|
|
||||||
|
private int val;
|
||||||
|
|
||||||
|
private Status(int val) {
|
||||||
|
this.val = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean requirementFulfilled(Status minVal) {
|
||||||
|
return this.val >= minVal.val;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
public record TextReference(String id) {}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
public class XPHelper {
|
||||||
|
|
||||||
|
private static final int BASE_VAL = 450;
|
||||||
|
private static final int GROTH_A = 5;
|
||||||
|
private static final int GROTH_B = 45;
|
||||||
|
|
||||||
|
private XPHelper() {}
|
||||||
|
|
||||||
|
public static int getXpRequired(int level) {
|
||||||
|
return (GROTH_A * (level * level)) + (GROTH_B * level) + BASE_VAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package de.blight.common.model.abilities;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class Abilities {
|
||||||
|
|
||||||
|
private int lvlMagic; // 1-10
|
||||||
|
private int lvlStaffCombat; // 1-10
|
||||||
|
|
||||||
|
private int lvlSwordsmanship; // 1-10
|
||||||
|
private int lvlArchery; // 1-10
|
||||||
|
|
||||||
|
private int lvlHeavyWeapons; // 1-10
|
||||||
|
private int lvlCrossbow; // 1-10
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Level 1: Ermöglicht das Knacken von Schlössern
|
||||||
|
* Level 2: Ermöglicht den Taschendiebstahl
|
||||||
|
* Level 3: Auf leisen Sohlen - Schleichen ist effektiver
|
||||||
|
*/
|
||||||
|
private int lvlThievery; // 1-3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Level 1: Ermöglicht das Brauen von Trönken, die die Wirkung von Kräutern verstärken
|
||||||
|
* Level 2: Ermöglicht das Brauen von Tränken, die den Helden temporär
|
||||||
|
* Level 3: Ermöglicht das Brauen von Tränken, die den Helden dauerhaft verstärken
|
||||||
|
*/
|
||||||
|
private int lvlAlchemy; // 1-3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Level 1: Ermöglicht das Herstellen von Bandagen und das Herstellen von Spritzen, die temporär den Helden verbessern
|
||||||
|
* Level 2: Ermöglicht die Herstllung von Wurfbomben und ähnlichem
|
||||||
|
* Level 3: Ermöglicht die Verwendung von Attelerie
|
||||||
|
*/
|
||||||
|
private int lvlEngineering; // 1-3
|
||||||
|
|
||||||
|
|
||||||
|
public enum StaffAbilities {
|
||||||
|
BASE_ATTACK(1), // Eine Basisattacke mit einem Stab
|
||||||
|
BLOCK(3), // Ein Block für Nahkampfattacken
|
||||||
|
HEAVY_ATTACK(5), // Ein Schwerer Schlag, der aufgeladen werden kann
|
||||||
|
STEADINESS(7), // Erhöht für 3 Sekunden die Standfestigkeit und der Held nimmt weniger Schaden
|
||||||
|
FEINT(9), // Lässt den Gegner bei einem Angriff ins leere laufen
|
||||||
|
DISARM(10); // Entwaffnet einen Angreifer temporär
|
||||||
|
|
||||||
|
private int minLvl;
|
||||||
|
|
||||||
|
private StaffAbilities(int minLvl) {
|
||||||
|
this.minLvl = minLvl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMinLvl() {
|
||||||
|
return minLvl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum MagicAbilities {
|
||||||
|
FIREBALL(1), // Feuert einen Feuerball auf einen Gegner der Schaden verursacht
|
||||||
|
LIGHT(1), // Erzeugt ein Licht, dass für 1 minute die umgebung erleuchtet
|
||||||
|
SHIELD(3), // Erzeugt für eine Sekunde einen Shild um den MainChar der Fernkampf Angriffe blockiert
|
||||||
|
ROOT(5), // Wurzelt alle Gegner im Umkreis von 20m am für 3 Sekunden Boden fest
|
||||||
|
SHOCKWAVE(7), // Feuert eine Shockwelle ab, die im Umkreis von 5 Metern die Gegner wegstößt, je dichter der Gegner, desto größer der Schaden
|
||||||
|
DRAIN(9), // Entzieht dem Gegner leben und heilt gleichzeitig den Helden
|
||||||
|
CHAIN_LIGHTNING(10); // Feuert einen Kettenblitz der auf mehrere Gegner überspringen und schaden verursachen kann
|
||||||
|
|
||||||
|
private int minLvl;
|
||||||
|
|
||||||
|
private MagicAbilities(int minLvl) {
|
||||||
|
this.minLvl = minLvl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMinLvl() {
|
||||||
|
return minLvl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SwordAbilities {
|
||||||
|
BASE_ATTACK(1), // Eine Basisattacke mit einem Schwert
|
||||||
|
BLOCK(3), // Ein Block für Nahkampfattacken
|
||||||
|
HEAVY_ATTACK(5), // Ein Schwerer Schlag, der aufgeladen werden kann
|
||||||
|
QUICK_THRUST(7), // Führt einen gezielten Stich durch, der einen Block teilweise ignoriert
|
||||||
|
BLADE_DANCE(9), // Führt mehrere schnelle Schläge nacheinander durch, kann einen Block durchbrechen
|
||||||
|
DEATH_BLOW(10); // Exekutiert Gegner mit weniger als 10% HP
|
||||||
|
|
||||||
|
private int minLvl;
|
||||||
|
|
||||||
|
private SwordAbilities(int minLvl) {
|
||||||
|
this.minLvl = minLvl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMinLvl() {
|
||||||
|
return minLvl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum HeavyWeaponAbilities {
|
||||||
|
BASE_ATTACK(1), // Eine Basisattacke mit einem Zweihänder oder einer Helebarde
|
||||||
|
BLOCK(3), // Ein Block für Nahkampfattacken
|
||||||
|
HEAVY_ATTACK(5), // Ein Schwerer Schlag, der aufgeladen werden kann, Durchbricht sehr effektiv einen Block
|
||||||
|
SWIRL_ATTACK(7), // Rotiert mit der Waffe um sich und fügt allen Gegnern im Radius Schaden zu
|
||||||
|
DOUBLE_ATTACK(9), // Führt unmittelbar nacheinander zwei Agriff mit beiden enden der Waffe durch
|
||||||
|
EXECUTION(10); // Führt einen Angriff durch der kritischen Schaden verursacht
|
||||||
|
|
||||||
|
private int minLvl;
|
||||||
|
|
||||||
|
private HeavyWeaponAbilities(int minLvl) {
|
||||||
|
this.minLvl = minLvl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMinLvl() {
|
||||||
|
return minLvl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RangedWeaponAbilities {
|
||||||
|
BASE_ATTACK(1), // ermöglicht das Abfeuern einer Armbrust oder eines Bogens
|
||||||
|
PRECISION_SHOT(3), // Höhere Genauigkeit bei Schüssen auf Distanz
|
||||||
|
FAST_RELOAD(5), // Erhöht die Nachladegeschwindigkeit um 50%
|
||||||
|
LETHALITY(7), // Erhöht die Durchdringung von Schüssen mit Pfeilen oder Bolzen
|
||||||
|
DOUBLE_ATTACK(9), // ERmöglicht das Abfeuern von zwei Bolzen oder Pfeilen zur Selben Zeit, erhöht den Schaden
|
||||||
|
HAWK_EYE(10); // Zeitlupe für kurze Dauer beim Zielen, um Schwachstellen zu treffen.
|
||||||
|
|
||||||
|
private int minLvl;
|
||||||
|
|
||||||
|
private RangedWeaponAbilities(int minLvl) {
|
||||||
|
this.minLvl = minLvl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMinLvl() {
|
||||||
|
return minLvl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum MiscAbilities {
|
||||||
|
SNEAK,
|
||||||
|
MINE_ORE,
|
||||||
|
GUT_ANIMALS,
|
||||||
|
ACROBATICS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package de.blight.common.model.abilities;
|
||||||
|
|
||||||
|
import de.blight.common.model.MainCharacter;
|
||||||
|
|
||||||
|
public interface Ability {
|
||||||
|
|
||||||
|
boolean requirementsFulfilled(MainCharacter character);
|
||||||
|
|
||||||
|
void used(MainCharacter character);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package de.blight.common.model.quests;
|
||||||
|
|
||||||
|
import de.blight.common.model.Location;
|
||||||
|
import de.blight.common.model.NPC;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class BringQuest {
|
||||||
|
|
||||||
|
private NPC bring;
|
||||||
|
private Location bringTo;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package de.blight.common.model.quests;
|
||||||
|
|
||||||
|
import de.blight.common.model.Location;
|
||||||
|
import de.blight.common.model.NPC;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
|
public class FollowQuest implements QuestType {
|
||||||
|
|
||||||
|
private NPC follow;
|
||||||
|
private Location followTo;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package de.blight.common.model.quests;
|
||||||
|
|
||||||
|
import de.blight.common.model.Interactable;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class InteractQuest implements QuestType {
|
||||||
|
|
||||||
|
private Interactable interactWith;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package de.blight.common.model.quests;
|
||||||
|
|
||||||
|
import de.blight.common.model.Item;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class ItemQuest implements QuestType {
|
||||||
|
|
||||||
|
private Item item;
|
||||||
|
private int count;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package de.blight.common.model.quests;
|
||||||
|
|
||||||
|
import de.blight.common.model.TextReference;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class Quest {
|
||||||
|
|
||||||
|
private int xp;
|
||||||
|
private String questId;
|
||||||
|
private TextReference text;
|
||||||
|
private TextReference description;
|
||||||
|
private TextReference successText;
|
||||||
|
|
||||||
|
private QuestType questType;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package de.blight.common.model.quests;
|
||||||
|
|
||||||
|
public interface QuestType {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package de.blight.common.model.quests;
|
||||||
|
|
||||||
|
import de.blight.common.model.NPC;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class TalkQuest implements QuestType {
|
||||||
|
|
||||||
|
private NPC talkTo;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package de.blight.common.time;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwaltet die Spielzeit innerhalb eines Tages (0.0–1.0).
|
||||||
|
* 0.0 = Mitternacht, 0.25 = 6 Uhr (Sonnenaufgang), 0.5 = Mittag, 0.75 = 18 Uhr.
|
||||||
|
* Keine JME-Abhängigkeit – reine Zeitlogik.
|
||||||
|
*/
|
||||||
|
public class DayTime {
|
||||||
|
|
||||||
|
/** Standard-Tagesdauer in Echtzeit-Sekunden (5 Minuten). */
|
||||||
|
public static final float DEFAULT_DAY_DURATION = 300f;
|
||||||
|
|
||||||
|
private float timeOfDay;
|
||||||
|
private float timeScale;
|
||||||
|
private boolean paused;
|
||||||
|
private final List<TimeListener> listeners = new ArrayList<>();
|
||||||
|
|
||||||
|
public DayTime() {
|
||||||
|
this(DEFAULT_DAY_DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param dayDuration Echtzeit-Sekunden für einen kompletten Spieltag */
|
||||||
|
public DayTime(float dayDuration) {
|
||||||
|
this.timeScale = 1f / dayDuration;
|
||||||
|
this.timeOfDay = 0.25f; // Start: Sonnenaufgang
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(float tpf) {
|
||||||
|
if (paused) return;
|
||||||
|
timeOfDay = (timeOfDay + tpf * timeScale) % 1f;
|
||||||
|
for (TimeListener l : listeners) l.onTimeChanged(timeOfDay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tageszeit 0.0–1.0. */
|
||||||
|
public float getTimeOfDay() { return timeOfDay; }
|
||||||
|
|
||||||
|
public void setTimeOfDay(float t) { timeOfDay = ((t % 1f) + 1f) % 1f; }
|
||||||
|
|
||||||
|
/** @param seconds Echtzeit-Sekunden pro Spieltag */
|
||||||
|
public void setDayDuration(float seconds) { timeScale = 1f / seconds; }
|
||||||
|
|
||||||
|
public void setPaused(boolean paused) { this.paused = paused; }
|
||||||
|
|
||||||
|
public boolean isPaused() { return paused; }
|
||||||
|
|
||||||
|
/** Aktuelle Spielstunde (0–23). */
|
||||||
|
public int getHour() { return (int)(timeOfDay * 24f); }
|
||||||
|
|
||||||
|
/** Aktuelle Spielminute (0–59). */
|
||||||
|
public int getMinute() { return (int)(timeOfDay * 24f * 60f) % 60; }
|
||||||
|
|
||||||
|
public void addListener(TimeListener l) { listeners.add(l); }
|
||||||
|
public void removeListener(TimeListener l) { listeners.remove(l); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package de.blight.common.time;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface TimeListener {
|
||||||
|
/** @param timeOfDay 0.0 = Mitternacht, 0.25 = 6 Uhr, 0.5 = Mittag, 0.75 = 18 Uhr */
|
||||||
|
void onTimeChanged(float timeOfDay);
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ javafx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
mainClass = 'de.blight.editor.EditorLauncher'
|
mainClass = 'de.blight.editor.BlightEditor'
|
||||||
applicationDefaultJvmArgs = [
|
applicationDefaultJvmArgs = [
|
||||||
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
|
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
|
||||||
'--add-opens', 'java.desktop/sun.awt=ALL-UNNAMED',
|
'--add-opens', 'java.desktop/sun.awt=ALL-UNNAMED',
|
||||||
@@ -36,7 +36,13 @@ dependencies {
|
|||||||
implementation "org.jmonkeyengine:jme3-lwjgl3:${jmeVersion}"
|
implementation "org.jmonkeyengine:jme3-lwjgl3:${jmeVersion}"
|
||||||
implementation "org.jmonkeyengine:jme3-terrain:${jmeVersion}"
|
implementation "org.jmonkeyengine:jme3-terrain:${jmeVersion}"
|
||||||
implementation "org.jmonkeyengine:jme3-effects:${jmeVersion}"
|
implementation "org.jmonkeyengine:jme3-effects:${jmeVersion}"
|
||||||
|
implementation "org.jmonkeyengine:jme3-plugins:${jmeVersion}"
|
||||||
implementation "org.jmonkeyengine:jme3-testdata:${jmeVersion}"
|
implementation "org.jmonkeyengine:jme3-testdata:${jmeVersion}"
|
||||||
|
implementation 'com.google.code.gson:gson:2.11.0'
|
||||||
|
implementation 'org.slf4j:slf4j-api:2.0.17'
|
||||||
|
implementation 'org.slf4j:jul-to-slf4j:2.0.17'
|
||||||
|
compileOnly 'org.projectlombok:lombok:1.18.38'
|
||||||
|
annotationProcessor 'org.projectlombok:lombok:1.18.38'
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('extractNatives', Copy) {
|
tasks.register('extractNatives', Copy) {
|
||||||
@@ -65,6 +71,6 @@ run {
|
|||||||
|
|
||||||
jar {
|
jar {
|
||||||
manifest {
|
manifest {
|
||||||
attributes 'Main-Class': application.mainClass
|
attributes 'Main-Class': application.mainClass.get()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
package de.blight.editor;
|
package de.blight.editor;
|
||||||
|
|
||||||
|
import org.slf4j.bridge.SLF4JBridgeHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Separater Launcher-Einstiegspunkt, damit der JavaFX-Klassenpfad korrekt
|
* Separater Launcher-Einstiegspunkt, damit der JavaFX-Klassenpfad korrekt
|
||||||
* aufgelöst wird, bevor Application.launch() aufgerufen wird.
|
* aufgelöst wird, bevor Application.launch() aufgerufen wird.
|
||||||
*/
|
*/
|
||||||
public class EditorLauncher {
|
public class BlightEditor {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
SLF4JBridgeHandler.removeHandlersForRootLogger();
|
||||||
|
SLF4JBridgeHandler.install();
|
||||||
|
|
||||||
// ProjectRoot muss als erstes initialisiert werden, damit alle
|
// ProjectRoot muss als erstes initialisiert werden, damit alle
|
||||||
// relativen Pfade korrekt aufgelöst werden (auch bei IDE-Start mit
|
// relativen Pfade korrekt aufgelöst werden (auch bei IDE-Start mit
|
||||||
// workingDir = blight-editor/ statt Projekt-Root).
|
// workingDir = blight-editor/ statt Projekt-Root).
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,23 +7,37 @@ import com.jme3.system.JmeContext;
|
|||||||
import com.jme3.texture.FrameBuffer;
|
import com.jme3.texture.FrameBuffer;
|
||||||
import com.jme3.texture.Image;
|
import com.jme3.texture.Image;
|
||||||
import com.jme3.texture.Texture2D;
|
import com.jme3.texture.Texture2D;
|
||||||
|
import de.blight.editor.state.AnimPreviewState;
|
||||||
|
import de.blight.editor.state.EmitterState;
|
||||||
|
import de.blight.editor.state.MusicAreaState;
|
||||||
|
import de.blight.editor.state.PlayToolState;
|
||||||
|
import de.blight.editor.state.SoundAreaState;
|
||||||
|
import de.blight.editor.state.WaterBodyState;
|
||||||
import de.blight.editor.state.EzTreeState;
|
import de.blight.editor.state.EzTreeState;
|
||||||
|
import de.blight.editor.state.LightState;
|
||||||
import de.blight.editor.state.PalmGeneratorState;
|
import de.blight.editor.state.PalmGeneratorState;
|
||||||
import de.blight.editor.state.SceneObjectState;
|
import de.blight.editor.state.SceneObjectState;
|
||||||
import de.blight.editor.state.TerrainEditorState;
|
import de.blight.editor.state.TerrainEditorState;
|
||||||
import de.blight.editor.state.TreeGeneratorState;
|
import de.blight.editor.state.TreeGeneratorState;
|
||||||
|
import de.blight.game.console.JmeConsole;
|
||||||
|
import de.blight.game.state.DayNightState;
|
||||||
import javafx.scene.image.WritableImage;
|
import javafx.scene.image.WritableImage;
|
||||||
|
|
||||||
public class JmeEditorApp extends SimpleApplication {
|
public class JmeEditorApp extends SimpleApplication {
|
||||||
|
|
||||||
private final SharedInput input;
|
private final SharedInput input;
|
||||||
private final WritableImage jfxImage;
|
private final WritableImage initialImage;
|
||||||
private final int vpWidth;
|
private final int vpWidth;
|
||||||
private final int vpHeight;
|
private final int vpHeight;
|
||||||
|
|
||||||
|
private JmeConsole jmeConsole;
|
||||||
|
private FrameTransfer frameTransfer;
|
||||||
|
private int currentW;
|
||||||
|
private int currentH;
|
||||||
|
|
||||||
public JmeEditorApp(SharedInput input, WritableImage jfxImage, int vpWidth, int vpHeight) {
|
public JmeEditorApp(SharedInput input, WritableImage jfxImage, int vpWidth, int vpHeight) {
|
||||||
this.input = input;
|
this.input = input;
|
||||||
this.jfxImage = jfxImage;
|
this.initialImage = jfxImage;
|
||||||
this.vpWidth = vpWidth;
|
this.vpWidth = vpWidth;
|
||||||
this.vpHeight = vpHeight;
|
this.vpHeight = vpHeight;
|
||||||
}
|
}
|
||||||
@@ -53,33 +67,125 @@ public class JmeEditorApp extends SimpleApplication {
|
|||||||
@Override
|
@Override
|
||||||
public void simpleInitApp() {
|
public void simpleInitApp() {
|
||||||
flyCam.setEnabled(false);
|
flyCam.setEnabled(false);
|
||||||
// editor-assets/ im AssetManager registrieren, damit Texturen und Modelle
|
|
||||||
// aus diesem Verzeichnis geladen werden können (relativ zum Arbeitsverzeichnis).
|
|
||||||
try {
|
|
||||||
assetManager.registerLocator(
|
|
||||||
ProjectRoot.resolve("editor-assets").toAbsolutePath().toString(),
|
|
||||||
FileLocator.class);
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
|
|
||||||
// Texture2D-Attachment: readFrameBuffer() funktioniert nur mit Texture, nicht Renderbuffer
|
// Explizit registrieren, falls General.cfg die Klassen beim ersten Start
|
||||||
Texture2D colorTex = new Texture2D(vpWidth, vpHeight, Image.Format.RGBA8);
|
// noch nicht gefunden hat (jme3-plugins war zuvor nicht auf dem Classpath).
|
||||||
FrameBuffer fb = new FrameBuffer(vpWidth, vpHeight, 1);
|
assetManager.registerLoader(com.jme3.scene.plugins.gltf.GltfLoader.class, "gltf");
|
||||||
fb.setDepthBuffer(Image.Format.Depth);
|
assetManager.registerLoader(com.jme3.scene.plugins.gltf.GlbLoader.class, "glb");
|
||||||
fb.setColorTexture(colorTex);
|
|
||||||
viewPort.setOutputFrameBuffer(fb);
|
|
||||||
|
|
||||||
// Frame-Export in das JavaFX-WritableImage
|
java.nio.file.Path blightAssets = ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||||
viewPort.addProcessor(new FrameTransfer(jfxImage));
|
if (java.nio.file.Files.isDirectory(blightAssets)) {
|
||||||
|
assetManager.registerLocator(blightAssets.toAbsolutePath().toString(), FileLocator.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
currentW = vpWidth;
|
||||||
|
currentH = vpHeight;
|
||||||
|
buildFrameBuffer(vpWidth, vpHeight, initialImage);
|
||||||
|
|
||||||
stateManager.attach(new SceneObjectState(input));
|
stateManager.attach(new SceneObjectState(input));
|
||||||
stateManager.attach(new TerrainEditorState(input));
|
stateManager.attach(new TerrainEditorState(input));
|
||||||
stateManager.attach(new TreeGeneratorState(input));
|
stateManager.attach(new TreeGeneratorState(input));
|
||||||
stateManager.attach(new EzTreeState(input));
|
stateManager.attach(new EzTreeState(input));
|
||||||
stateManager.attach(new PalmGeneratorState(input));
|
stateManager.attach(new PalmGeneratorState(input));
|
||||||
|
stateManager.attach(new LightState(input));
|
||||||
|
stateManager.attach(new EmitterState(input));
|
||||||
|
stateManager.attach(new WaterBodyState(input));
|
||||||
|
stateManager.attach(new SoundAreaState(input));
|
||||||
|
stateManager.attach(new MusicAreaState(input));
|
||||||
|
stateManager.attach(new PlayToolState(input));
|
||||||
|
stateManager.attach(new AnimPreviewState(input));
|
||||||
|
|
||||||
|
// JME-Konsole (Editor-Modus: kein RawInputListener – Eingabe via SharedInput)
|
||||||
|
jmeConsole = new JmeConsole(false);
|
||||||
|
registerEditorCommands();
|
||||||
|
jmeConsole.setOnVisibilityChanged(open -> {
|
||||||
|
input.consoleIsOpen = open;
|
||||||
|
if (!open) input.consoleChars.clear();
|
||||||
|
});
|
||||||
|
stateManager.attach(jmeConsole);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerEditorCommands() {
|
||||||
|
jmeConsole.registerCommand("goto", args -> {
|
||||||
|
try {
|
||||||
|
if (args.length >= 4) {
|
||||||
|
float x = Float.parseFloat(args[1]);
|
||||||
|
float y = Float.parseFloat(args[2]);
|
||||||
|
float z = Float.parseFloat(args[3]);
|
||||||
|
cam.setLocation(new com.jme3.math.Vector3f(x, y, z));
|
||||||
|
return String.format("Goto → %.1f / %.1f / %.1f", x, y, z);
|
||||||
|
} else if (args.length >= 3) {
|
||||||
|
float x = Float.parseFloat(args[1]);
|
||||||
|
float z = Float.parseFloat(args[2]);
|
||||||
|
TerrainEditorState tes = stateManager.getState(TerrainEditorState.class);
|
||||||
|
float srcH = tes != null ? tes.getTerrainHeightAt(cam.getLocation().x, cam.getLocation().z) : 0f;
|
||||||
|
float dstH = tes != null ? tes.getTerrainHeightAt(x, z) : 0f;
|
||||||
|
float y = dstH + (cam.getLocation().y - srcH);
|
||||||
|
cam.setLocation(new com.jme3.math.Vector3f(x, y, z));
|
||||||
|
return String.format("Goto → X=%.1f Z=%.1f (Y=%.1f)", x, z, y);
|
||||||
|
}
|
||||||
|
return "Syntax: goto <x> <z> oder goto <x> <y> <z>";
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return "Fehler: Koordinaten müssen Zahlen sein";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
jmeConsole.registerCommand("time", args -> {
|
||||||
|
if (args.length < 2) return "Syntax: time <0–24> (0 = Mitternacht, 12 = Mittag)";
|
||||||
|
try {
|
||||||
|
float hours = Float.parseFloat(args[1]);
|
||||||
|
if (hours < 0 || hours > 24) return "Fehler: Wert zwischen 0 und 24";
|
||||||
|
DayNightState dns = stateManager.getState(DayNightState.class);
|
||||||
|
if (dns == null) return "Tag/Nacht-System nicht aktiv";
|
||||||
|
dns.getDayTime().setTimeOfDay(hours / 24f);
|
||||||
|
int h = (int) hours;
|
||||||
|
int m = (int)((hours - h) * 60f);
|
||||||
|
return String.format("Zeit gesetzt: %02d:%02d Uhr", h, m);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return "Fehler: Zahl erwartet";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Framebuffer-Verwaltung ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void buildFrameBuffer(int w, int h, WritableImage image) {
|
||||||
|
Texture2D colorTex = new Texture2D(w, h, Image.Format.RGBA8);
|
||||||
|
FrameBuffer fb = new FrameBuffer(w, h, 1);
|
||||||
|
fb.setDepthBuffer(Image.Format.Depth);
|
||||||
|
fb.setColorTexture(colorTex);
|
||||||
|
viewPort.setOutputFrameBuffer(fb);
|
||||||
|
guiViewPort.setOutputFrameBuffer(fb);
|
||||||
|
frameTransfer = new FrameTransfer(image);
|
||||||
|
guiViewPort.addProcessor(frameTransfer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resizeViewport(int newW, int newH) {
|
||||||
|
guiViewPort.removeProcessor(frameTransfer);
|
||||||
|
|
||||||
|
cam.resize(newW, newH, true);
|
||||||
|
guiViewPort.getCamera().resize(newW, newH, false);
|
||||||
|
|
||||||
|
WritableImage newImage = new WritableImage(newW, newH);
|
||||||
|
buildFrameBuffer(newW, newH, newImage);
|
||||||
|
|
||||||
|
if (jmeConsole != null) jmeConsole.rebuild();
|
||||||
|
|
||||||
|
input.resizedImage.set(newImage);
|
||||||
|
currentW = newW;
|
||||||
|
currentH = newH;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void simpleUpdate(float tpf) {
|
public void simpleUpdate(float tpf) {
|
||||||
|
// Viewport-Resize (von JavaFX angefordert)
|
||||||
|
int[] req = input.resizeRequest.getAndSet(null);
|
||||||
|
if (req != null && req[0] > 0 && req[1] > 0
|
||||||
|
&& (req[0] != currentW || req[1] != currentH)) {
|
||||||
|
resizeViewport(req[0], req[1]);
|
||||||
|
}
|
||||||
|
|
||||||
com.jme3.math.Vector3f loc = cam.getLocation();
|
com.jme3.math.Vector3f loc = cam.getLocation();
|
||||||
com.jme3.math.Vector3f dir = cam.getDirection();
|
com.jme3.math.Vector3f dir = cam.getDirection();
|
||||||
input.camX = loc.x;
|
input.camX = loc.x;
|
||||||
@@ -89,54 +195,28 @@ public class JmeEditorApp extends SimpleApplication {
|
|||||||
input.camPitch = (float) Math.toDegrees(
|
input.camPitch = (float) Math.toDegrees(
|
||||||
Math.asin(Math.max(-1f, Math.min(1f, dir.y))));
|
Math.asin(Math.max(-1f, Math.min(1f, dir.y))));
|
||||||
|
|
||||||
String cmd = input.pendingCommand;
|
if (jmeConsole == null) return;
|
||||||
if (cmd != null) {
|
|
||||||
input.pendingCommand = null;
|
// Toggle-Signal von JavaFX
|
||||||
processCommand(cmd);
|
if (input.consoleToggle) {
|
||||||
}
|
input.consoleToggle = false;
|
||||||
|
jmeConsole.toggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processCommand(String raw) {
|
if (!jmeConsole.isOpen()) return;
|
||||||
String[] parts = raw.trim().split("\\s+");
|
|
||||||
switch (parts[0].toLowerCase()) {
|
// Zeichen-Eingabe
|
||||||
case "goto" -> {
|
Character c;
|
||||||
try {
|
while ((c = input.consoleChars.poll()) != null) jmeConsole.feedChar(c);
|
||||||
if (parts.length >= 4) {
|
|
||||||
// goto x y z — direkte Koordinaten
|
// Sondertasten
|
||||||
float x = Float.parseFloat(parts[1]);
|
Integer key;
|
||||||
float y = Float.parseFloat(parts[2]);
|
while ((key = input.consoleKeys.poll()) != null) {
|
||||||
float z = Float.parseFloat(parts[3]);
|
switch (key) {
|
||||||
cam.setLocation(new com.jme3.math.Vector3f(x, y, z));
|
case 8 -> jmeConsole.feedBackspace();
|
||||||
input.consoleOutput = "Goto → " + x + " / " + y + " / " + z;
|
case 10 -> jmeConsole.feedEnter();
|
||||||
} else if (parts.length >= 3) {
|
case 27 -> jmeConsole.feedEscape();
|
||||||
// goto x z — Bodenabstand beibehalten
|
}
|
||||||
float x = Float.parseFloat(parts[1]);
|
|
||||||
float z = Float.parseFloat(parts[2]);
|
|
||||||
TerrainEditorState tes =
|
|
||||||
stateManager.getState(TerrainEditorState.class);
|
|
||||||
float srcGround = tes != null
|
|
||||||
? tes.getTerrainHeightAt(cam.getLocation().x,
|
|
||||||
cam.getLocation().z)
|
|
||||||
: 0f;
|
|
||||||
float heightAboveGround = cam.getLocation().y - srcGround;
|
|
||||||
float dstGround = tes != null
|
|
||||||
? tes.getTerrainHeightAt(x, z)
|
|
||||||
: 0f;
|
|
||||||
float y = dstGround + heightAboveGround;
|
|
||||||
cam.setLocation(new com.jme3.math.Vector3f(x, y, z));
|
|
||||||
input.consoleOutput = "Goto → X=" + x + " Z=" + z
|
|
||||||
+ " (Y=" + String.format("%.1f", y) + ")";
|
|
||||||
} else {
|
|
||||||
input.consoleOutput = "Syntax: goto <x> <z> oder goto <x> <y> <z>";
|
|
||||||
}
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
input.consoleOutput = "Fehler: Koordinaten müssen Zahlen sein";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "help" -> input.consoleOutput =
|
|
||||||
"Befehle: goto <x> <z> | goto <x> <y> <z> | help";
|
|
||||||
default ->
|
|
||||||
input.consoleOutput = "Unbekannter Befehl: " + parts[0];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import javafx.scene.image.WritableImage;
|
|||||||
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
/** Thread-safe Brücke: JavaFX-Events → JME3-Update-Schleife. */
|
/** Thread-safe Brücke: JavaFX-Events → JME3-Update-Schleife. */
|
||||||
public class SharedInput {
|
public class SharedInput {
|
||||||
@@ -19,18 +20,27 @@ public class SharedInput {
|
|||||||
// ── Aktive Tools ─────────────────────────────────────────────────────────
|
// ── Aktive Tools ─────────────────────────────────────────────────────────
|
||||||
public final HeightTool heightTool = new HeightTool();
|
public final HeightTool heightTool = new HeightTool();
|
||||||
public final UpperHeightTool upperHeightTool = new UpperHeightTool();
|
public final UpperHeightTool upperHeightTool = new UpperHeightTool();
|
||||||
public final HoleTool holeTool = new HoleTool();
|
|
||||||
public final GrassTool grassTool = new GrassTool();
|
public final GrassTool grassTool = new GrassTool();
|
||||||
public final TextureTool textureTool = new TextureTool();
|
public final TextureTool textureTool = new TextureTool();
|
||||||
|
public final HoleTool holeTool = new HoleTool();
|
||||||
public volatile EditorTool activeTool = heightTool;
|
public volatile EditorTool activeTool = heightTool;
|
||||||
|
|
||||||
// ── Aktive Ebene: 0=Basis-Terrain, 1=Gebirge, 2=Höhlen, 3=Gras, 4=Textur ──
|
// ── Aktive Ebene: 0=Basis-Terrain, 3=Gras, 4=Textur ─────────────────────
|
||||||
public volatile int activeLayer = 0;
|
public volatile int activeLayer = 0;
|
||||||
|
|
||||||
|
// ── Upper-Layer-Sichtbarkeit ─────────────────────────────────────────────
|
||||||
public volatile boolean upperLayerVisible = true;
|
public volatile boolean upperLayerVisible = true;
|
||||||
|
|
||||||
// ── Kamerabewegung (WASD + QE) ──────────────────────────────────────────
|
// ── Kamerabewegung (WASD + QE) ──────────────────────────────────────────
|
||||||
public volatile boolean forward, backward, left, right, up, down;
|
public volatile boolean forward, backward, left, right, up, down;
|
||||||
|
|
||||||
|
// ── Mausrad (JavaFX akkumuliert, JME konsumiert einmal pro Frame) ────────
|
||||||
|
public final java.util.concurrent.atomic.AtomicInteger scrollAccum =
|
||||||
|
new java.util.concurrent.atomic.AtomicInteger();
|
||||||
|
|
||||||
|
// ── Shift: horizontale Bewegung (Y-Höhe wird beibehalten) ───────────────
|
||||||
|
public volatile boolean shiftHeld;
|
||||||
|
|
||||||
// ── Kamerarotation (Maus-Drag mit mittlerer Taste) ───────────────────────
|
// ── Kamerarotation (Maus-Drag mit mittlerer Taste) ───────────────────────
|
||||||
private final AtomicInteger mouseDxAccum = new AtomicInteger();
|
private final AtomicInteger mouseDxAccum = new AtomicInteger();
|
||||||
private final AtomicInteger mouseDyAccum = new AtomicInteger();
|
private final AtomicInteger mouseDyAccum = new AtomicInteger();
|
||||||
@@ -48,10 +58,6 @@ public class SharedInput {
|
|||||||
public record TerrainEdit(float screenX, float screenY, int action) {}
|
public record TerrainEdit(float screenX, float screenY, int action) {}
|
||||||
public final ConcurrentLinkedQueue<TerrainEdit> editQueue = new ConcurrentLinkedQueue<>();
|
public final ConcurrentLinkedQueue<TerrainEdit> editQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
// ── Upper-Layer-Edits ─────────────────────────────────────────────────────
|
|
||||||
public record UpperLayerEdit(float screenX, float screenY, int action) {}
|
|
||||||
public final ConcurrentLinkedQueue<UpperLayerEdit> upperLayerEditQueue = new ConcurrentLinkedQueue<>();
|
|
||||||
|
|
||||||
// ── Gras-Edits ────────────────────────────────────────────────────────────
|
// ── Gras-Edits ────────────────────────────────────────────────────────────
|
||||||
/** action +1 = Dichte erhöhen (Linksklick), -1 = Dichte verringern (Rechtsklick). */
|
/** action +1 = Dichte erhöhen (Linksklick), -1 = Dichte verringern (Rechtsklick). */
|
||||||
public record GrassEdit(float screenX, float screenY, int action) {}
|
public record GrassEdit(float screenX, float screenY, int action) {}
|
||||||
@@ -62,10 +68,35 @@ public class SharedInput {
|
|||||||
public record TextureEdit(float screenX, float screenY, int action) {}
|
public record TextureEdit(float screenX, float screenY, int action) {}
|
||||||
public final ConcurrentLinkedQueue<TextureEdit> textureEditQueue = new ConcurrentLinkedQueue<>();
|
public final ConcurrentLinkedQueue<TextureEdit> textureEditQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
|
// ── Textur-Konfiguration (JavaFX → JME3) ─────────────────────────────────
|
||||||
|
/** Pfade der 4 Terrain-Textur-Slots ("" = Standard). JFX schreibt neue Referenz, JME liest. */
|
||||||
|
public volatile String[] terrainTexturePaths = new String[]{"", "", "", ""};
|
||||||
|
/** JFX setzt true, JME liest + resettet. */
|
||||||
|
public volatile boolean terrainTexturesChanged = false;
|
||||||
|
/** Normal-Map-Pfade der 4 Terrain-Slots ("" = keine). */
|
||||||
|
public volatile String[] terrainNormalMapPaths = new String[]{"", "", "", ""};
|
||||||
|
public volatile boolean terrainNormalMapsChanged = false;
|
||||||
|
|
||||||
|
/** Pfade der 4 Gebirge-Textur-Slots ("" = Standard). */
|
||||||
|
public volatile String[] upperTexturePaths = new String[]{"", "", "", ""};
|
||||||
|
public volatile boolean upperTexturesChanged = false;
|
||||||
|
/** Normal-Map-Pfade der 4 Gebirge-Slots ("" = keine). */
|
||||||
|
public volatile String[] upperNormalMapPaths = new String[]{"", "", "", ""};
|
||||||
|
public volatile boolean upperNormalMapsChanged = false;
|
||||||
|
|
||||||
|
/** JME setzt true nach Map-Load → JFX kann Textur-UI aktualisieren. */
|
||||||
|
public volatile boolean texturePathsLoaded = false;
|
||||||
|
|
||||||
// ── Viewport-Skalierung (JavaFX-Pixel → JME3-Pixel) ─────────────────────
|
// ── Viewport-Skalierung (JavaFX-Pixel → JME3-Pixel) ─────────────────────
|
||||||
public volatile double viewportScaleX = 1.0;
|
public volatile double viewportScaleX = 1.0;
|
||||||
public volatile double viewportScaleY = 1.0;
|
public volatile double viewportScaleY = 1.0;
|
||||||
|
|
||||||
|
// ── Viewport-Resize (JavaFX → JME → JavaFX) ──────────────────────────────
|
||||||
|
/** JavaFX setzt neue Zielgröße; JME liest einmalig per getAndSet(null). */
|
||||||
|
public final AtomicReference<int[]> resizeRequest = new AtomicReference<>();
|
||||||
|
/** JME setzt fertiges WritableImage nach Resize; JavaFX liest per getAndSet(null). */
|
||||||
|
public final AtomicReference<WritableImage> resizedImage = new AtomicReference<>();
|
||||||
|
|
||||||
// ── Mausposition im Viewport (JavaFX-Pixel, -1 = außerhalb) ─────────────
|
// ── Mausposition im Viewport (JavaFX-Pixel, -1 = außerhalb) ─────────────
|
||||||
public volatile float mouseScreenX = -1f;
|
public volatile float mouseScreenX = -1f;
|
||||||
public volatile float mouseScreenY = -1f;
|
public volatile float mouseScreenY = -1f;
|
||||||
@@ -83,6 +114,7 @@ public class SharedInput {
|
|||||||
public volatile int treePreviewH = 1024;
|
public volatile int treePreviewH = 1024;
|
||||||
public volatile String treeGenStatusMsg = null;
|
public volatile String treeGenStatusMsg = null;
|
||||||
public volatile boolean refreshAssets = false;
|
public volatile boolean refreshAssets = false;
|
||||||
|
public volatile boolean refreshTreeFolders = false;
|
||||||
/**
|
/**
|
||||||
* Aktuelles Vorschau-Bild. JME3 ersetzt die Referenz bei Größenänderung;
|
* Aktuelles Vorschau-Bild. JME3 ersetzt die Referenz bei Größenänderung;
|
||||||
* treePreviewResized signalisiert JavaFX, die ImageView zu aktualisieren.
|
* treePreviewResized signalisiert JavaFX, die ImageView zu aktualisieren.
|
||||||
@@ -95,7 +127,7 @@ public class SharedInput {
|
|||||||
public final ConcurrentLinkedQueue<TreeGenRequest> treeGenQueue = new ConcurrentLinkedQueue<>();
|
public final ConcurrentLinkedQueue<TreeGenRequest> treeGenQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
// ── EZ-Tree-Generator ─────────────────────────────────────────────────────
|
// ── EZ-Tree-Generator ─────────────────────────────────────────────────────
|
||||||
public record EzTreeGenRequest(de.blight.eztree.TreeOptions options, boolean exportAfter, String exportName) {}
|
public record EzTreeGenRequest(de.blight.eztree.TreeOptions options, String presetName, boolean exportAfter, String exportName, String treeCategory) {}
|
||||||
public final ConcurrentLinkedQueue<EzTreeGenRequest> ezTreeGenQueue = new ConcurrentLinkedQueue<>();
|
public final ConcurrentLinkedQueue<EzTreeGenRequest> ezTreeGenQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
// ── Palmen-Generator ──────────────────────────────────────────────────────
|
// ── Palmen-Generator ──────────────────────────────────────────────────────
|
||||||
@@ -107,9 +139,27 @@ public class SharedInput {
|
|||||||
public static final int LAYER_OBJECTS = 5;
|
public static final int LAYER_OBJECTS = 5;
|
||||||
/** activeLayer==6 → Objekte bearbeiten (Selektion + Gizmo) */
|
/** activeLayer==6 → Objekte bearbeiten (Selektion + Gizmo) */
|
||||||
public static final int LAYER_OBJECTS_EDIT = 6;
|
public static final int LAYER_OBJECTS_EDIT = 6;
|
||||||
|
/** activeLayer==7 → Lichtquellen platzieren und bearbeiten */
|
||||||
|
public static final int LAYER_LIGHTS = 7;
|
||||||
|
|
||||||
|
// Selektionsmodi (LAYER_OBJECTS_EDIT)
|
||||||
|
public static final int SEL_MODE_OBJECT = 0;
|
||||||
|
public static final int SEL_MODE_POLYGON = 1; // ganze Geometry (war SEL_MODE_FACE)
|
||||||
|
public static final int SEL_MODE_EDGE = 2;
|
||||||
|
public static final int SEL_MODE_VERTEX = 3; // einzelner Punkt
|
||||||
|
|
||||||
|
// Bearbeitungswerkzeuge (LAYER_OBJECTS_EDIT)
|
||||||
|
public static final int EDIT_TOOL_MOVE = 0;
|
||||||
|
public static final int EDIT_TOOL_ROTATE = 1;
|
||||||
|
public static final int EDIT_TOOL_SCALE = 2;
|
||||||
|
|
||||||
|
/** JavaFX → JME3: Aktiver Selektionsmodus. */
|
||||||
|
public volatile int objectSelectionMode = SEL_MODE_OBJECT;
|
||||||
|
/** JavaFX → JME3: Aktives Bearbeitungswerkzeug. */
|
||||||
|
public volatile int objectEditTool = EDIT_TOOL_MOVE;
|
||||||
|
|
||||||
/** Klick im Viewport: Objekt auswählen oder am Terrain-Treffpunkt platzieren. */
|
/** Klick im Viewport: Objekt auswählen oder am Terrain-Treffpunkt platzieren. */
|
||||||
public record ObjectClick(float screenX, float screenY, boolean rightButton) {}
|
public record ObjectClick(float screenX, float screenY, boolean rightButton, boolean shift) {}
|
||||||
public final ConcurrentLinkedQueue<ObjectClick> objectClickQueue = new ConcurrentLinkedQueue<>();
|
public final ConcurrentLinkedQueue<ObjectClick> objectClickQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -120,7 +170,9 @@ public class SharedInput {
|
|||||||
public final ConcurrentLinkedQueue<ObjectDrag> objectDragQueue = new ConcurrentLinkedQueue<>();
|
public final ConcurrentLinkedQueue<ObjectDrag> objectDragQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
/** Wird von JME3 gesetzt wenn ein neues Objekt oder eine neue Selektion vorliegt. */
|
/** Wird von JME3 gesetzt wenn ein neues Objekt oder eine neue Selektion vorliegt. */
|
||||||
public volatile String selectedObjectInfo = null; // "modelPath|solid|x|y|z|rotY|scale"
|
// Format: "1|modelPath|solid|x|y|z|rotX|rotY|rotZ|scale|texPath" (1 Objekt)
|
||||||
|
// "N" (N≥2 Objekte ausgewählt)
|
||||||
|
public volatile String selectedObjectInfo = null;
|
||||||
public volatile boolean objectSelectionChanged = false;
|
public volatile boolean objectSelectionChanged = false;
|
||||||
/** Wird von JME3 gesetzt, wenn ein Objekt gerade neu platziert wurde (nicht nur selektiert). */
|
/** Wird von JME3 gesetzt, wenn ein Objekt gerade neu platziert wurde (nicht nur selektiert). */
|
||||||
public volatile boolean objectJustPlaced = false;
|
public volatile boolean objectJustPlaced = false;
|
||||||
@@ -128,9 +180,41 @@ public class SharedInput {
|
|||||||
/** JavaFX → JME3: Modell-Pfad für nächste Platzierung (relativ zu editor-assets/). */
|
/** JavaFX → JME3: Modell-Pfad für nächste Platzierung (relativ zu editor-assets/). */
|
||||||
public volatile String pendingModelPath = null;
|
public volatile String pendingModelPath = null;
|
||||||
|
|
||||||
|
/** Wenn gesetzt: Baum-Ordner-Platzierungsmodus. Relativ zu blight-assets/src/main/resources. */
|
||||||
|
public volatile String treeFolderPath = null;
|
||||||
|
/** Status-Meldung aus JME: welcher Baum gerade ausgewählt ist. */
|
||||||
|
public volatile String randomTreeStatus = "";
|
||||||
|
|
||||||
|
/** JavaFX → JME3: Textur-Pfad für nächste Platzierung (relativ zu editor-assets/, "" = keine). */
|
||||||
|
public volatile String pendingTexturePath = "";
|
||||||
|
/** JavaFX → JME3: Normal-Map-Pfad für nächste Platzierung ("" = keine). */
|
||||||
|
public volatile String pendingNormalMapPath = "";
|
||||||
|
|
||||||
/** JavaFX → JME3: Solid-Flag des selektierten Objekts ändern. */
|
/** JavaFX → JME3: Solid-Flag des selektierten Objekts ändern. */
|
||||||
public volatile Boolean pendingSolidChange = null;
|
public volatile Boolean pendingSolidChange = null;
|
||||||
|
|
||||||
|
/** JavaFX → JME: AnimationLibrary-Clip-Key für das selektierte Objekt. null = kein Auftrag. */
|
||||||
|
public volatile String pendingAnimClip = null;
|
||||||
|
|
||||||
|
// Objekt-Eigenschaften (Position, Rotation, Textur) von JavaFX setzen
|
||||||
|
public record ObjectPropertyChange(
|
||||||
|
float x, float y, float z,
|
||||||
|
float rotX, float rotY, float rotZ,
|
||||||
|
boolean solid,
|
||||||
|
String texPath, // null = nicht ändern
|
||||||
|
String normalMapPath, // null = nicht ändern
|
||||||
|
String matPath // null = nicht ändern
|
||||||
|
) {}
|
||||||
|
public final ConcurrentLinkedQueue<ObjectPropertyChange> objectPropertyQueue =
|
||||||
|
new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
|
// Selektion zusammenfassen (JavaFX → JME)
|
||||||
|
public volatile boolean mergeSelectedRequested = false;
|
||||||
|
// Selektion löschen (JavaFX → JME)
|
||||||
|
public volatile boolean deleteSelectedRequested = false;
|
||||||
|
// Als Vorlage speichern: Name (JavaFX → JME, null = kein Auftrag)
|
||||||
|
public volatile String saveAsTemplateRequest = null;
|
||||||
|
|
||||||
// ── Mesh-Erstellung ───────────────────────────────────────────────────────
|
// ── Mesh-Erstellung ───────────────────────────────────────────────────────
|
||||||
/**
|
/**
|
||||||
* Form: "Box" | "Kugel" | "Zylinder" | "Ebene"
|
* Form: "Box" | "Kugel" | "Zylinder" | "Ebene"
|
||||||
@@ -159,21 +243,225 @@ public class SharedInput {
|
|||||||
/** Pitch in Grad: positiv = Blick nach oben, negativ = nach unten. */
|
/** Pitch in Grad: positiv = Blick nach oben, negativ = nach unten. */
|
||||||
public volatile float camPitch = 0f;
|
public volatile float camPitch = 0f;
|
||||||
|
|
||||||
// ── Konsole (JavaFX → JME3 und zurück) ──────────────────────────────────
|
// ── JME-Konsole: JavaFX → JME-Thread ────────────────────────────────────
|
||||||
/** Befehl, der beim nächsten JME3-Update ausgeführt werden soll. */
|
/** Toggle-Signal: JavaFX setzt true, JME liest und setzt zurück. */
|
||||||
public volatile String pendingCommand = null;
|
public volatile boolean consoleToggle = false;
|
||||||
/** Antworttext, den JME3 nach der Befehlsausführung setzt. */
|
/** Zeichen-Eingabe (druckbare Zeichen ≥ 0x20). */
|
||||||
|
public final ConcurrentLinkedQueue<Character> consoleChars = new ConcurrentLinkedQueue<>();
|
||||||
|
/** Sondertasten: 8=Backspace, 10=Enter, 27=Escape. */
|
||||||
|
public final ConcurrentLinkedQueue<Integer> consoleKeys = new ConcurrentLinkedQueue<>();
|
||||||
|
/** JME setzt diesen Wert; JavaFX liest ihn (z. B. für Eingabesperre). */
|
||||||
|
public volatile boolean consoleIsOpen = false;
|
||||||
|
|
||||||
|
/** @deprecated Nur noch für Rückwärtskompatibilität – nicht mehr verwenden. */
|
||||||
|
@Deprecated public volatile String pendingCommand = null;
|
||||||
|
/** Status-/Fehlermeldungen von JME an JavaFX-Statusleiste. */
|
||||||
public volatile String consoleOutput = null;
|
public volatile String consoleOutput = null;
|
||||||
|
|
||||||
|
// ── Vertex-Snap ───────────────────────────────────────────────────────────
|
||||||
|
/** Wenn true: Punkte, die beim Ziehen in Snap-Radius geraten, verschmelzen. */
|
||||||
|
public volatile boolean vertexSnapEnabled = false;
|
||||||
|
public volatile float vertexSnapRadius = 0.5f;
|
||||||
|
/** JavaFX setzt true beim Loslassen der Maustaste → JME3 führt Snap aus. */
|
||||||
|
public volatile boolean vertexSnapTrigger = false;
|
||||||
|
|
||||||
|
// ── Licht-Werkzeug ────────────────────────────────────────────────────────
|
||||||
|
/** Klick im Viewport im Licht-Modus: platzieren oder selektieren. */
|
||||||
|
public record LightClick(float screenX, float screenY, boolean rightButton) {}
|
||||||
|
public final ConcurrentLinkedQueue<LightClick> lightClickQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
|
/** JME → JavaFX: Info des selektierten Lichts. Format: "idx|x|y|z|r|g|b|intensity|radius" oder null. */
|
||||||
|
public volatile String selectedLightInfo = null;
|
||||||
|
public volatile boolean lightSelectionChanged = false;
|
||||||
|
|
||||||
|
/** JavaFX → JME: Eigenschaftsänderung des selektierten Lichts. */
|
||||||
|
public record LightPropertyChange(float r, float g, float b, float intensity, float radius) {}
|
||||||
|
public final AtomicReference<LightPropertyChange> pendingLightProp = new AtomicReference<>();
|
||||||
|
|
||||||
|
/** JavaFX → JME: Selektiertes Licht löschen. */
|
||||||
|
public volatile boolean deleteLightRequested = false;
|
||||||
|
|
||||||
|
// ── Emitter-Werkzeug ──────────────────────────────────────────────────────
|
||||||
|
/** activeLayer==8 → Partikel-Emitter platzieren und bearbeiten */
|
||||||
|
public static final int LAYER_EMITTERS = 8;
|
||||||
|
|
||||||
|
/** Klick im Viewport im Emitter-Modus: platzieren oder selektieren. */
|
||||||
|
public record EmitterClick(float screenX, float screenY, boolean rightButton) {}
|
||||||
|
public final ConcurrentLinkedQueue<EmitterClick> emitterClickQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JME → JavaFX: Info des selektierten Emitters.
|
||||||
|
* Format: "idx|x|y|z|activationRadius|texturePath|imagesX|imagesY|
|
||||||
|
* startR|startG|startB|startA|endR|endG|endB|endA|
|
||||||
|
* startSize|endSize|velX|velY|velZ|velVar|
|
||||||
|
* gravX|gravY|gravZ|lowLife|highLife|maxParticles|emitRate"
|
||||||
|
* oder null wenn nichts gewählt.
|
||||||
|
*/
|
||||||
|
public volatile String selectedEmitterInfo = null;
|
||||||
|
public volatile boolean emitterSelectionChanged = false;
|
||||||
|
|
||||||
|
/** JavaFX → JME: vollständige aktualisierte Parameter des selektierten Emitters. */
|
||||||
|
public final AtomicReference<de.blight.common.PlacedEmitter> pendingEmitter = new AtomicReference<>();
|
||||||
|
|
||||||
|
/** JavaFX → JME: Selektierten Emitter löschen. */
|
||||||
|
public volatile boolean deleteEmitterRequested = false;
|
||||||
|
|
||||||
|
/** JavaFX → JME: Preset für nächsten neu platzierten Emitter (0=Feuer,1=Rauch,2=Funken). */
|
||||||
|
public volatile int emitterPreset = 0;
|
||||||
|
|
||||||
|
// ── Wasser-Werkzeug ───────────────────────────────────────────────────────
|
||||||
|
/** activeLayer==9 → Wasseroberflächen platzieren und bearbeiten */
|
||||||
|
public static final int LAYER_WATER = 9;
|
||||||
|
|
||||||
|
// ── Sound-Bereiche ────────────────────────────────────────────────────────
|
||||||
|
/** activeLayer==10 → Sound-Bereiche (Polygon) platzieren und bearbeiten */
|
||||||
|
public static final int LAYER_SOUND_AREAS = 10;
|
||||||
|
|
||||||
|
// ── Musik-Bereiche ────────────────────────────────────────────────────────
|
||||||
|
/** activeLayer==11 → Musik-Bereiche (Polygon) platzieren und bearbeiten */
|
||||||
|
public static final int LAYER_MUSIC_AREAS = 11;
|
||||||
|
|
||||||
|
// ── Spiel-Starten-Werkzeug ────────────────────────────────────────────────
|
||||||
|
/** activeLayer==12 → Temporären Spawnpunkt setzen und Spiel starten */
|
||||||
|
public static final int LAYER_PLAY_TOOL = 12;
|
||||||
|
|
||||||
|
/** Klick im Viewport im Wasser-Modus: platzieren oder selektieren. */
|
||||||
|
public record WaterClick(float screenX, float screenY, boolean rightButton) {}
|
||||||
|
public final ConcurrentLinkedQueue<WaterClick> waterClickQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JME → JavaFX: Info der selektierten Wasseroberfläche.
|
||||||
|
* Format: "idx|x|y|z|width|depth" oder null.
|
||||||
|
*/
|
||||||
|
public volatile String selectedWaterInfo = null;
|
||||||
|
public volatile boolean waterSelectionChanged = false;
|
||||||
|
|
||||||
|
/** JavaFX → JME: aktualisierte Parameter der selektierten Wasseroberfläche. */
|
||||||
|
public final AtomicReference<de.blight.common.PlacedWater> pendingWater = new AtomicReference<>();
|
||||||
|
|
||||||
|
/** JavaFX → JME: Selektierte Wasseroberfläche löschen. */
|
||||||
|
public volatile boolean deleteWaterRequested = false;
|
||||||
|
|
||||||
|
// ── Sound-Bereich-Werkzeug ────────────────────────────────────────────────
|
||||||
|
public record SoundAreaClick(float screenX, float screenY, boolean rightButton) {}
|
||||||
|
public final ConcurrentLinkedQueue<SoundAreaClick> soundAreaClickQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
|
/** JME → JavaFX: Info des selektierten Sound-Bereichs. Format: "idx|soundPath|volume|crossfade" oder null. */
|
||||||
|
public volatile String selectedSoundAreaInfo = null;
|
||||||
|
public volatile boolean soundAreaSelectionChanged = false;
|
||||||
|
|
||||||
|
/** JavaFX → JME: aktualisierte Parameter des selektierten Sound-Bereichs. */
|
||||||
|
public final AtomicReference<de.blight.common.PlacedSoundArea> pendingSoundArea = new AtomicReference<>();
|
||||||
|
|
||||||
|
/** JavaFX → JME: Selektierten Sound-Bereich löschen. */
|
||||||
|
public volatile boolean deleteSoundAreaRequested = false;
|
||||||
|
|
||||||
|
// ── Musik-Bereich-Werkzeug ────────────────────────────────────────────────
|
||||||
|
public record MusicAreaClick(float screenX, float screenY, boolean rightButton) {}
|
||||||
|
public final ConcurrentLinkedQueue<MusicAreaClick> musicAreaClickQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
|
/** JME → JavaFX: Info des selektierten Musik-Bereichs. Format: "idx|dayTrack|nightTrack|combatTrack" oder null. */
|
||||||
|
public volatile String selectedMusicAreaInfo = null;
|
||||||
|
public volatile boolean musicAreaSelectionChanged = false;
|
||||||
|
|
||||||
|
/** JavaFX → JME: aktualisierte Parameter des selektierten Musik-Bereichs. */
|
||||||
|
public final AtomicReference<de.blight.common.PlacedMusicArea> pendingMusicArea = new AtomicReference<>();
|
||||||
|
|
||||||
|
/** JavaFX → JME: Selektierten Musik-Bereich löschen. */
|
||||||
|
public volatile boolean deleteMusicAreaRequested = false;
|
||||||
|
|
||||||
|
/** JavaFX → JME: Laufendes Polygon-Zeichnen abbrechen (ESC). */
|
||||||
|
public volatile boolean cancelZoneDrawing = false;
|
||||||
|
|
||||||
|
// ── Spiel-Starten-Werkzeug ────────────────────────────────────────────────
|
||||||
|
/** Klick im Viewport zum Setzen des temporären Spawnpunkts. */
|
||||||
|
public record PlayToolClick(float screenX, float screenY) {}
|
||||||
|
public final ConcurrentLinkedQueue<PlayToolClick> playToolClickQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JME → JavaFX: Terrain-Treffpunkt nach Spawn-Klick.
|
||||||
|
* Format: "x|z" oder null.
|
||||||
|
*/
|
||||||
|
public volatile String pickedSpawnInfo = null;
|
||||||
|
public volatile boolean spawnPickChanged = false;
|
||||||
|
|
||||||
|
/** Temporärer Spawnpunkt (NaN = nicht gesetzt). Wird beim Spielstart als System-Property übergeben. */
|
||||||
|
public volatile float tempSpawnX = Float.NaN;
|
||||||
|
public volatile float tempSpawnZ = Float.NaN;
|
||||||
|
|
||||||
|
// ── Animations-Vorschau ──────────────────────────────────────────────────
|
||||||
|
public volatile float animPreviewRotY = 0f;
|
||||||
|
public volatile float animPreviewRotX = 25f;
|
||||||
|
public volatile float animPreviewZoom = 1.0f;
|
||||||
|
public volatile float animPreviewSpeed = 1.0f;
|
||||||
|
public volatile int animPreviewW = 512;
|
||||||
|
public volatile int animPreviewH = 512;
|
||||||
|
public volatile WritableImage animPreviewImage = new WritableImage(512, 512);
|
||||||
|
public volatile boolean animPreviewResized = false;
|
||||||
|
/** JavaFX → JME3: Modell laden (relativer Asset-Pfad). null = kein Auftrag. */
|
||||||
|
public volatile String animPreviewLoadPath = null;
|
||||||
|
/** JavaFX → JME3: Clip abspielen; "" = Stop. null = kein Auftrag. */
|
||||||
|
public volatile String animPreviewPlayClip = null;
|
||||||
|
/** JavaFX → JME3: Animation-j3o-Pfad zum Retargeting + Hinzufügen. null = kein Auftrag. */
|
||||||
|
public volatile String animPreviewAddAnimPath = null;
|
||||||
|
/** JavaFX → JME3: Clip-Name zum Entfernen aus dem geladenen Modell. null = kein Auftrag. */
|
||||||
|
public volatile String animPreviewRemoveClip = null;
|
||||||
|
/** JavaFX → JME3: Scan aller j3o auf Skelett-Controls anstoßen. */
|
||||||
|
public volatile boolean scanSkeletalRequested = false;
|
||||||
|
public volatile boolean animDumpRequested = false;
|
||||||
|
/** JME3 → JavaFX: Relative Pfade (Assets-Root) aller j3o mit Skelett; getAndSet(null) konsumiert. */
|
||||||
|
public final java.util.concurrent.atomic.AtomicReference<java.util.Set<String>>
|
||||||
|
skeletalPaths = new java.util.concurrent.atomic.AtomicReference<>();
|
||||||
|
public volatile boolean animPreviewLoop = true;
|
||||||
|
/** JME3 → JavaFX: Status-Meldung nach Laden / Fehler. */
|
||||||
|
public volatile String animPreviewStatus = null;
|
||||||
|
/** JME3 → JavaFX: Clip-Namen nach Modell-Laden. getAndSet(null) konsumiert. */
|
||||||
|
public final java.util.concurrent.atomic.AtomicReference<java.util.List<String>>
|
||||||
|
animPreviewClips = new java.util.concurrent.atomic.AtomicReference<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JME3 → JavaFX: Relativer Asset-Pfad des gerade geladenen Modells.
|
||||||
|
* Wird von JavaFX benötigt, um die zugehörige .animset.json-Datei zu finden.
|
||||||
|
* getAndSet(null) konsumiert.
|
||||||
|
*/
|
||||||
|
public final java.util.concurrent.atomic.AtomicReference<String>
|
||||||
|
animPreviewLoadedPath = new java.util.concurrent.atomic.AtomicReference<>();
|
||||||
|
|
||||||
|
// ── Animations-Clip umbenennen ────────────────────────────────────────────
|
||||||
|
public record ClipRenameRequest(String oldName, String newName) {}
|
||||||
|
public final java.util.concurrent.atomic.AtomicReference<ClipRenameRequest>
|
||||||
|
clipRenameRequest = new java.util.concurrent.atomic.AtomicReference<>();
|
||||||
|
|
||||||
|
// ── Animations-Set speichern ──────────────────────────────────────────────
|
||||||
|
public record AnimSetSaveRequest(java.util.List<String> clips, String setName, java.util.Map<String, String> actionMap) {}
|
||||||
|
public final java.util.concurrent.atomic.AtomicReference<AnimSetSaveRequest>
|
||||||
|
animSetSaveRequest = new java.util.concurrent.atomic.AtomicReference<>();
|
||||||
|
|
||||||
|
/** JME3 → JavaFX: Status-Meldung für Clip- und Set-Operationen. */
|
||||||
|
public volatile String animOpStatus = null;
|
||||||
|
|
||||||
// ── Modell-Konvertierung ──────────────────────────────────────────────────
|
// ── Modell-Konvertierung ──────────────────────────────────────────────────
|
||||||
/**
|
/**
|
||||||
* Konvertiert ein natives Modell (OBJ/GLTF/…) zu .j3o.
|
* Konvertiert ein natives Modell (OBJ/GLTF/…) zu .j3o.
|
||||||
* assetPath : Pfad relativ zu editor-assets/ (z. B. "models/tree.obj")
|
* assetPath : Pfad relativ zu blight-assets/src/main/resources/
|
||||||
* destJ3o : absoluter Ziel-Pfad der .j3o-Datei
|
* destJ3o : absoluter Ziel-Pfad der .j3o-Datei
|
||||||
* srcToDelete : absoluter Pfad der Original-Datei (wird nach Konvertierung gelöscht)
|
* srcToDelete : absoluter Pfad der Original-Datei (wird nach Konvertierung gelöscht)
|
||||||
|
* keepControls : true = AnimComposer/SkinningControl bleiben erhalten (Animationen)
|
||||||
|
* yRotationDeg : Y-Rotation die beim Import auf das Root-Spatial angewendet wird (Grad)
|
||||||
|
* centerOrigin : true = Root-Translation auf (0,0,0) setzen
|
||||||
*/
|
*/
|
||||||
public record ModelConvertRequest(String assetPath, java.nio.file.Path destJ3o,
|
public record ModelConvertRequest(String assetPath, java.nio.file.Path destJ3o,
|
||||||
java.nio.file.Path srcToDelete) {}
|
java.nio.file.Path srcToDelete, boolean keepControls,
|
||||||
|
float yRotationDeg, boolean centerOrigin) {
|
||||||
|
public ModelConvertRequest(String assetPath, java.nio.file.Path destJ3o,
|
||||||
|
java.nio.file.Path srcToDelete) {
|
||||||
|
this(assetPath, destJ3o, srcToDelete, false, 0f, false);
|
||||||
|
}
|
||||||
|
public ModelConvertRequest(String assetPath, java.nio.file.Path destJ3o,
|
||||||
|
java.nio.file.Path srcToDelete, boolean keepControls) {
|
||||||
|
this(assetPath, destJ3o, srcToDelete, keepControls, 0f, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
public final ConcurrentLinkedQueue<ModelConvertRequest> modelConvertQueue =
|
public final ConcurrentLinkedQueue<ModelConvertRequest> modelConvertQueue =
|
||||||
new ConcurrentLinkedQueue<>();
|
new ConcurrentLinkedQueue<>();
|
||||||
}
|
}
|
||||||
|
|||||||
131
blight-editor/src/main/java/de/blight/editor/TripoGenerator.java
Normal file
131
blight-editor/src/main/java/de/blight/editor/TripoGenerator.java
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package de.blight.editor;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/** Thin wrapper around the Tripo3D v2 REST API. All methods are blocking. */
|
||||||
|
public class TripoGenerator {
|
||||||
|
|
||||||
|
private static final String BASE = "https://api.tripo3d.ai/v2/openapi";
|
||||||
|
|
||||||
|
private static final HttpClient HTTP = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(15))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// ── Data ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public record TaskResult(
|
||||||
|
String taskId,
|
||||||
|
String status, // "queued" | "running" | "success" | "failed" | "cancelled"
|
||||||
|
int progress, // 0–100
|
||||||
|
String modelUrl, // null until success
|
||||||
|
String previewUrl // null until success
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// ── Public API ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Creates a text-to-3D-model task and returns the task ID. */
|
||||||
|
public static String createTextToModelTask(String apiKey, String prompt) throws IOException {
|
||||||
|
String body = "{\"type\":\"text_to_model\",\"prompt\":" + jsonString(prompt) + "}";
|
||||||
|
return postTask(apiKey, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a skeleton-rigging task from a previously generated model.
|
||||||
|
* Tripo adds a humanoid rig suitable for animation retargeting.
|
||||||
|
*/
|
||||||
|
public static String createRigTask(String apiKey, String originalTaskId) throws IOException {
|
||||||
|
String body = "{\"type\":\"animate_rig\",\"original_model_task_id\":" + jsonString(originalTaskId) + "}";
|
||||||
|
return postTask(apiKey, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Polls task status once (non-blocking polling loop is the caller's responsibility). */
|
||||||
|
public static TaskResult pollTask(String apiKey, String taskId) throws IOException {
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(BASE + "/task/" + taskId))
|
||||||
|
.header("Authorization", "Bearer " + apiKey)
|
||||||
|
.GET()
|
||||||
|
.timeout(Duration.ofSeconds(30))
|
||||||
|
.build();
|
||||||
|
JsonObject data = send(req).getAsJsonObject("data");
|
||||||
|
String status = data.get("status").getAsString();
|
||||||
|
int progress = data.has("progress") ? data.get("progress").getAsInt() : 0;
|
||||||
|
String modelUrl = null, previewUrl = null;
|
||||||
|
if (data.has("output") && !data.get("output").isJsonNull()) {
|
||||||
|
JsonObject out = data.getAsJsonObject("output");
|
||||||
|
if (out.has("model")) modelUrl = out.get("model").getAsString();
|
||||||
|
if (out.has("rendered_image")) previewUrl = out.get("rendered_image").getAsString();
|
||||||
|
}
|
||||||
|
return new TaskResult(taskId, status, progress, modelUrl, previewUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Downloads a URL to the given path (replaces existing file). */
|
||||||
|
public static void downloadFile(String url, Path dest) throws IOException {
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(url))
|
||||||
|
.GET()
|
||||||
|
.timeout(Duration.ofSeconds(180))
|
||||||
|
.build();
|
||||||
|
try {
|
||||||
|
HttpResponse<InputStream> resp =
|
||||||
|
HTTP.send(req, HttpResponse.BodyHandlers.ofInputStream());
|
||||||
|
if (resp.statusCode() != 200)
|
||||||
|
throw new IOException("Download HTTP " + resp.statusCode());
|
||||||
|
Files.createDirectories(dest.getParent());
|
||||||
|
try (InputStream in = resp.body()) {
|
||||||
|
Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IOException("Download unterbrochen", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static String postTask(String apiKey, String body) throws IOException {
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(BASE + "/task"))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Authorization", "Bearer " + apiKey)
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(body))
|
||||||
|
.timeout(Duration.ofSeconds(30))
|
||||||
|
.build();
|
||||||
|
return send(req).getAsJsonObject("data").get("task_id").getAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject send(HttpRequest req) throws IOException {
|
||||||
|
try {
|
||||||
|
HttpResponse<String> resp = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
JsonObject json = JsonParser.parseString(resp.body()).getAsJsonObject();
|
||||||
|
int code = json.has("code") ? json.get("code").getAsInt() : -1;
|
||||||
|
if (code != 0) {
|
||||||
|
String msg = json.has("message") ? json.get("message").getAsString() : "?";
|
||||||
|
throw new IOException("Tripo3D-Fehler " + code + ": " + msg);
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IOException("HTTP-Anfrage unterbrochen", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String jsonString(String s) {
|
||||||
|
return "\"" + s.replace("\\", "\\\\")
|
||||||
|
.replace("\"", "\\\"")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "\\r")
|
||||||
|
+ "\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,9 +9,14 @@ public class SceneObject extends PlacedObject {
|
|||||||
private float worldXMut;
|
private float worldXMut;
|
||||||
private float worldZMut;
|
private float worldZMut;
|
||||||
private float rotY; // Y-Achsen-Rotation in Radiant
|
private float rotY; // Y-Achsen-Rotation in Radiant
|
||||||
|
private float rotX; // X-Achsen-Rotation in Radiant
|
||||||
|
private float rotZ; // Z-Achsen-Rotation in Radiant
|
||||||
private float scale;
|
private float scale;
|
||||||
public boolean solid; // Charakter-Kollision
|
public boolean solid; // Charakter-Kollision
|
||||||
public String modelPath; // relativ zu editor-assets/
|
public String modelPath; // relativ zu editor-assets/
|
||||||
|
public String texturePath = "";
|
||||||
|
public String normalMapPath = "";
|
||||||
|
public String materialPath = "";
|
||||||
|
|
||||||
public SceneObject(String modelPath, float worldX, float worldZ, float groundY,
|
public SceneObject(String modelPath, float worldX, float worldZ, float groundY,
|
||||||
boolean solid) {
|
boolean solid) {
|
||||||
@@ -19,6 +24,8 @@ public class SceneObject extends PlacedObject {
|
|||||||
this.worldXMut = worldX;
|
this.worldXMut = worldX;
|
||||||
this.worldZMut = worldZ;
|
this.worldZMut = worldZ;
|
||||||
this.rotY = 0f;
|
this.rotY = 0f;
|
||||||
|
this.rotX = 0f;
|
||||||
|
this.rotZ = 0f;
|
||||||
this.scale = 1f;
|
this.scale = 1f;
|
||||||
this.solid = solid;
|
this.solid = solid;
|
||||||
this.modelPath = modelPath;
|
this.modelPath = modelPath;
|
||||||
@@ -30,7 +37,14 @@ public class SceneObject extends PlacedObject {
|
|||||||
@Override public float getWorldZ() { return worldZMut; }
|
@Override public float getWorldZ() { return worldZMut; }
|
||||||
|
|
||||||
public float getRotY() { return rotY; }
|
public float getRotY() { return rotY; }
|
||||||
|
public float getRotX() { return rotX; }
|
||||||
|
public float getRotZ() { return rotZ; }
|
||||||
public float getScale() { return scale; }
|
public float getScale() { return scale; }
|
||||||
|
public String getTexturePath() { return texturePath; }
|
||||||
|
public String getNormalMapPath() { return normalMapPath; }
|
||||||
|
public String getMaterialPath() { return materialPath; }
|
||||||
|
public void setNormalMapPath(String p) { this.normalMapPath = p != null ? p : ""; }
|
||||||
|
public void setMaterialPath(String p) { this.materialPath = p != null ? p : ""; }
|
||||||
|
|
||||||
public void translate(float dx, float dy, float dz) {
|
public void translate(float dx, float dy, float dz) {
|
||||||
worldXMut += dx;
|
worldXMut += dx;
|
||||||
@@ -38,6 +52,20 @@ public class SceneObject extends PlacedObject {
|
|||||||
worldZMut += dz;
|
worldZMut += dz;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPosition(float x, float y, float z) {
|
||||||
|
worldXMut = x;
|
||||||
|
this.groundY = y;
|
||||||
|
worldZMut = z;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRotation(float rx, float ry, float rz) {
|
||||||
|
rotX = rx;
|
||||||
|
rotY = ry;
|
||||||
|
rotZ = rz;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTexturePath(String path) { this.texturePath = path != null ? path : ""; }
|
||||||
|
|
||||||
public void rotateY(float deltaRad) { rotY += deltaRad; }
|
public void rotateY(float deltaRad) { rotY += deltaRad; }
|
||||||
public void setScale(float s) { scale = s; }
|
public void setScale(float s) { scale = s; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,835 @@
|
|||||||
|
package de.blight.editor.state;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.jme3.anim.AnimClip;
|
||||||
|
import com.jme3.anim.AnimComposer;
|
||||||
|
import com.jme3.anim.SkinningControl;
|
||||||
|
import com.jme3.anim.tween.action.Action;
|
||||||
|
import com.jme3.app.Application;
|
||||||
|
import com.jme3.app.SimpleApplication;
|
||||||
|
import com.jme3.app.state.BaseAppState;
|
||||||
|
import com.jme3.asset.AssetManager;
|
||||||
|
import com.jme3.asset.ModelKey;
|
||||||
|
import com.jme3.bounding.BoundingBox;
|
||||||
|
import com.jme3.export.binary.BinaryExporter;
|
||||||
|
import com.jme3.export.binary.BinaryImporter;
|
||||||
|
import com.jme3.font.BitmapFont;
|
||||||
|
import com.jme3.font.BitmapText;
|
||||||
|
import com.jme3.light.AmbientLight;
|
||||||
|
import com.jme3.light.DirectionalLight;
|
||||||
|
import com.jme3.material.Material;
|
||||||
|
import com.jme3.math.ColorRGBA;
|
||||||
|
import com.jme3.math.FastMath;
|
||||||
|
import com.jme3.math.Vector3f;
|
||||||
|
import com.jme3.renderer.Camera;
|
||||||
|
import com.jme3.renderer.ViewPort;
|
||||||
|
import com.jme3.scene.Geometry;
|
||||||
|
import com.jme3.scene.Node;
|
||||||
|
import com.jme3.scene.Spatial;
|
||||||
|
import com.jme3.texture.FrameBuffer;
|
||||||
|
import com.jme3.texture.Image;
|
||||||
|
import com.jme3.texture.Texture2D;
|
||||||
|
|
||||||
|
import de.blight.editor.FrameTransfer;
|
||||||
|
import de.blight.editor.ProjectRoot;
|
||||||
|
import de.blight.editor.SharedInput;
|
||||||
|
import javafx.scene.image.WritableImage;
|
||||||
|
|
||||||
|
public class AnimPreviewState extends BaseAppState {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(AnimPreviewState.class);
|
||||||
|
|
||||||
|
private static final int PREVIEW_SIZE = 512;
|
||||||
|
private static final Path ASSET_ROOT =
|
||||||
|
ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||||
|
|
||||||
|
private final SharedInput input;
|
||||||
|
private SimpleApplication app;
|
||||||
|
private AssetManager assets;
|
||||||
|
|
||||||
|
private ViewPort previewVP;
|
||||||
|
private FrameBuffer previewFB;
|
||||||
|
private FrameTransfer previewTransfer;
|
||||||
|
private Node previewScene;
|
||||||
|
private Node previewHolder;
|
||||||
|
private Vector3f previewTarget = new Vector3f(0f, 1f, 0f);
|
||||||
|
private float previewCamDist = 3f;
|
||||||
|
private int currentW = PREVIEW_SIZE;
|
||||||
|
private int currentH = PREVIEW_SIZE;
|
||||||
|
|
||||||
|
private Spatial currentModel;
|
||||||
|
private String currentModelPath;
|
||||||
|
private Action currentAction;
|
||||||
|
private String currentClipName;
|
||||||
|
|
||||||
|
private Node axesNode;
|
||||||
|
|
||||||
|
public AnimPreviewState(SharedInput input) { this.input = input; }
|
||||||
|
|
||||||
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize(Application app) {
|
||||||
|
this.app = (SimpleApplication) app;
|
||||||
|
this.assets = app.getAssetManager();
|
||||||
|
|
||||||
|
previewFB = buildFrameBuffer(PREVIEW_SIZE, PREVIEW_SIZE);
|
||||||
|
|
||||||
|
Camera cam = new Camera(PREVIEW_SIZE, PREVIEW_SIZE);
|
||||||
|
cam.setFrustumPerspective(45f, 1f, 0.01f, 2000f);
|
||||||
|
|
||||||
|
previewVP = this.app.getRenderManager().createPostView("animPreview", cam);
|
||||||
|
previewVP.setOutputFrameBuffer(previewFB);
|
||||||
|
previewVP.setBackgroundColor(new ColorRGBA(0.18f, 0.18f, 0.22f, 1f));
|
||||||
|
previewVP.setClearFlags(true, true, true);
|
||||||
|
|
||||||
|
previewScene = new Node("animPreviewScene");
|
||||||
|
previewScene.addLight(new DirectionalLight(
|
||||||
|
new Vector3f(-0.5f, -1f, -0.4f).normalizeLocal(),
|
||||||
|
new ColorRGBA(1.8f, 1.7f, 1.4f, 1f)));
|
||||||
|
previewScene.addLight(new DirectionalLight(
|
||||||
|
new Vector3f(0.6f, -0.4f, 0.7f).normalizeLocal(),
|
||||||
|
new ColorRGBA(0.45f, 0.5f, 0.7f, 1f)));
|
||||||
|
previewScene.addLight(new AmbientLight(new ColorRGBA(0.4f, 0.4f, 0.45f, 1f)));
|
||||||
|
|
||||||
|
previewHolder = new Node("animHolder");
|
||||||
|
previewScene.attachChild(previewHolder);
|
||||||
|
|
||||||
|
axesNode = buildAxesNode();
|
||||||
|
previewScene.attachChild(axesNode);
|
||||||
|
|
||||||
|
previewVP.attachScene(previewScene);
|
||||||
|
|
||||||
|
previewTransfer = new FrameTransfer(input.animPreviewImage);
|
||||||
|
previewVP.addProcessor(previewTransfer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void cleanup(Application app) {
|
||||||
|
if (previewVP != null) {
|
||||||
|
this.app.getRenderManager().removePostView(previewVP);
|
||||||
|
previewVP = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void onEnable() {}
|
||||||
|
@Override protected void onDisable() {}
|
||||||
|
|
||||||
|
// ── Update-Schleife ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(float tpf) {
|
||||||
|
String loadPath = input.animPreviewLoadPath;
|
||||||
|
if (loadPath != null) {
|
||||||
|
input.animPreviewLoadPath = null;
|
||||||
|
loadModel(loadPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
String playClip = input.animPreviewPlayClip;
|
||||||
|
if (playClip != null) {
|
||||||
|
input.animPreviewPlayClip = null;
|
||||||
|
if (playClip.isEmpty()) stopAll();
|
||||||
|
else playClip(playClip);
|
||||||
|
}
|
||||||
|
|
||||||
|
String addAnimPath = input.animPreviewAddAnimPath;
|
||||||
|
if (addAnimPath != null) {
|
||||||
|
input.animPreviewAddAnimPath = null;
|
||||||
|
addAnimation(addAnimPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
String removeClip = input.animPreviewRemoveClip;
|
||||||
|
if (removeClip != null) {
|
||||||
|
input.animPreviewRemoveClip = null;
|
||||||
|
removeAnimation(removeClip);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.scanSkeletalRequested) {
|
||||||
|
input.scanSkeletalRequested = false;
|
||||||
|
new Thread(this::scanSkeletalModels, "skeletal-scan").start();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.animDumpRequested) {
|
||||||
|
input.animDumpRequested = false;
|
||||||
|
dumpCurrentModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedInput.ClipRenameRequest renameReq = input.clipRenameRequest.getAndSet(null);
|
||||||
|
if (renameReq != null) executeClipRename(renameReq);
|
||||||
|
|
||||||
|
SharedInput.AnimSetSaveRequest setReq = input.animSetSaveRequest.getAndSet(null);
|
||||||
|
if (setReq != null) executeAnimSetSave(setReq);
|
||||||
|
|
||||||
|
// Geschwindigkeit live anpassen
|
||||||
|
if (currentAction != null) {
|
||||||
|
try { currentAction.setSpeed(input.animPreviewSpeed); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop-Steuerung: AnimComposer spielt einmal ab – wir starten manuell neu
|
||||||
|
if (currentClipName != null && currentModel != null && currentAction != null) {
|
||||||
|
AnimComposer ac = findControl(currentModel, AnimComposer.class);
|
||||||
|
if (ac != null) {
|
||||||
|
double length = currentAction.getLength();
|
||||||
|
double time = ac.getTime();
|
||||||
|
if (length <= 0) {
|
||||||
|
System.err.println("[AnimPreview] Loop-Check: length=0, Clip hat keine Dauer!");
|
||||||
|
} else if (time >= length - 0.02) {
|
||||||
|
LOG.trace("[AnimPreview] Loop-Check fired: time={} length={}", time, length);
|
||||||
|
if (input.animPreviewLoop) {
|
||||||
|
currentAction = ac.setCurrentAction(currentClipName);
|
||||||
|
if (currentAction != null) currentAction.setSpeed(input.animPreviewSpeed);
|
||||||
|
} else {
|
||||||
|
currentAction = null;
|
||||||
|
currentClipName = null;
|
||||||
|
setSkinningEnabled(currentModel, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize
|
||||||
|
int reqW = Math.max(64, input.animPreviewW);
|
||||||
|
int reqH = Math.max(64, input.animPreviewH);
|
||||||
|
if (previewVP != null && (Math.abs(reqW - currentW) > 8 || Math.abs(reqH - currentH) > 8)) {
|
||||||
|
resizePreview(reqW, reqH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kamera-Orbit
|
||||||
|
if (previewVP != null) {
|
||||||
|
float rotY = input.animPreviewRotY * FastMath.DEG_TO_RAD;
|
||||||
|
float rotX = input.animPreviewRotX * FastMath.DEG_TO_RAD;
|
||||||
|
float dist = previewCamDist * input.animPreviewZoom;
|
||||||
|
Camera c = previewVP.getCamera();
|
||||||
|
c.setLocation(new Vector3f(
|
||||||
|
previewTarget.x + FastMath.sin(rotY) * FastMath.cos(rotX) * dist,
|
||||||
|
previewTarget.y + FastMath.sin(rotX) * dist,
|
||||||
|
previewTarget.z + FastMath.cos(rotY) * FastMath.cos(rotX) * dist));
|
||||||
|
c.lookAt(previewTarget, Vector3f.UNIT_Y);
|
||||||
|
|
||||||
|
// Achsen: Größe proportional zur Kameradistanz, immer am Ursprung
|
||||||
|
if (axesNode != null) {
|
||||||
|
float s = previewCamDist * input.animPreviewZoom * 0.18f;
|
||||||
|
axesNode.setLocalScale(s);
|
||||||
|
axesNode.setLocalTranslation(previewTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
previewScene.updateLogicalState(tpf);
|
||||||
|
previewScene.updateGeometricState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Model laden ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void loadModel(String assetPath) {
|
||||||
|
previewHolder.detachAllChildren();
|
||||||
|
currentModel = null;
|
||||||
|
currentModelPath = null;
|
||||||
|
currentAction = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Spatial model = loadFresh(assetPath);
|
||||||
|
// SkinningControl nur aktiv lassen wenn eine Animation läuft,
|
||||||
|
// sonst kollabiert das Mesh durch uninitalisierte Skin-Matrizen.
|
||||||
|
setSkinningEnabled(model, false);
|
||||||
|
|
||||||
|
// Im Animations-Editor soll der Charakter immer am Ursprung stehen.
|
||||||
|
// Eventuelle Translation die beim GLB-Export eingebacken wurde entfernen.
|
||||||
|
model.setLocalTranslation(Vector3f.ZERO);
|
||||||
|
|
||||||
|
currentModel = model;
|
||||||
|
currentModelPath = assetPath;
|
||||||
|
previewHolder.attachChild(model);
|
||||||
|
|
||||||
|
// Kamera auf Bounding Box ausrichten
|
||||||
|
model.updateGeometricState();
|
||||||
|
if (model.getWorldBound() instanceof BoundingBox bb) {
|
||||||
|
float ext = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
|
||||||
|
previewCamDist = ext * 2.8f;
|
||||||
|
previewTarget.set(bb.getCenter());
|
||||||
|
} else {
|
||||||
|
previewCamDist = 3f;
|
||||||
|
previewTarget.set(0, 1, 0);
|
||||||
|
}
|
||||||
|
input.animPreviewZoom = 1.0f;
|
||||||
|
|
||||||
|
// Clips sammeln und melden
|
||||||
|
List<String> clips = new ArrayList<>();
|
||||||
|
collectClips(model, clips);
|
||||||
|
input.animPreviewClips.set(Collections.unmodifiableList(clips));
|
||||||
|
input.animPreviewLoadedPath.set(assetPath);
|
||||||
|
if (clips.isEmpty()) {
|
||||||
|
if (!hasSkeleton(model)) {
|
||||||
|
input.animPreviewStatus =
|
||||||
|
"Kein Skelett gefunden. Modell wurde möglicherweise ohne " +
|
||||||
|
"\"keepControls\" importiert – bitte Datei neu importieren.";
|
||||||
|
} else {
|
||||||
|
input.animPreviewStatus = "Skelett vorhanden, aber keine Animations-Clips. " +
|
||||||
|
"Animation über 'Animation hinzufügen' laden.";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
input.animPreviewStatus = "Geladen: " + assetPath + " (" + clips.size() + " Clips)";
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
input.animPreviewStatus = "Ladefehler: " + e.getMessage();
|
||||||
|
input.animPreviewClips.set(List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectClips(Spatial s, List<String> out) {
|
||||||
|
AnimComposer ac = s.getControl(AnimComposer.class);
|
||||||
|
if (ac != null) {
|
||||||
|
List<String> names = new ArrayList<>(ac.getAnimClipsNames());
|
||||||
|
Collections.sort(names);
|
||||||
|
out.addAll(names);
|
||||||
|
}
|
||||||
|
if (s instanceof Node n) {
|
||||||
|
for (Spatial child : n.getChildren()) collectClips(child, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Animation abspielen ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void playClip(String clipName) {
|
||||||
|
if (currentModel == null) return;
|
||||||
|
currentAction = null;
|
||||||
|
currentClipName = null;
|
||||||
|
// SkinningControls auf dem gesamten Modell aktivieren – AnimComposer und
|
||||||
|
// SkinningControl sitzen oft auf verschiedenen Geschwisterknoten.
|
||||||
|
setSkinningEnabled(currentModel, true);
|
||||||
|
playOnSpatial(currentModel, clipName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean playOnSpatial(Spatial s, String clipName) {
|
||||||
|
AnimComposer ac = s.getControl(AnimComposer.class);
|
||||||
|
if (ac != null && ac.getAnimClipsNames().contains(clipName)) {
|
||||||
|
try {
|
||||||
|
currentAction = ac.setCurrentAction(clipName);
|
||||||
|
currentClipName = clipName;
|
||||||
|
if (currentAction != null) {
|
||||||
|
currentAction.setSpeed(input.animPreviewSpeed);
|
||||||
|
System.err.println("[AnimPreview] Play '" + clipName
|
||||||
|
+ "' length=" + currentAction.getLength());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
input.animPreviewStatus = "Abspielen fehlgeschlagen: " + e.getMessage();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (s instanceof Node n) {
|
||||||
|
for (Spatial child : n.getChildren()) {
|
||||||
|
if (playOnSpatial(child, clipName)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopAll() {
|
||||||
|
currentAction = null;
|
||||||
|
currentClipName = null;
|
||||||
|
if (currentModel != null) {
|
||||||
|
stopOnSpatial(currentModel);
|
||||||
|
setSkinningEnabled(currentModel, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopOnSpatial(Spatial s) {
|
||||||
|
AnimComposer ac = s.getControl(AnimComposer.class);
|
||||||
|
if (ac != null) {
|
||||||
|
try { ac.removeCurrentAction(AnimComposer.DEFAULT_LAYER); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
if (s instanceof Node n) {
|
||||||
|
for (Spatial child : n.getChildren()) stopOnSpatial(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Skelett-Scan ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void scanSkeletalModels() {
|
||||||
|
Set<String> result = new HashSet<>();
|
||||||
|
for (String dir : new String[]{"Models", "animations"}) {
|
||||||
|
Path base = ASSET_ROOT.resolve(dir);
|
||||||
|
if (!Files.isDirectory(base)) continue;
|
||||||
|
try (Stream<Path> walk = Files.walk(base)) {
|
||||||
|
walk.filter(p -> p.toString().endsWith(".j3o")).forEach(p -> {
|
||||||
|
String rel = ASSET_ROOT.relativize(p).toString().replace('\\', '/');
|
||||||
|
try {
|
||||||
|
Spatial model = assets.loadModel(rel);
|
||||||
|
if (hasSkeleton(model)) result.add(rel);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
});
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
input.skeletalPaths.set(Collections.unmodifiableSet(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasSkeleton(Spatial s) {
|
||||||
|
if (s.getControl(AnimComposer.class) != null) return true;
|
||||||
|
// Fallback auf altes AnimControl (Legacy-Modelle)
|
||||||
|
try {
|
||||||
|
if (s.getControl(com.jme3.animation.AnimControl.class) != null) return true;
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
if (s instanceof Node n) {
|
||||||
|
for (Spatial child : n.getChildren()) {
|
||||||
|
if (hasSkeleton(child)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Animation hinzufügen (Retargeting) ───────────────────────────────────
|
||||||
|
|
||||||
|
private void addAnimation(String animAssetPath) {
|
||||||
|
if (currentModel == null) {
|
||||||
|
input.animPreviewStatus = "Fehler: zuerst ein Modell laden";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AnimComposer targetAC = findControl(currentModel, AnimComposer.class);
|
||||||
|
SkinningControl targetSC = findControl(currentModel, SkinningControl.class);
|
||||||
|
if (targetAC == null) {
|
||||||
|
input.animPreviewStatus = "Fehler: Modell hat keinen AnimComposer";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Spatial animSource = loadFresh(animAssetPath);
|
||||||
|
AnimComposer sourceAC = findControl(animSource, AnimComposer.class);
|
||||||
|
if (sourceAC == null) {
|
||||||
|
String controls = listControlTypes(animSource);
|
||||||
|
String info = "Kein AnimComposer | Controls: " + controls
|
||||||
|
+ " | Nodes: " + countNodes(animSource);
|
||||||
|
input.animPreviewStatus = info;
|
||||||
|
System.err.println("[AnimPreview] addAnimation – " + info + " – Datei: " + animAssetPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sourceAC.getAnimClips().isEmpty()) {
|
||||||
|
input.animPreviewStatus = "AnimComposer leer (0 Clips) in: " + animAssetPath;
|
||||||
|
System.err.println("[AnimPreview] AnimComposer leer in " + animAssetPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SkinningControl sourceSC = findControl(animSource, SkinningControl.class);
|
||||||
|
com.jme3.anim.Armature srcArm = sourceSC != null ? sourceSC.getArmature() : null;
|
||||||
|
com.jme3.anim.Armature dstArm = targetSC != null ? targetSC.getArmature() : null;
|
||||||
|
|
||||||
|
// Diagnose: Knochen-Namen beider Skelette ausgeben
|
||||||
|
if (srcArm != null) {
|
||||||
|
System.err.println("[Retarget] Quell-Knochen (" + srcArm.getJointCount() + "):");
|
||||||
|
for (var j : srcArm.getJointList()) System.err.println(" src: " + j.getName());
|
||||||
|
} else {
|
||||||
|
System.err.println("[Retarget] Keine SkinningControl in Quelle!");
|
||||||
|
}
|
||||||
|
if (dstArm != null) {
|
||||||
|
System.err.println("[Retarget] Ziel-Knochen (" + dstArm.getJointCount() + "):");
|
||||||
|
for (var j : dstArm.getJointList()) System.err.println(" dst: " + j.getName());
|
||||||
|
} else {
|
||||||
|
System.err.println("[Retarget] Keine SkinningControl im Modell!");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean retarget = srcArm != null && dstArm != null && srcArm != dstArm;
|
||||||
|
if (retarget) {
|
||||||
|
var mapping = de.blight.game.animation.BoneNameMapping.buildMapping(srcArm, dstArm);
|
||||||
|
System.err.println("[Retarget] Mapping (" + mapping.size() + " Treffer): " + mapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blender-Duplikate herausfiltern: Clips deren Name mit ".NNN" endet und deren
|
||||||
|
// Basis-Name (ohne Suffix) ebenfalls in der Quelle vorkommt, werden übersprungen.
|
||||||
|
java.util.Set<String> srcNames = new java.util.HashSet<>();
|
||||||
|
for (AnimClip c : sourceAC.getAnimClips()) srcNames.add(c.getName());
|
||||||
|
|
||||||
|
int added = 0;
|
||||||
|
for (AnimClip clip : sourceAC.getAnimClips()) {
|
||||||
|
String name = clip.getName();
|
||||||
|
// Prüfen ob Name dem Muster "base.NNN" entspricht (Blender-Duplikat)
|
||||||
|
if (name.matches(".*\\.\\d{3}$")) {
|
||||||
|
String base = name.substring(0, name.length() - 4);
|
||||||
|
if (srcNames.contains(base)) {
|
||||||
|
System.err.println("[AnimPreview] Überspringe Blender-Duplikat: " + name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AnimClip result = retarget
|
||||||
|
? de.blight.game.animation.RetargetingSystem.retarget(clip, srcArm, dstArm)
|
||||||
|
: clip;
|
||||||
|
if (result != null) {
|
||||||
|
targetAC.addAnimClip(result);
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clip-Liste neu aufbauen
|
||||||
|
List<String> clips = new ArrayList<>();
|
||||||
|
collectClips(currentModel, clips);
|
||||||
|
input.animPreviewClips.set(Collections.unmodifiableList(clips));
|
||||||
|
input.animPreviewStatus = added + " Clip(s) hinzugefügt"
|
||||||
|
+ (retarget ? " (retargeted)" : " (direkt, kein Retargeting)");
|
||||||
|
|
||||||
|
// Modell mit neuem Clip persistieren, damit der Clip nach Editor-Neustart noch da ist
|
||||||
|
if (added > 0) saveModel();
|
||||||
|
} catch (Exception e) {
|
||||||
|
input.animPreviewStatus = "Fehler beim Hinzufügen: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Animation entfernen ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void removeAnimation(String clipName) {
|
||||||
|
if (currentModel == null) {
|
||||||
|
input.animPreviewStatus = "Fehler: kein Modell geladen";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AnimComposer ac = findControl(currentModel, AnimComposer.class);
|
||||||
|
if (ac == null) {
|
||||||
|
input.animPreviewStatus = "Fehler: kein AnimComposer";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AnimClip clip = ac.getAnimClip(clipName);
|
||||||
|
if (clip == null) {
|
||||||
|
input.animPreviewStatus = "Clip nicht gefunden: " + clipName;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (clipName.equals(currentClipName)) stopAll();
|
||||||
|
ac.removeAnimClip(clip);
|
||||||
|
List<String> clips = new ArrayList<>();
|
||||||
|
collectClips(currentModel, clips);
|
||||||
|
input.animPreviewClips.set(Collections.unmodifiableList(clips));
|
||||||
|
input.animPreviewStatus = "Clip entfernt: " + clipName;
|
||||||
|
saveModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends com.jme3.scene.control.Control> T findControl(Spatial s, Class<T> type) {
|
||||||
|
T c = s.getControl(type);
|
||||||
|
if (c != null) return c;
|
||||||
|
if (s instanceof Node n) {
|
||||||
|
for (Spatial child : n.getChildren()) {
|
||||||
|
T r = findControl(child, type);
|
||||||
|
if (r != null) return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Achsen-Visualisierung ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// ── Achsen: Rot=X Grün=Y Blau=Z ──────────────────────────────────────────
|
||||||
|
// Solid-Box-Geometrie, depthTest=false → immer vor dem Modell sichtbar.
|
||||||
|
// Der axesNode sitzt am previewTarget (Modell-Mittelpunkt) und wird per
|
||||||
|
// setLocalScale auf ~18 % der Kameradistanz skaliert.
|
||||||
|
private Node buildAxesNode() {
|
||||||
|
Node n = new Node("axes");
|
||||||
|
n.attachChild(makeAxisShaft(ColorRGBA.Red, 0.5f, 0f, 0f, "X"));
|
||||||
|
n.attachChild(makeAxisShaft(ColorRGBA.Green, 0f, 0.5f, 0f, "Y"));
|
||||||
|
n.attachChild(makeAxisShaft(ColorRGBA.Blue, 0f, 0f, 0.5f, "Z"));
|
||||||
|
n.attachChild(makeAxisTip(ColorRGBA.Red, 0.5f, 0f, 0f ));
|
||||||
|
n.attachChild(makeAxisTip(ColorRGBA.Green, 0f, 0.5f, 0f ));
|
||||||
|
n.attachChild(makeAxisTip(ColorRGBA.Blue, 0f, 0f, 0.5f ));
|
||||||
|
addAxisLabel(n, new Vector3f(0.60f, 0.00f, 0.00f), "X", ColorRGBA.Red);
|
||||||
|
addAxisLabel(n, new Vector3f(0.00f, 0.60f, 0.00f), "Y", ColorRGBA.Green);
|
||||||
|
addAxisLabel(n, new Vector3f(0.00f, 0.00f, 0.60f), "Z", ColorRGBA.Blue);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dünner Stab von (0,0,0) zum Punkt (ex,ey,ez), halb-Breite 0.015. */
|
||||||
|
private Geometry makeAxisShaft(ColorRGBA color, float ex, float ey, float ez, String name) {
|
||||||
|
float hw = 0.015f;
|
||||||
|
float hx = ex != 0 ? Math.abs(ex) / 2f : hw;
|
||||||
|
float hy = ey != 0 ? Math.abs(ey) / 2f : hw;
|
||||||
|
float hz = ez != 0 ? Math.abs(ez) / 2f : hw;
|
||||||
|
Geometry g = new Geometry("axis-" + name, new com.jme3.scene.shape.Box(hx, hy, hz));
|
||||||
|
// Mittelpunkt des Stabs liegt bei halber Länge
|
||||||
|
g.setLocalTranslation(ex / 2f, ey / 2f, ez / 2f);
|
||||||
|
g.setMaterial(unshadedDepthOff(color));
|
||||||
|
g.setQueueBucket(com.jme3.renderer.queue.RenderQueue.Bucket.Transparent);
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kleiner Würfel an der Spitze (tx,ty,tz). */
|
||||||
|
private Geometry makeAxisTip(ColorRGBA color, float tx, float ty, float tz) {
|
||||||
|
Geometry g = new Geometry("axis-tip",
|
||||||
|
new com.jme3.scene.shape.Box(0.04f, 0.04f, 0.04f));
|
||||||
|
g.setLocalTranslation(tx, ty, tz);
|
||||||
|
g.setMaterial(unshadedDepthOff(color));
|
||||||
|
g.setQueueBucket(com.jme3.renderer.queue.RenderQueue.Bucket.Transparent);
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Material unshadedDepthOff(ColorRGBA color) {
|
||||||
|
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
mat.setColor("Color", color);
|
||||||
|
mat.getAdditionalRenderState().setDepthTest(false);
|
||||||
|
mat.getAdditionalRenderState().setDepthWrite(false);
|
||||||
|
return mat;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAxisLabel(Node parent, Vector3f pos, String text, ColorRGBA color) {
|
||||||
|
try {
|
||||||
|
BitmapFont font = assets.loadFont("Interface/Fonts/Default.fnt");
|
||||||
|
BitmapText label = new BitmapText(font, false);
|
||||||
|
label.setSize(0.18f);
|
||||||
|
label.setColor(color);
|
||||||
|
label.setText(text);
|
||||||
|
label.setLocalTranslation(pos);
|
||||||
|
parent.attachChild(label);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Listet alle Control-Typen im Subgraph für Diagnose-Ausgaben. */
|
||||||
|
private String listControlTypes(Spatial s) {
|
||||||
|
List<String> types = new ArrayList<>();
|
||||||
|
collectControlTypes(s, types);
|
||||||
|
return types.isEmpty() ? "(keine)" : String.join(", ", types);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int countNodes(Spatial s) {
|
||||||
|
int n = 1;
|
||||||
|
if (s instanceof Node nd) for (Spatial c : nd.getChildren()) n += countNodes(c);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectControlTypes(Spatial s, List<String> out) {
|
||||||
|
for (int i = 0; i < s.getNumControls(); i++)
|
||||||
|
out.add(s.getControl(i).getClass().getSimpleName());
|
||||||
|
if (s instanceof Node n)
|
||||||
|
for (Spatial child : n.getChildren()) collectControlTypes(child, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Speichert das aktuelle Modell (inkl. aller AnimClips) zurück auf Disk. */
|
||||||
|
private void saveModel() {
|
||||||
|
if (currentModelPath == null || currentModel == null) return;
|
||||||
|
if (!currentModelPath.endsWith(".j3o")) {
|
||||||
|
System.err.println("[AnimPreview] Speichern übersprungen – kein .j3o: " + currentModelPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Path file = ASSET_ROOT.resolve(currentModelPath.replace('/', java.io.File.separatorChar));
|
||||||
|
try {
|
||||||
|
BinaryExporter.getInstance().save(currentModel, file.toFile());
|
||||||
|
System.err.println("[AnimPreview] Modell gespeichert: " + currentModelPath);
|
||||||
|
assets.deleteFromCache(new ModelKey(currentModelPath));
|
||||||
|
} catch (Exception e) {
|
||||||
|
input.animPreviewStatus += " | Speicherfehler: " + e.getMessage();
|
||||||
|
System.err.println("[AnimPreview] Speicherfehler: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lädt eine j3o-Datei direkt von Disk (BinaryImporter), ohne AssetManager-Cache. */
|
||||||
|
private Spatial loadFresh(String assetPath) throws Exception {
|
||||||
|
Path file = ASSET_ROOT.resolve(assetPath.replace('/', java.io.File.separatorChar));
|
||||||
|
if (assetPath.endsWith(".j3o") && Files.exists(file)) {
|
||||||
|
BinaryImporter bi = BinaryImporter.getInstance();
|
||||||
|
bi.setAssetManager(assets);
|
||||||
|
return (Spatial) bi.load(file.toFile());
|
||||||
|
}
|
||||||
|
assets.deleteFromCache(new ModelKey(assetPath));
|
||||||
|
return assets.loadModel(assetPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Aktiviert oder deaktiviert alle SkinningControls im Subgraph. */
|
||||||
|
private void setSkinningEnabled(Spatial s, boolean enabled) {
|
||||||
|
SkinningControl sc = s.getControl(SkinningControl.class);
|
||||||
|
if (sc != null) sc.setEnabled(enabled);
|
||||||
|
if (s instanceof Node n) {
|
||||||
|
for (Spatial child : n.getChildren()) setSkinningEnabled(child, enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Framebuffer ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private FrameBuffer buildFrameBuffer(int w, int h) {
|
||||||
|
FrameBuffer fb = new FrameBuffer(w, h, 1);
|
||||||
|
fb.addColorTexture(new Texture2D(w, h, Image.Format.RGBA8));
|
||||||
|
fb.setDepthTexture(new Texture2D(w, h, Image.Format.Depth));
|
||||||
|
return fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resizePreview(int newW, int newH) {
|
||||||
|
currentW = newW;
|
||||||
|
currentH = newH;
|
||||||
|
previewVP.removeProcessor(previewTransfer);
|
||||||
|
try { previewFB.dispose(); } catch (Exception ignored) {}
|
||||||
|
previewFB = buildFrameBuffer(newW, newH);
|
||||||
|
previewVP.setOutputFrameBuffer(previewFB);
|
||||||
|
Camera cam = previewVP.getCamera();
|
||||||
|
cam.resize(newW, newH, true);
|
||||||
|
cam.setFrustumPerspective(45f, (float) newW / newH, 0.01f, 2000f);
|
||||||
|
WritableImage newImg = new WritableImage(newW, newH);
|
||||||
|
previewTransfer = new FrameTransfer(newImg);
|
||||||
|
previewVP.addProcessor(previewTransfer);
|
||||||
|
input.animPreviewImage = newImg;
|
||||||
|
input.animPreviewResized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Skelett-Dump ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void dumpCurrentModel() {
|
||||||
|
if (currentModel == null) {
|
||||||
|
System.err.println("[Dump] Kein Modell geladen.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
System.err.println("=".repeat(70));
|
||||||
|
System.err.println("[Dump] Modell: " + currentModelPath);
|
||||||
|
|
||||||
|
com.jme3.anim.SkinningControl sc = findControl(currentModel, com.jme3.anim.SkinningControl.class);
|
||||||
|
com.jme3.anim.AnimComposer ac = findControl(currentModel, com.jme3.anim.AnimComposer.class);
|
||||||
|
|
||||||
|
if (sc == null) {
|
||||||
|
System.err.println("[Dump] Kein SkinningControl gefunden – kein Skelett.");
|
||||||
|
} else {
|
||||||
|
com.jme3.anim.Armature arm = sc.getArmature();
|
||||||
|
java.util.Map<com.jme3.anim.Joint, com.jme3.math.Quaternion> ms = buildMS(arm);
|
||||||
|
|
||||||
|
System.err.println("[Dump] Skelett: " + arm.getJointCount() + " Knochen");
|
||||||
|
System.err.println("[Dump] ── Knochen (Name | Elternteil | bind-local° | live-local° | ms°) ──");
|
||||||
|
for (com.jme3.anim.Joint j : arm.getJointList()) {
|
||||||
|
String parent = j.getParent() != null ? j.getParent().getName() : "(root)";
|
||||||
|
|
||||||
|
com.jme3.math.Transform bind = j.getInitialTransform();
|
||||||
|
com.jme3.math.Quaternion bindRot = bind != null ? bind.getRotation() : new com.jme3.math.Quaternion();
|
||||||
|
|
||||||
|
// live = was AnimComposer aktuell in den Knochen geschrieben hat
|
||||||
|
com.jme3.math.Transform live = j.getLocalTransform();
|
||||||
|
com.jme3.math.Quaternion liveRot = live != null ? live.getRotation() : new com.jme3.math.Quaternion();
|
||||||
|
|
||||||
|
float[] be = bindRot.toAngles(null);
|
||||||
|
float[] le = liveRot.toAngles(null);
|
||||||
|
float[] mse = ms.get(j).toAngles(null);
|
||||||
|
|
||||||
|
System.err.printf("[Dump] %-30s parent=%-25s bind=[%6.1f %6.1f %6.1f]° live=[%6.1f %6.1f %6.1f]° ms=[%6.1f %6.1f %6.1f]°%n",
|
||||||
|
j.getName(), parent,
|
||||||
|
Math.toDegrees(be[0]), Math.toDegrees(be[1]), Math.toDegrees(be[2]),
|
||||||
|
Math.toDegrees(le[0]), Math.toDegrees(le[1]), Math.toDegrees(le[2]),
|
||||||
|
Math.toDegrees(mse[0]), Math.toDegrees(mse[1]), Math.toDegrees(mse[2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bone-Name-Mapping gegen Standard-Namen
|
||||||
|
var mapping = de.blight.game.animation.BoneNameMapping.buildMapping(arm, arm);
|
||||||
|
System.err.println("[Dump] ── Bone-Name-Normalisierung ──");
|
||||||
|
for (com.jme3.anim.Joint j : arm.getJointList()) {
|
||||||
|
String norm = de.blight.game.animation.BoneNameMapping.normalize(j.getName());
|
||||||
|
System.err.printf("[Dump] %-30s → normalized: %s%n", j.getName(), norm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ac == null) {
|
||||||
|
System.err.println("[Dump] Kein AnimComposer gefunden – keine Clips.");
|
||||||
|
} else {
|
||||||
|
System.err.println("[Dump] ── Clips ──");
|
||||||
|
for (com.jme3.anim.AnimClip clip : ac.getAnimClips()) {
|
||||||
|
System.err.printf("[Dump] Clip: %-40s Dauer=%.3fs Tracks=%d%n",
|
||||||
|
clip.getName(), clip.getLength(), clip.getTracks().length);
|
||||||
|
for (com.jme3.anim.AnimTrack<?> track : clip.getTracks()) {
|
||||||
|
if (track instanceof com.jme3.anim.TransformTrack tt
|
||||||
|
&& tt.getTarget() instanceof com.jme3.anim.Joint j) {
|
||||||
|
System.err.printf("[Dump] Track: %-28s frames=%d%n",
|
||||||
|
j.getName(),
|
||||||
|
tt.getTimes() != null ? tt.getTimes().length : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.err.println("=".repeat(70));
|
||||||
|
input.animPreviewStatus = "Dump ins Log geschrieben (stderr)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Clip umbenennen / exportieren ─────────────────────────────────────────
|
||||||
|
|
||||||
|
private void executeClipRename(SharedInput.ClipRenameRequest req) {
|
||||||
|
if (currentModel == null) { input.animOpStatus = "Fehler: kein Modell geladen"; return; }
|
||||||
|
AnimComposer ac = findControl(currentModel, AnimComposer.class);
|
||||||
|
if (ac == null) { input.animOpStatus = "Fehler: kein AnimComposer"; return; }
|
||||||
|
AnimClip src = ac.getAnimClip(req.oldName());
|
||||||
|
if (src == null) { input.animOpStatus = "Clip nicht gefunden: " + req.oldName(); return; }
|
||||||
|
|
||||||
|
// Neuen AnimClip mit neuem Namen und denselben Tracks erstellen
|
||||||
|
AnimClip renamed = new AnimClip(req.newName());
|
||||||
|
renamed.setTracks(src.getTracks());
|
||||||
|
ac.addAnimClip(renamed);
|
||||||
|
saveModel();
|
||||||
|
|
||||||
|
// Als eigenständige .j3o nach animations/ exportieren
|
||||||
|
try {
|
||||||
|
com.jme3.scene.Node holder = new com.jme3.scene.Node("animExport");
|
||||||
|
AnimComposer expAC = new AnimComposer();
|
||||||
|
expAC.addAnimClip(renamed);
|
||||||
|
holder.addControl(expAC);
|
||||||
|
java.nio.file.Path outDir = ASSET_ROOT.resolve("animations");
|
||||||
|
java.nio.file.Files.createDirectories(outDir);
|
||||||
|
com.jme3.export.binary.BinaryExporter.getInstance()
|
||||||
|
.save(holder, outDir.resolve(req.newName() + ".j3o").toFile());
|
||||||
|
// Clip-Liste aktualisieren
|
||||||
|
java.util.List<String> clips = new java.util.ArrayList<>();
|
||||||
|
collectClips(currentModel, clips);
|
||||||
|
input.animPreviewClips.set(java.util.Collections.unmodifiableList(clips));
|
||||||
|
input.animOpStatus = "Clip als '" + req.newName() + "' gespeichert";
|
||||||
|
} catch (Exception e) {
|
||||||
|
input.animOpStatus = "Export-Fehler: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Animations-Set speichern ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void executeAnimSetSave(SharedInput.AnimSetSaveRequest req) {
|
||||||
|
if (currentModel == null) { input.animOpStatus = "Fehler: kein Modell geladen"; return; }
|
||||||
|
AnimComposer ac = findControl(currentModel, AnimComposer.class);
|
||||||
|
if (ac == null) { input.animOpStatus = "Fehler: kein AnimComposer"; return; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
com.jme3.scene.Node holder = new com.jme3.scene.Node("animSet");
|
||||||
|
AnimComposer setAC = new AnimComposer();
|
||||||
|
int added = 0;
|
||||||
|
for (String clipName : req.clips()) {
|
||||||
|
AnimClip clip = ac.getAnimClip(clipName);
|
||||||
|
if (clip != null) { setAC.addAnimClip(clip); added++; }
|
||||||
|
}
|
||||||
|
holder.addControl(setAC);
|
||||||
|
|
||||||
|
java.nio.file.Path setDir = ASSET_ROOT.resolve("animations").resolve("sets");
|
||||||
|
java.nio.file.Files.createDirectories(setDir);
|
||||||
|
java.nio.file.Path j3oPath = setDir.resolve(req.setName() + ".j3o");
|
||||||
|
com.jme3.export.binary.BinaryExporter.getInstance().save(holder, j3oPath.toFile());
|
||||||
|
|
||||||
|
// Begleitende .animset.json mit Clip-Namen und Aktions-Zuweisung
|
||||||
|
de.blight.game.animation.AnimSet animSet = new de.blight.game.animation.AnimSet();
|
||||||
|
animSet.setClips(req.clips());
|
||||||
|
animSet.setActionMap(req.actionMap() != null ? req.actionMap() : new java.util.LinkedHashMap<>());
|
||||||
|
animSet.save(setDir, req.setName());
|
||||||
|
|
||||||
|
input.animOpStatus = "Set '" + req.setName() + "' gespeichert (" + added + " Clips)";
|
||||||
|
} catch (Exception e) {
|
||||||
|
input.animOpStatus = "Set-Fehler: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static java.util.Map<com.jme3.anim.Joint, com.jme3.math.Quaternion>
|
||||||
|
buildMS(com.jme3.anim.Armature arm) {
|
||||||
|
java.util.Map<com.jme3.anim.Joint, com.jme3.math.Quaternion> cache = new java.util.HashMap<>();
|
||||||
|
for (com.jme3.anim.Joint j : arm.getJointList()) buildMSRec(j, cache);
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static com.jme3.math.Quaternion buildMSRec(
|
||||||
|
com.jme3.anim.Joint j,
|
||||||
|
java.util.Map<com.jme3.anim.Joint, com.jme3.math.Quaternion> cache) {
|
||||||
|
com.jme3.math.Quaternion cached = cache.get(j);
|
||||||
|
if (cached != null) return cached;
|
||||||
|
com.jme3.math.Quaternion local = j.getInitialTransform() != null
|
||||||
|
? j.getInitialTransform().getRotation() : new com.jme3.math.Quaternion();
|
||||||
|
com.jme3.math.Quaternion result = j.getParent() == null
|
||||||
|
? new com.jme3.math.Quaternion(local)
|
||||||
|
: buildMSRec(j.getParent(), cache).mult(local);
|
||||||
|
cache.put(j, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
package de.blight.editor.state;
|
||||||
|
|
||||||
|
import com.jme3.app.Application;
|
||||||
|
import com.jme3.app.SimpleApplication;
|
||||||
|
import com.jme3.app.state.BaseAppState;
|
||||||
|
import com.jme3.asset.AssetManager;
|
||||||
|
import com.jme3.collision.CollisionResults;
|
||||||
|
import com.jme3.effect.ParticleEmitter;
|
||||||
|
import com.jme3.effect.ParticleMesh;
|
||||||
|
import com.jme3.material.Material;
|
||||||
|
import com.jme3.material.RenderState;
|
||||||
|
import com.jme3.math.*;
|
||||||
|
import com.jme3.renderer.Camera;
|
||||||
|
import com.jme3.renderer.queue.RenderQueue;
|
||||||
|
import com.jme3.scene.*;
|
||||||
|
import com.jme3.scene.shape.Sphere;
|
||||||
|
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||||
|
import de.blight.common.PlacedEmitter;
|
||||||
|
import de.blight.editor.SharedInput;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class EmitterState extends BaseAppState {
|
||||||
|
|
||||||
|
private static final float MARKER_RADIUS = 0.4f;
|
||||||
|
private static final float ACTIVATION_ALPHA = 0.07f;
|
||||||
|
|
||||||
|
private static final ColorRGBA SEL_COLOR = new ColorRGBA(1f, 1f, 0f, 1f);
|
||||||
|
private static final String GEO_MARKER = "emitter_marker";
|
||||||
|
private static final String GEO_ACTRADIUS = "activation_sphere";
|
||||||
|
|
||||||
|
private final SharedInput input;
|
||||||
|
private SimpleApplication app;
|
||||||
|
private Camera cam;
|
||||||
|
private AssetManager assets;
|
||||||
|
private Node rootNode;
|
||||||
|
private TerrainQuad terrain;
|
||||||
|
|
||||||
|
// parallel lists: emitters[i] <-> markers[i] <-> particles[i]
|
||||||
|
private final List<PlacedEmitter> emitters = new ArrayList<>();
|
||||||
|
private final List<Node> markers = new ArrayList<>();
|
||||||
|
private final List<ParticleEmitter> particles = new ArrayList<>();
|
||||||
|
|
||||||
|
private int selectedIdx = -1;
|
||||||
|
private List<PlacedEmitter> pendingEmitters = null;
|
||||||
|
|
||||||
|
public EmitterState(SharedInput input) {
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize(Application application) {
|
||||||
|
app = (SimpleApplication) application;
|
||||||
|
cam = app.getCamera();
|
||||||
|
assets = app.getAssetManager();
|
||||||
|
rootNode = app.getRootNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void cleanup(Application application) { clearAll(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onEnable() {
|
||||||
|
if (pendingEmitters != null) {
|
||||||
|
loadPlacedEmitters(pendingEmitters);
|
||||||
|
pendingEmitters = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void onDisable() {}
|
||||||
|
|
||||||
|
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
||||||
|
|
||||||
|
// ── Update ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(float tpf) {
|
||||||
|
if (input.activeLayer != SharedInput.LAYER_EMITTERS) return;
|
||||||
|
|
||||||
|
SharedInput.EmitterClick click;
|
||||||
|
while ((click = input.emitterClickQueue.poll()) != null) {
|
||||||
|
handleClick(click);
|
||||||
|
}
|
||||||
|
|
||||||
|
PlacedEmitter pending = input.pendingEmitter.getAndSet(null);
|
||||||
|
if (pending != null && selectedIdx >= 0) {
|
||||||
|
applyProperty(selectedIdx, pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.deleteEmitterRequested) {
|
||||||
|
input.deleteEmitterRequested = false;
|
||||||
|
if (selectedIdx >= 0) removeEmitter(selectedIdx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Click handling ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void handleClick(SharedInput.EmitterClick click) {
|
||||||
|
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||||
|
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||||
|
|
||||||
|
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||||
|
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||||
|
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||||
|
|
||||||
|
int hit = pickMarker(ray);
|
||||||
|
if (hit >= 0) {
|
||||||
|
if (click.rightButton()) deselect();
|
||||||
|
else selectEmitter(hit);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (click.rightButton()) { deselect(); return; }
|
||||||
|
|
||||||
|
if (terrain == null) return;
|
||||||
|
CollisionResults hits = new CollisionResults();
|
||||||
|
terrain.collideWith(ray, hits);
|
||||||
|
if (hits.size() == 0) return;
|
||||||
|
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||||
|
|
||||||
|
PlacedEmitter pe = createPreset(input.emitterPreset, pt.x, pt.y, pt.z);
|
||||||
|
addEmitter(pe);
|
||||||
|
selectEmitter(emitters.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlacedEmitter createPreset(int preset, float x, float y, float z) {
|
||||||
|
return switch (preset) {
|
||||||
|
case 1 -> PlacedEmitter.smoke(x, y, z);
|
||||||
|
case 2 -> PlacedEmitter.sparks(x, y, z);
|
||||||
|
default -> PlacedEmitter.fire(x, y, z);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private int pickMarker(Ray ray) {
|
||||||
|
for (int i = 0; i < markers.size(); i++) {
|
||||||
|
CollisionResults res = new CollisionResults();
|
||||||
|
markers.get(i).collideWith(ray, res);
|
||||||
|
if (res.size() > 0) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Selection ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void selectEmitter(int idx) {
|
||||||
|
deselect();
|
||||||
|
selectedIdx = idx;
|
||||||
|
setMarkerColor(idx, SEL_COLOR);
|
||||||
|
publishSelection(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deselect() {
|
||||||
|
if (selectedIdx >= 0 && selectedIdx < emitters.size()) {
|
||||||
|
PlacedEmitter e = emitters.get(selectedIdx);
|
||||||
|
setMarkerColor(selectedIdx, new ColorRGBA(e.startR(), e.startG(), e.startB(), 1f));
|
||||||
|
}
|
||||||
|
selectedIdx = -1;
|
||||||
|
input.selectedEmitterInfo = null;
|
||||||
|
input.emitterSelectionChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publishSelection(int idx) {
|
||||||
|
PlacedEmitter e = emitters.get(idx);
|
||||||
|
input.selectedEmitterInfo = buildInfoString(idx, e);
|
||||||
|
input.emitterSelectionChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String buildInfoString(int idx, PlacedEmitter e) {
|
||||||
|
return String.format(java.util.Locale.ROOT,
|
||||||
|
"%d|%.3f|%.3f|%.3f|%.3f|%s|%d|%d"
|
||||||
|
+ "|%.4f|%.4f|%.4f|%.4f"
|
||||||
|
+ "|%.4f|%.4f|%.4f|%.4f"
|
||||||
|
+ "|%.4f|%.4f"
|
||||||
|
+ "|%.4f|%.4f|%.4f|%.4f"
|
||||||
|
+ "|%.4f|%.4f|%.4f"
|
||||||
|
+ "|%.4f|%.4f|%d|%.4f",
|
||||||
|
idx, e.x(), e.y(), e.z(), e.activationRadius(),
|
||||||
|
e.texturePath(), e.imagesX(), e.imagesY(),
|
||||||
|
e.startR(), e.startG(), e.startB(), e.startA(),
|
||||||
|
e.endR(), e.endG(), e.endB(), e.endA(),
|
||||||
|
e.startSize(), e.endSize(),
|
||||||
|
e.velX(), e.velY(), e.velZ(), e.velocityVariation(),
|
||||||
|
e.gravX(), e.gravY(), e.gravZ(),
|
||||||
|
e.lowLife(), e.highLife(), e.maxParticles(), e.emitRate());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Add / Remove ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void addEmitter(PlacedEmitter pe) {
|
||||||
|
Node marker = buildMarker(pe);
|
||||||
|
rootNode.attachChild(marker);
|
||||||
|
markers.add(marker);
|
||||||
|
|
||||||
|
ParticleEmitter particle = buildParticleEmitter(pe);
|
||||||
|
if (particle != null) rootNode.attachChild(particle);
|
||||||
|
particles.add(particle);
|
||||||
|
|
||||||
|
emitters.add(pe);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeEmitter(int idx) {
|
||||||
|
rootNode.detachChild(markers.get(idx));
|
||||||
|
ParticleEmitter pe = particles.get(idx);
|
||||||
|
if (pe != null) rootNode.detachChild(pe);
|
||||||
|
emitters.remove(idx);
|
||||||
|
markers.remove(idx);
|
||||||
|
particles.remove(idx);
|
||||||
|
selectedIdx = -1;
|
||||||
|
input.selectedEmitterInfo = null;
|
||||||
|
input.emitterSelectionChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearAll() {
|
||||||
|
for (Node m : markers) rootNode.detachChild(m);
|
||||||
|
for (ParticleEmitter pe : particles) if (pe != null) rootNode.detachChild(pe);
|
||||||
|
emitters.clear();
|
||||||
|
markers.clear();
|
||||||
|
particles.clear();
|
||||||
|
selectedIdx = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Property application ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void applyProperty(int idx, PlacedEmitter updated) {
|
||||||
|
rootNode.detachChild(markers.get(idx));
|
||||||
|
ParticleEmitter oldPe = particles.get(idx);
|
||||||
|
if (oldPe != null) rootNode.detachChild(oldPe);
|
||||||
|
|
||||||
|
Node newMarker = buildMarker(updated);
|
||||||
|
setMarkerColor(newMarker, SEL_COLOR);
|
||||||
|
rootNode.attachChild(newMarker);
|
||||||
|
markers.set(idx, newMarker);
|
||||||
|
|
||||||
|
ParticleEmitter newPe = buildParticleEmitter(updated);
|
||||||
|
if (newPe != null) rootNode.attachChild(newPe);
|
||||||
|
particles.set(idx, newPe);
|
||||||
|
|
||||||
|
emitters.set(idx, updated);
|
||||||
|
publishSelection(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Marker visuals ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Node buildMarker(PlacedEmitter pe) {
|
||||||
|
ColorRGBA color = new ColorRGBA(pe.startR(), pe.startG(), pe.startB(), 1f);
|
||||||
|
|
||||||
|
Material sphereMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
sphereMat.setColor("Color", color);
|
||||||
|
Geometry sphere = new Geometry(GEO_MARKER, new Sphere(10, 10, MARKER_RADIUS));
|
||||||
|
sphere.setMaterial(sphereMat);
|
||||||
|
|
||||||
|
Material activMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
activMat.setColor("Color", new ColorRGBA(color.r, color.g, color.b, ACTIVATION_ALPHA));
|
||||||
|
activMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||||
|
activMat.getAdditionalRenderState().setDepthWrite(false);
|
||||||
|
activMat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Front);
|
||||||
|
Geometry activSphere = new Geometry(GEO_ACTRADIUS, new Sphere(8, 8, pe.activationRadius()));
|
||||||
|
activSphere.setMaterial(activMat);
|
||||||
|
activSphere.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||||
|
|
||||||
|
Node node = new Node("emitter_node");
|
||||||
|
node.attachChild(activSphere);
|
||||||
|
node.attachChild(sphere);
|
||||||
|
node.setLocalTranslation(pe.x(), pe.y(), pe.z());
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ParticleEmitter buildParticleEmitter(PlacedEmitter pe) {
|
||||||
|
try {
|
||||||
|
ParticleEmitter effect = new ParticleEmitter(
|
||||||
|
"particle_effect", ParticleMesh.Type.Triangle, pe.maxParticles());
|
||||||
|
Material mat = new Material(assets, "Common/MatDefs/Misc/Particle.j3md");
|
||||||
|
mat.setTexture("Texture", assets.loadTexture(pe.texturePath()));
|
||||||
|
effect.setMaterial(mat);
|
||||||
|
effect.setImagesX(pe.imagesX());
|
||||||
|
effect.setImagesY(pe.imagesY());
|
||||||
|
effect.setStartColor(new ColorRGBA(pe.startR(), pe.startG(), pe.startB(), pe.startA()));
|
||||||
|
effect.setEndColor( new ColorRGBA(pe.endR(), pe.endG(), pe.endB(), pe.endA()));
|
||||||
|
effect.setStartSize(pe.startSize());
|
||||||
|
effect.setEndSize(pe.endSize());
|
||||||
|
effect.getParticleInfluencer()
|
||||||
|
.setInitialVelocity(new Vector3f(pe.velX(), pe.velY(), pe.velZ()));
|
||||||
|
effect.getParticleInfluencer().setVelocityVariation(pe.velocityVariation());
|
||||||
|
effect.setGravity(pe.gravX(), pe.gravY(), pe.gravZ());
|
||||||
|
effect.setLowLife(pe.lowLife());
|
||||||
|
effect.setHighLife(pe.highLife());
|
||||||
|
effect.setParticlesPerSec(pe.emitRate());
|
||||||
|
effect.setLocalTranslation(pe.x(), pe.y(), pe.z());
|
||||||
|
return effect;
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("[EmitterState] Textur nicht ladbar: " + pe.texturePath()
|
||||||
|
+ " — " + e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setMarkerColor(int idx, ColorRGBA color) {
|
||||||
|
setMarkerColor(markers.get(idx), color);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setMarkerColor(Node node, ColorRGBA color) {
|
||||||
|
for (Spatial child : node.getChildren()) {
|
||||||
|
if (child instanceof Geometry geo && GEO_MARKER.equals(geo.getName())) {
|
||||||
|
geo.getMaterial().setColor("Color", color);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Save / Load ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public List<PlacedEmitter> getPlacedEmitters() {
|
||||||
|
return new ArrayList<>(emitters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadPlacedEmitters(List<PlacedEmitter> loaded) {
|
||||||
|
if (rootNode == null) {
|
||||||
|
pendingEmitters = new ArrayList<>(loaded);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearAll();
|
||||||
|
for (PlacedEmitter pe : loaded) addEmitter(pe);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package de.blight.editor.state;
|
package de.blight.editor.state;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
import com.jme3.app.Application;
|
import com.jme3.app.Application;
|
||||||
import com.jme3.app.SimpleApplication;
|
import com.jme3.app.SimpleApplication;
|
||||||
import com.jme3.app.state.BaseAppState;
|
import com.jme3.app.state.BaseAppState;
|
||||||
@@ -19,8 +21,10 @@ import com.jme3.renderer.RenderManager;
|
|||||||
import com.jme3.renderer.ViewPort;
|
import com.jme3.renderer.ViewPort;
|
||||||
import com.jme3.renderer.queue.RenderQueue;
|
import com.jme3.renderer.queue.RenderQueue;
|
||||||
import com.jme3.scene.Geometry;
|
import com.jme3.scene.Geometry;
|
||||||
|
import com.jme3.scene.Mesh;
|
||||||
import com.jme3.scene.Node;
|
import com.jme3.scene.Node;
|
||||||
import com.jme3.scene.Spatial;
|
import com.jme3.scene.Spatial;
|
||||||
|
import com.jme3.scene.VertexBuffer;
|
||||||
import com.jme3.texture.FrameBuffer;
|
import com.jme3.texture.FrameBuffer;
|
||||||
import com.jme3.texture.Image;
|
import com.jme3.texture.Image;
|
||||||
import com.jme3.texture.Texture;
|
import com.jme3.texture.Texture;
|
||||||
@@ -29,37 +33,45 @@ import com.jme3.util.BufferUtils;
|
|||||||
import de.blight.editor.SharedInput;
|
import de.blight.editor.SharedInput;
|
||||||
import de.blight.eztree.Tree;
|
import de.blight.eztree.Tree;
|
||||||
import de.blight.eztree.TreeOptions;
|
import de.blight.eztree.TreeOptions;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
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.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JME3-AppState für den EZ-Tree-Generator.
|
* JME3-AppState für den EZ-Tree-Generator.
|
||||||
*
|
*
|
||||||
* Teilt den Vorschau-Viewport mit {@link TreeGeneratorState} (kein eigenes Framebuffer).
|
* Versucht zuerst, Geometrie über das npm-Paket @dgreenheck/ez-tree via Node.js
|
||||||
* Verarbeitet {@link SharedInput.EzTreeGenRequest}-Einträge aus der Queue,
|
* zu generieren (höhere Qualität). Fällt auf den Java-Port zurück wenn Node.js
|
||||||
* baut einen {@link Tree}-Node, weist Materialien zu und zeigt ihn in der Vorschau.
|
* nicht verfügbar ist oder fehlschlägt.
|
||||||
* Optional: .j3o-Export mit Impostor-PNG.
|
|
||||||
*/
|
*/
|
||||||
public class EzTreeState extends BaseAppState {
|
public class EzTreeState extends BaseAppState {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(EzTreeState.class);
|
||||||
|
|
||||||
private static final int IMPOSTOR_SIZE = 512;
|
private static final int IMPOSTOR_SIZE = 512;
|
||||||
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("editor-assets");
|
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("editor-assets");
|
||||||
|
private static final Path BLIGHT_ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||||
|
private static final Path TOOLS_DIR = de.blight.editor.ProjectRoot.resolve("tools");
|
||||||
|
private static final Gson GSON = new Gson();
|
||||||
|
|
||||||
private final SharedInput input;
|
private final SharedInput input;
|
||||||
private SimpleApplication app;
|
private SimpleApplication app;
|
||||||
private AssetManager assets;
|
private AssetManager assets;
|
||||||
private TreeGeneratorState previewHost;
|
private TreeGeneratorState previewHost;
|
||||||
|
|
||||||
// ── Laufende Capture-Operation ────────────────────────────────────────────
|
// ── Capture-Phase ────────────────────────────────────────────────────────
|
||||||
private SharedInput.EzTreeGenRequest pendingRequest = null;
|
private SharedInput.EzTreeGenRequest pendingRequest = null;
|
||||||
private Node pendingTreeNode = null;
|
private Node pendingTreeNode = null;
|
||||||
private ViewPort captureVP = null;
|
private ViewPort captureVP = null;
|
||||||
@@ -74,7 +86,6 @@ public class EzTreeState extends BaseAppState {
|
|||||||
protected void initialize(Application app) {
|
protected void initialize(Application app) {
|
||||||
this.app = (SimpleApplication) app;
|
this.app = (SimpleApplication) app;
|
||||||
this.assets = app.getAssetManager();
|
this.assets = app.getAssetManager();
|
||||||
// previewHost via lazy-init in update() – TreeGeneratorState evtl. noch nicht attached
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override protected void cleanup(Application app) { cleanupCapture(); }
|
@Override protected void cleanup(Application app) { cleanupCapture(); }
|
||||||
@@ -85,7 +96,6 @@ public class EzTreeState extends BaseAppState {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void update(float tpf) {
|
public void update(float tpf) {
|
||||||
// Lazy-init: TreeGeneratorState muss initialisiert sein, bevor wir darauf zugreifen
|
|
||||||
if (previewHost == null) {
|
if (previewHost == null) {
|
||||||
previewHost = getStateManager().getState(TreeGeneratorState.class);
|
previewHost = getStateManager().getState(TreeGeneratorState.class);
|
||||||
if (previewHost == null) return;
|
if (previewHost == null) return;
|
||||||
@@ -104,12 +114,14 @@ public class EzTreeState extends BaseAppState {
|
|||||||
private void startGeneration(SharedInput.EzTreeGenRequest req) {
|
private void startGeneration(SharedInput.EzTreeGenRequest req) {
|
||||||
cleanupCapture();
|
cleanupCapture();
|
||||||
|
|
||||||
Tree tree = new Tree(req.options());
|
Node treeNode = tryNodeJsGeneration(req);
|
||||||
tree.generate();
|
if (treeNode == null) {
|
||||||
applyMaterials(tree, req.options());
|
treeNode = javaFallback(req);
|
||||||
tree.updateGeometricState();
|
}
|
||||||
|
final Node finalNode = treeNode;
|
||||||
|
finalNode.updateGeometricState();
|
||||||
|
|
||||||
BoundingBox bb = boundsOf(tree);
|
BoundingBox bb = boundsOf(finalNode);
|
||||||
float camDist = bb != null
|
float camDist = bb != null
|
||||||
? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3.2f
|
? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3.2f
|
||||||
: 20f;
|
: 20f;
|
||||||
@@ -117,14 +129,12 @@ public class EzTreeState extends BaseAppState {
|
|||||||
? new Vector3f(0f, bb.getCenter().y, 0f)
|
? new Vector3f(0f, bb.getCenter().y, 0f)
|
||||||
: new Vector3f(0f, 5f, 0f);
|
: new Vector3f(0f, 5f, 0f);
|
||||||
|
|
||||||
// Szenenänderung über enqueue() – läuft am Anfang des nächsten Frames,
|
|
||||||
// bevor TreeGeneratorState.update() updateGeometricState() aufruft.
|
|
||||||
final float dist = camDist;
|
final float dist = camDist;
|
||||||
final Vector3f tgt = target;
|
final Vector3f tgt = target;
|
||||||
app.enqueue(() -> {
|
app.enqueue(() -> {
|
||||||
previewHost.setPreviewContent(tree, dist, tgt);
|
previewHost.setPreviewContent(finalNode, dist, tgt);
|
||||||
if (req.exportAfter()) {
|
if (req.exportAfter()) {
|
||||||
setupCapture(tree, boundsOf(tree), req);
|
setupCapture(finalNode, boundsOf(finalNode), req);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,9 +145,196 @@ public class EzTreeState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Node.js-Generierung ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Node tryNodeJsGeneration(SharedInput.EzTreeGenRequest req) {
|
||||||
|
String presetName = req.presetName();
|
||||||
|
if (presetName == null || presetName.isBlank()) return null;
|
||||||
|
|
||||||
|
Path polyfill = TOOLS_DIR.resolve("dom_polyfill.cjs");
|
||||||
|
Path script = TOOLS_DIR.resolve("ez_tree_generate.mjs");
|
||||||
|
if (!Files.exists(polyfill) || !Files.exists(script)) return null;
|
||||||
|
|
||||||
|
String nodeInput = GSON.toJson(new NodeInput(presetName, buildJsParams(req.options())));
|
||||||
|
|
||||||
|
try {
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(
|
||||||
|
"node",
|
||||||
|
"--require", polyfill.toAbsolutePath().toString(),
|
||||||
|
script.toAbsolutePath().toString(),
|
||||||
|
nodeInput
|
||||||
|
);
|
||||||
|
pb.directory(TOOLS_DIR.toFile());
|
||||||
|
pb.redirectErrorStream(false);
|
||||||
|
Process proc = pb.start();
|
||||||
|
|
||||||
|
// read stdout and stderr concurrently to avoid pipe-buffer deadlock
|
||||||
|
StringBuilder out = new StringBuilder();
|
||||||
|
StringBuilder err = new StringBuilder();
|
||||||
|
Thread outT = new Thread(() -> {
|
||||||
|
try (InputStream is = proc.getInputStream()) {
|
||||||
|
out.append(new String(is.readAllBytes(), StandardCharsets.UTF_8));
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
});
|
||||||
|
Thread errT = new Thread(() -> {
|
||||||
|
try (InputStream is = proc.getErrorStream()) {
|
||||||
|
err.append(new String(is.readAllBytes(), StandardCharsets.UTF_8));
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
});
|
||||||
|
outT.start(); errT.start();
|
||||||
|
|
||||||
|
boolean ok = proc.waitFor(30, TimeUnit.SECONDS);
|
||||||
|
outT.join(5000); errT.join(1000);
|
||||||
|
|
||||||
|
if (!ok || proc.exitValue() != 0) {
|
||||||
|
log.warn("[EzTree] Node.js Fehler (exit {}): {}", proc.exitValue(), err.toString().trim());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!err.isEmpty()) log.debug("[EzTree] Node.js stderr: {}", err.toString().trim());
|
||||||
|
|
||||||
|
return buildNodeFromJson(out.toString(), req);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[EzTree] Node.js nicht verfügbar: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record NodeInput(String preset, java.util.Map<String, Object> params) {}
|
||||||
|
|
||||||
|
private static java.util.Map<String, Object> buildJsParams(TreeOptions opts) {
|
||||||
|
var p = new java.util.LinkedHashMap<String, Object>();
|
||||||
|
p.put("seed", opts.seed);
|
||||||
|
p.put("type", opts.type == de.blight.eztree.TreeType.EVERGREEN ? "evergreen" : "deciduous");
|
||||||
|
|
||||||
|
// bark
|
||||||
|
var bark = new java.util.LinkedHashMap<String, Object>();
|
||||||
|
bark.put("tint", rgbToTint(opts.bark.r, opts.bark.g, opts.bark.b));
|
||||||
|
bark.put("flatShading", opts.bark.flatShading);
|
||||||
|
bark.put("textureScale", java.util.Map.of("x", opts.bark.textureScaleX, "y", opts.bark.textureScaleY));
|
||||||
|
p.put("bark", bark);
|
||||||
|
|
||||||
|
// branch — only send user-controlled value; all internal preset values (angle, children,
|
||||||
|
// gnarliness, length, radius, sections, segments, start, taper, twist) are left to the
|
||||||
|
// JS preset file, because Java has adapted them for its own renderer (capped pine children,
|
||||||
|
// JME-scaled length/radius, degrees instead of radians for twist, etc.)
|
||||||
|
p.put("branch", java.util.Map.of("levels", opts.branch.levels));
|
||||||
|
|
||||||
|
// leaves
|
||||||
|
var leaves = new java.util.LinkedHashMap<String, Object>();
|
||||||
|
leaves.put("tint", rgbToTint(opts.leaves.r, opts.leaves.g, opts.leaves.b));
|
||||||
|
leaves.put("billboard", opts.leaves.billboard == de.blight.eztree.Billboard.CROSS ? "double" : "single");
|
||||||
|
leaves.put("count", opts.leaves.count);
|
||||||
|
leaves.put("start", opts.leaves.start);
|
||||||
|
leaves.put("size", opts.leaves.size * 5f); // Java stores size ÷ 5 vs JS absolute
|
||||||
|
leaves.put("sizeVariance", opts.leaves.sizeVariance);
|
||||||
|
leaves.put("alphaTest", opts.leaves.alphaTest);
|
||||||
|
leaves.put("angle", opts.leaves.angle);
|
||||||
|
p.put("leaves", leaves);
|
||||||
|
|
||||||
|
// trellis
|
||||||
|
p.put("trellis", java.util.Map.of("enabled", opts.trellis.enabled));
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int rgbToTint(float r, float g, float b) {
|
||||||
|
return (Math.round(r * 255) << 16) | (Math.round(g * 255) << 8) | Math.round(b * 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node buildNodeFromJson(String json, SharedInput.EzTreeGenRequest req) {
|
||||||
|
try {
|
||||||
|
JsonObject root = GSON.fromJson(json, JsonObject.class);
|
||||||
|
|
||||||
|
Mesh branchMesh = parseMesh(root.getAsJsonObject("branches"), true);
|
||||||
|
Mesh leafMesh = parseMesh(root.getAsJsonObject("leaves"), true);
|
||||||
|
|
||||||
|
Geometry barkGeo = new Geometry("bark", branchMesh);
|
||||||
|
Geometry leavesGeo = new Geometry("leaves", leafMesh);
|
||||||
|
|
||||||
|
barkGeo.setMaterial(buildBarkMat(req.options()));
|
||||||
|
barkGeo.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
|
||||||
|
|
||||||
|
leavesGeo.setMaterial(buildLeafMat(req.options()));
|
||||||
|
leavesGeo.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||||
|
leavesGeo.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
|
||||||
|
|
||||||
|
Node node = new Node("EzTree");
|
||||||
|
node.attachChild(barkGeo);
|
||||||
|
node.attachChild(leavesGeo);
|
||||||
|
return node;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[EzTree] JSON→Mesh Fehler: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Mesh parseMesh(JsonObject geo, boolean hasUvs) {
|
||||||
|
float[] positions = toFloatArray(geo.getAsJsonArray("positions"));
|
||||||
|
float[] normals = toFloatArray(geo.getAsJsonArray("normals"));
|
||||||
|
int[] indices = toIntArray(geo.getAsJsonArray("indices"));
|
||||||
|
|
||||||
|
Mesh mesh = new Mesh();
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Position, 3,
|
||||||
|
BufferUtils.createFloatBuffer(positions));
|
||||||
|
if (normals.length > 0) {
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Normal, 3,
|
||||||
|
BufferUtils.createFloatBuffer(normals));
|
||||||
|
}
|
||||||
|
if (hasUvs && geo.has("uvs")) {
|
||||||
|
float[] uvs = toFloatArray(geo.getAsJsonArray("uvs"));
|
||||||
|
if (uvs.length > 0) {
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2,
|
||||||
|
BufferUtils.createFloatBuffer(uvs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Use short buffer if fits, otherwise int buffer
|
||||||
|
if (indices.length > 0) {
|
||||||
|
if (canFitShort(indices)) {
|
||||||
|
short[] shorts = new short[indices.length];
|
||||||
|
for (int i = 0; i < indices.length; i++) shorts[i] = (short) indices[i];
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Index, 3,
|
||||||
|
BufferUtils.createShortBuffer(shorts));
|
||||||
|
} else {
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Index, 3,
|
||||||
|
BufferUtils.createIntBuffer(indices));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mesh.updateBound();
|
||||||
|
mesh.updateCounts();
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float[] toFloatArray(com.google.gson.JsonArray arr) {
|
||||||
|
float[] out = new float[arr.size()];
|
||||||
|
for (int i = 0; i < out.length; i++) out[i] = arr.get(i).getAsFloat();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] toIntArray(com.google.gson.JsonArray arr) {
|
||||||
|
int[] out = new int[arr.size()];
|
||||||
|
for (int i = 0; i < out.length; i++) out[i] = arr.get(i).getAsInt();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean canFitShort(int[] indices) {
|
||||||
|
for (int idx : indices) if (idx > 65535) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Java-Fallback ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Node javaFallback(SharedInput.EzTreeGenRequest req) {
|
||||||
|
Tree tree = new Tree(req.options());
|
||||||
|
tree.generate();
|
||||||
|
applyMaterials(tree, req.options());
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Phase 2: Impostor-Capture ─────────────────────────────────────────────
|
// ── Phase 2: Impostor-Capture ─────────────────────────────────────────────
|
||||||
|
|
||||||
private void setupCapture(Tree tree, BoundingBox bb, SharedInput.EzTreeGenRequest req) {
|
private void setupCapture(Node treeNode, BoundingBox bb, SharedInput.EzTreeGenRequest req) {
|
||||||
BoundingBox safeBb = bb != null ? bb : new BoundingBox(Vector3f.ZERO, 5f, 10f, 5f);
|
BoundingBox safeBb = bb != null ? bb : new BoundingBox(Vector3f.ZERO, 5f, 10f, 5f);
|
||||||
|
|
||||||
Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);
|
Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);
|
||||||
@@ -145,22 +342,25 @@ public class EzTreeState extends BaseAppState {
|
|||||||
captureFB.addColorTexture(capTex);
|
captureFB.addColorTexture(capTex);
|
||||||
captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth));
|
captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth));
|
||||||
|
|
||||||
captureVP = buildCaptureViewPort(tree, safeBb, captureFB);
|
captureVP = buildCaptureViewPort(treeNode, safeBb, captureFB);
|
||||||
captureReady = false;
|
captureReady = false;
|
||||||
pendingRequest = req;
|
pendingRequest = req;
|
||||||
pendingTreeNode = tree;
|
pendingTreeNode = treeNode;
|
||||||
input.treeGenStatusMsg = "EZ-Tree: rendere Impostor…";
|
input.treeGenStatusMsg = "EZ-Tree: rendere Impostor…";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void finishCapture() {
|
private void finishCapture() {
|
||||||
ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4);
|
ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4);
|
||||||
app.getRenderer().readFrameBuffer(captureFB, pixels);
|
app.getRenderer().readFrameBuffer(captureFB, pixels);
|
||||||
|
|
||||||
|
SharedInput.EzTreeGenRequest req = pendingRequest;
|
||||||
|
Node treeNode = pendingTreeNode;
|
||||||
cleanupCapture();
|
cleanupCapture();
|
||||||
|
|
||||||
String exportName = pendingRequest.exportName() + "_"
|
String exportName = req.exportName() + "_"
|
||||||
+ DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
|
+ DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
|
||||||
saveImpostor(pixels, "ez_impostor_" + exportName);
|
saveImpostor(pixels, "ez_impostor_" + exportName);
|
||||||
exportTree(pendingTreeNode, exportName);
|
exportTree(treeNode, req.exportName(), req.treeCategory());
|
||||||
|
|
||||||
pendingRequest = null;
|
pendingRequest = null;
|
||||||
pendingTreeNode = null;
|
pendingTreeNode = null;
|
||||||
@@ -183,7 +383,6 @@ public class EzTreeState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (child instanceof Node trellis) {
|
} else if (child instanceof Node trellis) {
|
||||||
// Trellis-Node: Rinden-Material auf alle Geometrien
|
|
||||||
Material mat = buildBarkMat(opts);
|
Material mat = buildBarkMat(opts);
|
||||||
for (Spatial s : trellis.getChildren()) {
|
for (Spatial s : trellis.getChildren()) {
|
||||||
if (s instanceof Geometry g) g.setMaterial(mat.clone());
|
if (s instanceof Geometry g) g.setMaterial(mat.clone());
|
||||||
@@ -238,7 +437,7 @@ public class EzTreeState extends BaseAppState {
|
|||||||
|
|
||||||
// ── Offscreen-Viewport für Impostor ───────────────────────────────────────
|
// ── Offscreen-Viewport für Impostor ───────────────────────────────────────
|
||||||
|
|
||||||
private ViewPort buildCaptureViewPort(Tree src, BoundingBox bb, FrameBuffer fb) {
|
private ViewPort buildCaptureViewPort(Node src, BoundingBox bb, FrameBuffer fb) {
|
||||||
Vector3f center = bb.getCenter().add(0f, 2f, 0f);
|
Vector3f center = bb.getCenter().add(0f, 2f, 0f);
|
||||||
float extent = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
|
float extent = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
|
||||||
float dist = extent * 3f;
|
float dist = extent * 3f;
|
||||||
@@ -279,7 +478,7 @@ public class EzTreeState extends BaseAppState {
|
|||||||
return vp;
|
return vp;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Node cloneForCapture(Tree src) {
|
private static Node cloneForCapture(Node src) {
|
||||||
Node copy = new Node("ezCap");
|
Node copy = new Node("ezCap");
|
||||||
copy.setLocalTranslation(src.getLocalTranslation());
|
copy.setLocalTranslation(src.getLocalTranslation());
|
||||||
for (Spatial child : src.getChildren()) {
|
for (Spatial child : src.getChildren()) {
|
||||||
@@ -304,8 +503,9 @@ public class EzTreeState extends BaseAppState {
|
|||||||
|
|
||||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static BoundingBox boundsOf(Tree tree) {
|
private static BoundingBox boundsOf(Node node) {
|
||||||
if (tree.getWorldBound() instanceof BoundingBox bb) return bb;
|
node.updateModelBound();
|
||||||
|
if (node.getWorldBound() instanceof BoundingBox bb) return bb;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,19 +537,24 @@ public class EzTreeState extends BaseAppState {
|
|||||||
Files.createDirectories(texDir);
|
Files.createDirectories(texDir);
|
||||||
ImageIO.write(img, "PNG", texDir.resolve(name + ".png").toFile());
|
ImageIO.write(img, "PNG", texDir.resolve(name + ".png").toFile());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
System.err.println("[EzTreeState] Impostor-Fehler: " + e.getMessage());
|
log.error("[EzTree] Impostor-Fehler: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exportTree(Node treeNode, String name) {
|
private void exportTree(Node treeNode, String name, String treeCategory) {
|
||||||
try {
|
try {
|
||||||
Path modelDir = ASSET_ROOT.resolve("models");
|
Path baseDir = (treeCategory != null && !treeCategory.isBlank())
|
||||||
Files.createDirectories(modelDir);
|
? BLIGHT_ASSET_ROOT.resolve("trees").resolve(treeCategory)
|
||||||
File out = modelDir.resolve("EzTree_" + name + ".j3o").toFile();
|
: ASSET_ROOT.resolve("models");
|
||||||
|
Files.createDirectories(baseDir);
|
||||||
|
File out = baseDir.resolve(name + ".j3o").toFile();
|
||||||
BinaryExporter.getInstance().save(treeNode, out);
|
BinaryExporter.getInstance().save(treeNode, out);
|
||||||
|
log.info("[EZ-Tree] Gespeichert: {}", out.getAbsolutePath());
|
||||||
input.treeGenStatusMsg = "EZ-Tree exportiert: " + out.getName();
|
input.treeGenStatusMsg = "EZ-Tree exportiert: " + out.getName();
|
||||||
input.refreshAssets = true;
|
input.refreshAssets = true;
|
||||||
|
input.refreshTreeFolders = true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
log.error("[EZ-Tree] Export-Fehler: {}", e.getMessage());
|
||||||
input.treeGenStatusMsg = "EZ-Tree Export-Fehler: " + e.getMessage();
|
input.treeGenStatusMsg = "EZ-Tree Export-Fehler: " + e.getMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,320 @@
|
|||||||
|
package de.blight.editor.state;
|
||||||
|
|
||||||
|
import com.jme3.app.Application;
|
||||||
|
import com.jme3.app.SimpleApplication;
|
||||||
|
import com.jme3.app.state.BaseAppState;
|
||||||
|
import com.jme3.asset.AssetManager;
|
||||||
|
import com.jme3.collision.CollisionResults;
|
||||||
|
import com.jme3.light.PointLight;
|
||||||
|
import com.jme3.material.Material;
|
||||||
|
import com.jme3.material.RenderState;
|
||||||
|
import com.jme3.math.*;
|
||||||
|
import com.jme3.renderer.Camera;
|
||||||
|
import com.jme3.renderer.queue.RenderQueue;
|
||||||
|
import com.jme3.scene.*;
|
||||||
|
import com.jme3.scene.shape.Cylinder;
|
||||||
|
import com.jme3.scene.shape.Sphere;
|
||||||
|
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||||
|
import de.blight.common.PlacedLight;
|
||||||
|
import de.blight.editor.SharedInput;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class LightState extends BaseAppState {
|
||||||
|
|
||||||
|
// ── Glühbirnen-Geometrie ──────────────────────────────────────────────────
|
||||||
|
private static final float BULB_RADIUS = 0.38f;
|
||||||
|
private static final float SOCKET_RADIUS = 0.14f;
|
||||||
|
private static final float SOCKET_HEIGHT = 0.28f;
|
||||||
|
private static final float GLOW_RADIUS = BULB_RADIUS * 2.6f;
|
||||||
|
private static final float GLOW_ALPHA = 0.16f;
|
||||||
|
|
||||||
|
private static final String GEO_BULB = "bulb";
|
||||||
|
private static final String GEO_SOCKET = "socket";
|
||||||
|
private static final String GEO_GLOW = "glow";
|
||||||
|
|
||||||
|
private static final ColorRGBA SEL_COLOR = new ColorRGBA(1f, 1f, 0f, 1f);
|
||||||
|
private static final ColorRGBA SOCKET_COLOR = new ColorRGBA(0.22f, 0.22f, 0.22f, 1f);
|
||||||
|
|
||||||
|
private final SharedInput input;
|
||||||
|
private SimpleApplication app;
|
||||||
|
private Camera cam;
|
||||||
|
private AssetManager assets;
|
||||||
|
private Node rootNode;
|
||||||
|
private TerrainQuad terrain;
|
||||||
|
|
||||||
|
// parallel lists: lights[i] <-> markers[i] <-> pointLights[i]
|
||||||
|
private final List<PlacedLight> lights = new ArrayList<>();
|
||||||
|
private final List<Node> markers = new ArrayList<>();
|
||||||
|
private final List<PointLight> pointLights = new ArrayList<>();
|
||||||
|
|
||||||
|
private int selectedIdx = -1;
|
||||||
|
private List<PlacedLight> pendingLights = null;
|
||||||
|
|
||||||
|
public LightState(SharedInput input) {
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize(Application application) {
|
||||||
|
app = (SimpleApplication) application;
|
||||||
|
cam = app.getCamera();
|
||||||
|
assets = app.getAssetManager();
|
||||||
|
rootNode = app.getRootNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void cleanup(Application application) {
|
||||||
|
clearAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onEnable() {
|
||||||
|
if (pendingLights != null) {
|
||||||
|
loadPlacedLights(pendingLights);
|
||||||
|
pendingLights = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override protected void onDisable() {}
|
||||||
|
|
||||||
|
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
||||||
|
|
||||||
|
// ── Update ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(float tpf) {
|
||||||
|
if (input.activeLayer != SharedInput.LAYER_LIGHTS) return;
|
||||||
|
|
||||||
|
SharedInput.LightClick click;
|
||||||
|
while ((click = input.lightClickQueue.poll()) != null) {
|
||||||
|
handleClick(click);
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedInput.LightPropertyChange prop = input.pendingLightProp.getAndSet(null);
|
||||||
|
if (prop != null && selectedIdx >= 0) {
|
||||||
|
applyProperty(selectedIdx, prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.deleteLightRequested) {
|
||||||
|
input.deleteLightRequested = false;
|
||||||
|
if (selectedIdx >= 0) removeLight(selectedIdx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Click handling ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void handleClick(SharedInput.LightClick click) {
|
||||||
|
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||||
|
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||||
|
|
||||||
|
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||||
|
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||||
|
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||||
|
|
||||||
|
int hit = pickMarker(ray);
|
||||||
|
if (hit >= 0) {
|
||||||
|
if (click.rightButton()) {
|
||||||
|
deselect();
|
||||||
|
} else {
|
||||||
|
selectLight(hit);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (click.rightButton()) {
|
||||||
|
deselect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neues Licht auf dem Terrain platzieren
|
||||||
|
if (terrain == null) return;
|
||||||
|
CollisionResults hits = new CollisionResults();
|
||||||
|
terrain.collideWith(ray, hits);
|
||||||
|
if (hits.size() == 0) return;
|
||||||
|
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||||
|
|
||||||
|
PlacedLight pl = new PlacedLight(pt.x, pt.y + 1f, pt.z, 1f, 1f, 1f, 1f, 20f);
|
||||||
|
addLight(pl);
|
||||||
|
selectLight(lights.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Trifft den Glow-Halo oder den Bulb irgendeines Markers. */
|
||||||
|
private int pickMarker(Ray ray) {
|
||||||
|
for (int i = 0; i < markers.size(); i++) {
|
||||||
|
CollisionResults res = new CollisionResults();
|
||||||
|
markers.get(i).collideWith(ray, res);
|
||||||
|
if (res.size() > 0) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Selection ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void selectLight(int idx) {
|
||||||
|
deselect();
|
||||||
|
selectedIdx = idx;
|
||||||
|
setBulbColor(idx, SEL_COLOR);
|
||||||
|
publishSelection(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deselect() {
|
||||||
|
if (selectedIdx >= 0 && selectedIdx < lights.size()) {
|
||||||
|
PlacedLight l = lights.get(selectedIdx);
|
||||||
|
setBulbColor(selectedIdx, new ColorRGBA(l.r(), l.g(), l.b(), 1f));
|
||||||
|
}
|
||||||
|
selectedIdx = -1;
|
||||||
|
input.selectedLightInfo = null;
|
||||||
|
input.lightSelectionChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publishSelection(int idx) {
|
||||||
|
PlacedLight l = lights.get(idx);
|
||||||
|
input.selectedLightInfo = String.format(
|
||||||
|
java.util.Locale.ROOT,
|
||||||
|
"%d|%.3f|%.3f|%.3f|%.3f|%.3f|%.3f|%.3f|%.3f",
|
||||||
|
idx, l.x(), l.y(), l.z(), l.r(), l.g(), l.b(), l.intensity(), l.radius());
|
||||||
|
input.lightSelectionChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Add / Remove ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void addLight(PlacedLight pl) {
|
||||||
|
PointLight pt = new PointLight();
|
||||||
|
pt.setColor(new ColorRGBA(pl.r(), pl.g(), pl.b(), 1f).mult(pl.intensity()));
|
||||||
|
pt.setRadius(pl.radius());
|
||||||
|
pt.setPosition(new Vector3f(pl.x(), pl.y(), pl.z()));
|
||||||
|
rootNode.addLight(pt);
|
||||||
|
|
||||||
|
Node marker = buildMarker(pl);
|
||||||
|
rootNode.attachChild(marker);
|
||||||
|
|
||||||
|
lights.add(pl);
|
||||||
|
markers.add(marker);
|
||||||
|
pointLights.add(pt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeLight(int idx) {
|
||||||
|
rootNode.removeLight(pointLights.get(idx));
|
||||||
|
rootNode.detachChild(markers.get(idx));
|
||||||
|
lights.remove(idx);
|
||||||
|
markers.remove(idx);
|
||||||
|
pointLights.remove(idx);
|
||||||
|
selectedIdx = -1;
|
||||||
|
input.selectedLightInfo = null;
|
||||||
|
input.lightSelectionChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearAll() {
|
||||||
|
for (PointLight pl : pointLights) rootNode.removeLight(pl);
|
||||||
|
for (Node m : markers) rootNode.detachChild(m);
|
||||||
|
lights.clear();
|
||||||
|
markers.clear();
|
||||||
|
pointLights.clear();
|
||||||
|
selectedIdx = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Property application ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void applyProperty(int idx, SharedInput.LightPropertyChange prop) {
|
||||||
|
PlacedLight old = lights.get(idx);
|
||||||
|
PlacedLight updated = new PlacedLight(
|
||||||
|
old.x(), old.y(), old.z(),
|
||||||
|
prop.r(), prop.g(), prop.b(),
|
||||||
|
prop.intensity(), prop.radius());
|
||||||
|
lights.set(idx, updated);
|
||||||
|
|
||||||
|
PointLight pt = pointLights.get(idx);
|
||||||
|
pt.setColor(new ColorRGBA(prop.r(), prop.g(), prop.b(), 1f).mult(prop.intensity()));
|
||||||
|
pt.setRadius(prop.radius());
|
||||||
|
|
||||||
|
setGlowColor(idx, prop.r(), prop.g(), prop.b());
|
||||||
|
setBulbColor(idx, SEL_COLOR); // gelb während Selektion
|
||||||
|
publishSelection(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Marker visuals ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Glühbirne: Glaskolben (Sphere) + Sockel (Cylinder) + weicher Leucht-Halo (Sphere, transparent).
|
||||||
|
* Naming convention:
|
||||||
|
* "bulb" – Glaskolben, Farbe = Lichtfarbe (gelb wenn selektiert)
|
||||||
|
* "socket" – Metallsockel, immer dunkelgrau
|
||||||
|
* "glow" – Leuchthalo, immer Lichtfarbe (halbtransparent)
|
||||||
|
*/
|
||||||
|
private Node buildMarker(PlacedLight pl) {
|
||||||
|
ColorRGBA lightColor = new ColorRGBA(pl.r(), pl.g(), pl.b(), 1f);
|
||||||
|
|
||||||
|
// Glaskolben
|
||||||
|
Material bulbMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
bulbMat.setColor("Color", lightColor);
|
||||||
|
Geometry bulb = new Geometry(GEO_BULB, new Sphere(12, 12, BULB_RADIUS));
|
||||||
|
bulb.setMaterial(bulbMat);
|
||||||
|
|
||||||
|
// Metallsockel (Zylinder unterhalb des Kolbens)
|
||||||
|
Material socketMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
socketMat.setColor("Color", SOCKET_COLOR);
|
||||||
|
Geometry socket = new Geometry(GEO_SOCKET,
|
||||||
|
new Cylinder(4, 10, SOCKET_RADIUS, SOCKET_HEIGHT, true));
|
||||||
|
socket.setMaterial(socketMat);
|
||||||
|
socket.rotate(FastMath.HALF_PI, 0, 0);
|
||||||
|
socket.setLocalTranslation(0, -(BULB_RADIUS + SOCKET_HEIGHT * 0.5f), 0);
|
||||||
|
|
||||||
|
// Leuchthalo (halbtransparente Sphere um den Kolben)
|
||||||
|
Material glowMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
glowMat.setColor("Color", new ColorRGBA(pl.r(), pl.g(), pl.b(), GLOW_ALPHA));
|
||||||
|
glowMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||||
|
glowMat.getAdditionalRenderState().setDepthWrite(false);
|
||||||
|
glowMat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Front);
|
||||||
|
Geometry glow = new Geometry(GEO_GLOW, new Sphere(8, 8, GLOW_RADIUS));
|
||||||
|
glow.setMaterial(glowMat);
|
||||||
|
glow.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||||
|
|
||||||
|
Node node = new Node("light_node");
|
||||||
|
node.attachChild(glow); // zuerst → transparent nach opaken
|
||||||
|
node.attachChild(bulb);
|
||||||
|
node.attachChild(socket);
|
||||||
|
node.setLocalTranslation(pl.x(), pl.y(), pl.z());
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Setzt die Farbe des Glaskolbens (Auswahl-Feedback oder Lichtfarbe). */
|
||||||
|
private void setBulbColor(int idx, ColorRGBA color) {
|
||||||
|
findGeo(markers.get(idx), GEO_BULB, geo ->
|
||||||
|
geo.getMaterial().setColor("Color", color));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Aktualisiert den Glow-Halo auf die neue Lichtfarbe. */
|
||||||
|
private void setGlowColor(int idx, float r, float g, float b) {
|
||||||
|
findGeo(markers.get(idx), GEO_GLOW, geo ->
|
||||||
|
geo.getMaterial().setColor("Color", new ColorRGBA(r, g, b, GLOW_ALPHA)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void findGeo(Node node, String name, java.util.function.Consumer<Geometry> action) {
|
||||||
|
for (Spatial child : node.getChildren()) {
|
||||||
|
if (child instanceof Geometry geo && name.equals(geo.getName())) {
|
||||||
|
action.accept(geo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Save / Load ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public List<PlacedLight> getPlacedLights() {
|
||||||
|
return new ArrayList<>(lights);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadPlacedLights(List<PlacedLight> loaded) {
|
||||||
|
if (rootNode == null) {
|
||||||
|
pendingLights = new ArrayList<>(loaded);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearAll();
|
||||||
|
for (PlacedLight pl : loaded) {
|
||||||
|
addLight(pl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
package de.blight.editor.state;
|
||||||
|
|
||||||
|
import com.jme3.app.Application;
|
||||||
|
import com.jme3.app.SimpleApplication;
|
||||||
|
import com.jme3.app.state.BaseAppState;
|
||||||
|
import com.jme3.asset.AssetManager;
|
||||||
|
import com.jme3.collision.CollisionResults;
|
||||||
|
import com.jme3.material.Material;
|
||||||
|
import com.jme3.math.*;
|
||||||
|
import com.jme3.renderer.Camera;
|
||||||
|
import com.jme3.scene.*;
|
||||||
|
import com.jme3.scene.VertexBuffer;
|
||||||
|
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||||
|
import com.jme3.util.BufferUtils;
|
||||||
|
import de.blight.common.PlacedMusicArea;
|
||||||
|
import de.blight.editor.SharedInput;
|
||||||
|
|
||||||
|
import java.nio.FloatBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MusicAreaState extends BaseAppState {
|
||||||
|
|
||||||
|
private static final float LINE_OFFSET_Y = 0.35f;
|
||||||
|
|
||||||
|
private static final ColorRGBA COLOR_NORMAL = new ColorRGBA(0.5f, 0.3f, 1f, 1f);
|
||||||
|
private static final ColorRGBA COLOR_SELECTED = new ColorRGBA(1f, 0.9f, 0.2f, 1f);
|
||||||
|
private static final ColorRGBA COLOR_INPROG = new ColorRGBA(0.8f, 0.5f, 1f, 1f);
|
||||||
|
|
||||||
|
private final SharedInput input;
|
||||||
|
private SimpleApplication app;
|
||||||
|
private Camera cam;
|
||||||
|
private AssetManager assets;
|
||||||
|
private Node rootNode;
|
||||||
|
private TerrainQuad terrain;
|
||||||
|
|
||||||
|
private final List<PlacedMusicArea> areas = new ArrayList<>();
|
||||||
|
private final List<Geometry> areaGeos = new ArrayList<>();
|
||||||
|
private int selectedIdx = -1;
|
||||||
|
|
||||||
|
private boolean placing = false;
|
||||||
|
private final List<Float> currX = new ArrayList<>();
|
||||||
|
private final List<Float> currZ = new ArrayList<>();
|
||||||
|
private Geometry inProgGeo = null;
|
||||||
|
private Geometry lastPointMarker = null;
|
||||||
|
|
||||||
|
private List<PlacedMusicArea> pendingAreas = null;
|
||||||
|
|
||||||
|
public MusicAreaState(SharedInput input) {
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize(Application application) {
|
||||||
|
app = (SimpleApplication) application;
|
||||||
|
cam = app.getCamera();
|
||||||
|
assets = app.getAssetManager();
|
||||||
|
rootNode = app.getRootNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void cleanup(Application application) { clearAll(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onEnable() {
|
||||||
|
if (pendingAreas != null) {
|
||||||
|
loadAreas(pendingAreas);
|
||||||
|
pendingAreas = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void onDisable() {}
|
||||||
|
|
||||||
|
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
||||||
|
|
||||||
|
// ── Update ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(float tpf) {
|
||||||
|
if (input.activeLayer != SharedInput.LAYER_MUSIC_AREAS) {
|
||||||
|
if (placing) cancelPoly();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedInput.MusicAreaClick click;
|
||||||
|
while ((click = input.musicAreaClickQueue.poll()) != null) {
|
||||||
|
handleClick(click);
|
||||||
|
}
|
||||||
|
|
||||||
|
PlacedMusicArea pending = input.pendingMusicArea.getAndSet(null);
|
||||||
|
if (pending != null && selectedIdx >= 0) {
|
||||||
|
applyProperty(selectedIdx, pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.cancelZoneDrawing) {
|
||||||
|
input.cancelZoneDrawing = false;
|
||||||
|
if (placing) cancelPoly();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.deleteMusicAreaRequested) {
|
||||||
|
input.deleteMusicAreaRequested = false;
|
||||||
|
if (selectedIdx >= 0) removeArea(selectedIdx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Click handling ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void handleClick(SharedInput.MusicAreaClick click) {
|
||||||
|
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||||
|
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||||
|
|
||||||
|
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||||
|
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||||
|
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||||
|
|
||||||
|
if (click.rightButton()) {
|
||||||
|
if (placing) closePoly();
|
||||||
|
else deselect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (terrain == null) return;
|
||||||
|
CollisionResults hits = new CollisionResults();
|
||||||
|
terrain.collideWith(ray, hits);
|
||||||
|
if (hits.size() == 0) return;
|
||||||
|
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||||
|
float hitX = pt.x, hitZ = pt.z;
|
||||||
|
|
||||||
|
if (placing) {
|
||||||
|
float[] snapped = snapVertex(hitX, hitZ);
|
||||||
|
hitX = snapped[0];
|
||||||
|
hitZ = snapped[1];
|
||||||
|
|
||||||
|
if (currX.size() >= 3) {
|
||||||
|
float dx = hitX - currX.get(0);
|
||||||
|
float dz = hitZ - currZ.get(0);
|
||||||
|
if (dx * dx + dz * dz < SoundAreaState.SNAP_DIST * SoundAreaState.SNAP_DIST * 0.25f) {
|
||||||
|
closePoly();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currX.add(hitX);
|
||||||
|
currZ.add(hitZ);
|
||||||
|
updateInProgressGeo();
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < areas.size(); i++) {
|
||||||
|
PlacedMusicArea a = areas.get(i);
|
||||||
|
if (SoundAreaState.pointInPolygon(hitX, hitZ, a.pointsX(), a.pointsZ())) {
|
||||||
|
selectArea(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deselect();
|
||||||
|
placing = true;
|
||||||
|
currX.clear();
|
||||||
|
currZ.clear();
|
||||||
|
currX.add(hitX);
|
||||||
|
currZ.add(hitZ);
|
||||||
|
updateInProgressGeo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float[] snapVertex(float x, float z) {
|
||||||
|
float bestDist2 = SoundAreaState.SNAP_DIST * SoundAreaState.SNAP_DIST;
|
||||||
|
float bx = x, bz = z;
|
||||||
|
for (PlacedMusicArea a : areas) {
|
||||||
|
for (int i = 0; i < a.pointsX().length; i++) {
|
||||||
|
float dx = x - a.pointsX()[i];
|
||||||
|
float dz = z - a.pointsZ()[i];
|
||||||
|
float d2 = dx * dx + dz * dz;
|
||||||
|
if (d2 < bestDist2) { bestDist2 = d2; bx = a.pointsX()[i]; bz = a.pointsZ()[i]; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < currX.size(); i++) {
|
||||||
|
float dx = x - currX.get(i);
|
||||||
|
float dz = z - currZ.get(i);
|
||||||
|
float d2 = dx * dx + dz * dz;
|
||||||
|
if (d2 < bestDist2) { bestDist2 = d2; bx = currX.get(i); bz = currZ.get(i); }
|
||||||
|
}
|
||||||
|
return new float[]{bx, bz};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closePoly() {
|
||||||
|
if (currX.size() < 3) { cancelPoly(); return; }
|
||||||
|
float[] xs = toArray(currX);
|
||||||
|
float[] zs = toArray(currZ);
|
||||||
|
PlacedMusicArea area = new PlacedMusicArea(xs, zs, "", "", "");
|
||||||
|
addArea(area);
|
||||||
|
selectArea(areas.size() - 1);
|
||||||
|
cancelPoly();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelPoly() {
|
||||||
|
placing = false;
|
||||||
|
currX.clear();
|
||||||
|
currZ.clear();
|
||||||
|
if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; }
|
||||||
|
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateInProgressGeo() {
|
||||||
|
if (inProgGeo != null) rootNode.detachChild(inProgGeo);
|
||||||
|
int n = currX.size();
|
||||||
|
if (n == 0) { inProgGeo = null; updateLastPointMarker(); return; }
|
||||||
|
inProgGeo = buildLineGeo("music_inprog", currX, currZ, COLOR_INPROG, Mesh.Mode.LineStrip);
|
||||||
|
rootNode.attachChild(inProgGeo);
|
||||||
|
updateLastPointMarker();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateLastPointMarker() {
|
||||||
|
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
|
||||||
|
if (currX.isEmpty()) return;
|
||||||
|
|
||||||
|
float x = currX.get(currX.size() - 1);
|
||||||
|
float z = currZ.get(currZ.size() - 1);
|
||||||
|
float y = (terrain != null ? terrain.getHeight(new Vector2f(x, z)) : 0f) + LINE_OFFSET_Y + 0.05f;
|
||||||
|
float s = 1.5f;
|
||||||
|
|
||||||
|
FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3);
|
||||||
|
buf.put(x - s).put(y).put(z - s); buf.put(x + s).put(y).put(z + s);
|
||||||
|
buf.put(x - s).put(y).put(z + s); buf.put(x + s).put(y).put(z - s);
|
||||||
|
buf.flip();
|
||||||
|
|
||||||
|
Mesh mesh = new Mesh();
|
||||||
|
mesh.setMode(Mesh.Mode.Lines);
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Position, 3, buf);
|
||||||
|
mesh.updateBound();
|
||||||
|
|
||||||
|
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
mat.setColor("Color", new ColorRGBA(1f, 0.15f, 0.15f, 1f));
|
||||||
|
mat.getAdditionalRenderState().setLineWidth(3f);
|
||||||
|
|
||||||
|
lastPointMarker = new Geometry("music_lastpoint", mesh);
|
||||||
|
lastPointMarker.setMaterial(mat);
|
||||||
|
rootNode.attachChild(lastPointMarker);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Selection ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void selectArea(int idx) {
|
||||||
|
if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) {
|
||||||
|
areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
|
||||||
|
}
|
||||||
|
selectedIdx = idx;
|
||||||
|
areaGeos.get(idx).getMaterial().setColor("Color", COLOR_SELECTED);
|
||||||
|
publishSelection(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deselect() {
|
||||||
|
if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) {
|
||||||
|
areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
|
||||||
|
}
|
||||||
|
selectedIdx = -1;
|
||||||
|
input.selectedMusicAreaInfo = null;
|
||||||
|
input.musicAreaSelectionChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publishSelection(int idx) {
|
||||||
|
PlacedMusicArea a = areas.get(idx);
|
||||||
|
input.selectedMusicAreaInfo = idx + "|" + a.dayTrack() + "|" + a.nightTrack() + "|" + a.combatTrack();
|
||||||
|
input.musicAreaSelectionChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Add / Remove / Apply ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void addArea(PlacedMusicArea area) {
|
||||||
|
areas.add(area);
|
||||||
|
List<Float> xs = toList(area.pointsX());
|
||||||
|
List<Float> zs = toList(area.pointsZ());
|
||||||
|
Geometry geo = buildLineGeo("music_area_" + (areas.size() - 1), xs, zs, COLOR_NORMAL, Mesh.Mode.LineLoop);
|
||||||
|
rootNode.attachChild(geo);
|
||||||
|
areaGeos.add(geo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeArea(int idx) {
|
||||||
|
rootNode.detachChild(areaGeos.get(idx));
|
||||||
|
areas.remove(idx);
|
||||||
|
areaGeos.remove(idx);
|
||||||
|
selectedIdx = -1;
|
||||||
|
input.selectedMusicAreaInfo = null;
|
||||||
|
input.musicAreaSelectionChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearAll() {
|
||||||
|
for (Geometry g : areaGeos) rootNode.detachChild(g);
|
||||||
|
areas.clear();
|
||||||
|
areaGeos.clear();
|
||||||
|
cancelPoly();
|
||||||
|
selectedIdx = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyProperty(int idx, PlacedMusicArea updated) {
|
||||||
|
if (updated.pointsX().length == 0) {
|
||||||
|
PlacedMusicArea existing = areas.get(idx);
|
||||||
|
areas.set(idx, new PlacedMusicArea(
|
||||||
|
existing.pointsX(), existing.pointsZ(),
|
||||||
|
updated.dayTrack(), updated.nightTrack(), updated.combatTrack()));
|
||||||
|
} else {
|
||||||
|
areas.set(idx, updated);
|
||||||
|
}
|
||||||
|
publishSelection(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Geometry buildLineGeo(String name, List<Float> xs, List<Float> zs,
|
||||||
|
ColorRGBA color, Mesh.Mode mode) {
|
||||||
|
int n = xs.size();
|
||||||
|
FloatBuffer posBuffer = BufferUtils.createFloatBuffer(n * 3);
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
float hy = terrain != null ? terrain.getHeight(new Vector2f(xs.get(i), zs.get(i))) : 0f;
|
||||||
|
posBuffer.put(xs.get(i)).put(hy + LINE_OFFSET_Y).put(zs.get(i));
|
||||||
|
}
|
||||||
|
posBuffer.flip();
|
||||||
|
|
||||||
|
Mesh mesh = new Mesh();
|
||||||
|
mesh.setMode(mode);
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Position, 3, posBuffer);
|
||||||
|
mesh.updateBound();
|
||||||
|
|
||||||
|
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
mat.setColor("Color", color);
|
||||||
|
mat.getAdditionalRenderState().setLineWidth(2f);
|
||||||
|
|
||||||
|
Geometry geo = new Geometry(name, mesh);
|
||||||
|
geo.setMaterial(mat);
|
||||||
|
return geo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Save / Load ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public List<PlacedMusicArea> getPlacedAreas() {
|
||||||
|
return new ArrayList<>(areas);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadAreas(List<PlacedMusicArea> loaded) {
|
||||||
|
if (rootNode == null) {
|
||||||
|
pendingAreas = new ArrayList<>(loaded);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearAll();
|
||||||
|
for (PlacedMusicArea a : loaded) addArea(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static float[] toArray(List<Float> list) {
|
||||||
|
float[] a = new float[list.size()];
|
||||||
|
for (int i = 0; i < list.size(); i++) a[i] = list.get(i);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Float> toList(float[] arr) {
|
||||||
|
List<Float> l = new ArrayList<>(arr.length);
|
||||||
|
for (float f : arr) l.add(f);
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ package de.blight.editor.state;
|
|||||||
import com.jme3.app.Application;
|
import com.jme3.app.Application;
|
||||||
import com.jme3.app.SimpleApplication;
|
import com.jme3.app.SimpleApplication;
|
||||||
import com.jme3.app.state.BaseAppState;
|
import com.jme3.app.state.BaseAppState;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import com.jme3.asset.AssetManager;
|
import com.jme3.asset.AssetManager;
|
||||||
import com.jme3.bounding.BoundingBox;
|
import com.jme3.bounding.BoundingBox;
|
||||||
import com.jme3.export.binary.BinaryExporter;
|
import com.jme3.export.binary.BinaryExporter;
|
||||||
@@ -29,6 +31,8 @@ import java.time.format.DateTimeFormatter;
|
|||||||
|
|
||||||
public class PalmGeneratorState extends BaseAppState {
|
public class PalmGeneratorState extends BaseAppState {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(PalmGeneratorState.class);
|
||||||
|
|
||||||
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("editor-assets");
|
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("editor-assets");
|
||||||
|
|
||||||
private final SharedInput input;
|
private final SharedInput input;
|
||||||
@@ -194,9 +198,11 @@ public class PalmGeneratorState extends BaseAppState {
|
|||||||
+ DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
|
+ DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
|
||||||
File out = modelDir.resolve("Palm_" + stampedName + ".j3o").toFile();
|
File out = modelDir.resolve("Palm_" + stampedName + ".j3o").toFile();
|
||||||
BinaryExporter.getInstance().save(palmNode, out);
|
BinaryExporter.getInstance().save(palmNode, out);
|
||||||
|
log.info("[Palme] Gespeichert: {}", out.getAbsolutePath());
|
||||||
input.treeGenStatusMsg = "Palme exportiert: " + out.getName();
|
input.treeGenStatusMsg = "Palme exportiert: " + out.getName();
|
||||||
input.refreshAssets = true;
|
input.refreshAssets = true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
log.error("[Palme] Export-Fehler: {}", e.getMessage());
|
||||||
input.treeGenStatusMsg = "Palme Export-Fehler: " + e.getMessage();
|
input.treeGenStatusMsg = "Palme Export-Fehler: " + e.getMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package de.blight.editor.state;
|
||||||
|
|
||||||
|
import com.jme3.app.Application;
|
||||||
|
import com.jme3.app.SimpleApplication;
|
||||||
|
import com.jme3.app.state.BaseAppState;
|
||||||
|
import com.jme3.asset.AssetManager;
|
||||||
|
import com.jme3.collision.CollisionResults;
|
||||||
|
import com.jme3.material.Material;
|
||||||
|
import com.jme3.math.*;
|
||||||
|
import com.jme3.renderer.Camera;
|
||||||
|
import com.jme3.scene.*;
|
||||||
|
import com.jme3.scene.shape.Cylinder;
|
||||||
|
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||||
|
import de.blight.editor.SharedInput;
|
||||||
|
|
||||||
|
public class PlayToolState extends BaseAppState {
|
||||||
|
|
||||||
|
private final SharedInput input;
|
||||||
|
private SimpleApplication app;
|
||||||
|
private Camera cam;
|
||||||
|
private AssetManager assets;
|
||||||
|
private Node rootNode;
|
||||||
|
private TerrainQuad terrain;
|
||||||
|
|
||||||
|
private Geometry spawnMarker;
|
||||||
|
|
||||||
|
public PlayToolState(SharedInput input) {
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize(Application application) {
|
||||||
|
app = (SimpleApplication) application;
|
||||||
|
cam = app.getCamera();
|
||||||
|
assets = app.getAssetManager();
|
||||||
|
rootNode = app.getRootNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void cleanup(Application application) { removeMarker(); }
|
||||||
|
@Override protected void onEnable() {}
|
||||||
|
@Override protected void onDisable() {}
|
||||||
|
|
||||||
|
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(float tpf) {
|
||||||
|
if (input.activeLayer != SharedInput.LAYER_PLAY_TOOL) return;
|
||||||
|
|
||||||
|
SharedInput.PlayToolClick click;
|
||||||
|
while ((click = input.playToolClickQueue.poll()) != null) {
|
||||||
|
handleClick(click);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update marker position if spawn changed from text fields
|
||||||
|
if (!Float.isNaN(input.tempSpawnX) && !Float.isNaN(input.tempSpawnZ)) {
|
||||||
|
placeMarkerAt(input.tempSpawnX, input.tempSpawnZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleClick(SharedInput.PlayToolClick click) {
|
||||||
|
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||||
|
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||||
|
|
||||||
|
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||||
|
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||||
|
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||||
|
|
||||||
|
if (terrain == null) return;
|
||||||
|
CollisionResults hits = new CollisionResults();
|
||||||
|
terrain.collideWith(ray, hits);
|
||||||
|
if (hits.size() == 0) return;
|
||||||
|
|
||||||
|
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||||
|
input.tempSpawnX = pt.x;
|
||||||
|
input.tempSpawnZ = pt.z;
|
||||||
|
input.pickedSpawnInfo = pt.x + "|" + pt.z;
|
||||||
|
input.spawnPickChanged = true;
|
||||||
|
placeMarkerAt(pt.x, pt.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void placeMarkerAt(float x, float z) {
|
||||||
|
float y = terrain != null ? terrain.getHeight(new Vector2f(x, z)) : 0f;
|
||||||
|
if (Float.isNaN(y)) y = 0f;
|
||||||
|
|
||||||
|
if (spawnMarker == null) {
|
||||||
|
Cylinder cyl = new Cylinder(8, 16, 0.4f, 0.1f, true);
|
||||||
|
spawnMarker = new Geometry("spawn_marker", cyl);
|
||||||
|
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
mat.setColor("Color", new ColorRGBA(0f, 1f, 0f, 1f));
|
||||||
|
spawnMarker.setMaterial(mat);
|
||||||
|
spawnMarker.rotate(FastMath.HALF_PI, 0, 0);
|
||||||
|
rootNode.attachChild(spawnMarker);
|
||||||
|
}
|
||||||
|
spawnMarker.setLocalTranslation(x, y + 0.05f, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeMarker() {
|
||||||
|
if (spawnMarker != null) {
|
||||||
|
rootNode.detachChild(spawnMarker);
|
||||||
|
spawnMarker = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,390 @@
|
|||||||
|
package de.blight.editor.state;
|
||||||
|
|
||||||
|
import com.jme3.app.Application;
|
||||||
|
import com.jme3.app.SimpleApplication;
|
||||||
|
import com.jme3.app.state.BaseAppState;
|
||||||
|
import com.jme3.asset.AssetManager;
|
||||||
|
import com.jme3.collision.CollisionResults;
|
||||||
|
import com.jme3.material.Material;
|
||||||
|
import com.jme3.math.*;
|
||||||
|
import com.jme3.renderer.Camera;
|
||||||
|
import com.jme3.scene.*;
|
||||||
|
import com.jme3.scene.VertexBuffer;
|
||||||
|
import com.jme3.scene.mesh.IndexBuffer;
|
||||||
|
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||||
|
import com.jme3.util.BufferUtils;
|
||||||
|
import de.blight.common.PlacedSoundArea;
|
||||||
|
import de.blight.editor.SharedInput;
|
||||||
|
|
||||||
|
import java.nio.FloatBuffer;
|
||||||
|
import java.nio.ShortBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SoundAreaState extends BaseAppState {
|
||||||
|
|
||||||
|
static final float SNAP_DIST = 8f;
|
||||||
|
private static final float LINE_OFFSET_Y = 0.3f;
|
||||||
|
|
||||||
|
private static final ColorRGBA COLOR_NORMAL = new ColorRGBA(0.1f, 0.85f, 0.4f, 1f);
|
||||||
|
private static final ColorRGBA COLOR_SELECTED = new ColorRGBA(1f, 1f, 0f, 1f);
|
||||||
|
private static final ColorRGBA COLOR_INPROG = new ColorRGBA(0.3f, 0.9f, 1f, 1f);
|
||||||
|
|
||||||
|
private final SharedInput input;
|
||||||
|
private SimpleApplication app;
|
||||||
|
private Camera cam;
|
||||||
|
private AssetManager assets;
|
||||||
|
private Node rootNode;
|
||||||
|
private TerrainQuad terrain;
|
||||||
|
|
||||||
|
private final List<PlacedSoundArea> areas = new ArrayList<>();
|
||||||
|
private final List<Geometry> areaGeos = new ArrayList<>();
|
||||||
|
private int selectedIdx = -1;
|
||||||
|
|
||||||
|
// polygon being placed
|
||||||
|
private boolean placing = false;
|
||||||
|
private final List<Float> currX = new ArrayList<>();
|
||||||
|
private final List<Float> currZ = new ArrayList<>();
|
||||||
|
private Geometry inProgGeo = null;
|
||||||
|
private Geometry lastPointMarker = null;
|
||||||
|
|
||||||
|
private List<PlacedSoundArea> pendingAreas = null;
|
||||||
|
|
||||||
|
public SoundAreaState(SharedInput input) {
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize(Application application) {
|
||||||
|
app = (SimpleApplication) application;
|
||||||
|
cam = app.getCamera();
|
||||||
|
assets = app.getAssetManager();
|
||||||
|
rootNode = app.getRootNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void cleanup(Application application) { clearAll(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onEnable() {
|
||||||
|
if (pendingAreas != null) {
|
||||||
|
loadAreas(pendingAreas);
|
||||||
|
pendingAreas = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void onDisable() {}
|
||||||
|
|
||||||
|
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
||||||
|
|
||||||
|
// ── Update ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(float tpf) {
|
||||||
|
if (input.activeLayer != SharedInput.LAYER_SOUND_AREAS) {
|
||||||
|
if (placing) cancelPoly();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedInput.SoundAreaClick click;
|
||||||
|
while ((click = input.soundAreaClickQueue.poll()) != null) {
|
||||||
|
handleClick(click);
|
||||||
|
}
|
||||||
|
|
||||||
|
PlacedSoundArea pending = input.pendingSoundArea.getAndSet(null);
|
||||||
|
if (pending != null && selectedIdx >= 0) {
|
||||||
|
applyProperty(selectedIdx, pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.cancelZoneDrawing) {
|
||||||
|
input.cancelZoneDrawing = false;
|
||||||
|
if (placing) cancelPoly();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.deleteSoundAreaRequested) {
|
||||||
|
input.deleteSoundAreaRequested = false;
|
||||||
|
if (selectedIdx >= 0) removeArea(selectedIdx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Click handling ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void handleClick(SharedInput.SoundAreaClick click) {
|
||||||
|
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||||
|
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||||
|
|
||||||
|
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||||
|
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||||
|
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||||
|
|
||||||
|
if (click.rightButton()) {
|
||||||
|
if (placing) closePoly();
|
||||||
|
else deselect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get terrain hit
|
||||||
|
if (terrain == null) return;
|
||||||
|
CollisionResults hits = new CollisionResults();
|
||||||
|
terrain.collideWith(ray, hits);
|
||||||
|
if (hits.size() == 0) return;
|
||||||
|
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||||
|
float hitX = pt.x, hitZ = pt.z;
|
||||||
|
|
||||||
|
if (placing) {
|
||||||
|
// snap to existing vertex?
|
||||||
|
float[] snapped = snapVertex(hitX, hitZ);
|
||||||
|
hitX = snapped[0];
|
||||||
|
hitZ = snapped[1];
|
||||||
|
|
||||||
|
// auto-close: if close to first vertex and ≥3 points
|
||||||
|
if (currX.size() >= 3) {
|
||||||
|
float dx = hitX - currX.get(0);
|
||||||
|
float dz = hitZ - currZ.get(0);
|
||||||
|
if (dx * dx + dz * dz < SNAP_DIST * SNAP_DIST * 0.25f) {
|
||||||
|
closePoly();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currX.add(hitX);
|
||||||
|
currZ.add(hitZ);
|
||||||
|
updateInProgressGeo();
|
||||||
|
} else {
|
||||||
|
// try to select existing area
|
||||||
|
for (int i = 0; i < areas.size(); i++) {
|
||||||
|
PlacedSoundArea a = areas.get(i);
|
||||||
|
if (pointInPolygon(hitX, hitZ, a.pointsX(), a.pointsZ())) {
|
||||||
|
selectArea(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// no area hit – start new polygon
|
||||||
|
deselect();
|
||||||
|
placing = true;
|
||||||
|
currX.clear();
|
||||||
|
currZ.clear();
|
||||||
|
currX.add(hitX);
|
||||||
|
currZ.add(hitZ);
|
||||||
|
updateInProgressGeo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float[] snapVertex(float x, float z) {
|
||||||
|
float bestDist2 = SNAP_DIST * SNAP_DIST;
|
||||||
|
float bx = x, bz = z;
|
||||||
|
for (PlacedSoundArea a : areas) {
|
||||||
|
for (int i = 0; i < a.pointsX().length; i++) {
|
||||||
|
float dx = x - a.pointsX()[i];
|
||||||
|
float dz = z - a.pointsZ()[i];
|
||||||
|
float d2 = dx * dx + dz * dz;
|
||||||
|
if (d2 < bestDist2) { bestDist2 = d2; bx = a.pointsX()[i]; bz = a.pointsZ()[i]; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < currX.size(); i++) {
|
||||||
|
float dx = x - currX.get(i);
|
||||||
|
float dz = z - currZ.get(i);
|
||||||
|
float d2 = dx * dx + dz * dz;
|
||||||
|
if (d2 < bestDist2) { bestDist2 = d2; bx = currX.get(i); bz = currZ.get(i); }
|
||||||
|
}
|
||||||
|
return new float[]{bx, bz};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closePoly() {
|
||||||
|
if (currX.size() < 3) {
|
||||||
|
cancelPoly();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float[] xs = toArray(currX);
|
||||||
|
float[] zs = toArray(currZ);
|
||||||
|
PlacedSoundArea area = new PlacedSoundArea(xs, zs, "", 1f, false);
|
||||||
|
addArea(area);
|
||||||
|
selectArea(areas.size() - 1);
|
||||||
|
cancelPoly();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelPoly() {
|
||||||
|
placing = false;
|
||||||
|
currX.clear();
|
||||||
|
currZ.clear();
|
||||||
|
if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; }
|
||||||
|
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── In-progress visual ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void updateInProgressGeo() {
|
||||||
|
if (inProgGeo != null) rootNode.detachChild(inProgGeo);
|
||||||
|
int n = currX.size();
|
||||||
|
if (n == 0) { inProgGeo = null; updateLastPointMarker(); return; }
|
||||||
|
inProgGeo = buildLineGeo("sound_inprog", currX, currZ, COLOR_INPROG, Mesh.Mode.LineStrip);
|
||||||
|
rootNode.attachChild(inProgGeo);
|
||||||
|
updateLastPointMarker();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateLastPointMarker() {
|
||||||
|
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
|
||||||
|
if (currX.isEmpty()) return;
|
||||||
|
|
||||||
|
float x = currX.get(currX.size() - 1);
|
||||||
|
float z = currZ.get(currZ.size() - 1);
|
||||||
|
float y = (terrain != null ? terrain.getHeight(new Vector2f(x, z)) : 0f) + LINE_OFFSET_Y + 0.05f;
|
||||||
|
float s = 1.5f;
|
||||||
|
|
||||||
|
FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3);
|
||||||
|
buf.put(x - s).put(y).put(z - s); buf.put(x + s).put(y).put(z + s);
|
||||||
|
buf.put(x - s).put(y).put(z + s); buf.put(x + s).put(y).put(z - s);
|
||||||
|
buf.flip();
|
||||||
|
|
||||||
|
Mesh mesh = new Mesh();
|
||||||
|
mesh.setMode(Mesh.Mode.Lines);
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Position, 3, buf);
|
||||||
|
mesh.updateBound();
|
||||||
|
|
||||||
|
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
mat.setColor("Color", new ColorRGBA(1f, 0.15f, 0.15f, 1f));
|
||||||
|
mat.getAdditionalRenderState().setLineWidth(3f);
|
||||||
|
|
||||||
|
lastPointMarker = new Geometry("sound_lastpoint", mesh);
|
||||||
|
lastPointMarker.setMaterial(mat);
|
||||||
|
rootNode.attachChild(lastPointMarker);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Selection ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void selectArea(int idx) {
|
||||||
|
if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) {
|
||||||
|
areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
|
||||||
|
}
|
||||||
|
selectedIdx = idx;
|
||||||
|
areaGeos.get(idx).getMaterial().setColor("Color", COLOR_SELECTED);
|
||||||
|
publishSelection(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deselect() {
|
||||||
|
if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) {
|
||||||
|
areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
|
||||||
|
}
|
||||||
|
selectedIdx = -1;
|
||||||
|
input.selectedSoundAreaInfo = null;
|
||||||
|
input.soundAreaSelectionChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publishSelection(int idx) {
|
||||||
|
PlacedSoundArea a = areas.get(idx);
|
||||||
|
input.selectedSoundAreaInfo = idx + "|" + a.soundPath() + "|" + a.volume() + "|" + a.crossfade();
|
||||||
|
input.soundAreaSelectionChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Add / Remove / Apply ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void addArea(PlacedSoundArea area) {
|
||||||
|
areas.add(area);
|
||||||
|
List<Float> xs = toList(area.pointsX());
|
||||||
|
List<Float> zs = toList(area.pointsZ());
|
||||||
|
Geometry geo = buildLineGeo("sound_area_" + (areas.size() - 1), xs, zs, COLOR_NORMAL, Mesh.Mode.LineLoop);
|
||||||
|
rootNode.attachChild(geo);
|
||||||
|
areaGeos.add(geo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeArea(int idx) {
|
||||||
|
rootNode.detachChild(areaGeos.get(idx));
|
||||||
|
areas.remove(idx);
|
||||||
|
areaGeos.remove(idx);
|
||||||
|
selectedIdx = -1;
|
||||||
|
input.selectedSoundAreaInfo = null;
|
||||||
|
input.soundAreaSelectionChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearAll() {
|
||||||
|
for (Geometry g : areaGeos) rootNode.detachChild(g);
|
||||||
|
areas.clear();
|
||||||
|
areaGeos.clear();
|
||||||
|
cancelPoly();
|
||||||
|
selectedIdx = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyProperty(int idx, PlacedSoundArea updated) {
|
||||||
|
if (updated.pointsX().length == 0) {
|
||||||
|
// zero-length polygon = only update metadata, keep existing polygon
|
||||||
|
PlacedSoundArea existing = areas.get(idx);
|
||||||
|
areas.set(idx, new PlacedSoundArea(
|
||||||
|
existing.pointsX(), existing.pointsZ(),
|
||||||
|
updated.soundPath(), updated.volume(), updated.crossfade()));
|
||||||
|
} else {
|
||||||
|
areas.set(idx, updated);
|
||||||
|
}
|
||||||
|
publishSelection(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Line geometry builder ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Geometry buildLineGeo(String name, List<Float> xs, List<Float> zs,
|
||||||
|
ColorRGBA color, Mesh.Mode mode) {
|
||||||
|
int n = xs.size();
|
||||||
|
FloatBuffer posBuffer = BufferUtils.createFloatBuffer(n * 3);
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
float hy = terrain != null ? terrain.getHeight(new Vector2f(xs.get(i), zs.get(i))) : 0f;
|
||||||
|
posBuffer.put(xs.get(i)).put(hy + LINE_OFFSET_Y).put(zs.get(i));
|
||||||
|
}
|
||||||
|
posBuffer.flip();
|
||||||
|
|
||||||
|
Mesh mesh = new Mesh();
|
||||||
|
mesh.setMode(mode);
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Position, 3, posBuffer);
|
||||||
|
mesh.updateBound();
|
||||||
|
|
||||||
|
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
mat.setColor("Color", color);
|
||||||
|
mat.getAdditionalRenderState().setLineWidth(2f);
|
||||||
|
|
||||||
|
Geometry geo = new Geometry(name, mesh);
|
||||||
|
geo.setMaterial(mat);
|
||||||
|
return geo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Save / Load ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public List<PlacedSoundArea> getPlacedAreas() {
|
||||||
|
return new ArrayList<>(areas);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadAreas(List<PlacedSoundArea> loaded) {
|
||||||
|
if (rootNode == null) {
|
||||||
|
pendingAreas = new ArrayList<>(loaded);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearAll();
|
||||||
|
for (PlacedSoundArea a : loaded) addArea(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Point-in-polygon (ray casting) ────────────────────────────────────────
|
||||||
|
|
||||||
|
static boolean pointInPolygon(float px, float pz, float[] xs, float[] zs) {
|
||||||
|
int n = xs.length;
|
||||||
|
boolean inside = false;
|
||||||
|
for (int i = 0, j = n - 1; i < n; j = i++) {
|
||||||
|
float xi = xs[i], zi = zs[i];
|
||||||
|
float xj = xs[j], zj = zs[j];
|
||||||
|
if ((zi > pz) != (zj > pz) && (px < (xj - xi) * (pz - zi) / (zj - zi) + xi)) {
|
||||||
|
inside = !inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static float[] toArray(List<Float> list) {
|
||||||
|
float[] a = new float[list.size()];
|
||||||
|
for (int i = 0; i < list.size(); i++) a[i] = list.get(i);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Float> toList(float[] arr) {
|
||||||
|
List<Float> l = new ArrayList<>(arr.length);
|
||||||
|
for (float f : arr) l.add(f);
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -52,6 +52,8 @@ import de.blight.editor.FrameTransfer;
|
|||||||
import de.blight.editor.SharedInput;
|
import de.blight.editor.SharedInput;
|
||||||
import de.blight.editor.tree.TreeMeshBuilder;
|
import de.blight.editor.tree.TreeMeshBuilder;
|
||||||
import de.blight.editor.tree.TreeParams;
|
import de.blight.editor.tree.TreeParams;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JME3-Zustand für den prozeduralen Baum-Generator.
|
* JME3-Zustand für den prozeduralen Baum-Generator.
|
||||||
@@ -64,6 +66,8 @@ import de.blight.editor.tree.TreeParams;
|
|||||||
*/
|
*/
|
||||||
public class TreeGeneratorState extends BaseAppState {
|
public class TreeGeneratorState extends BaseAppState {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(TreeGeneratorState.class);
|
||||||
|
|
||||||
private static final int IMPOSTOR_SIZE = 512;
|
private static final int IMPOSTOR_SIZE = 512;
|
||||||
private static final int PREVIEW_SIZE = 1024;
|
private static final int PREVIEW_SIZE = 1024;
|
||||||
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("editor-assets");
|
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("editor-assets");
|
||||||
@@ -563,12 +567,12 @@ public class TreeGeneratorState extends BaseAppState {
|
|||||||
while (treeNode.getNumControls() > 0)
|
while (treeNode.getNumControls() > 0)
|
||||||
treeNode.removeControl(treeNode.getControl(0));
|
treeNode.removeControl(treeNode.getControl(0));
|
||||||
BinaryExporter.getInstance().save(treeNode, out);
|
BinaryExporter.getInstance().save(treeNode, out);
|
||||||
|
log.info("[Blight-Baum] Gespeichert: {}", out.getAbsolutePath());
|
||||||
input.treeGenStatusMsg = "Exportiert: " + out.getName();
|
input.treeGenStatusMsg = "Exportiert: " + out.getName();
|
||||||
input.refreshAssets = true;
|
input.refreshAssets = true;
|
||||||
System.out.println("[TreeGenerator] Exportiert: " + out.getAbsolutePath());
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
log.error("[Blight-Baum] Export-Fehler: {}", e.getMessage());
|
||||||
input.treeGenStatusMsg = "Export-Fehler: " + e.getMessage();
|
input.treeGenStatusMsg = "Export-Fehler: " + e.getMessage();
|
||||||
System.err.println("[TreeGenerator] Export-Fehler: " + e.getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
package de.blight.editor.state;
|
||||||
|
|
||||||
|
import com.jme3.app.Application;
|
||||||
|
import com.jme3.app.SimpleApplication;
|
||||||
|
import com.jme3.app.state.BaseAppState;
|
||||||
|
import com.jme3.asset.AssetManager;
|
||||||
|
import com.jme3.collision.CollisionResults;
|
||||||
|
import com.jme3.material.Material;
|
||||||
|
import com.jme3.material.RenderState;
|
||||||
|
import com.jme3.math.*;
|
||||||
|
import com.jme3.renderer.Camera;
|
||||||
|
import com.jme3.renderer.queue.RenderQueue;
|
||||||
|
import com.jme3.scene.*;
|
||||||
|
import com.jme3.scene.shape.Quad;
|
||||||
|
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||||
|
import com.jme3.util.BufferUtils;
|
||||||
|
import de.blight.common.PlacedWater;
|
||||||
|
import de.blight.editor.SharedInput;
|
||||||
|
|
||||||
|
import java.nio.FloatBuffer;
|
||||||
|
import java.nio.IntBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class WaterBodyState extends BaseAppState {
|
||||||
|
|
||||||
|
private static final ColorRGBA WATER_COLOR = new ColorRGBA(0.05f, 0.25f, 0.70f, 0.52f);
|
||||||
|
private static final ColorRGBA BORDER_COLOR = new ColorRGBA(0.30f, 0.60f, 1.00f, 0.85f);
|
||||||
|
private static final ColorRGBA BORDER_SEL = new ColorRGBA(1.00f, 1.00f, 0.00f, 1.00f);
|
||||||
|
|
||||||
|
private static final String GEO_SURFACE = "water_surface";
|
||||||
|
private static final String GEO_BORDER = "water_border";
|
||||||
|
|
||||||
|
private final SharedInput input;
|
||||||
|
private SimpleApplication app;
|
||||||
|
private Camera cam;
|
||||||
|
private AssetManager assets;
|
||||||
|
private Node rootNode;
|
||||||
|
private TerrainQuad terrain;
|
||||||
|
|
||||||
|
// parallel lists
|
||||||
|
private final List<PlacedWater> bodies = new ArrayList<>();
|
||||||
|
private final List<Node> markers = new ArrayList<>();
|
||||||
|
|
||||||
|
private int selectedIdx = -1;
|
||||||
|
private List<PlacedWater> pendingBodies = null;
|
||||||
|
|
||||||
|
public WaterBodyState(SharedInput input) {
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize(Application application) {
|
||||||
|
app = (SimpleApplication) application;
|
||||||
|
cam = app.getCamera();
|
||||||
|
assets = app.getAssetManager();
|
||||||
|
rootNode = app.getRootNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void cleanup(Application application) { clearAll(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onEnable() {
|
||||||
|
if (pendingBodies != null) {
|
||||||
|
loadPlacedBodies(pendingBodies);
|
||||||
|
pendingBodies = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void onDisable() {}
|
||||||
|
|
||||||
|
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
||||||
|
|
||||||
|
// ── Update ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(float tpf) {
|
||||||
|
if (input.activeLayer != SharedInput.LAYER_WATER) return;
|
||||||
|
|
||||||
|
SharedInput.WaterClick click;
|
||||||
|
while ((click = input.waterClickQueue.poll()) != null) {
|
||||||
|
handleClick(click);
|
||||||
|
}
|
||||||
|
|
||||||
|
PlacedWater pending = input.pendingWater.getAndSet(null);
|
||||||
|
if (pending != null && selectedIdx >= 0) {
|
||||||
|
applyProperty(selectedIdx, pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.deleteWaterRequested) {
|
||||||
|
input.deleteWaterRequested = false;
|
||||||
|
if (selectedIdx >= 0) removeBody(selectedIdx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Click handling ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void handleClick(SharedInput.WaterClick click) {
|
||||||
|
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||||
|
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||||
|
|
||||||
|
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||||
|
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||||
|
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||||
|
|
||||||
|
int hit = pickMarker(ray);
|
||||||
|
if (hit >= 0) {
|
||||||
|
if (click.rightButton()) deselect();
|
||||||
|
else selectBody(hit);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (click.rightButton()) { deselect(); return; }
|
||||||
|
|
||||||
|
if (terrain == null) return;
|
||||||
|
CollisionResults hits = new CollisionResults();
|
||||||
|
terrain.collideWith(ray, hits);
|
||||||
|
if (hits.size() == 0) return;
|
||||||
|
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||||
|
|
||||||
|
addBody(new PlacedWater(pt.x, pt.y + 0.05f, pt.z, 30f, 30f));
|
||||||
|
selectBody(bodies.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int pickMarker(Ray ray) {
|
||||||
|
for (int i = 0; i < markers.size(); i++) {
|
||||||
|
CollisionResults res = new CollisionResults();
|
||||||
|
markers.get(i).collideWith(ray, res);
|
||||||
|
if (res.size() > 0) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Selection ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void selectBody(int idx) {
|
||||||
|
deselect();
|
||||||
|
selectedIdx = idx;
|
||||||
|
setBorderColor(idx, BORDER_SEL);
|
||||||
|
publishSelection(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deselect() {
|
||||||
|
if (selectedIdx >= 0 && selectedIdx < bodies.size()) {
|
||||||
|
setBorderColor(selectedIdx, BORDER_COLOR);
|
||||||
|
}
|
||||||
|
selectedIdx = -1;
|
||||||
|
input.selectedWaterInfo = null;
|
||||||
|
input.waterSelectionChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publishSelection(int idx) {
|
||||||
|
PlacedWater b = bodies.get(idx);
|
||||||
|
input.selectedWaterInfo = String.format(java.util.Locale.ROOT,
|
||||||
|
"%d|%.3f|%.3f|%.3f|%.3f|%.3f",
|
||||||
|
idx, b.x(), b.y(), b.z(), b.width(), b.depth());
|
||||||
|
input.waterSelectionChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Add / Remove ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void addBody(PlacedWater b) {
|
||||||
|
Node marker = buildMarker(b);
|
||||||
|
rootNode.attachChild(marker);
|
||||||
|
markers.add(marker);
|
||||||
|
bodies.add(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeBody(int idx) {
|
||||||
|
rootNode.detachChild(markers.get(idx));
|
||||||
|
bodies.remove(idx);
|
||||||
|
markers.remove(idx);
|
||||||
|
selectedIdx = -1;
|
||||||
|
input.selectedWaterInfo = null;
|
||||||
|
input.waterSelectionChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearAll() {
|
||||||
|
for (Node m : markers) rootNode.detachChild(m);
|
||||||
|
bodies.clear();
|
||||||
|
markers.clear();
|
||||||
|
selectedIdx = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Property application ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void applyProperty(int idx, PlacedWater updated) {
|
||||||
|
rootNode.detachChild(markers.get(idx));
|
||||||
|
Node newMarker = buildMarker(updated);
|
||||||
|
setBorderColorOnNode(newMarker, BORDER_SEL);
|
||||||
|
rootNode.attachChild(newMarker);
|
||||||
|
markers.set(idx, newMarker);
|
||||||
|
bodies.set(idx, updated);
|
||||||
|
publishSelection(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Marker visuals ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Node buildMarker(PlacedWater b) {
|
||||||
|
// Water surface (semi-transparent quad)
|
||||||
|
Quad quad = new Quad(b.width(), b.depth());
|
||||||
|
Geometry surface = new Geometry(GEO_SURFACE, quad);
|
||||||
|
Material waterMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
waterMat.setColor("Color", WATER_COLOR);
|
||||||
|
waterMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||||
|
waterMat.getAdditionalRenderState().setDepthWrite(false);
|
||||||
|
surface.setMaterial(waterMat);
|
||||||
|
surface.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||||
|
surface.rotate(-FastMath.HALF_PI, 0, 0);
|
||||||
|
surface.setLocalTranslation(-b.width() * 0.5f, 0f, b.depth() * 0.5f);
|
||||||
|
|
||||||
|
// Border outline (Line mesh forming a rectangle)
|
||||||
|
Geometry border = new Geometry(GEO_BORDER, buildBorderMesh(b.width(), b.depth()));
|
||||||
|
Material borderMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
borderMat.setColor("Color", BORDER_COLOR);
|
||||||
|
borderMat.getAdditionalRenderState().setDepthTest(false);
|
||||||
|
border.setMaterial(borderMat);
|
||||||
|
|
||||||
|
Node node = new Node("water_node");
|
||||||
|
node.attachChild(surface);
|
||||||
|
node.attachChild(border);
|
||||||
|
node.setLocalTranslation(b.x(), b.y(), b.z());
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Mesh buildBorderMesh(float w, float d) {
|
||||||
|
// 4 corner points at +0.02 above water surface (local coords, XZ plane)
|
||||||
|
float hw = w * 0.5f, hd = d * 0.5f, y = 0.02f;
|
||||||
|
FloatBuffer pos = BufferUtils.createFloatBuffer(4 * 3);
|
||||||
|
pos.put(-hw).put(y).put(-hd);
|
||||||
|
pos.put( hw).put(y).put(-hd);
|
||||||
|
pos.put( hw).put(y).put( hd);
|
||||||
|
pos.put(-hw).put(y).put( hd);
|
||||||
|
IntBuffer idx = BufferUtils.createIntBuffer(8); // 4 edges
|
||||||
|
idx.put(0).put(1).put(1).put(2).put(2).put(3).put(3).put(0);
|
||||||
|
Mesh mesh = new Mesh();
|
||||||
|
mesh.setMode(Mesh.Mode.Lines);
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Index, 2, idx);
|
||||||
|
mesh.updateBound();
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setBorderColor(int idx, ColorRGBA color) {
|
||||||
|
setBorderColorOnNode(markers.get(idx), color);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setBorderColorOnNode(Node node, ColorRGBA color) {
|
||||||
|
for (Spatial child : node.getChildren()) {
|
||||||
|
if (child instanceof Geometry geo && GEO_BORDER.equals(geo.getName())) {
|
||||||
|
geo.getMaterial().setColor("Color", color);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Save / Load ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public List<PlacedWater> getPlacedBodies() {
|
||||||
|
return new ArrayList<>(bodies);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadPlacedBodies(List<PlacedWater> loaded) {
|
||||||
|
if (rootNode == null) {
|
||||||
|
pendingBodies = new ArrayList<>(loaded);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearAll();
|
||||||
|
for (PlacedWater b : loaded) addBody(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,8 @@ public class HeightTool extends EditorTool {
|
|||||||
|
|
||||||
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 50.0, 1.0, 500.0);
|
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 50.0, 1.0, 500.0);
|
||||||
public final ToolParameter brushStrength = new ToolParameter("Pinselstärke", 2.0, 0.1, 50.0);
|
public final ToolParameter brushStrength = new ToolParameter("Pinselstärke", 2.0, 0.1, 50.0);
|
||||||
|
public final ToolParameter plateauHeight = new ToolParameter("Plateau-Höhe", 0.0, -500.0, 500.0);
|
||||||
|
public volatile boolean plateauHeightChanged = false;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() { return "Höhe"; }
|
public String getName() { return "Höhe"; }
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package de.blight.editor.ui;
|
||||||
|
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog zur Auswahl einer JME-Material-Definition (.j3md).
|
||||||
|
*
|
||||||
|
* Zeigt JME-Standard-Materialien und eigene MatDefs aus dem Asset-Verzeichnis.
|
||||||
|
* Gibt den relativen Asset-Pfad (z.B. {@code Common/MatDefs/Light/Lighting.j3md}
|
||||||
|
* oder {@code MatDefs/Grass.j3md}) zurück, oder {@code null} bei Abbruch.
|
||||||
|
*/
|
||||||
|
public class MaterialChooser extends Dialog<String> {
|
||||||
|
|
||||||
|
private static final List<String> JME_MATERIALS = List.of(
|
||||||
|
"Common/MatDefs/Misc/Unshaded.j3md",
|
||||||
|
"Common/MatDefs/Light/Lighting.j3md",
|
||||||
|
"Common/MatDefs/Light/PBRLighting.j3md"
|
||||||
|
);
|
||||||
|
|
||||||
|
private final ToggleGroup toggleGroup = new ToggleGroup();
|
||||||
|
private final List<ToggleButton> allButtons = new ArrayList<>();
|
||||||
|
private String selectedPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param matDefsRoot Verzeichnis mit eigenen .j3md-Dateien (darf {@code null} sein)
|
||||||
|
*/
|
||||||
|
public MaterialChooser(Path matDefsRoot) {
|
||||||
|
setTitle("Material auswählen");
|
||||||
|
initModality(Modality.APPLICATION_MODAL);
|
||||||
|
setResizable(true);
|
||||||
|
|
||||||
|
VBox contentBox = new VBox(12);
|
||||||
|
contentBox.setPadding(new Insets(4));
|
||||||
|
|
||||||
|
contentBox.getChildren().add(buildSection("JME Standard", JME_MATERIALS));
|
||||||
|
|
||||||
|
if (matDefsRoot != null && Files.isDirectory(matDefsRoot)) {
|
||||||
|
List<String> custom = new ArrayList<>();
|
||||||
|
try (Stream<Path> walk = Files.walk(matDefsRoot)) {
|
||||||
|
walk.filter(p -> p.toString().toLowerCase().endsWith(".j3md"))
|
||||||
|
.sorted()
|
||||||
|
.forEach(p -> custom.add("MatDefs/" + p.getFileName().toString()));
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
if (!custom.isEmpty())
|
||||||
|
contentBox.getChildren().add(buildSection("Eigene MatDefs", custom));
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField filterField = new TextField();
|
||||||
|
filterField.setPromptText("Filtern...");
|
||||||
|
filterField.textProperty().addListener(
|
||||||
|
(obs, o, n) -> applyFilter(n == null ? "" : n.toLowerCase()));
|
||||||
|
|
||||||
|
ScrollPane scroll = new ScrollPane(contentBox);
|
||||||
|
scroll.setFitToWidth(true);
|
||||||
|
scroll.setPrefSize(460, 380);
|
||||||
|
scroll.setStyle("-fx-background-color: transparent;");
|
||||||
|
|
||||||
|
VBox root = new VBox(8, filterField, scroll);
|
||||||
|
root.setPadding(new Insets(10));
|
||||||
|
getDialogPane().setContent(root);
|
||||||
|
getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||||
|
|
||||||
|
Button okBtn = (Button) getDialogPane().lookupButton(ButtonType.OK);
|
||||||
|
okBtn.setText("OK");
|
||||||
|
okBtn.setDisable(true);
|
||||||
|
|
||||||
|
toggleGroup.selectedToggleProperty().addListener((obs, o, n) -> {
|
||||||
|
okBtn.setDisable(n == null);
|
||||||
|
selectedPath = (n instanceof ToggleButton tb) ? (String) tb.getUserData() : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
setResultConverter(btn -> btn == ButtonType.OK ? selectedPath : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private VBox buildSection(String title, List<String> paths) {
|
||||||
|
Label lbl = new Label(title);
|
||||||
|
lbl.setStyle("-fx-font-weight: bold; -fx-font-size: 12;");
|
||||||
|
|
||||||
|
VBox items = new VBox(3);
|
||||||
|
for (String path : paths) {
|
||||||
|
String name = path.substring(path.lastIndexOf('/') + 1);
|
||||||
|
if (name.toLowerCase().endsWith(".j3md"))
|
||||||
|
name = name.substring(0, name.length() - 5);
|
||||||
|
|
||||||
|
ToggleButton btn = new ToggleButton(name);
|
||||||
|
btn.setUserData(path);
|
||||||
|
btn.setToggleGroup(toggleGroup);
|
||||||
|
btn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
btn.setTooltip(new Tooltip(path));
|
||||||
|
btn.setOnMouseClicked(e -> {
|
||||||
|
if (e.getClickCount() == 2) {
|
||||||
|
selectedPath = path;
|
||||||
|
Button ok = (Button) getDialogPane().lookupButton(ButtonType.OK);
|
||||||
|
if (ok != null) ok.fire();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
items.getChildren().add(btn);
|
||||||
|
allButtons.add(btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new VBox(4, lbl, new Separator(), items);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyFilter(String lower) {
|
||||||
|
for (ToggleButton btn : allButtons) {
|
||||||
|
String path = (String) btn.getUserData();
|
||||||
|
boolean vis = lower.isBlank() || path.toLowerCase().contains(lower);
|
||||||
|
btn.setVisible(vis);
|
||||||
|
btn.setManaged(vis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
package de.blight.editor.ui;
|
||||||
|
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Photo-explorer-style texture picker dialog.
|
||||||
|
*
|
||||||
|
* Shows user-imported textures from {@code assetRoot/textures/} and optionally
|
||||||
|
* JME built-in terrain textures from the classpath (jme3-testdata).
|
||||||
|
* Returns the selected JME asset path (relative to asset root for user textures,
|
||||||
|
* classpath path for JME textures), or {@code null} when cancelled.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* <pre>
|
||||||
|
* new TextureChooser(assetRoot, true)
|
||||||
|
* .showAndWait()
|
||||||
|
* .ifPresent(path -> { ... });
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public class TextureChooser extends Dialog<String> {
|
||||||
|
|
||||||
|
private static final int THUMB_SIZE = 80;
|
||||||
|
private static final int CARD_W = THUMB_SIZE + 20;
|
||||||
|
private static final int CARD_H = THUMB_SIZE + 38;
|
||||||
|
|
||||||
|
private static final Set<String> IMAGE_EXTS = Set.of(
|
||||||
|
".jpg", ".jpeg", ".png", ".bmp", ".tga", ".dds");
|
||||||
|
|
||||||
|
/** Curated list of JME built-in terrain diffuse textures (jme3-testdata). */
|
||||||
|
private static final List<String> JME_TEXTURES = List.of(
|
||||||
|
"Textures/Terrain/splat/grass.jpg",
|
||||||
|
"Textures/Terrain/splat/dirt.jpg",
|
||||||
|
"Textures/Terrain/splat/road.jpg",
|
||||||
|
"Textures/Terrain/Rock2/rock.jpg",
|
||||||
|
"Textures/Terrain/Rocky/RockyTexture.jpg",
|
||||||
|
"Textures/Terrain/BrickWall/BrickWall.jpg",
|
||||||
|
"Textures/Terrain/Pond/Pond.jpg",
|
||||||
|
"Textures/Terrain/Rock/Rock.PNG",
|
||||||
|
"Textures/Terrain/PBR/Gravel015_1K_Color.png",
|
||||||
|
"Textures/Terrain/PBR/Ground036_1K_Color.png",
|
||||||
|
"Textures/Terrain/PBR/Ground037_1K_Color.png",
|
||||||
|
"Textures/Terrain/PBR/Marble013_1K_Color.png",
|
||||||
|
"Textures/Terrain/PBR/Rock035_1K_Color.png",
|
||||||
|
"Textures/Terrain/PBR/Snow006_1K_Color.png",
|
||||||
|
"Textures/Terrain/PBR/Tiles083_1K_Color.png"
|
||||||
|
);
|
||||||
|
|
||||||
|
private final ToggleGroup toggleGroup = new ToggleGroup();
|
||||||
|
private final VBox contentBox = new VBox(16);
|
||||||
|
private final TextField filterField = new TextField();
|
||||||
|
private final List<ToggleButton> allCards = new ArrayList<>();
|
||||||
|
private String selectedPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param assetRoot path to the editor-assets directory (may be {@code null} to skip user textures)
|
||||||
|
* @param includeJmeBuiltin whether to include JME built-in terrain textures
|
||||||
|
*/
|
||||||
|
public TextureChooser(Path assetRoot, boolean includeJmeBuiltin) {
|
||||||
|
setTitle("Textur auswählen");
|
||||||
|
initModality(Modality.APPLICATION_MODAL);
|
||||||
|
setResizable(true);
|
||||||
|
|
||||||
|
contentBox.setPadding(new Insets(4));
|
||||||
|
|
||||||
|
// ── User-imported textures ────────────────────────────────────────────
|
||||||
|
if (assetRoot != null) {
|
||||||
|
Path texturesDir = assetRoot.resolve("Textures");
|
||||||
|
if (Files.isDirectory(texturesDir)) {
|
||||||
|
FlowPane userPane = newFlowPane();
|
||||||
|
addUserTextures(userPane, texturesDir, assetRoot);
|
||||||
|
if (!userPane.getChildren().isEmpty()) {
|
||||||
|
contentBox.getChildren().add(section("Eigene Texturen", userPane));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JME built-in textures ─────────────────────────────────────────────
|
||||||
|
if (includeJmeBuiltin) {
|
||||||
|
FlowPane jmePane = newFlowPane();
|
||||||
|
for (String jmePath : JME_TEXTURES) {
|
||||||
|
ToggleButton card = buildJmeCard(jmePath);
|
||||||
|
jmePane.getChildren().add(card);
|
||||||
|
allCards.add(card);
|
||||||
|
}
|
||||||
|
if (!jmePane.getChildren().isEmpty()) {
|
||||||
|
contentBox.getChildren().add(section("JME Texturen", jmePane));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Filter ────────────────────────────────────────────────────────────
|
||||||
|
filterField.setPromptText("Filtern...");
|
||||||
|
filterField.textProperty().addListener(
|
||||||
|
(obs, o, n) -> applyFilter(n == null ? "" : n.toLowerCase()));
|
||||||
|
|
||||||
|
ScrollPane scroll = new ScrollPane(contentBox);
|
||||||
|
scroll.setFitToWidth(true);
|
||||||
|
scroll.setPrefSize(680, 440);
|
||||||
|
scroll.setStyle("-fx-background-color: transparent;");
|
||||||
|
|
||||||
|
VBox root = new VBox(8, filterField, scroll);
|
||||||
|
root.setPadding(new Insets(10));
|
||||||
|
|
||||||
|
getDialogPane().setContent(root);
|
||||||
|
getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||||
|
|
||||||
|
Button okBtn = (Button) getDialogPane().lookupButton(ButtonType.OK);
|
||||||
|
okBtn.setText("OK");
|
||||||
|
okBtn.setDisable(true);
|
||||||
|
|
||||||
|
// ── Selection tracking ────────────────────────────────────────────────
|
||||||
|
toggleGroup.selectedToggleProperty().addListener((obs, o, n) -> {
|
||||||
|
okBtn.setDisable(n == null);
|
||||||
|
selectedPath = (n instanceof ToggleButton tb) ? (String) tb.getUserData() : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
setResultConverter(btn -> btn == ButtonType.OK ? selectedPath : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Builder helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static FlowPane newFlowPane() {
|
||||||
|
FlowPane fp = new FlowPane(8, 8);
|
||||||
|
fp.setPadding(new Insets(6, 4, 4, 4));
|
||||||
|
return fp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VBox section(String title, FlowPane pane) {
|
||||||
|
Label lbl = new Label(title);
|
||||||
|
lbl.setStyle("-fx-font-weight: bold; -fx-font-size: 12;");
|
||||||
|
Separator sep = new Separator();
|
||||||
|
VBox box = new VBox(4, lbl, sep, pane);
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── User textures ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void addUserTextures(FlowPane pane, Path texturesDir, Path assetRoot) {
|
||||||
|
try (Stream<Path> walk = Files.walk(texturesDir)) {
|
||||||
|
walk.filter(Files::isRegularFile)
|
||||||
|
.filter(p -> {
|
||||||
|
String low = p.getFileName().toString().toLowerCase();
|
||||||
|
return IMAGE_EXTS.stream().anyMatch(low::endsWith);
|
||||||
|
})
|
||||||
|
.sorted()
|
||||||
|
.forEach(file -> {
|
||||||
|
String assetPath = assetRoot.relativize(file)
|
||||||
|
.toString().replace('\\', '/');
|
||||||
|
Image img;
|
||||||
|
try {
|
||||||
|
img = new Image(file.toUri().toString(),
|
||||||
|
THUMB_SIZE, THUMB_SIZE, true, true, true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
img = null;
|
||||||
|
}
|
||||||
|
String name = file.getFileName().toString();
|
||||||
|
ToggleButton card = buildCard(name, assetPath, img);
|
||||||
|
pane.getChildren().add(card);
|
||||||
|
allCards.add(card);
|
||||||
|
});
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JME textures ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private ToggleButton buildJmeCard(String jmePath) {
|
||||||
|
Image img = null;
|
||||||
|
try {
|
||||||
|
InputStream is = TextureChooser.class.getClassLoader()
|
||||||
|
.getResourceAsStream(jmePath);
|
||||||
|
if (is != null) {
|
||||||
|
try (is) {
|
||||||
|
img = new Image(is, THUMB_SIZE, THUMB_SIZE, true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
String name = jmePath.substring(jmePath.lastIndexOf('/') + 1);
|
||||||
|
return buildCard(name, jmePath, img);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Card factory ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private ToggleButton buildCard(String displayName, String assetPath, Image img) {
|
||||||
|
ImageView iv = new ImageView(img);
|
||||||
|
iv.setFitWidth(THUMB_SIZE);
|
||||||
|
iv.setFitHeight(THUMB_SIZE);
|
||||||
|
iv.setPreserveRatio(true);
|
||||||
|
|
||||||
|
// Checkerboard background for transparency indication
|
||||||
|
iv.setStyle("-fx-background-color: #cccccc;");
|
||||||
|
|
||||||
|
Label lbl = new Label(truncate(displayName, 13));
|
||||||
|
lbl.setMaxWidth(CARD_W - 4);
|
||||||
|
lbl.setAlignment(Pos.CENTER);
|
||||||
|
lbl.setStyle("-fx-font-size: 10;");
|
||||||
|
|
||||||
|
VBox graphic = new VBox(4, iv, lbl);
|
||||||
|
graphic.setAlignment(Pos.TOP_CENTER);
|
||||||
|
graphic.setPrefSize(THUMB_SIZE, THUMB_SIZE + 18);
|
||||||
|
|
||||||
|
ToggleButton btn = new ToggleButton();
|
||||||
|
btn.setGraphic(graphic);
|
||||||
|
btn.setUserData(assetPath);
|
||||||
|
btn.setToggleGroup(toggleGroup);
|
||||||
|
btn.setPrefSize(CARD_W, CARD_H);
|
||||||
|
btn.setTooltip(new Tooltip(assetPath));
|
||||||
|
|
||||||
|
btn.setOnMouseClicked(e -> {
|
||||||
|
if (e.getClickCount() == 2) {
|
||||||
|
selectedPath = assetPath;
|
||||||
|
Button okBtn = (Button) getDialogPane().lookupButton(ButtonType.OK);
|
||||||
|
if (okBtn != null) okBtn.fire();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Filter ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void applyFilter(String lowerFilter) {
|
||||||
|
for (ToggleButton card : allCards) {
|
||||||
|
String path = (String) card.getUserData();
|
||||||
|
boolean visible = lowerFilter.isBlank()
|
||||||
|
|| path.toLowerCase().contains(lowerFilter);
|
||||||
|
card.setVisible(visible);
|
||||||
|
card.setManaged(visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Utilities ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static String truncate(String s, int max) {
|
||||||
|
if (s.length() <= max) return s;
|
||||||
|
int dot = s.lastIndexOf('.');
|
||||||
|
if (dot > 0 && s.length() - dot <= 5) {
|
||||||
|
String ext = s.substring(dot);
|
||||||
|
int keep = max - ext.length() - 1;
|
||||||
|
return (keep > 0 ? s.substring(0, keep) : "") + "…" + ext;
|
||||||
|
}
|
||||||
|
return s.substring(0, max - 1) + "…";
|
||||||
|
}
|
||||||
|
}
|
||||||
22
blight-editor/src/main/resources/editor.css
Normal file
22
blight-editor/src/main/resources/editor.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/* Globale Textfarben – verhindert weiße Labels bei dunklen System-Themes.
|
||||||
|
Inline-Styles (setStyle) haben höhere Spezifität und überschreiben diese Regeln. */
|
||||||
|
|
||||||
|
.label {
|
||||||
|
-fx-text-fill: #111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-box .text {
|
||||||
|
-fx-fill: #111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-button .text {
|
||||||
|
-fx-fill: #111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button {
|
||||||
|
-fx-text-fill: #111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titled-pane > .title > .text {
|
||||||
|
-fx-fill: #111111;
|
||||||
|
}
|
||||||
30
blight-editor/src/main/resources/logback.xml
Normal file
30
blight-editor/src/main/resources/logback.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<configuration>
|
||||||
|
|
||||||
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{HH:mm:ss.SSS} %-5level [%logger{30}] %msg%n%ex</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>logs/blight-editor.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>logs/blight-editor.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
|
<maxHistory>7</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%logger{30}] %msg%n%ex</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- JME-interne JUL-Logs auf WARN reduzieren -->
|
||||||
|
<logger name="com.jme3" level="WARN"/>
|
||||||
|
<!-- GltfLoader meldet bei jeder Animation "only supports linear interpolation" – bekanntes JME-Verhalten, kein Fehler -->
|
||||||
|
<logger name="com.jme3.scene.plugins.gltf" level="ERROR"/>
|
||||||
|
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="CONSOLE"/>
|
||||||
|
<appender-ref ref="FILE"/>
|
||||||
|
</root>
|
||||||
|
|
||||||
|
</configuration>
|
||||||
@@ -5,7 +5,7 @@ plugins {
|
|||||||
id 'java'
|
id 'java'
|
||||||
}
|
}
|
||||||
|
|
||||||
ext { mainClassName = 'de.blight.game.BlightApp' }
|
ext { mainClassName = 'de.blight.game.BlightGame' }
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
jmeVersion = '3.9.0-stable'
|
jmeVersion = '3.9.0-stable'
|
||||||
@@ -15,6 +15,7 @@ dependencies {
|
|||||||
implementation project(':blight-common')
|
implementation project(':blight-common')
|
||||||
implementation project(':blight-assets')
|
implementation project(':blight-assets')
|
||||||
implementation project(':blight-map')
|
implementation project(':blight-map')
|
||||||
|
implementation project(':blight-lang')
|
||||||
|
|
||||||
implementation "org.jmonkeyengine:jme3-core:${jmeVersion}"
|
implementation "org.jmonkeyengine:jme3-core:${jmeVersion}"
|
||||||
implementation "org.jmonkeyengine:jme3-desktop:${jmeVersion}"
|
implementation "org.jmonkeyengine:jme3-desktop:${jmeVersion}"
|
||||||
@@ -22,8 +23,14 @@ dependencies {
|
|||||||
implementation "org.jmonkeyengine:jme3-terrain:${jmeVersion}"
|
implementation "org.jmonkeyengine:jme3-terrain:${jmeVersion}"
|
||||||
implementation "org.jmonkeyengine:jme3-effects:${jmeVersion}"
|
implementation "org.jmonkeyengine:jme3-effects:${jmeVersion}"
|
||||||
implementation "org.jmonkeyengine:jme3-jbullet:${jmeVersion}"
|
implementation "org.jmonkeyengine:jme3-jbullet:${jmeVersion}"
|
||||||
|
implementation "org.jmonkeyengine:jme3-jogg:${jmeVersion}"
|
||||||
|
implementation "org.jmonkeyengine:jme3-plugins:${jmeVersion}"
|
||||||
implementation "org.jmonkeyengine:jme3-testdata:${jmeVersion}"
|
implementation "org.jmonkeyengine:jme3-testdata:${jmeVersion}"
|
||||||
implementation 'com.google.code.gson:gson:2.11.0'
|
implementation 'com.google.code.gson:gson:2.11.0'
|
||||||
|
implementation 'org.slf4j:slf4j-api:2.0.17'
|
||||||
|
implementation 'org.slf4j:jul-to-slf4j:2.0.17'
|
||||||
|
compileOnly 'org.projectlombok:lombok:1.18.38'
|
||||||
|
annotationProcessor 'org.projectlombok:lombok:1.18.38'
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('extractNatives', Copy) {
|
tasks.register('extractNatives', Copy) {
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import com.jme3.input.controls.ActionListener;
|
|||||||
import com.jme3.input.controls.KeyTrigger;
|
import com.jme3.input.controls.KeyTrigger;
|
||||||
import com.jme3.system.AppSettings;
|
import com.jme3.system.AppSettings;
|
||||||
import de.blight.game.config.*;
|
import de.blight.game.config.*;
|
||||||
|
import org.slf4j.bridge.SLF4JBridgeHandler;
|
||||||
import de.blight.game.scene.WorldScene;
|
import de.blight.game.scene.WorldScene;
|
||||||
|
|
||||||
public class BlightApp extends SimpleApplication {
|
public class BlightGame extends SimpleApplication {
|
||||||
|
|
||||||
private KeyBindings keyBindings;
|
private KeyBindings keyBindings;
|
||||||
private GraphicsSettings graphicsSettings;
|
private GraphicsSettings graphicsSettings;
|
||||||
@@ -18,7 +19,10 @@ public class BlightApp extends SimpleApplication {
|
|||||||
private PauseMenu pauseMenu;
|
private PauseMenu pauseMenu;
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
BlightApp app = new BlightApp();
|
SLF4JBridgeHandler.removeHandlersForRootLogger();
|
||||||
|
SLF4JBridgeHandler.install();
|
||||||
|
|
||||||
|
BlightGame app = new BlightGame();
|
||||||
|
|
||||||
GraphicsSettings gs = GraphicsStore.load();
|
GraphicsSettings gs = GraphicsStore.load();
|
||||||
AppSettings settings = new AppSettings(true);
|
AppSettings settings = new AppSettings(true);
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package de.blight.game.animation;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Beschreibt ein Animations-Set: eine Liste von Clip-Namen sowie die
|
||||||
|
* Zuordnung semantischer Aktionen (IDLE, WALK, …) zu Clip-Namen.
|
||||||
|
*
|
||||||
|
* Wird als {@code <setName>.animset.json} neben der {@code .j3o}-Datei gespeichert
|
||||||
|
* und ersetzt sowohl die alte {@code .clips.json} als auch die {@code .animmap}-Datei.
|
||||||
|
*/
|
||||||
|
public class AnimSet {
|
||||||
|
|
||||||
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||||
|
private static final String SUFFIX = ".animset.json";
|
||||||
|
|
||||||
|
private List<String> clips = new ArrayList<>();
|
||||||
|
private Map<String, String> actionMap = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
public List<String> getClips() { return clips; }
|
||||||
|
public void setClips(List<String> clips) { this.clips = clips; }
|
||||||
|
public Map<String, String> getActionMap() { return actionMap; }
|
||||||
|
public void setActionMap(Map<String, String> actionMap) { this.actionMap = actionMap; }
|
||||||
|
|
||||||
|
/** Speichert dieses Set als {@code <setName>.animset.json} im Verzeichnis {@code setDir}. */
|
||||||
|
public void save(Path setDir, String setName) throws IOException {
|
||||||
|
Files.writeString(setDir.resolve(setName + SUFFIX), GSON.toJson(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt ein AnimSet aus dem Verzeichnis {@code setDir}.
|
||||||
|
* Existiert keine Datei, wird ein leeres {@code AnimSet} zurückgegeben.
|
||||||
|
*/
|
||||||
|
public static AnimSet load(Path setDir, String setName) throws IOException {
|
||||||
|
Path f = setDir.resolve(setName + SUFFIX);
|
||||||
|
if (!Files.exists(f)) return new AnimSet();
|
||||||
|
return GSON.fromJson(Files.readString(f), AnimSet.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt ein AnimSet anhand des Asset-Pfades der zugehörigen {@code .j3o}-Datei.
|
||||||
|
*
|
||||||
|
* @param assetRoot absoluter Pfad zum Assets-Wurzelverzeichnis
|
||||||
|
* @param j3oAssetPath relativer Pfad zur {@code .j3o}-Datei (z. B. {@code "animations/sets/foo.j3o"})
|
||||||
|
*/
|
||||||
|
public static AnimSet loadByJ3oPath(Path assetRoot, String j3oAssetPath) {
|
||||||
|
Path j3o = assetRoot.resolve(j3oAssetPath.replace('/', java.io.File.separatorChar));
|
||||||
|
String name = j3o.getFileName().toString().replaceFirst("\\.j3o$", "");
|
||||||
|
try {
|
||||||
|
return load(j3o.getParent(), name);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return new AnimSet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt den Companion-Pfad der {@code .animset.json}-Datei neben einer {@code .j3o}-Datei zurück. */
|
||||||
|
public static Path companionPath(Path j3oPath) {
|
||||||
|
String name = j3oPath.getFileName().toString().replaceFirst("\\.j3o$", "");
|
||||||
|
return j3oPath.getParent().resolve(name + SUFFIX);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package de.blight.game.animation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Semantische Aktionen, denen ein Animations-Clip zugewiesen werden kann.
|
||||||
|
* Im Editor festgelegt, vom Spiel zur Laufzeit abgerufen.
|
||||||
|
*/
|
||||||
|
public enum AnimationAction {
|
||||||
|
IDLE,
|
||||||
|
WALK,
|
||||||
|
RUN,
|
||||||
|
JUMP,
|
||||||
|
DUCK;
|
||||||
|
|
||||||
|
/** Lesbare Bezeichnung für UI-Anzeige. */
|
||||||
|
public String displayName() {
|
||||||
|
return switch (this) {
|
||||||
|
case IDLE -> "Idle";
|
||||||
|
case WALK -> "Walk";
|
||||||
|
case RUN -> "Run";
|
||||||
|
case JUMP -> "Jump";
|
||||||
|
case DUCK -> "Duck";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
package de.blight.game.animation;
|
||||||
|
|
||||||
|
import com.jme3.anim.*;
|
||||||
|
import com.jme3.app.Application;
|
||||||
|
import com.jme3.app.SimpleApplication;
|
||||||
|
import com.jme3.app.state.BaseAppState;
|
||||||
|
import com.jme3.asset.AssetManager;
|
||||||
|
import com.jme3.scene.Spatial;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads all .j3o animation files from the animations/ asset folder at startup.
|
||||||
|
* Provides retargeted animation clips for any model with a SkinningControl.
|
||||||
|
*
|
||||||
|
* Clip keys follow the pattern "filename/clipname" (e.g. "walk/Run").
|
||||||
|
*/
|
||||||
|
public class AnimationLibrary extends BaseAppState {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AnimationLibrary.class);
|
||||||
|
|
||||||
|
// Possible base paths for the animations folder (relative to working dir)
|
||||||
|
private static final String[] ASSET_BASES = {
|
||||||
|
"blight-assets/src/main/resources",
|
||||||
|
"assets",
|
||||||
|
".",
|
||||||
|
};
|
||||||
|
|
||||||
|
private AssetManager assetManager;
|
||||||
|
|
||||||
|
/** clip key → clip (bound to the SOURCE armature; retargeted before use) */
|
||||||
|
private final Map<String, AnimClip> clips = new LinkedHashMap<>();
|
||||||
|
/** clip key → armature the clip was loaded from */
|
||||||
|
private final Map<String, Armature> armatures = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize(Application app) {
|
||||||
|
assetManager = ((SimpleApplication) app).getAssetManager();
|
||||||
|
loadAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void cleanup(Application app) { clips.clear(); armatures.clear(); }
|
||||||
|
@Override protected void onEnable() {}
|
||||||
|
@Override protected void onDisable() {}
|
||||||
|
|
||||||
|
// ── Public API ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** All loaded clip keys (filename/clipname). */
|
||||||
|
public Collection<String> getClipKeys() {
|
||||||
|
return Collections.unmodifiableSet(clips.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retargets the clip to {@code model}'s skeleton and registers it
|
||||||
|
* in the model's AnimComposer (idempotent).
|
||||||
|
*
|
||||||
|
* @return true if the clip was applied successfully
|
||||||
|
*/
|
||||||
|
public boolean applyTo(String clipKey, Spatial model) {
|
||||||
|
AnimClip src = clips.get(clipKey);
|
||||||
|
Armature srcArm = armatures.get(clipKey);
|
||||||
|
if (src == null) return false;
|
||||||
|
|
||||||
|
AnimComposer ac = RetargetingSystem.findAnimComposer(model);
|
||||||
|
SkinningControl sc = RetargetingSystem.findSkinningControl(model);
|
||||||
|
if (ac == null || sc == null) return false;
|
||||||
|
|
||||||
|
String shortName = shortName(clipKey);
|
||||||
|
if (ac.getAnimClip(shortName) != null) return true; // already present
|
||||||
|
|
||||||
|
AnimClip target;
|
||||||
|
if (srcArm != null && srcArm != sc.getArmature()) {
|
||||||
|
// Pre-baked animations (Blender retargeting) have identical bone names →
|
||||||
|
// copy directly without retargeting. Different skeleton → retarget.
|
||||||
|
if (haveSameBoneNames(srcArm, sc.getArmature())) {
|
||||||
|
target = src;
|
||||||
|
} else {
|
||||||
|
target = RetargetingSystem.retarget(src, srcArm, sc.getArmature());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
target = src;
|
||||||
|
}
|
||||||
|
if (target == null) return false;
|
||||||
|
|
||||||
|
ac.addAnimClip(target);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies all loaded clips to {@code model} (only if the model has a skinned rig).
|
||||||
|
* Useful for auto-equipping all available animations on a freshly loaded character.
|
||||||
|
*/
|
||||||
|
public void applyAllTo(Spatial model) {
|
||||||
|
if (RetargetingSystem.findSkinningControl(model) == null) return;
|
||||||
|
int applied = 0;
|
||||||
|
for (String key : clips.keySet()) {
|
||||||
|
if (applyTo(key, model)) applied++;
|
||||||
|
}
|
||||||
|
if (applied > 0)
|
||||||
|
log.info("[AnimLib] {} Clips auf '{}' angewendet", applied, model.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the clip and immediately starts playing it.
|
||||||
|
*
|
||||||
|
* @return true on success
|
||||||
|
*/
|
||||||
|
public boolean playOn(String clipKey, Spatial model) {
|
||||||
|
if (!applyTo(clipKey, model)) return false;
|
||||||
|
AnimComposer ac = RetargetingSystem.findAnimComposer(model);
|
||||||
|
if (ac == null) return false;
|
||||||
|
ac.setCurrentAction(shortName(clipKey));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den Clip-Namen zurück, der einer semantischen Aktion in einem Animations-Set zugeordnet ist.
|
||||||
|
*
|
||||||
|
* @param assetRoot absoluter Pfad zum Assets-Wurzelverzeichnis
|
||||||
|
* @param j3oAssetPath relativer Asset-Pfad der {@code .j3o}-Set-Datei (z. B. {@code "animations/sets/hero.j3o"})
|
||||||
|
* @param action semantische Aktion, z. B. {@code AnimationAction.IDLE}
|
||||||
|
* @return Clip-Name oder {@code null} wenn keine Zuweisung existiert
|
||||||
|
*/
|
||||||
|
public static String getClipForAction(Path assetRoot, String j3oAssetPath, AnimationAction action) {
|
||||||
|
AnimSet set = AnimSet.loadByJ3oPath(assetRoot, j3oAssetPath);
|
||||||
|
return set.getActionMap().get(action.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Loading ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void loadAll() {
|
||||||
|
Path animDir = findAnimDir();
|
||||||
|
if (animDir == null) {
|
||||||
|
log.info("[AnimLib] Kein Animations-Verzeichnis gefunden – Bibliothek leer.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (var walk = Files.walk(animDir)) {
|
||||||
|
walk.filter(p -> p.toString().endsWith(".j3o"))
|
||||||
|
.forEach(this::loadFromFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("[AnimLib] Fehler beim Scannen: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
log.info("[AnimLib] {} Clips geladen.", clips.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadFromFile(Path file) {
|
||||||
|
Path animDir = findAnimDir();
|
||||||
|
if (animDir == null) return;
|
||||||
|
String relPath = animDir.relativize(file).toString().replace('\\', '/');
|
||||||
|
String assetKey = "animations/" + relPath;
|
||||||
|
String fileBase = relPath.replaceFirst("\\.j3o$", "");
|
||||||
|
|
||||||
|
try {
|
||||||
|
Spatial loaded = assetManager.loadModel(assetKey);
|
||||||
|
AnimComposer ac = RetargetingSystem.findAnimComposer(loaded);
|
||||||
|
SkinningControl sc = RetargetingSystem.findSkinningControl(loaded);
|
||||||
|
if (ac == null) {
|
||||||
|
log.debug("[AnimLib] Kein AnimComposer in {}", assetKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Armature armature = sc != null ? sc.getArmature() : null;
|
||||||
|
|
||||||
|
for (String clipName : ac.getAnimClipsNames()) {
|
||||||
|
String key = fileBase + "/" + clipName;
|
||||||
|
clips.put(key, ac.getAnimClip(clipName));
|
||||||
|
if (armature != null) armatures.put(key, armature);
|
||||||
|
log.info("[AnimLib] Clip: {}", key);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[AnimLib] Fehler beim Laden von {}: {}", assetKey, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path findAnimDir() {
|
||||||
|
for (String base : ASSET_BASES) {
|
||||||
|
Path p = Paths.get(base, "animations");
|
||||||
|
if (Files.isDirectory(p)) return p;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean haveSameBoneNames(Armature a, Armature b) {
|
||||||
|
Set<String> namesA = a.getJointList().stream().map(Joint::getName).collect(Collectors.toSet());
|
||||||
|
Set<String> namesB = b.getJointList().stream().map(Joint::getName).collect(Collectors.toSet());
|
||||||
|
return namesA.equals(namesB);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String shortName(String clipKey) {
|
||||||
|
int slash = clipKey.lastIndexOf('/');
|
||||||
|
return slash >= 0 ? clipKey.substring(slash + 1) : clipKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package de.blight.game.animation;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ordnet {@link AnimationAction}-Werte Clip-Namen zu.
|
||||||
|
* Wird als JSON-Companion-Datei neben dem Modell gespeichert:
|
||||||
|
* Models/charakter.j3o → Models/charakter.animmap
|
||||||
|
*
|
||||||
|
* @deprecated Ersetzt durch {@link AnimSet}, das sowohl Clip-Liste als auch
|
||||||
|
* Aktions-Zuweisung in einer einzigen {@code .animset.json}-Datei speichert.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public class AnimationMap {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AnimationMap.class);
|
||||||
|
private static final Gson GSON = new Gson();
|
||||||
|
|
||||||
|
private final EnumMap<AnimationAction, String> map = new EnumMap<>(AnimationAction.class);
|
||||||
|
|
||||||
|
// ── API ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public void set(AnimationAction action, String clipName) {
|
||||||
|
if (clipName == null || clipName.isBlank()) {
|
||||||
|
map.remove(action);
|
||||||
|
} else {
|
||||||
|
map.put(action, clipName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt den zugewiesenen Clip-Namen zurück, oder {@code null} wenn nicht belegt. */
|
||||||
|
public String get(AnimationAction action) {
|
||||||
|
return map.get(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<AnimationAction, String> asMap() {
|
||||||
|
return new EnumMap<>(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Persistenz ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Speichert die Map in die Companion-Datei neben {@code modelPath}
|
||||||
|
* (Extension {@code .j3o} wird durch {@code .animmap} ersetzt).
|
||||||
|
*/
|
||||||
|
public void save(Path modelPath) {
|
||||||
|
Path target = companionPath(modelPath);
|
||||||
|
try {
|
||||||
|
Map<String, String> raw = new HashMap<>();
|
||||||
|
map.forEach((action, clip) -> raw.put(action.name(), clip));
|
||||||
|
Files.writeString(target, GSON.toJson(raw), StandardCharsets.UTF_8);
|
||||||
|
log.debug("[AnimMap] Gespeichert: {}", target);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("[AnimMap] Speicherfehler {}: {}", target, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt die Companion-Datei und gibt eine {@link AnimationMap} zurück.
|
||||||
|
* Existiert keine Datei, wird eine leere Map zurückgegeben.
|
||||||
|
*/
|
||||||
|
public static AnimationMap load(Path modelPath) {
|
||||||
|
AnimationMap result = new AnimationMap();
|
||||||
|
Path source = companionPath(modelPath);
|
||||||
|
if (!Files.exists(source)) return result;
|
||||||
|
try {
|
||||||
|
String json = Files.readString(source, StandardCharsets.UTF_8);
|
||||||
|
Type type = new TypeToken<Map<String, String>>(){}.getType();
|
||||||
|
Map<String, String> raw = GSON.fromJson(json, type);
|
||||||
|
if (raw != null) {
|
||||||
|
raw.forEach((key, clip) -> {
|
||||||
|
try {
|
||||||
|
result.map.put(AnimationAction.valueOf(key), clip);
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
// Unbekannte Aktion aus zukünftiger Version – ignorieren
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
log.debug("[AnimMap] Geladen: {}", source);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("[AnimMap] Ladefehler {}: {}", source, e.getMessage());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Leitet den Companion-Pfad aus dem Modell-Pfad ab. */
|
||||||
|
public static Path companionPath(Path modelPath) {
|
||||||
|
String name = modelPath.getFileName().toString();
|
||||||
|
String base = name.endsWith(".j3o") ? name.substring(0, name.length() - 4) : name;
|
||||||
|
return modelPath.resolveSibling(base + ".animmap");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package de.blight.game.animation;
|
||||||
|
|
||||||
|
import com.jme3.anim.Armature;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes bone names across different skeleton conventions (Mixamo, standard, Tripo3D).
|
||||||
|
* Used by RetargetingSystem to map source joints to target joints.
|
||||||
|
*/
|
||||||
|
public final class BoneNameMapping {
|
||||||
|
|
||||||
|
private BoneNameMapping() {}
|
||||||
|
|
||||||
|
// Mixamo "mixamorig:" prefix → unprefixed standard name
|
||||||
|
private static final Map<String, String> MIXAMO_TO_STD = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
String[] bones = {
|
||||||
|
"Hips", "Spine", "Spine1", "Spine2",
|
||||||
|
"Neck", "Head", "HeadTop_End",
|
||||||
|
"LeftEye", "RightEye",
|
||||||
|
"LeftShoulder", "LeftArm", "LeftForeArm", "LeftHand",
|
||||||
|
"LeftHandThumb1", "LeftHandThumb2", "LeftHandThumb3", "LeftHandThumb4",
|
||||||
|
"LeftHandIndex1", "LeftHandIndex2", "LeftHandIndex3", "LeftHandIndex4",
|
||||||
|
"LeftHandMiddle1", "LeftHandMiddle2", "LeftHandMiddle3", "LeftHandMiddle4",
|
||||||
|
"LeftHandRing1", "LeftHandRing2", "LeftHandRing3", "LeftHandRing4",
|
||||||
|
"LeftHandPinky1", "LeftHandPinky2", "LeftHandPinky3", "LeftHandPinky4",
|
||||||
|
"RightShoulder", "RightArm", "RightForeArm", "RightHand",
|
||||||
|
"RightHandThumb1", "RightHandThumb2", "RightHandThumb3", "RightHandThumb4",
|
||||||
|
"RightHandIndex1", "RightHandIndex2", "RightHandIndex3", "RightHandIndex4",
|
||||||
|
"RightHandMiddle1", "RightHandMiddle2", "RightHandMiddle3", "RightHandMiddle4",
|
||||||
|
"RightHandRing1", "RightHandRing2", "RightHandRing3", "RightHandRing4",
|
||||||
|
"RightHandPinky1", "RightHandPinky2", "RightHandPinky3", "RightHandPinky4",
|
||||||
|
"LeftUpLeg", "LeftLeg", "LeftFoot", "LeftToeBase", "LeftToe_End",
|
||||||
|
"RightUpLeg", "RightLeg", "RightFoot", "RightToeBase", "RightToe_End",
|
||||||
|
};
|
||||||
|
for (String b : bones) {
|
||||||
|
MIXAMO_TO_STD.put("mixamorig:" + b, b);
|
||||||
|
MIXAMO_TO_STD.put("mixamorig_" + b, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finger joints carry no useful motion in walking clips.
|
||||||
|
private static final Set<String> FINGER_EXCLUDE = Set.of(
|
||||||
|
"lefthandthumb1", "lefthandthumb2", "lefthandthumb3", "lefthandthumb4",
|
||||||
|
"lefthandindex1", "lefthandindex2", "lefthandindex3", "lefthandindex4",
|
||||||
|
"lefthandmiddle1", "lefthandmiddle2", "lefthandmiddle3", "lefthandmiddle4",
|
||||||
|
"lefthandring1", "lefthandring2", "lefthandring3", "lefthandring4",
|
||||||
|
"lefthandpinky1", "lefthandpinky2", "lefthandpinky3", "lefthandpinky4",
|
||||||
|
"righthandthumb1", "righthandthumb2", "righthandthumb3", "righthandthumb4",
|
||||||
|
"righthandindex1", "righthandindex2", "righthandindex3", "righthandindex4",
|
||||||
|
"righthandmiddle1", "righthandmiddle2", "righthandmiddle3", "righthandmiddle4",
|
||||||
|
"righthandring1", "righthandring2", "righthandring3", "righthandring4",
|
||||||
|
"righthandpinky1", "righthandpinky2", "righthandpinky3", "righthandpinky4"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tripo3D humanoid rig → Mixamo standard name
|
||||||
|
private static final Map<String, String> TRIPO_TO_STD = new HashMap<>();
|
||||||
|
static {
|
||||||
|
// Hip ist Elternteil von Pelvis UND Waist — kein direktes Mixamo-Äquivalent.
|
||||||
|
// Pelvis ist der direkte Elternteil der Oberschenkelknochen, wie Mixamo-Hips.
|
||||||
|
TRIPO_TO_STD.put("Pelvis", "Hips");
|
||||||
|
TRIPO_TO_STD.put("Waist", "Spine");
|
||||||
|
TRIPO_TO_STD.put("Spine01", "Spine1");
|
||||||
|
TRIPO_TO_STD.put("Spine02", "Spine2");
|
||||||
|
// Nur NeckTwist01 → Neck; NeckTwist02 ist ein Twist-Knochen und folgt NeckTwist01.
|
||||||
|
TRIPO_TO_STD.put("NeckTwist01", "Neck");
|
||||||
|
TRIPO_TO_STD.put("L_Clavicle", "LeftShoulder");
|
||||||
|
TRIPO_TO_STD.put("L_Upperarm", "LeftArm");
|
||||||
|
TRIPO_TO_STD.put("L_Forearm", "LeftForeArm");
|
||||||
|
TRIPO_TO_STD.put("L_Hand", "LeftHand");
|
||||||
|
TRIPO_TO_STD.put("R_Clavicle", "RightShoulder");
|
||||||
|
TRIPO_TO_STD.put("R_Upperarm", "RightArm");
|
||||||
|
TRIPO_TO_STD.put("R_Forearm", "RightForeArm");
|
||||||
|
TRIPO_TO_STD.put("R_Hand", "RightHand");
|
||||||
|
TRIPO_TO_STD.put("L_Thigh", "LeftUpLeg");
|
||||||
|
TRIPO_TO_STD.put("L_Calf", "LeftLeg");
|
||||||
|
TRIPO_TO_STD.put("L_Foot", "LeftFoot");
|
||||||
|
TRIPO_TO_STD.put("L_ToeBase", "LeftToeBase");
|
||||||
|
TRIPO_TO_STD.put("R_Thigh", "RightUpLeg");
|
||||||
|
TRIPO_TO_STD.put("R_Calf", "RightLeg");
|
||||||
|
TRIPO_TO_STD.put("R_Foot", "RightFoot");
|
||||||
|
TRIPO_TO_STD.put("R_ToeBase", "RightToeBase");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, String> buildMapping(Armature source, Armature target) {
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
|
||||||
|
Map<String, String> targetByNorm = new HashMap<>();
|
||||||
|
for (var joint : target.getJointList())
|
||||||
|
targetByNorm.put(normalize(joint.getName()), joint.getName());
|
||||||
|
|
||||||
|
for (var joint : source.getJointList()) {
|
||||||
|
String sName = joint.getName();
|
||||||
|
String norm = normalize(sName);
|
||||||
|
|
||||||
|
if (FINGER_EXCLUDE.contains(norm)) continue;
|
||||||
|
|
||||||
|
if (target.getJoint(sName) != null) {
|
||||||
|
result.put(sName, sName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String tName = targetByNorm.get(norm);
|
||||||
|
if (tName != null) result.put(sName, tName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String normalize(String name) {
|
||||||
|
String n = MIXAMO_TO_STD.getOrDefault(name, name);
|
||||||
|
n = TRIPO_TO_STD.getOrDefault(n, n);
|
||||||
|
if (n.startsWith("mixamorig:")) n = n.substring("mixamorig:".length());
|
||||||
|
if (n.startsWith("mixamorig_")) n = n.substring("mixamorig_".length());
|
||||||
|
return n.replace("_", "").replace(".", "").replace(" ", "").toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
package de.blight.game.animation;
|
||||||
|
|
||||||
|
import com.jme3.anim.*;
|
||||||
|
import com.jme3.math.FastMath;
|
||||||
|
import com.jme3.math.Quaternion;
|
||||||
|
import com.jme3.math.Vector3f;
|
||||||
|
import com.jme3.scene.Node;
|
||||||
|
import com.jme3.scene.Spatial;
|
||||||
|
import com.jme3.scene.control.Control;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model-space retargeting with correct parent-chain propagation.
|
||||||
|
*
|
||||||
|
* For each mapped dst joint j:
|
||||||
|
* dstActualMS[j][k] = dstBindMS[j] × inv(srcBindMS[j]) × srcAnimMS[j][k]
|
||||||
|
*
|
||||||
|
* For each unmapped dst joint j (needed for parent propagation):
|
||||||
|
* dstActualMS[j][k] = dstActualMS[parent][k] × dstBindLocal[j]
|
||||||
|
*
|
||||||
|
* Local rotation for rendering:
|
||||||
|
* dstLocal[j][k] = inv(dstActualMS[parent][k]) × dstActualMS[j][k]
|
||||||
|
*
|
||||||
|
* This correctly handles the case where a mapped parent joint is animated —
|
||||||
|
* the child's local rotation is computed relative to the parent's ANIMATED
|
||||||
|
* model-space, not its bind model-space.
|
||||||
|
*/
|
||||||
|
public final class RetargetingSystem {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(RetargetingSystem.class);
|
||||||
|
|
||||||
|
// Manuelle Model-Space-Korrekturen pro normalisiertem Knochen-Namen (via BoneNameMapping.normalize).
|
||||||
|
// Wird vom Bone-Editor befüllt und per retarget(clip, src, dst, corrections) übergeben.
|
||||||
|
private static final Map<String, Quaternion> MS_CORRECTIONS = new HashMap<>();
|
||||||
|
|
||||||
|
private RetargetingSystem() {}
|
||||||
|
|
||||||
|
// ── Public API ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static AnimClip retarget(AnimClip sourceClip,
|
||||||
|
Armature sourceArmature,
|
||||||
|
Armature targetArmature) {
|
||||||
|
return retarget(sourceClip, sourceArmature, targetArmature, MS_CORRECTIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AnimClip retarget(AnimClip sourceClip,
|
||||||
|
Armature sourceArmature,
|
||||||
|
Armature targetArmature,
|
||||||
|
Map<String, Quaternion> corrections) {
|
||||||
|
Map<String, String> nameMap = BoneNameMapping.buildMapping(sourceArmature, targetArmature);
|
||||||
|
if (nameMap.isEmpty()) {
|
||||||
|
log.warn("[Retarget] Keine Knochen-Übereinstimmung für '{}'", sourceClip.getName());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Same-rig fast path: nur wenn Namen UND Bind-Posen übereinstimmen ──
|
||||||
|
// Mixamo-Clips, deren Knochen in Blender nur umbenannt (nicht retargeted) wurden,
|
||||||
|
// haben denselben Knochennamen aber Mixamo-Bind-Pose → benötigen die volle Formel.
|
||||||
|
if (isSameRig(nameMap, sourceArmature, targetArmature)) {
|
||||||
|
log.warn("[Retarget] '{}' same-rig detected – fast path (redirect only)", sourceClip.getName());
|
||||||
|
return redirectTracks(sourceClip, targetArmature);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Source track lookup ───────────────────────────────────────────────
|
||||||
|
Map<String, TransformTrack> srcTrackMap = new HashMap<>();
|
||||||
|
for (AnimTrack<?> t : sourceClip.getTracks())
|
||||||
|
if (t instanceof TransformTrack tt && tt.getTarget() instanceof Joint j)
|
||||||
|
srcTrackMap.put(j.getName(), tt);
|
||||||
|
|
||||||
|
// Reference keyframe times
|
||||||
|
float[] times = null;
|
||||||
|
for (String name : nameMap.keySet()) {
|
||||||
|
TransformTrack tt = srcTrackMap.get(name);
|
||||||
|
if (tt != null) { times = tt.getTimes(); break; }
|
||||||
|
}
|
||||||
|
if (times == null || times.length == 0) {
|
||||||
|
log.warn("[Retarget] Keine Keyframe-Zeiten für '{}'", sourceClip.getName());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int numFrames = times.length;
|
||||||
|
|
||||||
|
// ── Bind-pose model-space for both skeletons ──────────────────────────
|
||||||
|
Map<Joint, Quaternion> srcBindMS = buildModelSpaceBind(sourceArmature);
|
||||||
|
Map<Joint, Quaternion> dstBindMS = buildModelSpaceBind(targetArmature);
|
||||||
|
|
||||||
|
// ── Source animated model-space per frame ─────────────────────────────
|
||||||
|
Map<Joint, Quaternion[]> srcAnimMS = new HashMap<>();
|
||||||
|
for (Joint j : sourceArmature.getJointList())
|
||||||
|
computeModelSpaceAnim(j, srcTrackMap, numFrames, srcAnimMS);
|
||||||
|
|
||||||
|
// ── Arm-Diagnose: Quell-Local, Quell-ModelSpace und Bind-MS bei Frame 0 ──
|
||||||
|
for (var e : nameMap.entrySet()) {
|
||||||
|
String dstName = e.getValue();
|
||||||
|
if (!dstName.equals("L_Upperarm") && !dstName.equals("R_Upperarm")
|
||||||
|
&& !dstName.equals("L_Clavicle") && !dstName.equals("R_Clavicle")) continue;
|
||||||
|
Joint srcJ = sourceArmature.getJoint(e.getKey());
|
||||||
|
Joint dstJ = targetArmature.getJoint(dstName);
|
||||||
|
if (srcJ == null || dstJ == null) continue;
|
||||||
|
TransformTrack tt = srcTrackMap.get(e.getKey());
|
||||||
|
Quaternion[] localRots = tt != null ? tt.getRotations() : null;
|
||||||
|
float[] loc = localRots != null && localRots.length > 0
|
||||||
|
? localRots[0].toAngles(null) : new float[3];
|
||||||
|
Quaternion[] ms = srcAnimMS.get(srcJ);
|
||||||
|
float[] msA = ms != null ? ms[0].toAngles(null) : new float[3];
|
||||||
|
float[] sbsA = srcBindMS.get(srcJ).toAngles(null);
|
||||||
|
float[] dbsA = dstBindMS.get(dstJ).toAngles(null);
|
||||||
|
log.warn("[ArmDiag] '{}' {} → {} | srcLocal=[{} {} {}]° | srcMS=[{} {} {}]° | srcBindMS=[{} {} {}]° | dstBindMS=[{} {} {}]°",
|
||||||
|
sourceClip.getName(), e.getKey(), dstName,
|
||||||
|
String.format("%.1f", Math.toDegrees(loc[0])),
|
||||||
|
String.format("%.1f", Math.toDegrees(loc[1])),
|
||||||
|
String.format("%.1f", Math.toDegrees(loc[2])),
|
||||||
|
String.format("%.1f", Math.toDegrees(msA[0])),
|
||||||
|
String.format("%.1f", Math.toDegrees(msA[1])),
|
||||||
|
String.format("%.1f", Math.toDegrees(msA[2])),
|
||||||
|
String.format("%.1f", Math.toDegrees(sbsA[0])),
|
||||||
|
String.format("%.1f", Math.toDegrees(sbsA[1])),
|
||||||
|
String.format("%.1f", Math.toDegrees(sbsA[2])),
|
||||||
|
String.format("%.1f", Math.toDegrees(dbsA[0])),
|
||||||
|
String.format("%.1f", Math.toDegrees(dbsA[1])),
|
||||||
|
String.format("%.1f", Math.toDegrees(dbsA[2])));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── dst name → src Joint (reverse map) ───────────────────────────────
|
||||||
|
Map<String, Joint> dstToSrc = new HashMap<>();
|
||||||
|
for (var e : nameMap.entrySet()) {
|
||||||
|
Joint src = sourceArmature.getJoint(e.getKey());
|
||||||
|
if (src != null) dstToSrc.put(e.getValue(), src);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manuelle Korrekturen (vom Bone-Editor übergeben) – keine automatischen Arm-Korrekturen mehr.
|
||||||
|
// Die Standardformel dstBind × inv(srcBind) × srcAnimMS überträgt die relative Bewegung
|
||||||
|
// korrekt, unabhängig davon ob Source und Target unterschiedliche globale Orientierungen haben.
|
||||||
|
Map<String, Quaternion> effectiveCorrections = new HashMap<>(corrections);
|
||||||
|
|
||||||
|
// ── Allocate result arrays for mapped dst joints ──────────────────────
|
||||||
|
Map<Joint, Quaternion[]> dstLocalArrays = new LinkedHashMap<>();
|
||||||
|
for (String dstName : dstToSrc.keySet()) {
|
||||||
|
Joint dst = targetArmature.getJoint(dstName);
|
||||||
|
if (dst != null) dstLocalArrays.put(dst, new Quaternion[numFrames]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Per-frame retargeting ─────────────────────────────────────────────
|
||||||
|
for (int k = 0; k < numFrames; k++) {
|
||||||
|
// Compute dstActualMS for all dst joints (recursive, cached per frame)
|
||||||
|
Map<Joint, Quaternion> dstActualMS = new HashMap<>();
|
||||||
|
for (Joint dst : targetArmature.getJointList())
|
||||||
|
computeDstActualMS(dst, k, dstToSrc, srcAnimMS, srcBindMS, dstBindMS, dstActualMS);
|
||||||
|
|
||||||
|
// Welt-Raum-Korrekturen: Pre-Multiplikation auf Model-Space aller betroffenen Gelenke.
|
||||||
|
// Dadurch drehen sich linke und rechte Seite symmetrisch (keine Spiegelproblematik).
|
||||||
|
// Nur die Schulter-Tracks ändern sich sichtbar; Kind-Locals kürzen sich gegenseitig raus,
|
||||||
|
// die Korrektur propagiert beim Rendern durch die Hierarchie automatisch weiter.
|
||||||
|
for (Joint dst : targetArmature.getJointList()) {
|
||||||
|
Quaternion corr = effectiveCorrections.get(BoneNameMapping.normalize(dst.getName()));
|
||||||
|
if (corr != null) {
|
||||||
|
Quaternion orig = dstActualMS.get(dst);
|
||||||
|
if (orig != null) dstActualMS.put(dst, corr.mult(orig));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ziel-ModelSpace-Diagnose bei Frame 0 ────────────────────────────
|
||||||
|
if (k == 0) {
|
||||||
|
// Root-Bones: bestimmen die Blickrichtung des Charakters
|
||||||
|
for (Joint d : targetArmature.getJointList()) {
|
||||||
|
if (d.getParent() != null) continue; // nur Wurzel-Bones
|
||||||
|
Quaternion ams = dstActualMS.get(d);
|
||||||
|
if (ams == null) continue;
|
||||||
|
Quaternion bind = dstBindMS.get(d);
|
||||||
|
float[] a = ams.toAngles(null);
|
||||||
|
float[] b = bind != null ? bind.toAngles(null) : new float[3];
|
||||||
|
log.warn("[RootDiag] '{}' root='{}' | dstActualMS=[{} {} {}]° | dstBind=[{} {} {}]°",
|
||||||
|
sourceClip.getName(), d.getName(),
|
||||||
|
String.format("%.1f", Math.toDegrees(a[0])),
|
||||||
|
String.format("%.1f", Math.toDegrees(a[1])),
|
||||||
|
String.format("%.1f", Math.toDegrees(a[2])),
|
||||||
|
String.format("%.1f", Math.toDegrees(b[0])),
|
||||||
|
String.format("%.1f", Math.toDegrees(b[1])),
|
||||||
|
String.format("%.1f", Math.toDegrees(b[2])));
|
||||||
|
// Auch direkte Kinder des Root loggen
|
||||||
|
for (Joint child : d.getChildren()) {
|
||||||
|
Quaternion cms = dstActualMS.get(child);
|
||||||
|
Quaternion cbind = dstBindMS.get(child);
|
||||||
|
if (cms == null) continue;
|
||||||
|
float[] ca = cms.toAngles(null);
|
||||||
|
float[] cb = cbind != null ? cbind.toAngles(null) : new float[3];
|
||||||
|
Quaternion cl = ams.inverse().mult(cms);
|
||||||
|
float[] cl_ = cl.toAngles(null);
|
||||||
|
log.warn("[RootDiag] '{}' child='{}' | dstActualMS=[{} {} {}]° | dstBind=[{} {} {}]° | local=[{} {} {}]°",
|
||||||
|
sourceClip.getName(), child.getName(),
|
||||||
|
String.format("%.1f", Math.toDegrees(ca[0])),
|
||||||
|
String.format("%.1f", Math.toDegrees(ca[1])),
|
||||||
|
String.format("%.1f", Math.toDegrees(ca[2])),
|
||||||
|
String.format("%.1f", Math.toDegrees(cb[0])),
|
||||||
|
String.format("%.1f", Math.toDegrees(cb[1])),
|
||||||
|
String.format("%.1f", Math.toDegrees(cb[2])),
|
||||||
|
String.format("%.1f", Math.toDegrees(cl_[0])),
|
||||||
|
String.format("%.1f", Math.toDegrees(cl_[1])),
|
||||||
|
String.format("%.1f", Math.toDegrees(cl_[2])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Arm-Bones
|
||||||
|
for (String dName : new String[]{"L_Clavicle","L_Upperarm","R_Clavicle","R_Upperarm"}) {
|
||||||
|
Joint d = targetArmature.getJoint(dName);
|
||||||
|
if (d == null) continue;
|
||||||
|
Quaternion ams = dstActualMS.get(d);
|
||||||
|
if (ams == null) continue;
|
||||||
|
Quaternion pms = d.getParent() != null
|
||||||
|
? dstActualMS.get(d.getParent()) : new Quaternion();
|
||||||
|
Quaternion local0 = pms.inverse().mult(ams);
|
||||||
|
float[] a = ams.toAngles(null);
|
||||||
|
float[] l = local0.toAngles(null);
|
||||||
|
log.warn("[DstDiag] '{}' {} | dstActualMS=[{} {} {}]° | dstLocal=[{} {} {}]°",
|
||||||
|
sourceClip.getName(), dName,
|
||||||
|
String.format("%.1f", Math.toDegrees(a[0])),
|
||||||
|
String.format("%.1f", Math.toDegrees(a[1])),
|
||||||
|
String.format("%.1f", Math.toDegrees(a[2])),
|
||||||
|
String.format("%.1f", Math.toDegrees(l[0])),
|
||||||
|
String.format("%.1f", Math.toDegrees(l[1])),
|
||||||
|
String.format("%.1f", Math.toDegrees(l[2])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert model-space → local for each mapped joint
|
||||||
|
for (var entry : dstLocalArrays.entrySet()) {
|
||||||
|
Joint dst = entry.getKey();
|
||||||
|
Quaternion ms = dstActualMS.get(dst);
|
||||||
|
Quaternion parentMS = dst.getParent() != null
|
||||||
|
? dstActualMS.get(dst.getParent())
|
||||||
|
: new Quaternion();
|
||||||
|
entry.getValue()[k] = parentMS.inverse().mult(ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Build tracks ──────────────────────────────────────────────────────
|
||||||
|
if (dstLocalArrays.isEmpty()) {
|
||||||
|
log.warn("[Retarget] Keine Tracks gemappt für '{}'", sourceClip.getName());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<AnimTrack<?>> newTracks = new ArrayList<>();
|
||||||
|
for (var entry : dstLocalArrays.entrySet())
|
||||||
|
newTracks.add(new TransformTrack(entry.getKey(), times, null, entry.getValue(), null));
|
||||||
|
|
||||||
|
AnimClip result = new AnimClip(sourceClip.getName());
|
||||||
|
result.setTracks(newTracks.toArray(new AnimTrack[0]));
|
||||||
|
log.warn("[Retarget] '{}' retargeted: {} Tracks", sourceClip.getName(), newTracks.size());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Compute desired dst model-space for one joint at one frame ────────────
|
||||||
|
|
||||||
|
private static Quaternion computeDstActualMS(
|
||||||
|
Joint dst, int k,
|
||||||
|
Map<String, Joint> dstToSrc,
|
||||||
|
Map<Joint, Quaternion[]> srcAnimMS,
|
||||||
|
Map<Joint, Quaternion> srcBindMS,
|
||||||
|
Map<Joint, Quaternion> dstBindMS,
|
||||||
|
Map<Joint, Quaternion> cache) {
|
||||||
|
|
||||||
|
Quaternion cached = cache.get(dst);
|
||||||
|
if (cached != null) return cached;
|
||||||
|
|
||||||
|
Joint srcJoint = dstToSrc.get(dst.getName());
|
||||||
|
Quaternion result;
|
||||||
|
|
||||||
|
if (srcJoint != null) {
|
||||||
|
Quaternion[] srcFrames = srcAnimMS.get(srcJoint);
|
||||||
|
Quaternion srcMS_k = srcFrames[Math.min(k, srcFrames.length - 1)];
|
||||||
|
|
||||||
|
// When both this bone AND its parent are mapped to the same source parent,
|
||||||
|
// use local-space retargeting. This avoids axis-swap artifacts caused by
|
||||||
|
// different root orientation conventions (Rx vs Ry) between skeletons:
|
||||||
|
// the relative arm/leg motion is transferred in the parent's own frame,
|
||||||
|
// so "arm rotates Z -90° relative to shoulder" stays exactly that.
|
||||||
|
Joint dstParent = dst.getParent();
|
||||||
|
Joint srcViaMap = dstParent != null ? dstToSrc.get(dstParent.getName()) : null;
|
||||||
|
Joint srcParentDirect = srcJoint.getParent();
|
||||||
|
|
||||||
|
if (srcViaMap != null && srcViaMap == srcParentDirect) {
|
||||||
|
// src_local_anim = inv(srcAnimMS_parent) × srcAnimMS
|
||||||
|
Quaternion[] pFrames = srcAnimMS.get(srcParentDirect);
|
||||||
|
Quaternion srcPar_k = pFrames[Math.min(k, pFrames.length - 1)];
|
||||||
|
Quaternion srcLocal = srcPar_k.inverse().mult(srcMS_k);
|
||||||
|
|
||||||
|
// correction = dstBind_local × inv(srcBind_local)
|
||||||
|
// = inv(dstBind_parent) × dstBind × inv(srcBind) × srcBind_parent
|
||||||
|
Quaternion correction = dstBindMS.get(dstParent).inverse()
|
||||||
|
.mult(dstBindMS.get(dst))
|
||||||
|
.mult(srcBindMS.get(srcJoint).inverse())
|
||||||
|
.mult(srcBindMS.get(srcParentDirect));
|
||||||
|
|
||||||
|
Quaternion parentActual = computeDstActualMS(dstParent, k,
|
||||||
|
dstToSrc, srcAnimMS, srcBindMS, dstBindMS, cache);
|
||||||
|
result = parentActual.mult(correction.mult(srcLocal));
|
||||||
|
} else {
|
||||||
|
// Root of mapped chain (parent unmapped): model-space formula
|
||||||
|
result = dstBindMS.get(dst).mult(srcBindMS.get(srcJoint).inverse().mult(srcMS_k));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unmapped: propagate parent's actual MS × own bind local
|
||||||
|
Quaternion parentMS = dst.getParent() != null
|
||||||
|
? computeDstActualMS(dst.getParent(), k, dstToSrc, srcAnimMS, srcBindMS, dstBindMS, cache)
|
||||||
|
: new Quaternion();
|
||||||
|
Quaternion bindLocal = dst.getInitialTransform() != null
|
||||||
|
? dst.getInitialTransform().getRotation() : new Quaternion();
|
||||||
|
result = parentMS.mult(bindLocal);
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.put(dst, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Model-space bind accumulation ─────────────────────────────────────────
|
||||||
|
|
||||||
|
private static Map<Joint, Quaternion> buildModelSpaceBind(Armature arm) {
|
||||||
|
Map<Joint, Quaternion> cache = new HashMap<>();
|
||||||
|
for (Joint j : arm.getJointList())
|
||||||
|
buildModelSpaceBindRec(j, cache);
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Quaternion buildModelSpaceBindRec(Joint j, Map<Joint, Quaternion> cache) {
|
||||||
|
Quaternion cached = cache.get(j);
|
||||||
|
if (cached != null) return cached;
|
||||||
|
Quaternion local = j.getInitialTransform() != null
|
||||||
|
? j.getInitialTransform().getRotation() : new Quaternion();
|
||||||
|
Quaternion result = j.getParent() == null
|
||||||
|
? new Quaternion(local)
|
||||||
|
: buildModelSpaceBindRec(j.getParent(), cache).mult(local);
|
||||||
|
cache.put(j, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Source animated model-space accumulation ──────────────────────────────
|
||||||
|
|
||||||
|
private static Quaternion[] computeModelSpaceAnim(Joint j,
|
||||||
|
Map<String, TransformTrack> tracks,
|
||||||
|
int numFrames,
|
||||||
|
Map<Joint, Quaternion[]> cache) {
|
||||||
|
Quaternion[] cached = cache.get(j);
|
||||||
|
if (cached != null) return cached;
|
||||||
|
|
||||||
|
Quaternion[] parentMS = j.getParent() != null
|
||||||
|
? computeModelSpaceAnim(j.getParent(), tracks, numFrames, cache)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
TransformTrack tt = tracks.get(j.getName());
|
||||||
|
Quaternion[] srcR = tt != null ? tt.getRotations() : null;
|
||||||
|
Quaternion bind = j.getInitialTransform() != null
|
||||||
|
? j.getInitialTransform().getRotation() : new Quaternion();
|
||||||
|
|
||||||
|
Quaternion[] ms = new Quaternion[numFrames];
|
||||||
|
for (int i = 0; i < numFrames; i++) {
|
||||||
|
Quaternion local = (srcR != null && srcR.length > 0)
|
||||||
|
? srcR[Math.min(i, srcR.length - 1)] : bind;
|
||||||
|
ms[i] = parentMS != null ? parentMS[i].mult(local) : new Quaternion(local);
|
||||||
|
}
|
||||||
|
cache.put(j, ms);
|
||||||
|
return ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Same-rig detection & redirect ────────────────────────────────────────
|
||||||
|
|
||||||
|
/** True nur wenn alle Namen exakt selbst-gemappt UND alle Bind-Posen übereinstimmen. */
|
||||||
|
private static boolean isSameRig(Map<String, String> nameMap, Armature src, Armature dst) {
|
||||||
|
for (var e : nameMap.entrySet()) {
|
||||||
|
if (!e.getKey().equals(e.getValue())) return false;
|
||||||
|
Joint srcJ = src.getJoint(e.getKey());
|
||||||
|
Joint dstJ = dst.getJoint(e.getValue());
|
||||||
|
if (srcJ == null || dstJ == null) continue;
|
||||||
|
Quaternion sb = srcJ.getInitialTransform() != null
|
||||||
|
? srcJ.getInitialTransform().getRotation() : new Quaternion();
|
||||||
|
Quaternion db = dstJ.getInitialTransform() != null
|
||||||
|
? dstJ.getInitialTransform().getRotation() : new Quaternion();
|
||||||
|
if (Math.abs(sb.dot(db)) < 0.9999f) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnimClip redirectTracks(AnimClip sourceClip, Armature targetArmature) {
|
||||||
|
List<AnimTrack<?>> newTracks = new ArrayList<>();
|
||||||
|
for (AnimTrack<?> t : sourceClip.getTracks()) {
|
||||||
|
if (t instanceof TransformTrack tt && tt.getTarget() instanceof Joint srcJoint) {
|
||||||
|
Joint dstJoint = targetArmature.getJoint(srcJoint.getName());
|
||||||
|
if (dstJoint == null) continue;
|
||||||
|
newTracks.add(new TransformTrack(dstJoint, tt.getTimes(),
|
||||||
|
tt.getTranslations(), tt.getRotations(), tt.getScales()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newTracks.isEmpty()) return null;
|
||||||
|
AnimClip result = new AnimClip(sourceClip.getName());
|
||||||
|
result.setTracks(newTracks.toArray(new AnimTrack[0]));
|
||||||
|
log.warn("[Retarget] '{}' redirected: {} Tracks", sourceClip.getName(), newTracks.size());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static AnimComposer findAnimComposer(Spatial s) {
|
||||||
|
return findControl(s, AnimComposer.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SkinningControl findSkinningControl(Spatial s) {
|
||||||
|
return findControl(s, SkinningControl.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
static <T extends Control> T findControl(Spatial s, Class<T> type) {
|
||||||
|
T c = s.getControl(type);
|
||||||
|
if (c != null) return c;
|
||||||
|
if (s instanceof Node n) {
|
||||||
|
for (Spatial child : n.getChildren()) {
|
||||||
|
c = findControl(child, type);
|
||||||
|
if (c != null) return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
253
blight-game/src/main/java/de/blight/game/console/JmeConsole.java
Normal file
253
blight-game/src/main/java/de/blight/game/console/JmeConsole.java
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
package de.blight.game.console;
|
||||||
|
|
||||||
|
import com.jme3.app.Application;
|
||||||
|
import com.jme3.app.SimpleApplication;
|
||||||
|
import com.jme3.app.state.BaseAppState;
|
||||||
|
import com.jme3.font.BitmapFont;
|
||||||
|
import com.jme3.font.BitmapText;
|
||||||
|
import com.jme3.input.KeyInput;
|
||||||
|
import com.jme3.input.RawInputListener;
|
||||||
|
import com.jme3.input.event.*;
|
||||||
|
import com.jme3.material.Material;
|
||||||
|
import com.jme3.material.RenderState;
|
||||||
|
import com.jme3.math.ColorRGBA;
|
||||||
|
import com.jme3.renderer.queue.RenderQueue;
|
||||||
|
import com.jme3.scene.Geometry;
|
||||||
|
import com.jme3.scene.Node;
|
||||||
|
import com.jme3.scene.Spatial;
|
||||||
|
import com.jme3.scene.shape.Quad;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JME-native Konsole.
|
||||||
|
* Game-Modus : useRawInput=true → RawInputListener fängt Tastatur direkt ab.
|
||||||
|
* Editor-Modus: useRawInput=false → Zeichen per feedChar/feedBackspace/… zuführen.
|
||||||
|
*
|
||||||
|
* Toggle-Taste (Game): KEY_GRAVE (^ auf DE-Tastatur).
|
||||||
|
*/
|
||||||
|
public class JmeConsole extends BaseAppState {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(JmeConsole.class);
|
||||||
|
|
||||||
|
// ── Layout ────────────────────────────────────────────────────────────────
|
||||||
|
private static final int HISTORY = 6;
|
||||||
|
private static final float LINE_H = 18f;
|
||||||
|
private static final float PAD = 6f;
|
||||||
|
private static final float BG_H = PAD + LINE_H * (HISTORY + 1) + PAD;
|
||||||
|
|
||||||
|
/** Taste zum Öffnen/Schließen der Konsole im Game. */
|
||||||
|
public static final int KEY_TOGGLE = KeyInput.KEY_GRAVE;
|
||||||
|
|
||||||
|
// ── Befehle ───────────────────────────────────────────────────────────────
|
||||||
|
private final Map<String, Function<String[], String>> commands = new LinkedHashMap<>();
|
||||||
|
private Consumer<Boolean> onVisibilityChanged;
|
||||||
|
|
||||||
|
// ── Zustand ───────────────────────────────────────────────────────────────
|
||||||
|
private boolean open = false;
|
||||||
|
private StringBuilder inputBuf = new StringBuilder();
|
||||||
|
private final Deque<String> history = new ArrayDeque<>();
|
||||||
|
private float blinkTimer = 0f;
|
||||||
|
private boolean cursorOn = true;
|
||||||
|
private final boolean useRawInput;
|
||||||
|
|
||||||
|
// ── JME-Objekte ───────────────────────────────────────────────────────────
|
||||||
|
private SimpleApplication app;
|
||||||
|
private Node guiNode;
|
||||||
|
private Node consoleNode;
|
||||||
|
private BitmapText inputLine;
|
||||||
|
private BitmapText[] histLines;
|
||||||
|
|
||||||
|
// ── Raw-Input (Game-Modus) ────────────────────────────────────────────────
|
||||||
|
private final RawInputListener rawListener = new RawInputListener() {
|
||||||
|
@Override public void beginInput() {}
|
||||||
|
@Override public void endInput() {}
|
||||||
|
@Override public void onMouseMotionEvent(MouseMotionEvent e) {}
|
||||||
|
@Override public void onMouseButtonEvent(MouseButtonEvent e) {}
|
||||||
|
@Override public void onJoyAxisEvent(JoyAxisEvent e) {}
|
||||||
|
@Override public void onJoyButtonEvent(JoyButtonEvent e) {}
|
||||||
|
@Override public void onTouchEvent(TouchEvent e) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onKeyEvent(KeyInputEvent e) {
|
||||||
|
if (!e.isPressed()) return;
|
||||||
|
int key = e.getKeyCode();
|
||||||
|
if (key == KEY_TOGGLE) { toggle(); return; }
|
||||||
|
if (!open) return;
|
||||||
|
char c = e.getKeyChar();
|
||||||
|
if (key == KeyInput.KEY_RETURN) feedEnter();
|
||||||
|
else if (key == KeyInput.KEY_BACK) feedBackspace();
|
||||||
|
else if (key == KeyInput.KEY_ESCAPE) feedEscape();
|
||||||
|
else if (c >= ' ') feedChar(c);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Konstruktoren ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Game-Modus: verwendet RawInputListener. */
|
||||||
|
public JmeConsole() { this(true); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param useRawInput true = Game (RawInputListener),
|
||||||
|
* false = Editor (Zeichen per feed-Methoden zuführen)
|
||||||
|
*/
|
||||||
|
public JmeConsole(boolean useRawInput) {
|
||||||
|
this.useRawInput = useRawInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Öffentliche API ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Registriert einen Konsolenbefehl. */
|
||||||
|
public void registerCommand(String name, Function<String[], String> handler) {
|
||||||
|
commands.put(name.toLowerCase(), handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Callback: wird beim Öffnen (true) und Schließen (false) der Konsole aufgerufen. */
|
||||||
|
public void setOnVisibilityChanged(Consumer<Boolean> cb) { onVisibilityChanged = cb; }
|
||||||
|
|
||||||
|
public boolean isOpen() { return open; }
|
||||||
|
|
||||||
|
public void toggle() { if (open) hide(); else show(); }
|
||||||
|
|
||||||
|
/** Baut die Konsolen-UI nach einem Viewport-Resize neu auf. */
|
||||||
|
public void rebuild() {
|
||||||
|
boolean wasOpen = open;
|
||||||
|
if (consoleNode != null) {
|
||||||
|
guiNode.detachChild(consoleNode);
|
||||||
|
consoleNode = null;
|
||||||
|
}
|
||||||
|
buildUI();
|
||||||
|
if (!wasOpen) consoleNode.setCullHint(Spatial.CullHint.Always);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void show() {
|
||||||
|
open = true;
|
||||||
|
consoleNode.setCullHint(Spatial.CullHint.Inherit);
|
||||||
|
if (onVisibilityChanged != null) onVisibilityChanged.accept(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hide() {
|
||||||
|
open = false;
|
||||||
|
consoleNode.setCullHint(Spatial.CullHint.Always);
|
||||||
|
inputBuf.setLength(0);
|
||||||
|
refreshInput();
|
||||||
|
if (onVisibilityChanged != null) onVisibilityChanged.accept(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feed-Methoden für Editor-Modus (SharedInput → JME-Thread)
|
||||||
|
public void feedChar(char c) { inputBuf.append(c); refreshInput(); }
|
||||||
|
public void feedBackspace() { if (inputBuf.length() > 0) { inputBuf.deleteCharAt(inputBuf.length()-1); refreshInput(); } }
|
||||||
|
public void feedEnter() { String raw = inputBuf.toString().trim(); inputBuf.setLength(0); execute(raw); refreshInput(); }
|
||||||
|
public void feedEscape() { hide(); }
|
||||||
|
|
||||||
|
/** Gibt eine Zeile in der Konsole aus. */
|
||||||
|
public void print(String msg) {
|
||||||
|
history.addFirst(msg);
|
||||||
|
while (history.size() > HISTORY) history.pollLast();
|
||||||
|
refreshHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize(Application app) {
|
||||||
|
this.app = (SimpleApplication) app;
|
||||||
|
this.guiNode = this.app.getGuiNode();
|
||||||
|
buildUI();
|
||||||
|
registerBuiltin();
|
||||||
|
if (useRawInput) app.getInputManager().addRawInputListener(rawListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void cleanup(Application app) {
|
||||||
|
if (useRawInput) app.getInputManager().removeRawInputListener(rawListener);
|
||||||
|
guiNode.detachChild(consoleNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void onEnable() {}
|
||||||
|
@Override protected void onDisable() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(float tpf) {
|
||||||
|
if (!open) return;
|
||||||
|
blinkTimer += tpf;
|
||||||
|
if (blinkTimer >= 0.5f) {
|
||||||
|
blinkTimer = 0f;
|
||||||
|
cursorOn = !cursorOn;
|
||||||
|
refreshInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── UI ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void buildUI() {
|
||||||
|
int sw = app.getCamera().getWidth();
|
||||||
|
float topY = app.getCamera().getHeight(); // Bildschirmoberseite
|
||||||
|
float baseY = topY - BG_H; // Unterkante der Konsole
|
||||||
|
|
||||||
|
Geometry bg = new Geometry("consoleBg", new Quad(sw, BG_H));
|
||||||
|
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
mat.setColor("Color", new ColorRGBA(0f, 0f, 0f, 0.82f));
|
||||||
|
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||||
|
bg.setMaterial(mat);
|
||||||
|
bg.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||||
|
bg.setLocalTranslation(0, baseY, -1f);
|
||||||
|
|
||||||
|
BitmapFont font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
|
||||||
|
|
||||||
|
inputLine = makeLine(font, new ColorRGBA(0.25f, 1f, 0.25f, 1f),
|
||||||
|
PAD, baseY + PAD + LINE_H);
|
||||||
|
|
||||||
|
histLines = new BitmapText[HISTORY];
|
||||||
|
for (int i = 0; i < HISTORY; i++) {
|
||||||
|
histLines[i] = makeLine(font, ColorRGBA.White,
|
||||||
|
PAD, baseY + PAD + LINE_H * (i + 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
consoleNode = new Node("jmeConsole");
|
||||||
|
consoleNode.attachChild(bg);
|
||||||
|
consoleNode.attachChild(inputLine);
|
||||||
|
for (BitmapText t : histLines) consoleNode.attachChild(t);
|
||||||
|
consoleNode.setCullHint(Spatial.CullHint.Always);
|
||||||
|
guiNode.attachChild(consoleNode);
|
||||||
|
|
||||||
|
refreshInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
private BitmapText makeLine(BitmapFont font, ColorRGBA color, float x, float y) {
|
||||||
|
BitmapText t = new BitmapText(font, false);
|
||||||
|
t.setSize(LINE_H - 2f);
|
||||||
|
t.setColor(color);
|
||||||
|
t.setLocalTranslation(x, y, 0f);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshInput() {
|
||||||
|
if (inputLine != null)
|
||||||
|
inputLine.setText("> " + inputBuf + (cursorOn ? "_" : " "));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshHistory() {
|
||||||
|
String[] arr = history.toArray(new String[0]);
|
||||||
|
for (int i = 0; i < histLines.length; i++)
|
||||||
|
histLines[i].setText(i < arr.length ? arr[i] : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void execute(String raw) {
|
||||||
|
if (raw.isEmpty()) return;
|
||||||
|
print("> " + raw);
|
||||||
|
String[] parts = raw.split("\\s+");
|
||||||
|
Function<String[], String> h = commands.get(parts[0].toLowerCase());
|
||||||
|
String out = (h != null) ? h.apply(parts) : "Unbekannter Befehl: " + parts[0] + " (help)";
|
||||||
|
if (out != null && !out.isEmpty()) print(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerBuiltin() {
|
||||||
|
registerCommand("help", args ->
|
||||||
|
"Befehle: " + String.join(" | ", commands.keySet()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import com.jme3.bullet.control.CharacterControl;
|
|||||||
import com.jme3.input.InputManager;
|
import com.jme3.input.InputManager;
|
||||||
import com.jme3.input.controls.ActionListener;
|
import com.jme3.input.controls.ActionListener;
|
||||||
import com.jme3.input.controls.KeyTrigger;
|
import com.jme3.input.controls.KeyTrigger;
|
||||||
|
import com.jme3.math.FastMath;
|
||||||
import com.jme3.math.Quaternion;
|
import com.jme3.math.Quaternion;
|
||||||
import com.jme3.math.Vector3f;
|
import com.jme3.math.Vector3f;
|
||||||
import com.jme3.renderer.Camera;
|
import com.jme3.renderer.Camera;
|
||||||
@@ -102,6 +103,9 @@ public class PlayerInputControl {
|
|||||||
if (visual != null) {
|
if (visual != null) {
|
||||||
Quaternion targetRot = new Quaternion();
|
Quaternion targetRot = new Quaternion();
|
||||||
targetRot.lookAt(moveDir, Vector3f.UNIT_Y);
|
targetRot.lookAt(moveDir, Vector3f.UNIT_Y);
|
||||||
|
// Modell hat +X als Vorwärtsrichtung; lookAt zeigt -Z nach vorne →
|
||||||
|
// 90°-Y-Versatz korrigiert den Orientierungsunterschied.
|
||||||
|
targetRot.multLocal(new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_Y));
|
||||||
Quaternion current = visual.getLocalRotation().clone();
|
Quaternion current = visual.getLocalRotation().clone();
|
||||||
current.slerp(targetRot, ROTATE_SPEED * tpf);
|
current.slerp(targetRot, ROTATE_SPEED * tpf);
|
||||||
visual.setLocalRotation(current);
|
visual.setLocalRotation(current);
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package de.blight.game.state;
|
||||||
|
|
||||||
|
import com.jme3.app.Application;
|
||||||
|
import com.jme3.app.SimpleApplication;
|
||||||
|
import com.jme3.app.state.BaseAppState;
|
||||||
|
import com.jme3.asset.AssetManager;
|
||||||
|
import com.jme3.audio.AudioData;
|
||||||
|
import com.jme3.audio.AudioNode;
|
||||||
|
import com.jme3.math.Vector3f;
|
||||||
|
import com.jme3.scene.Node;
|
||||||
|
import de.blight.common.PlacedSoundArea;
|
||||||
|
import de.blight.common.SoundAreaIO;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Distanzbasierter Ambient-Sound pro Polygon-Bereich.
|
||||||
|
* Innerhalb des Polygons: volle Lautstärke.
|
||||||
|
* Außerhalb: lineares Fade bis zur Reichweite (volume * CROSSFADE_SCALE Einheiten).
|
||||||
|
*/
|
||||||
|
public class AmbientSoundSystem extends BaseAppState {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AmbientSoundSystem.class);
|
||||||
|
private static final float CROSSFADE_SCALE = 30f; // Einheiten Reichweite außerhalb bei volume=1.0
|
||||||
|
private static final float FADE_DURATION = 2f; // Sekunden für vollen Lautstärke-Hub
|
||||||
|
|
||||||
|
private SimpleApplication app;
|
||||||
|
private AssetManager assets;
|
||||||
|
private Node rootNode;
|
||||||
|
|
||||||
|
private final List<PlacedSoundArea> data = new ArrayList<>();
|
||||||
|
private final List<AudioNode> sounds = new ArrayList<>();
|
||||||
|
private final List<Boolean> attached = new ArrayList<>();
|
||||||
|
|
||||||
|
private final Vector3f playerPos = new Vector3f();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize(Application application) {
|
||||||
|
app = (SimpleApplication) application;
|
||||||
|
assets = app.getAssetManager();
|
||||||
|
rootNode = app.getRootNode();
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (PlacedSoundArea area : SoundAreaIO.load()) {
|
||||||
|
if (area.soundPath().isEmpty()) continue;
|
||||||
|
try {
|
||||||
|
AudioNode node = new AudioNode(assets, area.soundPath(), AudioData.DataType.Stream);
|
||||||
|
node.setLooping(true);
|
||||||
|
node.setVolume(0f);
|
||||||
|
node.setPositional(false);
|
||||||
|
data.add(area);
|
||||||
|
sounds.add(node);
|
||||||
|
attached.add(false);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[AmbientSoundSystem] Sound nicht ladbar '{}': {}", area.soundPath(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.isEmpty()) log.info("[AmbientSoundSystem] Keine Sound-Bereiche geladen.");
|
||||||
|
else log.info("[AmbientSoundSystem] {} Sound-Bereiche geladen.", data.size());
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("[AmbientSoundSystem] Sound-Bereich-Datei nicht ladbar: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void cleanup(Application application) {
|
||||||
|
for (int i = 0; i < sounds.size(); i++) {
|
||||||
|
if (attached.get(i)) {
|
||||||
|
sounds.get(i).stop();
|
||||||
|
rootNode.detachChild(sounds.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.clear();
|
||||||
|
sounds.clear();
|
||||||
|
attached.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void onEnable() {}
|
||||||
|
@Override protected void onDisable() {}
|
||||||
|
|
||||||
|
public void setPlayerPosition(Vector3f pos) {
|
||||||
|
playerPos.set(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(float tpf) {
|
||||||
|
if (data.isEmpty()) return;
|
||||||
|
|
||||||
|
for (int i = 0; i < data.size(); i++) {
|
||||||
|
PlacedSoundArea area = data.get(i);
|
||||||
|
AudioNode node = sounds.get(i);
|
||||||
|
float target = computeTarget(area);
|
||||||
|
float cur = node.getVolume();
|
||||||
|
boolean wasOn = attached.get(i);
|
||||||
|
|
||||||
|
if (target > 0f && !wasOn) {
|
||||||
|
node.setVolume(0f);
|
||||||
|
rootNode.attachChild(node);
|
||||||
|
node.play();
|
||||||
|
attached.set(i, true);
|
||||||
|
log.info("[AmbientSoundSystem] Bereich {} hörbar → spiele: {}", i, area.soundPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attached.get(i)) {
|
||||||
|
float step = area.volume() * tpf / FADE_DURATION;
|
||||||
|
float nv = target > cur
|
||||||
|
? Math.min(cur + step, target)
|
||||||
|
: Math.max(cur - step, target);
|
||||||
|
node.setVolume(nv);
|
||||||
|
|
||||||
|
if (nv <= 0f) {
|
||||||
|
node.stop();
|
||||||
|
rootNode.detachChild(node);
|
||||||
|
attached.set(i, false);
|
||||||
|
log.info("[AmbientSoundSystem] Bereich {} unhörbar → gestoppt: {}", i, area.soundPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zielvolumen basierend auf signiertem Abstand zur Polygongrenze.
|
||||||
|
* Innen (≥0): volle Lautstärke.
|
||||||
|
* Außen: linear von voll (Grenze) auf null (hearRange = volume * CROSSFADE_SCALE).
|
||||||
|
*/
|
||||||
|
private float computeTarget(PlacedSoundArea area) {
|
||||||
|
float signedDist = signedDistToPolygon(playerPos.x, playerPos.z, area.pointsX(), area.pointsZ());
|
||||||
|
if (signedDist >= 0f) return area.volume();
|
||||||
|
float hearRange = CROSSFADE_SCALE * area.volume();
|
||||||
|
if (signedDist <= -hearRange) return 0f;
|
||||||
|
return area.volume() * (1f + signedDist / hearRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Positiv = Spieler ist innen (Distanz zur nächsten Kante).
|
||||||
|
* Negativ = Spieler ist außen (negierte Distanz zur nächsten Kante).
|
||||||
|
*/
|
||||||
|
private static float signedDistToPolygon(float px, float pz, float[] xs, float[] zs) {
|
||||||
|
float edgeDist = minDistToPolygonEdge(px, pz, xs, zs);
|
||||||
|
return pointInPolygon(px, pz, xs, zs) ? edgeDist : -edgeDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float minDistToPolygonEdge(float px, float pz, float[] xs, float[] zs) {
|
||||||
|
int n = xs.length;
|
||||||
|
float minD2 = Float.MAX_VALUE;
|
||||||
|
for (int i = 0, j = n - 1; i < n; j = i++) {
|
||||||
|
float d2 = pointToSegmentDist2(px, pz, xs[j], zs[j], xs[i], zs[i]);
|
||||||
|
if (d2 < minD2) minD2 = d2;
|
||||||
|
}
|
||||||
|
return (float) Math.sqrt(minD2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float pointToSegmentDist2(float px, float pz,
|
||||||
|
float ax, float az,
|
||||||
|
float bx, float bz) {
|
||||||
|
float dx = bx - ax, dz = bz - az;
|
||||||
|
float lenSq = dx * dx + dz * dz;
|
||||||
|
float t = lenSq == 0f ? 0f : Math.max(0f, Math.min(1f, ((px - ax) * dx + (pz - az) * dz) / lenSq));
|
||||||
|
float cx = ax + t * dx - px;
|
||||||
|
float cz = az + t * dz - pz;
|
||||||
|
return cx * cx + cz * cz;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean pointInPolygon(float px, float pz, float[] xs, float[] zs) {
|
||||||
|
int n = xs.length;
|
||||||
|
boolean inside = false;
|
||||||
|
for (int i = 0, j = n - 1; i < n; j = i++) {
|
||||||
|
float xi = xs[i], zi = zs[i];
|
||||||
|
float xj = xs[j], zj = zs[j];
|
||||||
|
if ((zi > pz) != (zj > pz) && (px < (xj - xi) * (pz - zi) / (zj - zi) + xi))
|
||||||
|
inside = !inside;
|
||||||
|
}
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package de.blight.game.state;
|
||||||
|
|
||||||
|
import com.jme3.asset.AssetManager;
|
||||||
|
import com.jme3.material.Material;
|
||||||
|
import com.jme3.material.RenderState;
|
||||||
|
import com.jme3.math.ColorRGBA;
|
||||||
|
import com.jme3.math.Vector3f;
|
||||||
|
import com.jme3.renderer.queue.RenderQueue;
|
||||||
|
import com.jme3.scene.Geometry;
|
||||||
|
import com.jme3.scene.Node;
|
||||||
|
import com.jme3.scene.shape.Box;
|
||||||
|
|
||||||
|
public class CloudsNode extends Node {
|
||||||
|
|
||||||
|
private static final int COUNT = 14;
|
||||||
|
private static final float Y = 800f;
|
||||||
|
private static final float WRAP_HALF = 1800f;
|
||||||
|
|
||||||
|
private final Geometry[] geoms = new Geometry[COUNT];
|
||||||
|
private final float[] offsetX = new float[COUNT];
|
||||||
|
private final float[] offsetZ = new float[COUNT];
|
||||||
|
private final Material mat;
|
||||||
|
|
||||||
|
private static final float[] W = { 350,250,420,180,300,380,220,160,480,260,310,200,440,190 };
|
||||||
|
private static final float[] D = { 280,160,300,220,350,200,280,180,320,240,180,350,260,300 };
|
||||||
|
private static final float[] H = { 30, 20, 25, 18, 35, 28, 22, 15, 40, 24, 20, 30, 28, 22 };
|
||||||
|
private static final float[] IX = {-600,200,-100,500,-800,300,700,-400,0,-200,600,-700,100,-500};
|
||||||
|
private static final float[] IZ = { 400,-300,700,-500,100,-700,-200,600,-100,800,-400,200,-600,500};
|
||||||
|
|
||||||
|
public CloudsNode(AssetManager assetManager) {
|
||||||
|
super("clouds");
|
||||||
|
|
||||||
|
mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
mat.setColor("Color", new ColorRGBA(0.95f, 0.95f, 0.95f, 0.45f));
|
||||||
|
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||||
|
mat.getAdditionalRenderState().setDepthWrite(false);
|
||||||
|
|
||||||
|
for (int i = 0; i < COUNT; i++) {
|
||||||
|
Box box = new Box(W[i] * 0.5f, H[i] * 0.5f, D[i] * 0.5f);
|
||||||
|
Geometry g = new Geometry("cloud_" + i, box);
|
||||||
|
g.setMaterial(mat);
|
||||||
|
g.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||||
|
g.setShadowMode(RenderQueue.ShadowMode.Off);
|
||||||
|
offsetX[i] = IX[i];
|
||||||
|
offsetZ[i] = IZ[i];
|
||||||
|
attachChild(g);
|
||||||
|
geoms[i] = g;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCloudColor(ColorRGBA color) {
|
||||||
|
mat.setColor("Color", color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(float tpf, Vector3f windDir, float windSpeed, Vector3f camPos) {
|
||||||
|
float dx = windDir.x * windSpeed * tpf;
|
||||||
|
float dz = windDir.z * windSpeed * tpf;
|
||||||
|
for (int i = 0; i < COUNT; i++) {
|
||||||
|
offsetX[i] += dx;
|
||||||
|
offsetZ[i] += dz;
|
||||||
|
if (offsetX[i] > WRAP_HALF) offsetX[i] -= WRAP_HALF * 2f;
|
||||||
|
if (offsetX[i] < -WRAP_HALF) offsetX[i] += WRAP_HALF * 2f;
|
||||||
|
if (offsetZ[i] > WRAP_HALF) offsetZ[i] -= WRAP_HALF * 2f;
|
||||||
|
if (offsetZ[i] < -WRAP_HALF) offsetZ[i] += WRAP_HALF * 2f;
|
||||||
|
geoms[i].setLocalTranslation(camPos.x + offsetX[i], Y, camPos.z + offsetZ[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
package de.blight.game.state;
|
||||||
|
|
||||||
|
import com.jme3.app.Application;
|
||||||
|
import com.jme3.app.SimpleApplication;
|
||||||
|
import com.jme3.app.state.BaseAppState;
|
||||||
|
import com.jme3.light.AmbientLight;
|
||||||
|
import com.jme3.light.DirectionalLight;
|
||||||
|
import com.jme3.material.Material;
|
||||||
|
import com.jme3.math.*;
|
||||||
|
import com.jme3.renderer.queue.RenderQueue;
|
||||||
|
import com.jme3.scene.*;
|
||||||
|
import com.jme3.scene.shape.Sphere;
|
||||||
|
import com.jme3.shadow.DirectionalLightShadowFilter;
|
||||||
|
import com.jme3.shadow.EdgeFilteringMode;
|
||||||
|
import com.jme3.util.SkyFactory;
|
||||||
|
import de.blight.common.time.DayTime;
|
||||||
|
import de.blight.common.time.TimeListener;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag/Nacht-Zyklus: Sonne, Ambiente, Schatten, Himmelsfarbe.
|
||||||
|
* Wiederverwendbar im Game und Editor (withShadows=false für Editor).
|
||||||
|
*/
|
||||||
|
public class DayNightState extends BaseAppState implements TimeListener {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DayNightState.class);
|
||||||
|
|
||||||
|
// ── Farb-Konstanten ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static final ColorRGBA SUN_DAY = new ColorRGBA(1.00f, 0.95f, 0.88f, 1f);
|
||||||
|
private static final ColorRGBA SUN_DAWN = new ColorRGBA(1.00f, 0.55f, 0.20f, 1f);
|
||||||
|
private static final ColorRGBA AMB_DAY = new ColorRGBA(0.35f, 0.38f, 0.46f, 1f);
|
||||||
|
private static final ColorRGBA AMB_NIGHT = new ColorRGBA(0.04f, 0.04f, 0.12f, 1f);
|
||||||
|
private static final ColorRGBA BG_DAY = new ColorRGBA(0.35f, 0.55f, 0.85f, 1f);
|
||||||
|
private static final ColorRGBA BG_NIGHT = new ColorRGBA(0.01f, 0.01f, 0.06f, 1f);
|
||||||
|
|
||||||
|
// ── Konfiguration ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private final DayTime dayTime;
|
||||||
|
private final boolean withShadows;
|
||||||
|
|
||||||
|
// ── JME-Objekte ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private SimpleApplication app;
|
||||||
|
private Node rootNode;
|
||||||
|
private DirectionalLight sun;
|
||||||
|
private AmbientLight ambient;
|
||||||
|
private DirectionalLightShadowFilter shadowFilter;
|
||||||
|
private Spatial sky;
|
||||||
|
private Geometry sunSphere;
|
||||||
|
|
||||||
|
// ── Konstruktoren ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Game-Modus: Schatten aktiv, 5-Minuten-Tag. */
|
||||||
|
public DayNightState() {
|
||||||
|
this(new DayTime(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param withShadows false für Editor (kein Shadow-Renderer) */
|
||||||
|
public DayNightState(boolean withShadows) {
|
||||||
|
this(new DayTime(), withShadows);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DayNightState(DayTime dayTime, boolean withShadows) {
|
||||||
|
this.dayTime = dayTime;
|
||||||
|
this.withShadows = withShadows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DayTime getDayTime() { return dayTime; }
|
||||||
|
|
||||||
|
public void setPaused(boolean paused) { dayTime.setPaused(paused); }
|
||||||
|
|
||||||
|
/** Aktuelle Sonnenrichtung (normalisiert), oder (0,-1,0) falls noch nicht initialisiert. */
|
||||||
|
public com.jme3.math.Vector3f getSunDirection() {
|
||||||
|
return sun != null ? sun.getDirection() : new com.jme3.math.Vector3f(0f, -1f, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize(Application app) {
|
||||||
|
this.app = (SimpleApplication) app;
|
||||||
|
this.rootNode = this.app.getRootNode();
|
||||||
|
|
||||||
|
// Himmel
|
||||||
|
try {
|
||||||
|
sky = SkyFactory.createSky(app.getAssetManager(),
|
||||||
|
"Textures/Sky/Bright/BrightSky.dds",
|
||||||
|
SkyFactory.EnvMapType.CubeMap);
|
||||||
|
rootNode.attachChild(sky);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Sky-Textur nicht geladen – Viewport-Farbe als Fallback");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sonnen-Sphere (sichtbare Sonne am Himmel)
|
||||||
|
Sphere sphereMesh = new Sphere(16, 16, 18f);
|
||||||
|
sunSphere = new Geometry("sunSphere", sphereMesh);
|
||||||
|
Material sunMat = new Material(app.getAssetManager(),
|
||||||
|
"Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
sunMat.setColor("Color", new ColorRGBA(1f, 0.92f, 0.65f, 1f));
|
||||||
|
sunSphere.setMaterial(sunMat);
|
||||||
|
sunSphere.setQueueBucket(RenderQueue.Bucket.Sky);
|
||||||
|
sunSphere.setShadowMode(RenderQueue.ShadowMode.Off);
|
||||||
|
rootNode.attachChild(sunSphere);
|
||||||
|
|
||||||
|
// Sonne (DirectionalLight)
|
||||||
|
sun = new DirectionalLight();
|
||||||
|
rootNode.addLight(sun);
|
||||||
|
|
||||||
|
// Ambient
|
||||||
|
ambient = new AmbientLight();
|
||||||
|
rootNode.addLight(ambient);
|
||||||
|
|
||||||
|
// Schatten (nur im Game) – als Filter, damit WorldScene ihn in den FPP einhängen kann
|
||||||
|
if (withShadows) {
|
||||||
|
try {
|
||||||
|
shadowFilter = new DirectionalLightShadowFilter(app.getAssetManager(), 4096, 4);
|
||||||
|
shadowFilter.setLight(sun);
|
||||||
|
shadowFilter.setLambda(0.75f);
|
||||||
|
shadowFilter.setShadowZExtend(40f);
|
||||||
|
shadowFilter.setShadowZFadeLength(8f);
|
||||||
|
shadowFilter.setEdgeFilteringMode(EdgeFilteringMode.PCFPOISSON);
|
||||||
|
// Nicht direkt zum Viewport hinzufügen – WorldScene hängt ihn in den FPP ein
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Shadow-Filter konnte nicht erstellt werden", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dayTime.addListener(this);
|
||||||
|
onTimeChanged(dayTime.getTimeOfDay());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt den Shadow-Filter zurück, damit WorldScene ihn in den FPP einhängen kann. */
|
||||||
|
public DirectionalLightShadowFilter getShadowFilter() { return shadowFilter; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void cleanup(Application app) {
|
||||||
|
dayTime.removeListener(this);
|
||||||
|
rootNode.removeLight(sun);
|
||||||
|
rootNode.removeLight(ambient);
|
||||||
|
shadowFilter = null; // FPP-Cleanup liegt bei WorldScene
|
||||||
|
if (sky != null && sky.getParent() != null) rootNode.detachChild(sky);
|
||||||
|
if (sunSphere != null && sunSphere.getParent() != null) rootNode.detachChild(sunSphere);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void onEnable() {}
|
||||||
|
@Override protected void onDisable() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(float tpf) {
|
||||||
|
dayTime.update(tpf);
|
||||||
|
|
||||||
|
// Sky/Sonnen-Sphere nach rootNode.detachAllChildren() wiederherstellen
|
||||||
|
if (sky != null && sky.getParent() == null)
|
||||||
|
rootNode.attachChild(sky);
|
||||||
|
if (sunSphere != null && sunSphere.getParent() == null)
|
||||||
|
rootNode.attachChild(sunSphere);
|
||||||
|
|
||||||
|
// Sonnen-Sphere immer relativ zur Kamera positionieren
|
||||||
|
if (sunSphere != null && sunSphere.getCullHint() != Spatial.CullHint.Always) {
|
||||||
|
Vector3f camPos = app.getCamera().getLocation();
|
||||||
|
sunSphere.setLocalTranslation(camPos.add(sun.getDirection().negate().mult(480f)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Zeit-Callback ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTimeChanged(float t) {
|
||||||
|
float elev = sunElevation(t);
|
||||||
|
float elevC = FastMath.clamp(elev, 0f, 1f);
|
||||||
|
|
||||||
|
// ── Sonnenrichtung ──────────────────────────────────────────────────
|
||||||
|
float angle = t * FastMath.TWO_PI;
|
||||||
|
// Sonne bewegt sich von Ost (X+) über Süden nach West (X-)
|
||||||
|
Vector3f sunPos = new Vector3f(
|
||||||
|
FastMath.sin(angle) * 0.85f,
|
||||||
|
elev,
|
||||||
|
-0.15f
|
||||||
|
);
|
||||||
|
sun.setDirection(sunPos.negate().normalizeLocal());
|
||||||
|
|
||||||
|
// ── Sonnenfarbe: Dämmerung (orange) → Tag (warm-weiß) ──────────────
|
||||||
|
float dawnFactor = 1f - FastMath.clamp(elev * 5f, 0f, 1f);
|
||||||
|
ColorRGBA sunColor = SUN_DAWN.clone().interpolateLocal(SUN_DAY, 1f - dawnFactor);
|
||||||
|
sun.setColor(sunColor.multLocal(elevC * 1.35f));
|
||||||
|
|
||||||
|
// ── Sonnen-Sphere ausblenden wenn unter Horizont ────────────────────
|
||||||
|
if (sunSphere != null) {
|
||||||
|
sunSphere.setCullHint(elev > -0.02f
|
||||||
|
? Spatial.CullHint.Inherit
|
||||||
|
: Spatial.CullHint.Always);
|
||||||
|
// Farbe bei Dämmerung orange, bei Tag weiß-gelb
|
||||||
|
Material m = sunSphere.getMaterial();
|
||||||
|
if (m != null) {
|
||||||
|
ColorRGBA sphereColor = new ColorRGBA(1f, 0.6f + elevC * 0.32f, 0.3f + elevC * 0.35f, 1f);
|
||||||
|
m.setColor("Color", sphereColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ambient: Nacht → Tag ────────────────────────────────────────────
|
||||||
|
float ambFactor = FastMath.clamp((elev + 0.2f) / 0.6f, 0f, 1f);
|
||||||
|
ambient.setColor(AMB_NIGHT.clone().interpolateLocal(AMB_DAY, ambFactor));
|
||||||
|
|
||||||
|
// ── Schatten ────────────────────────────────────────────────────────
|
||||||
|
if (shadowFilter != null)
|
||||||
|
shadowFilter.setShadowIntensity(FastMath.clamp(elev * 0.8f, 0f, 0.5f));
|
||||||
|
|
||||||
|
// ── Himmel & Hintergrundfarbe ────────────────────────────────────────
|
||||||
|
float skyFactor = FastMath.clamp((elev + 0.05f) / 0.2f, 0f, 1f);
|
||||||
|
if (sky != null)
|
||||||
|
sky.setCullHint(skyFactor > 0.01f
|
||||||
|
? Spatial.CullHint.Inherit
|
||||||
|
: Spatial.CullHint.Always);
|
||||||
|
app.getViewPort().setBackgroundColor(
|
||||||
|
BG_NIGHT.clone().interpolateLocal(BG_DAY, skyFactor));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Sonnenhöhe: −1 = Mitternacht, 0 = Horizont, +1 = Mittag. */
|
||||||
|
private static float sunElevation(float t) {
|
||||||
|
return -FastMath.cos(t * FastMath.TWO_PI);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package de.blight.game.state;
|
||||||
|
|
||||||
|
import com.jme3.app.Application;
|
||||||
|
import com.jme3.app.SimpleApplication;
|
||||||
|
import com.jme3.app.state.BaseAppState;
|
||||||
|
import com.jme3.asset.AssetManager;
|
||||||
|
import com.jme3.effect.ParticleEmitter;
|
||||||
|
import com.jme3.effect.ParticleMesh;
|
||||||
|
import com.jme3.material.Material;
|
||||||
|
import com.jme3.math.*;
|
||||||
|
import com.jme3.scene.Node;
|
||||||
|
import de.blight.common.EmitterIO;
|
||||||
|
import de.blight.common.PlacedEmitter;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt platzierte Partikel-Emitter und aktiviert/deaktiviert sie basierend
|
||||||
|
* auf der Nähe des Spielers (proximity-based activation).
|
||||||
|
*/
|
||||||
|
public class EmitterSystem extends BaseAppState {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(EmitterSystem.class);
|
||||||
|
private static final float CHECK_INTERVAL = 0.25f;
|
||||||
|
|
||||||
|
private SimpleApplication app;
|
||||||
|
private AssetManager assets;
|
||||||
|
private Node rootNode;
|
||||||
|
|
||||||
|
private final List<PlacedEmitter> data = new ArrayList<>();
|
||||||
|
private final List<ParticleEmitter> effects = new ArrayList<>();
|
||||||
|
private final List<Boolean> active = new ArrayList<>();
|
||||||
|
|
||||||
|
private Vector3f playerPos = new Vector3f();
|
||||||
|
private float checkTimer = 0f;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize(Application application) {
|
||||||
|
app = (SimpleApplication) application;
|
||||||
|
assets = app.getAssetManager();
|
||||||
|
rootNode = app.getRootNode();
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (PlacedEmitter pe : EmitterIO.load()) {
|
||||||
|
ParticleEmitter effect = buildEffect(pe);
|
||||||
|
if (effect != null) {
|
||||||
|
data.add(pe);
|
||||||
|
effects.add(effect);
|
||||||
|
active.add(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!data.isEmpty())
|
||||||
|
log.info("[EmitterSystem] {} Emitter geladen.", data.size());
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("[EmitterSystem] Emitter-Datei nicht ladbar: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void cleanup(Application application) {
|
||||||
|
for (int i = 0; i < effects.size(); i++) {
|
||||||
|
if (active.get(i)) rootNode.detachChild(effects.get(i));
|
||||||
|
}
|
||||||
|
data.clear();
|
||||||
|
effects.clear();
|
||||||
|
active.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void onEnable() {}
|
||||||
|
@Override protected void onDisable() {}
|
||||||
|
|
||||||
|
public void setPlayerPosition(Vector3f pos) {
|
||||||
|
playerPos.set(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(float tpf) {
|
||||||
|
if (data.isEmpty()) return;
|
||||||
|
|
||||||
|
checkTimer += tpf;
|
||||||
|
if (checkTimer < CHECK_INTERVAL) return;
|
||||||
|
checkTimer = 0f;
|
||||||
|
|
||||||
|
for (int i = 0; i < data.size(); i++) {
|
||||||
|
PlacedEmitter pe = data.get(i);
|
||||||
|
float dx = playerPos.x - pe.x();
|
||||||
|
float dz = playerPos.z - pe.z();
|
||||||
|
float r = pe.activationRadius();
|
||||||
|
boolean inRange = (dx * dx + dz * dz) <= (r * r);
|
||||||
|
|
||||||
|
if (inRange && !active.get(i)) {
|
||||||
|
rootNode.attachChild(effects.get(i));
|
||||||
|
effects.get(i).setParticlesPerSec(pe.emitRate());
|
||||||
|
active.set(i, true);
|
||||||
|
} else if (!inRange && active.get(i)) {
|
||||||
|
effects.get(i).setParticlesPerSec(0);
|
||||||
|
rootNode.detachChild(effects.get(i));
|
||||||
|
active.set(i, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ParticleEmitter buildEffect(PlacedEmitter pe) {
|
||||||
|
try {
|
||||||
|
ParticleEmitter effect = new ParticleEmitter(
|
||||||
|
"emitter_" + pe.x() + "_" + pe.z(),
|
||||||
|
ParticleMesh.Type.Triangle, pe.maxParticles());
|
||||||
|
Material mat = new Material(assets, "Common/MatDefs/Misc/Particle.j3md");
|
||||||
|
mat.setTexture("Texture", assets.loadTexture(pe.texturePath()));
|
||||||
|
effect.setMaterial(mat);
|
||||||
|
effect.setImagesX(pe.imagesX());
|
||||||
|
effect.setImagesY(pe.imagesY());
|
||||||
|
effect.setStartColor(new ColorRGBA(pe.startR(), pe.startG(), pe.startB(), pe.startA()));
|
||||||
|
effect.setEndColor( new ColorRGBA(pe.endR(), pe.endG(), pe.endB(), pe.endA()));
|
||||||
|
effect.setStartSize(pe.startSize());
|
||||||
|
effect.setEndSize(pe.endSize());
|
||||||
|
effect.getParticleInfluencer()
|
||||||
|
.setInitialVelocity(new Vector3f(pe.velX(), pe.velY(), pe.velZ()));
|
||||||
|
effect.getParticleInfluencer().setVelocityVariation(pe.velocityVariation());
|
||||||
|
effect.setGravity(pe.gravX(), pe.gravY(), pe.gravZ());
|
||||||
|
effect.setLowLife(pe.lowLife());
|
||||||
|
effect.setHighLife(pe.highLife());
|
||||||
|
effect.setParticlesPerSec(0); // inaktiv bis Spieler in Reichweite
|
||||||
|
effect.setLocalTranslation(pe.x(), pe.y(), pe.z());
|
||||||
|
return effect;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[EmitterSystem] Emitter konnte nicht erstellt werden: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
180
blight-game/src/main/java/de/blight/game/state/MusicSystem.java
Normal file
180
blight-game/src/main/java/de/blight/game/state/MusicSystem.java
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
package de.blight.game.state;
|
||||||
|
|
||||||
|
import com.jme3.app.Application;
|
||||||
|
import com.jme3.app.SimpleApplication;
|
||||||
|
import com.jme3.app.state.BaseAppState;
|
||||||
|
import com.jme3.asset.AssetManager;
|
||||||
|
import com.jme3.audio.AudioData;
|
||||||
|
import com.jme3.audio.AudioNode;
|
||||||
|
import com.jme3.math.Vector3f;
|
||||||
|
import com.jme3.scene.Node;
|
||||||
|
import de.blight.common.MusicAreaIO;
|
||||||
|
import de.blight.common.PlacedMusicArea;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proximity-based ambient music per polygon area.
|
||||||
|
* Three parallel tracks (day / night / combat) play when the player is inside.
|
||||||
|
* All three are attached + played on entry and detached on exit.
|
||||||
|
* Which one is actually audible is controlled via volume (wiring to day/night deferred).
|
||||||
|
*/
|
||||||
|
public class MusicSystem extends BaseAppState {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(MusicSystem.class);
|
||||||
|
private static final float CHECK_INTERVAL = 0.25f;
|
||||||
|
private static final float FADE_DURATION = 3f;
|
||||||
|
private static final float MUSIC_VOLUME = 0.7f;
|
||||||
|
|
||||||
|
private enum FadeState { INACTIVE, FADING_IN, ACTIVE, FADING_OUT }
|
||||||
|
|
||||||
|
private SimpleApplication app;
|
||||||
|
private AssetManager assets;
|
||||||
|
private Node rootNode;
|
||||||
|
|
||||||
|
private final List<PlacedMusicArea> data = new ArrayList<>();
|
||||||
|
// three nodes per area: [0]=day, [1]=night, [2]=combat; element may be null
|
||||||
|
private final List<AudioNode[]> tracks = new ArrayList<>();
|
||||||
|
private final List<FadeState> fadeStates = new ArrayList<>();
|
||||||
|
|
||||||
|
private Vector3f playerPos = new Vector3f();
|
||||||
|
private float checkTimer = 0f;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize(Application application) {
|
||||||
|
app = (SimpleApplication) application;
|
||||||
|
assets = app.getAssetManager();
|
||||||
|
rootNode = app.getRootNode();
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (PlacedMusicArea area : MusicAreaIO.load()) {
|
||||||
|
AudioNode[] arr = {
|
||||||
|
loadAmbient(area.dayTrack()),
|
||||||
|
loadAmbient(area.nightTrack()),
|
||||||
|
loadAmbient(area.combatTrack())
|
||||||
|
};
|
||||||
|
boolean hasAny = false;
|
||||||
|
for (AudioNode n : arr) if (n != null) { hasAny = true; break; }
|
||||||
|
if (!hasAny) continue;
|
||||||
|
data.add(area);
|
||||||
|
tracks.add(arr);
|
||||||
|
fadeStates.add(FadeState.INACTIVE);
|
||||||
|
}
|
||||||
|
if (!data.isEmpty()) log.info("[MusicSystem] {} Musik-Bereiche geladen.", data.size());
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("[MusicSystem] Musik-Bereich-Datei nicht ladbar: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void cleanup(Application application) {
|
||||||
|
for (int i = 0; i < tracks.size(); i++) {
|
||||||
|
if (fadeStates.get(i) != FadeState.INACTIVE) {
|
||||||
|
for (AudioNode n : tracks.get(i)) {
|
||||||
|
if (n != null) { n.stop(); rootNode.detachChild(n); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.clear();
|
||||||
|
tracks.clear();
|
||||||
|
fadeStates.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void onEnable() {}
|
||||||
|
@Override protected void onDisable() {}
|
||||||
|
|
||||||
|
public void setPlayerPosition(Vector3f pos) {
|
||||||
|
playerPos.set(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(float tpf) {
|
||||||
|
if (data.isEmpty()) return;
|
||||||
|
|
||||||
|
for (int i = 0; i < tracks.size(); i++) {
|
||||||
|
FadeState fs = fadeStates.get(i);
|
||||||
|
if (fs == FadeState.INACTIVE || fs == FadeState.ACTIVE) continue;
|
||||||
|
|
||||||
|
AudioNode[] arr = tracks.get(i);
|
||||||
|
if (fs == FadeState.FADING_IN) {
|
||||||
|
boolean done = true;
|
||||||
|
for (AudioNode n : arr) {
|
||||||
|
if (n == null) continue;
|
||||||
|
float nv = Math.min(n.getVolume() + MUSIC_VOLUME * tpf / FADE_DURATION, MUSIC_VOLUME);
|
||||||
|
n.setVolume(nv);
|
||||||
|
if (nv < MUSIC_VOLUME) done = false;
|
||||||
|
}
|
||||||
|
if (done) fadeStates.set(i, FadeState.ACTIVE);
|
||||||
|
} else { // FADING_OUT
|
||||||
|
boolean done = true;
|
||||||
|
for (AudioNode n : arr) {
|
||||||
|
if (n == null) continue;
|
||||||
|
float nv = Math.max(n.getVolume() - MUSIC_VOLUME * tpf / FADE_DURATION, 0f);
|
||||||
|
n.setVolume(nv);
|
||||||
|
if (nv > 0f) done = false;
|
||||||
|
}
|
||||||
|
if (done) {
|
||||||
|
for (AudioNode n : arr) {
|
||||||
|
if (n != null) { n.stop(); rootNode.detachChild(n); }
|
||||||
|
}
|
||||||
|
fadeStates.set(i, FadeState.INACTIVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkTimer += tpf;
|
||||||
|
if (checkTimer < CHECK_INTERVAL) return;
|
||||||
|
checkTimer = 0f;
|
||||||
|
|
||||||
|
for (int i = 0; i < data.size(); i++) {
|
||||||
|
PlacedMusicArea area = data.get(i);
|
||||||
|
boolean inside = pointInPolygon(playerPos.x, playerPos.z, area.pointsX(), area.pointsZ());
|
||||||
|
FadeState fs = fadeStates.get(i);
|
||||||
|
|
||||||
|
if (inside && (fs == FadeState.INACTIVE || fs == FadeState.FADING_OUT)) {
|
||||||
|
if (fs == FadeState.INACTIVE) {
|
||||||
|
log.info("[MusicSystem] Bereich {} betreten → starte Tracks", i);
|
||||||
|
for (AudioNode n : tracks.get(i)) {
|
||||||
|
if (n != null) { n.setVolume(0f); rootNode.attachChild(n); n.play(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fadeStates.set(i, FadeState.FADING_IN);
|
||||||
|
} else if (!inside && (fs == FadeState.ACTIVE || fs == FadeState.FADING_IN)) {
|
||||||
|
log.info("[MusicSystem] Bereich {} verlassen → fade out", i);
|
||||||
|
fadeStates.set(i, FadeState.FADING_OUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AudioNode loadAmbient(String path) {
|
||||||
|
if (path == null || path.isEmpty()) return null;
|
||||||
|
try {
|
||||||
|
// Use Stream for music (files are large), Buffer for short sfx
|
||||||
|
AudioNode n = new AudioNode(assets, path, AudioData.DataType.Stream);
|
||||||
|
n.setLooping(true);
|
||||||
|
n.setPositional(false);
|
||||||
|
n.setVolume(0f);
|
||||||
|
return n;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[MusicSystem] Track nicht ladbar '{}': {}", path, e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean pointInPolygon(float px, float pz, float[] xs, float[] zs) {
|
||||||
|
int n = xs.length;
|
||||||
|
boolean inside = false;
|
||||||
|
for (int i = 0, j = n - 1; i < n; j = i++) {
|
||||||
|
float xi = xs[i], zi = zs[i];
|
||||||
|
float xj = xs[j], zj = zs[j];
|
||||||
|
if ((zi > pz) != (zj > pz) && (px < (xj - xi) * (pz - zi) / (zj - zi) + xi)) {
|
||||||
|
inside = !inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
188
blight-game/src/main/java/de/blight/game/state/WeatherState.java
Normal file
188
blight-game/src/main/java/de/blight/game/state/WeatherState.java
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package de.blight.game.state;
|
||||||
|
|
||||||
|
import com.jme3.app.Application;
|
||||||
|
import com.jme3.app.SimpleApplication;
|
||||||
|
import com.jme3.app.state.BaseAppState;
|
||||||
|
import com.jme3.math.ColorRGBA;
|
||||||
|
import com.jme3.math.FastMath;
|
||||||
|
import com.jme3.math.Vector3f;
|
||||||
|
import com.jme3.post.filters.FogFilter;
|
||||||
|
import com.jme3.water.WaterFilter;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class WeatherState extends BaseAppState {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(WeatherState.class);
|
||||||
|
|
||||||
|
public enum Weather { SUNNY, CLOUDY, OVERCAST, STORM }
|
||||||
|
|
||||||
|
// ── Per-weather targets (Reihenfolge: SUNNY, CLOUDY, OVERCAST, STORM) ─────
|
||||||
|
|
||||||
|
private static final float[] FOG_DENSITY = { 0.00f, 0.30f, 0.65f, 0.90f };
|
||||||
|
private static final float[] FOG_DISTANCE = { 600f, 350f, 140f, 50f };
|
||||||
|
private static final float[] WIND_SPEED = { 4f, 14f, 26f, 55f };
|
||||||
|
private static final float[] WAVE_SPEED = { 0.5f, 1.0f, 1.5f, 3.2f };
|
||||||
|
private static final float[] WAVE_AMP = { 0.3f, 0.5f, 0.8f, 1.8f };
|
||||||
|
private static final float[] WAVE_SCALE = { 0.008f, 0.007f, 0.006f, 0.005f};
|
||||||
|
private static final float[] WATER_TRANS = { 0.15f, 0.10f, 0.07f, 0.02f };
|
||||||
|
private static final float[] FOAM_INTENSITY= { 0.0f, 0.20f, 0.45f, 0.90f };
|
||||||
|
|
||||||
|
private static final ColorRGBA[] FOG_COLOR = {
|
||||||
|
new ColorRGBA(0.75f, 0.80f, 0.88f, 1f),
|
||||||
|
new ColorRGBA(0.62f, 0.65f, 0.70f, 1f),
|
||||||
|
new ColorRGBA(0.42f, 0.43f, 0.46f, 1f),
|
||||||
|
new ColorRGBA(0.18f, 0.19f, 0.21f, 1f),
|
||||||
|
};
|
||||||
|
private static final ColorRGBA[] CLOUD_COLOR = {
|
||||||
|
new ColorRGBA(0.95f, 0.95f, 0.95f, 0.40f),
|
||||||
|
new ColorRGBA(0.75f, 0.75f, 0.78f, 0.72f),
|
||||||
|
new ColorRGBA(0.35f, 0.35f, 0.37f, 0.92f),
|
||||||
|
new ColorRGBA(0.08f, 0.08f, 0.10f, 0.98f),
|
||||||
|
};
|
||||||
|
private static final ColorRGBA[] WATER_COLOR = {
|
||||||
|
new ColorRGBA(0.05f, 0.25f, 0.55f, 1f),
|
||||||
|
new ColorRGBA(0.04f, 0.18f, 0.42f, 1f),
|
||||||
|
new ColorRGBA(0.03f, 0.12f, 0.28f, 1f),
|
||||||
|
new ColorRGBA(0.02f, 0.06f, 0.14f, 1f),
|
||||||
|
};
|
||||||
|
private static final ColorRGBA[] DEEP_WATER_COLOR = {
|
||||||
|
new ColorRGBA(0.02f, 0.12f, 0.30f, 1f),
|
||||||
|
new ColorRGBA(0.01f, 0.08f, 0.20f, 1f),
|
||||||
|
new ColorRGBA(0.01f, 0.04f, 0.12f, 1f),
|
||||||
|
new ColorRGBA(0.00f, 0.02f, 0.06f, 1f),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── State ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Weather active = Weather.SUNNY;
|
||||||
|
private float changeTimer = 120f;
|
||||||
|
|
||||||
|
// Aktuell interpolierte Werte
|
||||||
|
private float fogDensity = 0f;
|
||||||
|
private float fogDistance = 600f;
|
||||||
|
private float windSpeed = 4f;
|
||||||
|
private float waveSpeed = 0.5f;
|
||||||
|
private float waveAmp = 0.3f;
|
||||||
|
private float waveScale = 0.008f;
|
||||||
|
private float waterTrans = 0.15f;
|
||||||
|
private float foamIntensity = 0f;
|
||||||
|
private float windAngle = 0f;
|
||||||
|
private float windAngleTgt = 0.4f;
|
||||||
|
private final ColorRGBA fogColor = new ColorRGBA(0.75f, 0.80f, 0.88f, 1f);
|
||||||
|
private final ColorRGBA cloudColor = new ColorRGBA(0.95f, 0.95f, 0.95f, 0.40f);
|
||||||
|
private final ColorRGBA waterColor = new ColorRGBA(0.05f, 0.25f, 0.55f, 1f);
|
||||||
|
private final ColorRGBA deepWaterColor= new ColorRGBA(0.02f, 0.12f, 0.30f, 1f);
|
||||||
|
|
||||||
|
// ── Externe Referenzen ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private FogFilter fogFilter;
|
||||||
|
private WaterFilter waterFilter;
|
||||||
|
private CloudsNode cloudsNode;
|
||||||
|
private Application app;
|
||||||
|
|
||||||
|
public void setFogFilter(FogFilter f) { this.fogFilter = f; }
|
||||||
|
public void setWaterFilter(WaterFilter f) { this.waterFilter = f; }
|
||||||
|
public void setCloudsNode(CloudsNode n) { this.cloudsNode = n; }
|
||||||
|
|
||||||
|
public Weather getActiveWeather() { return active; }
|
||||||
|
public float getWindSpeed() { return windSpeed; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt das Wetter sofort; Werte interpolieren sanft zum neuen Ziel.
|
||||||
|
* Der automatische Wechsel-Timer wird zurückgesetzt.
|
||||||
|
*/
|
||||||
|
public void forceWeather(Weather w) {
|
||||||
|
active = w;
|
||||||
|
changeTimer = 90f + FastMath.nextRandomFloat() * 150f;
|
||||||
|
windAngleTgt = FastMath.nextRandomFloat() * FastMath.TWO_PI;
|
||||||
|
log.info("[Weather] forceWeather → {}", w);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3f getWindDirection() {
|
||||||
|
return new Vector3f(FastMath.sin(windAngle), 0f, FastMath.cos(windAngle));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override protected void initialize(Application app) { this.app = app; }
|
||||||
|
@Override protected void cleanup(Application app) {}
|
||||||
|
@Override protected void onEnable() {}
|
||||||
|
@Override protected void onDisable() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(float tpf) {
|
||||||
|
changeTimer -= tpf;
|
||||||
|
if (changeTimer <= 0f) {
|
||||||
|
changeTimer = 90f + FastMath.nextRandomFloat() * 150f;
|
||||||
|
pickNextWeather();
|
||||||
|
}
|
||||||
|
|
||||||
|
int i = active.ordinal();
|
||||||
|
|
||||||
|
fogDensity = approach(fogDensity, FOG_DENSITY[i], tpf * 0.025f);
|
||||||
|
fogDistance = approach(fogDistance, FOG_DISTANCE[i], tpf * 0.025f);
|
||||||
|
windSpeed = approach(windSpeed, WIND_SPEED[i], tpf * 0.040f);
|
||||||
|
waveSpeed = approach(waveSpeed, WAVE_SPEED[i], tpf * 0.030f);
|
||||||
|
waveAmp = approach(waveAmp, WAVE_AMP[i], tpf * 0.025f);
|
||||||
|
waveScale = approach(waveScale, WAVE_SCALE[i], tpf * 0.020f);
|
||||||
|
waterTrans = approach(waterTrans, WATER_TRANS[i], tpf * 0.025f);
|
||||||
|
foamIntensity = approach(foamIntensity, FOAM_INTENSITY[i], tpf * 0.020f);
|
||||||
|
windAngle = approachAngle(windAngle, windAngleTgt, tpf * 0.012f);
|
||||||
|
|
||||||
|
fogColor.interpolateLocal(FOG_COLOR[i], tpf * 0.025f);
|
||||||
|
cloudColor.interpolateLocal(CLOUD_COLOR[i], tpf * 0.025f);
|
||||||
|
waterColor.interpolateLocal(WATER_COLOR[i], tpf * 0.020f);
|
||||||
|
deepWaterColor.interpolateLocal(DEEP_WATER_COLOR[i], tpf * 0.020f);
|
||||||
|
|
||||||
|
if (fogFilter != null) {
|
||||||
|
fogFilter.setFogDensity(fogDensity);
|
||||||
|
fogFilter.setFogDistance(fogDistance);
|
||||||
|
fogFilter.setFogColor(fogColor.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waterFilter != null) {
|
||||||
|
waterFilter.setSpeed(waveSpeed);
|
||||||
|
waterFilter.setMaxAmplitude(waveAmp);
|
||||||
|
waterFilter.setWaveScale(waveScale);
|
||||||
|
waterFilter.setWaterTransparency(waterTrans);
|
||||||
|
waterFilter.setFoamIntensity(foamIntensity);
|
||||||
|
waterFilter.setWaterColor(waterColor.clone());
|
||||||
|
waterFilter.setDeepWaterColor(deepWaterColor.clone());
|
||||||
|
waterFilter.setWindDirection(
|
||||||
|
new com.jme3.math.Vector2f(FastMath.sin(windAngle), FastMath.cos(windAngle)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cloudsNode != null) {
|
||||||
|
cloudsNode.setCloudColor(cloudColor.clone());
|
||||||
|
Vector3f camPos = ((SimpleApplication) app).getCamera().getLocation();
|
||||||
|
cloudsNode.update(tpf, getWindDirection(), windSpeed * 0.5f, camPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void pickNextWeather() {
|
||||||
|
float r = FastMath.nextRandomFloat();
|
||||||
|
Weather next = r < 0.40f ? Weather.SUNNY
|
||||||
|
: r < 0.70f ? Weather.CLOUDY
|
||||||
|
: r < 0.88f ? Weather.OVERCAST
|
||||||
|
: Weather.STORM;
|
||||||
|
windAngleTgt = FastMath.nextRandomFloat() * FastMath.TWO_PI;
|
||||||
|
if (next != active) {
|
||||||
|
active = next;
|
||||||
|
log.info("[Weather] transitioning to {}", active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float approach(float cur, float tgt, float alpha) {
|
||||||
|
return cur + (tgt - cur) * FastMath.clamp(alpha, 0f, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float approachAngle(float cur, float tgt, float alpha) {
|
||||||
|
float d = tgt - cur;
|
||||||
|
while (d > FastMath.PI) d -= FastMath.TWO_PI;
|
||||||
|
while (d < -FastMath.PI) d += FastMath.TWO_PI;
|
||||||
|
return cur + d * FastMath.clamp(alpha, 0f, 1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
blight-game/src/main/resources/logback.xml
Normal file
30
blight-game/src/main/resources/logback.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<configuration>
|
||||||
|
|
||||||
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{HH:mm:ss.SSS} %-5level [%logger{30}] %msg%n%ex</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>logs/blight-game.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>logs/blight-game.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
|
<maxHistory>7</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%logger{30}] %msg%n%ex</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- JME-interne JUL-Logs auf WARN reduzieren -->
|
||||||
|
<logger name="com.jme3" level="WARN"/>
|
||||||
|
<!-- GltfLoader meldet bei jeder Animation "only supports linear interpolation" – bekanntes JME-Verhalten, kein Fehler -->
|
||||||
|
<logger name="com.jme3.scene.plugins.gltf" level="ERROR"/>
|
||||||
|
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="CONSOLE"/>
|
||||||
|
<appender-ref ref="FILE"/>
|
||||||
|
</root>
|
||||||
|
|
||||||
|
</configuration>
|
||||||
11
blight-lang/build.gradle
Normal file
11
blight-lang/build.gradle
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// Sprachpakete — enthält TextResolver und die .properties-Dateien für alle Sprachen.
|
||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':blight-common')
|
||||||
|
compileOnly 'org.projectlombok:lombok:1.18.38'
|
||||||
|
annotationProcessor 'org.projectlombok:lombok:1.18.38'
|
||||||
|
implementation 'org.slf4j:slf4j-api:2.0.17'
|
||||||
|
}
|
||||||
71
blight-lang/src/main/java/de/blight/lang/TextResolver.java
Normal file
71
blight-lang/src/main/java/de/blight/lang/TextResolver.java
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package de.blight.lang;
|
||||||
|
|
||||||
|
import de.blight.common.model.TextReference;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.MissingResourceException;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löst TextReference-IDs zur Laufzeit in den lokalisierten Text auf.
|
||||||
|
* Vor dem ersten Aufruf von resolve() muss init(Locale) aufgerufen werden.
|
||||||
|
* Ohne explizites init() wird Englisch als Standard verwendet.
|
||||||
|
*/
|
||||||
|
public class TextResolver {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(TextResolver.class);
|
||||||
|
private static final String BASE_NAME = "lang/messages";
|
||||||
|
|
||||||
|
private static TextResolver instance;
|
||||||
|
|
||||||
|
private final ResourceBundle bundle;
|
||||||
|
private final ResourceBundle fallback;
|
||||||
|
|
||||||
|
private TextResolver(Locale locale) {
|
||||||
|
this.fallback = ResourceBundle.getBundle(BASE_NAME, Locale.ENGLISH);
|
||||||
|
if (locale.equals(Locale.ENGLISH)) {
|
||||||
|
this.bundle = this.fallback;
|
||||||
|
} else {
|
||||||
|
ResourceBundle loaded;
|
||||||
|
try {
|
||||||
|
loaded = ResourceBundle.getBundle(BASE_NAME, locale);
|
||||||
|
} catch (MissingResourceException e) {
|
||||||
|
LOG.warn("Kein Sprachpaket für {} gefunden, falle auf Englisch zurück.", locale);
|
||||||
|
loaded = this.fallback;
|
||||||
|
}
|
||||||
|
this.bundle = loaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void init(Locale locale) {
|
||||||
|
instance = new TextResolver(locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TextResolver get() {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new TextResolver(Locale.ENGLISH);
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolve(TextReference ref) {
|
||||||
|
if (ref == null) return "";
|
||||||
|
return resolveId(ref.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolveId(String id) {
|
||||||
|
if (id == null || id.isBlank()) return "";
|
||||||
|
try {
|
||||||
|
return bundle.getString(id);
|
||||||
|
} catch (MissingResourceException e) {
|
||||||
|
try {
|
||||||
|
return fallback.getString(id);
|
||||||
|
} catch (MissingResourceException ex) {
|
||||||
|
LOG.warn("Unbekannte Text-ID: {}", id);
|
||||||
|
return "[" + id + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user