Compare commits
4 Commits
1e0789461f
...
875c39ab27
| Author | SHA1 | Date | |
|---|---|---|---|
| 875c39ab27 | |||
| ed1bc8f0a3 | |||
| 3e7108954e | |||
| 50f496c864 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -10,3 +10,9 @@
|
||||
.classpath
|
||||
.settings/
|
||||
bin/
|
||||
|
||||
# Laufzeit-Logs
|
||||
**/logs/
|
||||
|
||||
# Lokale Downloads (nicht ins Repo)
|
||||
downloads/
|
||||
|
||||
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.
|
||||
//
|
||||
// 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/Models/Campfire.j3o
Normal file
BIN
blight-assets/src/main/resources/Models/Campfire.j3o
Normal file
Binary file not shown.
24
blight-assets/src/main/resources/Models/Campfire.mtl
Normal file
24
blight-assets/src/main/resources/Models/Campfire.mtl
Normal file
@@ -0,0 +1,24 @@
|
||||
# Blender 4.0.2 MTL File: 'Campfire.blend'
|
||||
# www.blender.org
|
||||
|
||||
newmtl Campfire_MAT
|
||||
Ka 0.500000 0.500000 0.500000
|
||||
Ks 0.222727 0.222727 0.222727
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 1.450000
|
||||
d 1.000000
|
||||
illum 3
|
||||
map_Kd Campfire_MAT_BaseColor_01.jpg
|
||||
map_Ns Campfire_MAT_Roughness.jpg
|
||||
map_refl Campfire_MAT_Metallic.jpg
|
||||
map_Bump -bm 1.000000 Campfire_MAT_Normal_JL.jpg
|
||||
|
||||
newmtl Campfire_fire_MAT
|
||||
Ns 1000.000000
|
||||
Ka 0.500000 0.500000 0.500000
|
||||
Ks 1.000000 1.000000 1.000000
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 1.450000
|
||||
illum 3
|
||||
map_Kd Campfire_fire_MAT_BaseColor_Alpha.png
|
||||
map_d Campfire_fire_MAT_BaseColor_Alpha.png
|
||||
BIN
blight-assets/src/main/resources/Models/Chars/mainchar.j3o
Normal file
BIN
blight-assets/src/main/resources/Models/Chars/mainchar.j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Models/Chars/silas.j3o
Normal file
BIN
blight-assets/src/main/resources/Models/Chars/silas.j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Models/Palm_Palme1.j3o
Normal file
BIN
blight-assets/src/main/resources/Models/Palm_Palme1.j3o
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-assets/src/main/resources/Models/custom_mesh_0.j3o
Normal file
BIN
blight-assets/src/main/resources/Models/custom_mesh_0.j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Models/custom_mesh_1.j3o
Normal file
BIN
blight-assets/src/main/resources/Models/custom_mesh_1.j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Models/custom_mesh_2.j3o
Normal file
BIN
blight-assets/src/main/resources/Models/custom_mesh_2.j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Models/custom_mesh_3.j3o
Normal file
BIN
blight-assets/src/main/resources/Models/custom_mesh_3.j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Models/custom_mesh_4.j3o
Normal file
BIN
blight-assets/src/main/resources/Models/custom_mesh_4.j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Models/custom_mesh_5.j3o
Normal file
BIN
blight-assets/src/main/resources/Models/custom_mesh_5.j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Models/custom_mesh_6.j3o
Normal file
BIN
blight-assets/src/main/resources/Models/custom_mesh_6.j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Models/custom_mesh_7.j3o
Normal file
BIN
blight-assets/src/main/resources/Models/custom_mesh_7.j3o
Normal file
Binary file not shown.
1
blight-assets/src/main/resources/Models/mainchar.animmap
Normal file
1
blight-assets/src/main/resources/Models/mainchar.animmap
Normal file
@@ -0,0 +1 @@
|
||||
{"IDLE":"stand_up"}
|
||||
BIN
blight-assets/src/main/resources/Models/tree.j3o
Normal file
BIN
blight-assets/src/main/resources/Models/tree.j3o
Normal file
Binary file not shown.
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 {
|
||||
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),
|
||||
* 8 Welteinheiten pro Zelle → Welt −2048 .. +2048.
|
||||
* Obere Schicht : 513 × 513 Vertices (= 512 × 512 Zellen), gleiche Weltausdehnung.
|
||||
* Splatmap : 513 × 513 Pixel (passt auf Spiel-Terrain 1:1).
|
||||
* Kanäle R/G/B = Gewicht für Tex2/Tex3/Tex4; Tex1 füllt den Rest.
|
||||
* Splatmap : 513 × 513 Pixel (1:1 zu beiden Terrain-Grids).
|
||||
* Kanäle R/G/B/A = Gewicht für Tex1-Helligkeit / Tex2 / Tex3 / Tex4.
|
||||
*/
|
||||
public final class MapData {
|
||||
|
||||
@@ -29,6 +29,9 @@ public final class MapData {
|
||||
/** Pixel pro Achse der Splatmap (entspricht UPPER_VERTS = Spiel-Terrain-Auflösung). */
|
||||
public static final int SPLAT_SIZE = 513;
|
||||
|
||||
/** Anzahl konfigurierbarer Textur-Slots pro Terrain-Layer. */
|
||||
public static final int TEXTURE_SLOTS = 4;
|
||||
|
||||
// ── Daten ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Y-Höhe jedes Vertex im Basis-Terrain [TERRAIN_VERTS²]. */
|
||||
@@ -40,29 +43,55 @@ public final class MapData {
|
||||
/** Y der Höhlendecke [UPPER_VERTS²]. */
|
||||
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;
|
||||
|
||||
/** Splatmap Grün-Kanal: Tex2 (Fels) mix-Faktor (Alpha.G) [SPLAT_SIZE²], Bytes 0–255. */
|
||||
/** Splatmap Grün-Kanal: Tex2-Blend (Alpha.G) [SPLAT_SIZE²]. */
|
||||
public final byte[] splatG;
|
||||
|
||||
/** Splatmap Blau-Kanal: Tex3 (Erde) mix-Faktor (Alpha.B) [SPLAT_SIZE²], Bytes 0–255. */
|
||||
/** Splatmap Blau-Kanal: Tex3-Blend (Alpha.B) [SPLAT_SIZE²]. */
|
||||
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). */
|
||||
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() {
|
||||
terrainHeight = new float[TERRAIN_VERTS * TERRAIN_VERTS];
|
||||
upperTop = 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];
|
||||
splatG = 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];
|
||||
upperHole = new byte [UPPER_CELLS * UPPER_CELLS];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ import java.util.zip.*;
|
||||
* 1 – Basis-Terrain + Obere Schicht (kein Splatmap)
|
||||
* 2 – wie 1 + Splatmap (R/G/B je 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 {
|
||||
|
||||
@@ -47,7 +50,7 @@ public final class MapIO {
|
||||
}
|
||||
|
||||
private static final int MAGIC = 0x424C4947; // "BLIG"
|
||||
private static final int VERSION = 3;
|
||||
private static final int VERSION = 6;
|
||||
|
||||
private MapIO() {}
|
||||
|
||||
@@ -84,13 +87,23 @@ public final class MapIO {
|
||||
writeFloats(out, data.terrainHeight);
|
||||
writeFloats(out, data.upperTop);
|
||||
writeFloats(out, data.upperBottom);
|
||||
out.write(data.upperHole);
|
||||
// v2: splatmap
|
||||
out.write(data.splatR);
|
||||
out.write(data.splatG);
|
||||
out.write(data.splatB);
|
||||
// v3: gras-dichte
|
||||
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.upperTop);
|
||||
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) {
|
||||
in.readFully(data.splatR);
|
||||
@@ -118,7 +134,22 @@ public final class MapIO {
|
||||
if (version >= 3) {
|
||||
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;
|
||||
}
|
||||
@@ -137,4 +168,14 @@ public final class MapIO {
|
||||
in.readFully(bytes);
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.blight.common.models;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import de.blight.common.model.XPHelper;
|
||||
|
||||
public class XPHelperTest {
|
||||
|
||||
@Test
|
||||
public void doTest() {
|
||||
assertEquals(500, XPHelper.getXpRequired(1));
|
||||
assertEquals(1400, XPHelper.getXpRequired(10));
|
||||
assertEquals(15200, XPHelper.getXpRequired(50));
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ javafx {
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass = 'de.blight.editor.EditorLauncher'
|
||||
mainClass = 'de.blight.editor.BlightEditor'
|
||||
applicationDefaultJvmArgs = [
|
||||
'--add-opens', 'java.base/java.lang=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-terrain:${jmeVersion}"
|
||||
implementation "org.jmonkeyengine:jme3-effects:${jmeVersion}"
|
||||
implementation "org.jmonkeyengine:jme3-plugins:${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) {
|
||||
@@ -65,6 +71,6 @@ run {
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes 'Main-Class': application.mainClass
|
||||
attributes 'Main-Class': application.mainClass.get()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package de.blight.editor;
|
||||
|
||||
import org.slf4j.bridge.SLF4JBridgeHandler;
|
||||
|
||||
/**
|
||||
* Separater Launcher-Einstiegspunkt, damit der JavaFX-Klassenpfad korrekt
|
||||
* aufgelöst wird, bevor Application.launch() aufgerufen wird.
|
||||
*/
|
||||
public class EditorLauncher {
|
||||
public class BlightEditor {
|
||||
public static void main(String[] args) {
|
||||
SLF4JBridgeHandler.removeHandlersForRootLogger();
|
||||
SLF4JBridgeHandler.install();
|
||||
|
||||
// ProjectRoot muss als erstes initialisiert werden, damit alle
|
||||
// relativen Pfade korrekt aufgelöst werden (auch bei IDE-Start mit
|
||||
// workingDir = blight-editor/ statt Projekt-Root).
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,142 +1,222 @@
|
||||
package de.blight.editor;
|
||||
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.asset.plugins.FileLocator;
|
||||
import com.jme3.system.AppSettings;
|
||||
import com.jme3.system.JmeContext;
|
||||
import com.jme3.texture.FrameBuffer;
|
||||
import com.jme3.texture.Image;
|
||||
import com.jme3.texture.Texture2D;
|
||||
import de.blight.editor.state.EzTreeState;
|
||||
import de.blight.editor.state.PalmGeneratorState;
|
||||
import de.blight.editor.state.SceneObjectState;
|
||||
import de.blight.editor.state.TerrainEditorState;
|
||||
import de.blight.editor.state.TreeGeneratorState;
|
||||
import javafx.scene.image.WritableImage;
|
||||
|
||||
public class JmeEditorApp extends SimpleApplication {
|
||||
|
||||
private final SharedInput input;
|
||||
private final WritableImage jfxImage;
|
||||
private final int vpWidth;
|
||||
private final int vpHeight;
|
||||
|
||||
public JmeEditorApp(SharedInput input, WritableImage jfxImage, int vpWidth, int vpHeight) {
|
||||
this.input = input;
|
||||
this.jfxImage = jfxImage;
|
||||
this.vpWidth = vpWidth;
|
||||
this.vpHeight = vpHeight;
|
||||
}
|
||||
|
||||
/** Startet JME3 in einem Daemon-Thread (blockierend bis App endet). */
|
||||
public static JmeEditorApp launch(SharedInput input, WritableImage jfxImage,
|
||||
int vpWidth, int vpHeight) {
|
||||
JmeEditorApp app = new JmeEditorApp(input, jfxImage, vpWidth, vpHeight);
|
||||
|
||||
AppSettings settings = new AppSettings(true);
|
||||
settings.setTitle("Blight Editor – JME3");
|
||||
settings.setResolution(vpWidth, vpHeight);
|
||||
settings.setRenderer(AppSettings.LWJGL_OPENGL32);
|
||||
settings.setAudioRenderer(null);
|
||||
settings.setSamples(1);
|
||||
|
||||
app.setSettings(settings);
|
||||
app.setShowSettings(false);
|
||||
app.setPauseOnLostFocus(false);
|
||||
|
||||
Thread t = new Thread(() -> app.start(JmeContext.Type.OffscreenSurface), "jme3-editor");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
return app;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void simpleInitApp() {
|
||||
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
|
||||
Texture2D colorTex = new Texture2D(vpWidth, vpHeight, Image.Format.RGBA8);
|
||||
FrameBuffer fb = new FrameBuffer(vpWidth, vpHeight, 1);
|
||||
fb.setDepthBuffer(Image.Format.Depth);
|
||||
fb.setColorTexture(colorTex);
|
||||
viewPort.setOutputFrameBuffer(fb);
|
||||
|
||||
// Frame-Export in das JavaFX-WritableImage
|
||||
viewPort.addProcessor(new FrameTransfer(jfxImage));
|
||||
|
||||
stateManager.attach(new SceneObjectState(input));
|
||||
stateManager.attach(new TerrainEditorState(input));
|
||||
stateManager.attach(new TreeGeneratorState(input));
|
||||
stateManager.attach(new EzTreeState(input));
|
||||
stateManager.attach(new PalmGeneratorState(input));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void simpleUpdate(float tpf) {
|
||||
com.jme3.math.Vector3f loc = cam.getLocation();
|
||||
com.jme3.math.Vector3f dir = cam.getDirection();
|
||||
input.camX = loc.x;
|
||||
input.camY = loc.y;
|
||||
input.camZ = loc.z;
|
||||
input.camYaw = (float) Math.toDegrees(Math.atan2(-dir.x, -dir.z));
|
||||
input.camPitch = (float) Math.toDegrees(
|
||||
Math.asin(Math.max(-1f, Math.min(1f, dir.y))));
|
||||
|
||||
String cmd = input.pendingCommand;
|
||||
if (cmd != null) {
|
||||
input.pendingCommand = null;
|
||||
processCommand(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
private void processCommand(String raw) {
|
||||
String[] parts = raw.trim().split("\\s+");
|
||||
switch (parts[0].toLowerCase()) {
|
||||
case "goto" -> {
|
||||
try {
|
||||
if (parts.length >= 4) {
|
||||
// goto x y z — direkte Koordinaten
|
||||
float x = Float.parseFloat(parts[1]);
|
||||
float y = Float.parseFloat(parts[2]);
|
||||
float z = Float.parseFloat(parts[3]);
|
||||
cam.setLocation(new com.jme3.math.Vector3f(x, y, z));
|
||||
input.consoleOutput = "Goto → " + x + " / " + y + " / " + z;
|
||||
} else if (parts.length >= 3) {
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
package de.blight.editor;
|
||||
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.asset.plugins.FileLocator;
|
||||
import com.jme3.system.AppSettings;
|
||||
import com.jme3.system.JmeContext;
|
||||
import com.jme3.texture.FrameBuffer;
|
||||
import com.jme3.texture.Image;
|
||||
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.LightState;
|
||||
import de.blight.editor.state.PalmGeneratorState;
|
||||
import de.blight.editor.state.SceneObjectState;
|
||||
import de.blight.editor.state.TerrainEditorState;
|
||||
import de.blight.editor.state.TreeGeneratorState;
|
||||
import de.blight.game.console.JmeConsole;
|
||||
import de.blight.game.state.DayNightState;
|
||||
import javafx.scene.image.WritableImage;
|
||||
|
||||
public class JmeEditorApp extends SimpleApplication {
|
||||
|
||||
private final SharedInput input;
|
||||
private final WritableImage initialImage;
|
||||
private final int vpWidth;
|
||||
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) {
|
||||
this.input = input;
|
||||
this.initialImage = jfxImage;
|
||||
this.vpWidth = vpWidth;
|
||||
this.vpHeight = vpHeight;
|
||||
}
|
||||
|
||||
/** Startet JME3 in einem Daemon-Thread (blockierend bis App endet). */
|
||||
public static JmeEditorApp launch(SharedInput input, WritableImage jfxImage,
|
||||
int vpWidth, int vpHeight) {
|
||||
JmeEditorApp app = new JmeEditorApp(input, jfxImage, vpWidth, vpHeight);
|
||||
|
||||
AppSettings settings = new AppSettings(true);
|
||||
settings.setTitle("Blight Editor – JME3");
|
||||
settings.setResolution(vpWidth, vpHeight);
|
||||
settings.setRenderer(AppSettings.LWJGL_OPENGL32);
|
||||
settings.setAudioRenderer(null);
|
||||
settings.setSamples(1);
|
||||
|
||||
app.setSettings(settings);
|
||||
app.setShowSettings(false);
|
||||
app.setPauseOnLostFocus(false);
|
||||
|
||||
Thread t = new Thread(() -> app.start(JmeContext.Type.OffscreenSurface), "jme3-editor");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
return app;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void simpleInitApp() {
|
||||
flyCam.setEnabled(false);
|
||||
|
||||
// Explizit registrieren, falls General.cfg die Klassen beim ersten Start
|
||||
// noch nicht gefunden hat (jme3-plugins war zuvor nicht auf dem Classpath).
|
||||
assetManager.registerLoader(com.jme3.scene.plugins.gltf.GltfLoader.class, "gltf");
|
||||
assetManager.registerLoader(com.jme3.scene.plugins.gltf.GlbLoader.class, "glb");
|
||||
|
||||
java.nio.file.Path blightAssets = ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||
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 TerrainEditorState(input));
|
||||
stateManager.attach(new TreeGeneratorState(input));
|
||||
stateManager.attach(new EzTreeState(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
|
||||
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 dir = cam.getDirection();
|
||||
input.camX = loc.x;
|
||||
input.camY = loc.y;
|
||||
input.camZ = loc.z;
|
||||
input.camYaw = (float) Math.toDegrees(Math.atan2(-dir.x, -dir.z));
|
||||
input.camPitch = (float) Math.toDegrees(
|
||||
Math.asin(Math.max(-1f, Math.min(1f, dir.y))));
|
||||
|
||||
if (jmeConsole == null) return;
|
||||
|
||||
// Toggle-Signal von JavaFX
|
||||
if (input.consoleToggle) {
|
||||
input.consoleToggle = false;
|
||||
jmeConsole.toggle();
|
||||
}
|
||||
|
||||
if (!jmeConsole.isOpen()) return;
|
||||
|
||||
// Zeichen-Eingabe
|
||||
Character c;
|
||||
while ((c = input.consoleChars.poll()) != null) jmeConsole.feedChar(c);
|
||||
|
||||
// Sondertasten
|
||||
Integer key;
|
||||
while ((key = input.consoleKeys.poll()) != null) {
|
||||
switch (key) {
|
||||
case 8 -> jmeConsole.feedBackspace();
|
||||
case 10 -> jmeConsole.feedEnter();
|
||||
case 27 -> jmeConsole.feedEscape();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import javafx.scene.image.WritableImage;
|
||||
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/** Thread-safe Brücke: JavaFX-Events → JME3-Update-Schleife. */
|
||||
public class SharedInput {
|
||||
@@ -19,18 +20,27 @@ public class SharedInput {
|
||||
// ── Aktive Tools ─────────────────────────────────────────────────────────
|
||||
public final HeightTool heightTool = new HeightTool();
|
||||
public final UpperHeightTool upperHeightTool = new UpperHeightTool();
|
||||
public final HoleTool holeTool = new HoleTool();
|
||||
public final GrassTool grassTool = new GrassTool();
|
||||
public final TextureTool textureTool = new TextureTool();
|
||||
public final HoleTool holeTool = new HoleTool();
|
||||
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;
|
||||
|
||||
// ── Upper-Layer-Sichtbarkeit ─────────────────────────────────────────────
|
||||
public volatile boolean upperLayerVisible = true;
|
||||
|
||||
// ── Kamerabewegung (WASD + QE) ──────────────────────────────────────────
|
||||
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) ───────────────────────
|
||||
private final AtomicInteger mouseDxAccum = 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 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 ────────────────────────────────────────────────────────────
|
||||
/** action +1 = Dichte erhöhen (Linksklick), -1 = Dichte verringern (Rechtsklick). */
|
||||
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 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) ─────────────────────
|
||||
public volatile double viewportScaleX = 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) ─────────────
|
||||
public volatile float mouseScreenX = -1f;
|
||||
public volatile float mouseScreenY = -1f;
|
||||
@@ -82,7 +113,8 @@ public class SharedInput {
|
||||
public volatile int treePreviewW = 1024;
|
||||
public volatile int treePreviewH = 1024;
|
||||
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;
|
||||
* treePreviewResized signalisiert JavaFX, die ImageView zu aktualisieren.
|
||||
@@ -95,7 +127,7 @@ public class SharedInput {
|
||||
public final ConcurrentLinkedQueue<TreeGenRequest> treeGenQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
// ── 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<>();
|
||||
|
||||
// ── Palmen-Generator ──────────────────────────────────────────────────────
|
||||
@@ -107,9 +139,27 @@ public class SharedInput {
|
||||
public static final int LAYER_OBJECTS = 5;
|
||||
/** activeLayer==6 → Objekte bearbeiten (Selektion + Gizmo) */
|
||||
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. */
|
||||
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<>();
|
||||
|
||||
/**
|
||||
@@ -120,17 +170,51 @@ public class SharedInput {
|
||||
public final ConcurrentLinkedQueue<ObjectDrag> objectDragQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
/** 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;
|
||||
/** Wird von JME3 gesetzt, wenn ein Objekt gerade neu platziert wurde (nicht nur selektiert). */
|
||||
public volatile boolean objectJustPlaced = false;
|
||||
|
||||
/** JavaFX → JME3: Modell-Pfad für nächste Platzierung (relativ zu editor-assets/). */
|
||||
/** JavaFX → JME3: Modell-Pfad für nächste Platzierung (relativ zu blight-assets/src/main/resources/). */
|
||||
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 blight-assets/src/main/resources/, "" = 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. */
|
||||
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 ───────────────────────────────────────────────────────
|
||||
/**
|
||||
* Form: "Box" | "Kugel" | "Zylinder" | "Ebene"
|
||||
@@ -138,7 +222,7 @@ public class SharedInput {
|
||||
* sizeY: Höhe (Box/Zylinder)
|
||||
* sizeZ: Tiefe (Box/Ebene)
|
||||
* matType: "Unshaded" | "Phong"
|
||||
* texturePath: relativ zu editor-assets/ oder null
|
||||
* texturePath: relativ zu blight-assets/src/main/resources/ oder null
|
||||
*/
|
||||
public record MeshCreateRequest(
|
||||
String form,
|
||||
@@ -159,21 +243,225 @@ public class SharedInput {
|
||||
/** Pitch in Grad: positiv = Blick nach oben, negativ = nach unten. */
|
||||
public volatile float camPitch = 0f;
|
||||
|
||||
// ── Konsole (JavaFX → JME3 und zurück) ──────────────────────────────────
|
||||
/** Befehl, der beim nächsten JME3-Update ausgeführt werden soll. */
|
||||
public volatile String pendingCommand = null;
|
||||
/** Antworttext, den JME3 nach der Befehlsausführung setzt. */
|
||||
// ── JME-Konsole: JavaFX → JME-Thread ────────────────────────────────────
|
||||
/** Toggle-Signal: JavaFX setzt true, JME liest und setzt zurück. */
|
||||
public volatile boolean consoleToggle = false;
|
||||
/** 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;
|
||||
|
||||
// ── 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 ──────────────────────────────────────────────────
|
||||
/**
|
||||
* Konvertiert ein natives Modell (OBJ/GLTF/…) zu .j3o.
|
||||
* assetPath : Pfad relativ zu editor-assets/ (z. B. "models/tree.obj")
|
||||
* destJ3o : absoluter Ziel-Pfad der .j3o-Datei
|
||||
* srcToDelete: absoluter Pfad der Original-Datei (wird nach Konvertierung gelöscht)
|
||||
* assetPath : Pfad relativ zu blight-assets/src/main/resources/
|
||||
* destJ3o : absoluter Ziel-Pfad der .j3o-Datei
|
||||
* 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,
|
||||
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 =
|
||||
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 worldZMut;
|
||||
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;
|
||||
public boolean solid; // Charakter-Kollision
|
||||
public String modelPath; // relativ zu editor-assets/
|
||||
public String modelPath; // relativ zu blight-assets/src/main/resources/
|
||||
public String texturePath = "";
|
||||
public String normalMapPath = "";
|
||||
public String materialPath = "";
|
||||
|
||||
public SceneObject(String modelPath, float worldX, float worldZ, float groundY,
|
||||
boolean solid) {
|
||||
@@ -19,6 +24,8 @@ public class SceneObject extends PlacedObject {
|
||||
this.worldXMut = worldX;
|
||||
this.worldZMut = worldZ;
|
||||
this.rotY = 0f;
|
||||
this.rotX = 0f;
|
||||
this.rotZ = 0f;
|
||||
this.scale = 1f;
|
||||
this.solid = solid;
|
||||
this.modelPath = modelPath;
|
||||
@@ -30,7 +37,14 @@ public class SceneObject extends PlacedObject {
|
||||
@Override public float getWorldZ() { return worldZMut; }
|
||||
|
||||
public float getRotY() { return rotY; }
|
||||
public float getRotX() { return rotX; }
|
||||
public float getRotZ() { return rotZ; }
|
||||
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) {
|
||||
worldXMut += dx;
|
||||
@@ -38,6 +52,20 @@ public class SceneObject extends PlacedObject {
|
||||
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 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;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
@@ -19,8 +21,10 @@ import com.jme3.renderer.RenderManager;
|
||||
import com.jme3.renderer.ViewPort;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Mesh;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.Spatial;
|
||||
import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.texture.FrameBuffer;
|
||||
import com.jme3.texture.Image;
|
||||
import com.jme3.texture.Texture;
|
||||
@@ -29,37 +33,44 @@ import com.jme3.util.BufferUtils;
|
||||
import de.blight.editor.SharedInput;
|
||||
import de.blight.eztree.Tree;
|
||||
import de.blight.eztree.TreeOptions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* JME3-AppState für den EZ-Tree-Generator.
|
||||
*
|
||||
* Teilt den Vorschau-Viewport mit {@link TreeGeneratorState} (kein eigenes Framebuffer).
|
||||
* Verarbeitet {@link SharedInput.EzTreeGenRequest}-Einträge aus der Queue,
|
||||
* baut einen {@link Tree}-Node, weist Materialien zu und zeigt ihn in der Vorschau.
|
||||
* Optional: .j3o-Export mit Impostor-PNG.
|
||||
* Versucht zuerst, Geometrie über das npm-Paket @dgreenheck/ez-tree via Node.js
|
||||
* zu generieren (höhere Qualität). Fällt auf den Java-Port zurück wenn Node.js
|
||||
* nicht verfügbar ist oder fehlschlägt.
|
||||
*/
|
||||
public class EzTreeState extends BaseAppState {
|
||||
|
||||
private static final int IMPOSTOR_SIZE = 512;
|
||||
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("editor-assets");
|
||||
private static final Logger log = LoggerFactory.getLogger(EzTreeState.class);
|
||||
|
||||
private static final int IMPOSTOR_SIZE = 512;
|
||||
private static final Path 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 SimpleApplication app;
|
||||
private AssetManager assets;
|
||||
private TreeGeneratorState previewHost;
|
||||
|
||||
// ── Laufende Capture-Operation ────────────────────────────────────────────
|
||||
// ── Capture-Phase ────────────────────────────────────────────────────────
|
||||
private SharedInput.EzTreeGenRequest pendingRequest = null;
|
||||
private Node pendingTreeNode = null;
|
||||
private ViewPort captureVP = null;
|
||||
@@ -74,7 +85,6 @@ public class EzTreeState extends BaseAppState {
|
||||
protected void initialize(Application app) {
|
||||
this.app = (SimpleApplication) app;
|
||||
this.assets = app.getAssetManager();
|
||||
// previewHost via lazy-init in update() – TreeGeneratorState evtl. noch nicht attached
|
||||
}
|
||||
|
||||
@Override protected void cleanup(Application app) { cleanupCapture(); }
|
||||
@@ -85,7 +95,6 @@ public class EzTreeState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
// Lazy-init: TreeGeneratorState muss initialisiert sein, bevor wir darauf zugreifen
|
||||
if (previewHost == null) {
|
||||
previewHost = getStateManager().getState(TreeGeneratorState.class);
|
||||
if (previewHost == null) return;
|
||||
@@ -104,12 +113,14 @@ public class EzTreeState extends BaseAppState {
|
||||
private void startGeneration(SharedInput.EzTreeGenRequest req) {
|
||||
cleanupCapture();
|
||||
|
||||
Tree tree = new Tree(req.options());
|
||||
tree.generate();
|
||||
applyMaterials(tree, req.options());
|
||||
tree.updateGeometricState();
|
||||
Node treeNode = tryNodeJsGeneration(req);
|
||||
if (treeNode == null) {
|
||||
treeNode = javaFallback(req);
|
||||
}
|
||||
final Node finalNode = treeNode;
|
||||
finalNode.updateGeometricState();
|
||||
|
||||
BoundingBox bb = boundsOf(tree);
|
||||
BoundingBox bb = boundsOf(finalNode);
|
||||
float camDist = bb != null
|
||||
? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3.2f
|
||||
: 20f;
|
||||
@@ -117,14 +128,12 @@ public class EzTreeState extends BaseAppState {
|
||||
? new Vector3f(0f, bb.getCenter().y, 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 Vector3f tgt = target;
|
||||
app.enqueue(() -> {
|
||||
previewHost.setPreviewContent(tree, dist, tgt);
|
||||
previewHost.setPreviewContent(finalNode, dist, tgt);
|
||||
if (req.exportAfter()) {
|
||||
setupCapture(tree, boundsOf(tree), req);
|
||||
setupCapture(finalNode, boundsOf(finalNode), req);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -135,9 +144,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 ─────────────────────────────────────────────
|
||||
|
||||
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);
|
||||
|
||||
Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);
|
||||
@@ -145,22 +341,25 @@ public class EzTreeState extends BaseAppState {
|
||||
captureFB.addColorTexture(capTex);
|
||||
captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth));
|
||||
|
||||
captureVP = buildCaptureViewPort(tree, safeBb, captureFB);
|
||||
captureVP = buildCaptureViewPort(treeNode, safeBb, captureFB);
|
||||
captureReady = false;
|
||||
pendingRequest = req;
|
||||
pendingTreeNode = tree;
|
||||
pendingTreeNode = treeNode;
|
||||
input.treeGenStatusMsg = "EZ-Tree: rendere Impostor…";
|
||||
}
|
||||
|
||||
private void finishCapture() {
|
||||
ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4);
|
||||
app.getRenderer().readFrameBuffer(captureFB, pixels);
|
||||
|
||||
SharedInput.EzTreeGenRequest req = pendingRequest;
|
||||
Node treeNode = pendingTreeNode;
|
||||
cleanupCapture();
|
||||
|
||||
String exportName = pendingRequest.exportName() + "_"
|
||||
String exportName = req.exportName() + "_"
|
||||
+ DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
|
||||
saveImpostor(pixels, "ez_impostor_" + exportName);
|
||||
exportTree(pendingTreeNode, exportName);
|
||||
exportTree(treeNode, req.exportName(), req.treeCategory());
|
||||
|
||||
pendingRequest = null;
|
||||
pendingTreeNode = null;
|
||||
@@ -183,7 +382,6 @@ public class EzTreeState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
} else if (child instanceof Node trellis) {
|
||||
// Trellis-Node: Rinden-Material auf alle Geometrien
|
||||
Material mat = buildBarkMat(opts);
|
||||
for (Spatial s : trellis.getChildren()) {
|
||||
if (s instanceof Geometry g) g.setMaterial(mat.clone());
|
||||
@@ -238,7 +436,7 @@ public class EzTreeState extends BaseAppState {
|
||||
|
||||
// ── 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);
|
||||
float extent = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
|
||||
float dist = extent * 3f;
|
||||
@@ -279,7 +477,7 @@ public class EzTreeState extends BaseAppState {
|
||||
return vp;
|
||||
}
|
||||
|
||||
private static Node cloneForCapture(Tree src) {
|
||||
private static Node cloneForCapture(Node src) {
|
||||
Node copy = new Node("ezCap");
|
||||
copy.setLocalTranslation(src.getLocalTranslation());
|
||||
for (Spatial child : src.getChildren()) {
|
||||
@@ -304,8 +502,9 @@ public class EzTreeState extends BaseAppState {
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
private static BoundingBox boundsOf(Tree tree) {
|
||||
if (tree.getWorldBound() instanceof BoundingBox bb) return bb;
|
||||
private static BoundingBox boundsOf(Node node) {
|
||||
node.updateModelBound();
|
||||
if (node.getWorldBound() instanceof BoundingBox bb) return bb;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -333,23 +532,28 @@ public class EzTreeState extends BaseAppState {
|
||||
img.setRGB(x, IMPOSTOR_SIZE - 1 - y, (a<<24)|(r<<16)|(g<<8)|b);
|
||||
}
|
||||
}
|
||||
Path texDir = ASSET_ROOT.resolve("textures");
|
||||
Path texDir = ASSET_ROOT.resolve("Textures").resolve("impostor");
|
||||
Files.createDirectories(texDir);
|
||||
ImageIO.write(img, "PNG", texDir.resolve(name + ".png").toFile());
|
||||
} 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 {
|
||||
Path modelDir = ASSET_ROOT.resolve("models");
|
||||
Files.createDirectories(modelDir);
|
||||
File out = modelDir.resolve("EzTree_" + name + ".j3o").toFile();
|
||||
Path baseDir = (treeCategory != null && !treeCategory.isBlank())
|
||||
? ASSET_ROOT.resolve("trees").resolve(treeCategory)
|
||||
: ASSET_ROOT.resolve("Models");
|
||||
Files.createDirectories(baseDir);
|
||||
File out = baseDir.resolve(name + ".j3o").toFile();
|
||||
BinaryExporter.getInstance().save(treeNode, out);
|
||||
input.treeGenStatusMsg = "EZ-Tree exportiert: " + out.getName();
|
||||
input.refreshAssets = true;
|
||||
log.info("[EZ-Tree] Gespeichert: {}", out.getAbsolutePath());
|
||||
input.treeGenStatusMsg = "EZ-Tree exportiert: " + out.getName();
|
||||
input.refreshAssets = true;
|
||||
input.refreshTreeFolders = true;
|
||||
} catch (IOException e) {
|
||||
log.error("[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.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.bounding.BoundingBox;
|
||||
import com.jme3.export.binary.BinaryExporter;
|
||||
@@ -29,7 +31,9 @@ import java.time.format.DateTimeFormatter;
|
||||
|
||||
public class PalmGeneratorState extends BaseAppState {
|
||||
|
||||
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("editor-assets");
|
||||
private static final Logger log = LoggerFactory.getLogger(PalmGeneratorState.class);
|
||||
|
||||
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
@@ -188,15 +192,17 @@ public class PalmGeneratorState extends BaseAppState {
|
||||
|
||||
private void exportPalm(Node palmNode, String name) {
|
||||
try {
|
||||
Path modelDir = ASSET_ROOT.resolve("models");
|
||||
Path modelDir = ASSET_ROOT.resolve("trees").resolve("palm");
|
||||
Files.createDirectories(modelDir);
|
||||
String stampedName = name + "_"
|
||||
+ DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
|
||||
File out = modelDir.resolve("Palm_" + stampedName + ".j3o").toFile();
|
||||
BinaryExporter.getInstance().save(palmNode, out);
|
||||
log.info("[Palme] Gespeichert: {}", out.getAbsolutePath());
|
||||
input.treeGenStatusMsg = "Palme exportiert: " + out.getName();
|
||||
input.refreshAssets = true;
|
||||
} catch (IOException e) {
|
||||
log.error("[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.tree.TreeMeshBuilder;
|
||||
import de.blight.editor.tree.TreeParams;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* JME3-Zustand für den prozeduralen Baum-Generator.
|
||||
@@ -64,9 +66,11 @@ import de.blight.editor.tree.TreeParams;
|
||||
*/
|
||||
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 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("blight-assets", "src", "main", "resources");
|
||||
|
||||
private final SharedInput input;
|
||||
|
||||
@@ -397,7 +401,7 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
mat.setTexture("BarkMap", barkTex);
|
||||
mat.setBoolean("HasBarkMap", true);
|
||||
} catch (Exception tex) {
|
||||
System.err.println("[TreeGenerator] Bark-Textur nicht gefunden: " + p.barkTexture);
|
||||
log.warn("[Blight-Baum] Bark-Textur nicht gefunden: {}", p.barkTexture);
|
||||
}
|
||||
}
|
||||
return mat;
|
||||
@@ -531,14 +535,14 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
img.setRGB(x, IMPOSTOR_SIZE - 1 - y, (a<<24)|(r<<16)|(g<<8)|b);
|
||||
}
|
||||
}
|
||||
Path texDir = ASSET_ROOT.resolve("textures");
|
||||
Path texDir = ASSET_ROOT.resolve("Textures").resolve("impostor");
|
||||
Files.createDirectories(texDir);
|
||||
File pngFile = texDir.resolve(name + ".png").toFile();
|
||||
ImageIO.write(img, "PNG", pngFile);
|
||||
System.out.println("[TreeGenerator] Impostor: " + pngFile.getAbsolutePath());
|
||||
log.info("[Blight-Baum] Impostor: {}", pngFile.getAbsolutePath());
|
||||
|
||||
try {
|
||||
return (Texture2D) assets.loadTexture("textures/" + name + ".png");
|
||||
return (Texture2D) assets.loadTexture("Textures/impostor/" + name + ".png");
|
||||
} catch (Exception loadEx) {
|
||||
pixels.rewind();
|
||||
Image jmeImg = new Image(Image.Format.RGBA8, IMPOSTOR_SIZE, IMPOSTOR_SIZE,
|
||||
@@ -555,7 +559,7 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
|
||||
private void exportTree(Node treeNode, String name) {
|
||||
try {
|
||||
Path modelDir = ASSET_ROOT.resolve("models");
|
||||
Path modelDir = ASSET_ROOT.resolve("Models");
|
||||
Files.createDirectories(modelDir);
|
||||
File out = modelDir.resolve("GeneratedTree_" + name + ".j3o").toFile();
|
||||
// Strip runtime controls before export — they lack no-arg constructors
|
||||
@@ -563,12 +567,12 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
while (treeNode.getNumControls() > 0)
|
||||
treeNode.removeControl(treeNode.getControl(0));
|
||||
BinaryExporter.getInstance().save(treeNode, out);
|
||||
log.info("[Blight-Baum] Gespeichert: {}", out.getAbsolutePath());
|
||||
input.treeGenStatusMsg = "Exportiert: " + out.getName();
|
||||
input.refreshAssets = true;
|
||||
System.out.println("[TreeGenerator] Exportiert: " + out.getAbsolutePath());
|
||||
} catch (IOException e) {
|
||||
log.error("[Blight-Baum] Export-Fehler: {}", e.getMessage());
|
||||
input.treeGenStatusMsg = "Export-Fehler: " + e.getMessage();
|
||||
System.err.println("[TreeGenerator] Export-Fehler: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user