Commit vor Voxel Update für die Klippen
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -16,3 +16,9 @@ bin/
|
||||
|
||||
# Lokale Downloads (nicht ins Repo)
|
||||
downloads/
|
||||
|
||||
# Ingame-Karten-Sessions (vom Editor beim Spielstart erzeugt)
|
||||
run/
|
||||
|
||||
# Spielstände
|
||||
saves/
|
||||
|
||||
25
blight-assets/src/main/resources/MatDefs/Voxel.j3md
Normal file
25
blight-assets/src/main/resources/MatDefs/Voxel.j3md
Normal file
@@ -0,0 +1,25 @@
|
||||
MaterialDef Voxel {
|
||||
|
||||
MaterialParameters {
|
||||
Texture2D Tex0
|
||||
Texture2D Tex1
|
||||
Texture2D Tex2
|
||||
Texture2D Tex3
|
||||
Float TexScale : 4.0
|
||||
}
|
||||
|
||||
Technique {
|
||||
VertexShader GLSL150 : Shaders/Voxel.vert
|
||||
FragmentShader GLSL150 : Shaders/Voxel.frag
|
||||
|
||||
WorldParameters {
|
||||
WorldViewProjectionMatrix
|
||||
WorldMatrix
|
||||
NormalMatrix
|
||||
}
|
||||
|
||||
RenderState {
|
||||
FaceCull Off
|
||||
}
|
||||
}
|
||||
}
|
||||
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.
54
blight-assets/src/main/resources/Shaders/Voxel.frag
Normal file
54
blight-assets/src/main/resources/Shaders/Voxel.frag
Normal file
@@ -0,0 +1,54 @@
|
||||
uniform sampler2D m_Tex0;
|
||||
uniform sampler2D m_Tex1;
|
||||
uniform sampler2D m_Tex2;
|
||||
uniform sampler2D m_Tex3;
|
||||
uniform float m_TexScale;
|
||||
|
||||
in vec3 vWorldPos;
|
||||
in vec3 vNormal;
|
||||
in vec4 vMatWeights;
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
vec3 blendWeights = abs(vNormal);
|
||||
blendWeights = max(blendWeights - 0.2, 0.0);
|
||||
blendWeights /= (blendWeights.x + blendWeights.y + blendWeights.z + 0.001);
|
||||
|
||||
vec2 uvX = vWorldPos.yz / m_TexScale;
|
||||
vec2 uvY = vWorldPos.xz / m_TexScale;
|
||||
vec2 uvZ = vWorldPos.xy / m_TexScale;
|
||||
|
||||
vec4 col = vec4(0.0);
|
||||
|
||||
float w0 = vMatWeights.r;
|
||||
if (w0 > 0.001) {
|
||||
col += w0 * (texture(m_Tex0, uvX) * blendWeights.x
|
||||
+ texture(m_Tex0, uvY) * blendWeights.y
|
||||
+ texture(m_Tex0, uvZ) * blendWeights.z);
|
||||
}
|
||||
float w1 = vMatWeights.g;
|
||||
if (w1 > 0.001) {
|
||||
col += w1 * (texture(m_Tex1, uvX) * blendWeights.x
|
||||
+ texture(m_Tex1, uvY) * blendWeights.y
|
||||
+ texture(m_Tex1, uvZ) * blendWeights.z);
|
||||
}
|
||||
float w2 = vMatWeights.b;
|
||||
if (w2 > 0.001) {
|
||||
col += w2 * (texture(m_Tex2, uvX) * blendWeights.x
|
||||
+ texture(m_Tex2, uvY) * blendWeights.y
|
||||
+ texture(m_Tex2, uvZ) * blendWeights.z);
|
||||
}
|
||||
float w3 = vMatWeights.a;
|
||||
if (w3 > 0.001) {
|
||||
col += w3 * (texture(m_Tex3, uvX) * blendWeights.x
|
||||
+ texture(m_Tex3, uvY) * blendWeights.y
|
||||
+ texture(m_Tex3, uvZ) * blendWeights.z);
|
||||
}
|
||||
|
||||
vec3 lightDir = normalize(vec3(0.5, 1.0, 0.3));
|
||||
float diff = max(dot(vNormal, lightDir), 0.0) * 0.7 + 0.3;
|
||||
|
||||
outColor = vec4(col.rgb * diff, col.a);
|
||||
if (outColor.a < 0.1) discard;
|
||||
}
|
||||
19
blight-assets/src/main/resources/Shaders/Voxel.vert
Normal file
19
blight-assets/src/main/resources/Shaders/Voxel.vert
Normal file
@@ -0,0 +1,19 @@
|
||||
uniform mat4 g_WorldViewProjectionMatrix;
|
||||
uniform mat4 g_WorldMatrix;
|
||||
uniform mat3 g_NormalMatrix;
|
||||
|
||||
in vec3 inPosition;
|
||||
in vec3 inNormal;
|
||||
in vec4 inColor; // material blend weights (r=tex0, g=tex1, b=tex2, a=tex3)
|
||||
|
||||
out vec3 vWorldPos;
|
||||
out vec3 vNormal;
|
||||
out vec4 vMatWeights;
|
||||
|
||||
void main() {
|
||||
vec4 worldPos = g_WorldMatrix * vec4(inPosition, 1.0);
|
||||
vWorldPos = worldPos.xyz;
|
||||
vNormal = normalize(g_NormalMatrix * inNormal);
|
||||
vMatWeights = inColor;
|
||||
gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0);
|
||||
}
|
||||
@@ -7,7 +7,8 @@
|
||||
"sprint",
|
||||
"stand_up",
|
||||
"tpose",
|
||||
"walking"
|
||||
"walking",
|
||||
"pickup"
|
||||
],
|
||||
"actionMap": {
|
||||
"DEFAULT": "tpose",
|
||||
@@ -16,6 +17,7 @@
|
||||
"RUN": "running",
|
||||
"SPRINT": "sprint",
|
||||
"JUMP": "idle_jump",
|
||||
"RUNNING_JUMP": "running_jump"
|
||||
"RUNNING_JUMP": "running_jump",
|
||||
"PICK_UP": "pickup"
|
||||
}
|
||||
}
|
||||
BIN
blight-assets/src/main/resources/audio/static/pickup.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/static/pickup.ogg
Normal file
Binary file not shown.
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"itemId": "blutagave",
|
||||
"category": "MISC",
|
||||
"category": "CRAFTING_ITEMS",
|
||||
"subCategory": "PLANT",
|
||||
"name": {
|
||||
"id": "blutagave.name"
|
||||
},
|
||||
@@ -10,5 +11,12 @@
|
||||
"worthGold": 25,
|
||||
"modelRef": {
|
||||
"path": "Models/plants/usable/blutagave.j3o"
|
||||
},
|
||||
"consumable": true,
|
||||
"effects": [
|
||||
{
|
||||
"stat": "CURRENT_HP",
|
||||
"value": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"itemId": "erzmoss",
|
||||
"category": "MISC",
|
||||
"category": "CRAFTING_ITEMS",
|
||||
"subCategory": "PLANT",
|
||||
"name": {
|
||||
"id": "erzmoss.name"
|
||||
},
|
||||
@@ -10,5 +11,12 @@
|
||||
"worthGold": 100,
|
||||
"modelRef": {
|
||||
"path": "Models/plants/usable/erzmoss.j3o"
|
||||
},
|
||||
"consumable": true,
|
||||
"effects": [
|
||||
{
|
||||
"stat": "MAX_STAMINA",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
16
blight-common/src/main/java/de/blight/common/BlightHome.java
Normal file
16
blight-common/src/main/java/de/blight/common/BlightHome.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package de.blight.common;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/** Basisverzeichnis für alle benutzerspezifischen Laufzeitdaten: {@code ~/.blight/}. */
|
||||
public final class BlightHome {
|
||||
|
||||
public static final Path PATH = Paths.get(System.getProperty("user.home"), ".blight");
|
||||
|
||||
private BlightHome() {}
|
||||
|
||||
public static Path resolve(String first, String... more) {
|
||||
return PATH.resolve(Paths.get(first, more));
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,9 @@ import java.util.zip.*;
|
||||
*/
|
||||
public final class MapIO {
|
||||
|
||||
/** System-Property, das vom Editor beim Spielstart auf eine temporäre Session-Kopie gesetzt wird. */
|
||||
public static final String PROP_SESSION_MAP = "blight.session.map.path";
|
||||
|
||||
private static final Path PROJECT_ROOT = findProjectRoot();
|
||||
private static final Path MAP_PATH = PROJECT_ROOT.resolve(
|
||||
Paths.get("blight-map", "src", "main", "map", "blight_map.blm"));
|
||||
@@ -66,10 +69,11 @@ public final class MapIO {
|
||||
// ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
public static boolean exists() {
|
||||
System.out.println("[MapIO] Suche Karte: " + MAP_PATH.toAbsolutePath());
|
||||
if (Files.exists(MAP_PATH)) return true;
|
||||
// Einmalige Migration vom alten Speicherort (world/blight_map.blm)
|
||||
if (Files.exists(MAP_PATH_OLD)) {
|
||||
Path p = getMapPath();
|
||||
System.out.println("[MapIO] Suche Karte: " + p.toAbsolutePath());
|
||||
if (Files.exists(p)) return true;
|
||||
// Einmalige Migration vom alten Speicherort (world/blight_map.blm) – nur im Nicht-Session-Modus
|
||||
if (System.getProperty(PROP_SESSION_MAP) == null && Files.exists(MAP_PATH_OLD)) {
|
||||
try {
|
||||
Files.createDirectories(MAP_PATH.getParent());
|
||||
Files.move(MAP_PATH_OLD, MAP_PATH);
|
||||
@@ -82,7 +86,16 @@ public final class MapIO {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Pfad zur aktiven Kartendatei zurück.
|
||||
* Im Spielmodus (gestartet vom Editor) wird {@value PROP_SESSION_MAP} bevorzugt
|
||||
* und zeigt auf eine temporäre Kopie, sodass die Editor-Karte unberührt bleibt.
|
||||
*/
|
||||
public static Path getProjectRoot() { return PROJECT_ROOT; }
|
||||
|
||||
public static Path getMapPath() {
|
||||
String session = System.getProperty(PROP_SESSION_MAP);
|
||||
if (session != null) return Paths.get(session).toAbsolutePath();
|
||||
return MAP_PATH.toAbsolutePath();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
package de.blight.common;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/** Ein Item-Pickup das auf der Spielwelt liegt (Kartendaten). */
|
||||
public record PlacedItem(String itemId, float x, float y, float z) {}
|
||||
public record PlacedItem(String uuid, String itemId, float x, float y, float z) {
|
||||
|
||||
/** Erzeugt ein neues PlacedItem mit automatisch generierter UUID. */
|
||||
public static PlacedItem create(String itemId, float x, float y, float z) {
|
||||
return new PlacedItem(UUID.randomUUID().toString(), itemId, x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package de.blight.common;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Liest und schreibt auf der Karte liegende Items (Pickups).
|
||||
* Datei: {@code blight_placed_items.bpi} neben {@code blight_map.blm}.
|
||||
* Format: {@code itemId\tx\ty\tz}
|
||||
* Format: {@code uuid\titemId\tx\ty\tz}
|
||||
*
|
||||
* Migration: altes 4-Spalten-Format (itemId\tx\ty\tz) wird automatisch
|
||||
* erkannt und deterministisch mit einer UUID versehen.
|
||||
*/
|
||||
public final class PlacedItemIO {
|
||||
|
||||
@@ -21,11 +25,11 @@ public final class PlacedItemIO {
|
||||
Path p = getPath();
|
||||
Files.createDirectories(p.getParent());
|
||||
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
||||
w.write("# itemId\tx\ty\tz");
|
||||
w.write("# uuid\titemId\tx\ty\tz");
|
||||
w.newLine();
|
||||
for (PlacedItem it : items) {
|
||||
w.write(String.format(Locale.ROOT, "%s\t%.5f\t%.5f\t%.5f%n",
|
||||
it.itemId(), it.x(), it.y(), it.z()));
|
||||
w.write(String.format(Locale.ROOT, "%s\t%s\t%.5f\t%.5f\t%.5f%n",
|
||||
it.uuid(), it.itemId(), it.x(), it.y(), it.z()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,16 +38,23 @@ public final class PlacedItemIO {
|
||||
Path p = getPath();
|
||||
if (!Files.exists(p)) return List.of();
|
||||
List<PlacedItem> list = new ArrayList<>();
|
||||
for (String line : Files.readAllLines(p)) {
|
||||
for (String line : Files.readAllLines(p, StandardCharsets.UTF_8)) {
|
||||
line = line.strip();
|
||||
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||
String[] f = line.split("\t", -1);
|
||||
if (f.length < 4) continue;
|
||||
try {
|
||||
list.add(new PlacedItem(f[0],
|
||||
Float.parseFloat(f[1]),
|
||||
Float.parseFloat(f[2]),
|
||||
Float.parseFloat(f[3])));
|
||||
if (f.length >= 5) {
|
||||
// Aktuelles Format: uuid itemId x y z
|
||||
list.add(new PlacedItem(f[0], f[1],
|
||||
Float.parseFloat(f[2]), Float.parseFloat(f[3]), Float.parseFloat(f[4])));
|
||||
} else if (f.length == 4) {
|
||||
// Altes Format: itemId x y z → UUID deterministisch aus Inhalt ableiten
|
||||
String deterministicId = UUID.nameUUIDFromBytes(
|
||||
("legacy:" + f[0] + ":" + f[1] + ":" + f[2] + ":" + f[3])
|
||||
.getBytes(StandardCharsets.UTF_8)).toString();
|
||||
list.add(new PlacedItem(deterministicId, f[0],
|
||||
Float.parseFloat(f[1]), Float.parseFloat(f[2]), Float.parseFloat(f[3])));
|
||||
}
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
return list;
|
||||
|
||||
34
blight-common/src/main/java/de/blight/common/SaveGame.java
Normal file
34
blight-common/src/main/java/de/blight/common/SaveGame.java
Normal file
@@ -0,0 +1,34 @@
|
||||
package de.blight.common;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Spielstand-Modell (Delta-basiert).
|
||||
* Speichert nur Abweichungen vom Ausgangs-Kartenzustand:
|
||||
* aufgesammelte Items, besiegte Gegner und den Charakter-Zustand.
|
||||
*/
|
||||
public class SaveGame {
|
||||
|
||||
public static final int CURRENT_VERSION = 1;
|
||||
|
||||
public int version = CURRENT_VERSION;
|
||||
public String savedAt;
|
||||
|
||||
public CharacterSave character = new CharacterSave();
|
||||
public WorldSave world = new WorldSave();
|
||||
|
||||
public static class CharacterSave {
|
||||
/** true sobald mindestens einmal eine Position gespeichert wurde. */
|
||||
public boolean positionSaved = false;
|
||||
public float x, y, z;
|
||||
/** itemId → Anzahl */
|
||||
public Map<String, Integer> inventory = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
public static class WorldSave {
|
||||
/** UUIDs der PlacedItems, die der Spieler aufgesammelt hat. */
|
||||
public Set<String> pickedUpItems = new HashSet<>();
|
||||
/** IDs besiegter Gegner (für zukünftige Implementierung). */
|
||||
public Set<String> defeatedEnemies = new HashSet<>();
|
||||
}
|
||||
}
|
||||
47
blight-common/src/main/java/de/blight/common/SaveGameIO.java
Normal file
47
blight-common/src/main/java/de/blight/common/SaveGameIO.java
Normal file
@@ -0,0 +1,47 @@
|
||||
package de.blight.common;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/** Liest und schreibt {@link SaveGame}-Instanzen als JSON. */
|
||||
public final class SaveGameIO {
|
||||
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
private SaveGameIO() {}
|
||||
|
||||
public static Path getSavePath() {
|
||||
return BlightHome.resolve("saves", "savegame.json");
|
||||
}
|
||||
|
||||
public static boolean exists() {
|
||||
return Files.exists(getSavePath());
|
||||
}
|
||||
|
||||
public static SaveGame load() throws IOException {
|
||||
SaveGame sg = GSON.fromJson(
|
||||
Files.readString(getSavePath(), StandardCharsets.UTF_8), SaveGame.class);
|
||||
// Gson setzt fehlende Collections auf null → sicher initialisieren
|
||||
if (sg.character == null) sg.character = new SaveGame.CharacterSave();
|
||||
if (sg.character.inventory == null) sg.character.inventory = new java.util.LinkedHashMap<>();
|
||||
if (sg.world == null) sg.world = new SaveGame.WorldSave();
|
||||
if (sg.world.pickedUpItems == null) sg.world.pickedUpItems = new java.util.HashSet<>();
|
||||
if (sg.world.defeatedEnemies == null) sg.world.defeatedEnemies = new java.util.HashSet<>();
|
||||
return sg;
|
||||
}
|
||||
|
||||
public static void save(SaveGame sg) throws IOException {
|
||||
sg.savedAt = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
|
||||
Path p = getSavePath();
|
||||
Files.createDirectories(p.getParent());
|
||||
Path tmp = p.resolveSibling("savegame.tmp");
|
||||
Files.writeString(tmp, GSON.toJson(sg), StandardCharsets.UTF_8);
|
||||
Files.move(tmp, p, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
164
blight-common/src/main/java/de/blight/common/VoxelChunk.java
Normal file
164
blight-common/src/main/java/de/blight/common/VoxelChunk.java
Normal file
@@ -0,0 +1,164 @@
|
||||
package de.blight.common;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.zip.*;
|
||||
|
||||
/**
|
||||
* Voxel-Daten für einen 128×128×128m Welt-Bereich (1m/Voxel, passend zu CHUNK_VERTS).
|
||||
*
|
||||
* Koordinaten-Schema:
|
||||
* cx, cz – horizontaler Chunk-Index (identisch zu TerrainChunk)
|
||||
* cy – vertikaler Layer: 0 = Welt-Y 0..128m, -1 = -128..0m, usw.
|
||||
*
|
||||
* density[]: signed byte, < 0 = Luft, > 0 = Solid, 0 = Isosurface.
|
||||
* Standard: Byte.MIN_VALUE (= -128, vollständig Luft).
|
||||
* material[]: byte 0–3, welche der 4 Voxel-Texturen.
|
||||
* Lazy-initialisiert; null → alles Material 0.
|
||||
*/
|
||||
public final class VoxelChunk {
|
||||
|
||||
/** Sampling-Punkte pro Achse (= CHUNK_VERTS = 129). */
|
||||
public static final int SIZE = 129;
|
||||
/** Zellen pro Achse (SIZE - 1 = 128). */
|
||||
public static final int CELLS = SIZE - 1;
|
||||
|
||||
public final int cx, cy, cz;
|
||||
|
||||
/** Dichte-Feld, lazy: null = alles Luft (Byte.MIN_VALUE). */
|
||||
private byte[] density;
|
||||
/** Material-IDs, lazy: null = alles 0. */
|
||||
private byte[] material;
|
||||
|
||||
public volatile boolean dirty = false;
|
||||
|
||||
private static final int MAGIC = 0x424C5643; // "BLVC"
|
||||
private static final int VERSION = 1;
|
||||
|
||||
public VoxelChunk(int cx, int cy, int cz) {
|
||||
this.cx = cx; this.cy = cy; this.cz = cz;
|
||||
}
|
||||
|
||||
// ── Zugriff ────────────────────────────────────────────────────────────────
|
||||
|
||||
public int idx(int x, int y, int z) { return y * SIZE * SIZE + z * SIZE + x; }
|
||||
|
||||
public byte getDensity(int x, int y, int z) {
|
||||
return density == null ? Byte.MIN_VALUE : density[idx(x, y, z)];
|
||||
}
|
||||
|
||||
public void setDensity(int x, int y, int z, byte d) {
|
||||
if (density == null) { density = new byte[SIZE*SIZE*SIZE]; Arrays.fill(density, Byte.MIN_VALUE); }
|
||||
density[idx(x, y, z)] = d;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
public byte getMaterial(int x, int y, int z) {
|
||||
return material == null ? 0 : material[idx(x, y, z)];
|
||||
}
|
||||
|
||||
public void setMaterial(int x, int y, int z, byte m) {
|
||||
if (material == null) material = new byte[SIZE*SIZE*SIZE];
|
||||
material[idx(x, y, z)] = m;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
public boolean isSolid(int x, int y, int z) { return getDensity(x, y, z) > 0; }
|
||||
|
||||
public boolean isEmpty() { return density == null; }
|
||||
|
||||
// ── Kugelförmiger Pinsel ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Setzt alle Voxel innerhalb des Radius (in lokalen Einheiten) auf den gegebenen Wert.
|
||||
* localX/Y/Z sind Mittelpunkt-Koordinaten im lokalen Voxel-Raum (0..128).
|
||||
* densityVal: > 0 = solid hinzufügen, Byte.MIN_VALUE = Luft (entfernen).
|
||||
* matId: 0–3, wird nur gesetzt wenn densityVal > 0.
|
||||
*/
|
||||
public void applyBrush(float localX, float localY, float localZ,
|
||||
float radius, byte densityVal, byte matId) {
|
||||
int x0 = Math.max(0, (int)(localX - radius));
|
||||
int x1 = Math.min(SIZE-1, (int)Math.ceil(localX + radius));
|
||||
int y0 = Math.max(0, (int)(localY - radius));
|
||||
int y1 = Math.min(SIZE-1, (int)Math.ceil(localY + radius));
|
||||
int z0 = Math.max(0, (int)(localZ - radius));
|
||||
int z1 = Math.min(SIZE-1, (int)Math.ceil(localZ + radius));
|
||||
float r2 = radius * radius;
|
||||
for (int y = y0; y <= y1; y++) {
|
||||
float dy = y - localY;
|
||||
for (int z = z0; z <= z1; z++) {
|
||||
float dz = z - localZ;
|
||||
for (int x = x0; x <= x1; x++) {
|
||||
float dx = x - localX;
|
||||
if (dx*dx + dy*dy + dz*dz <= r2) {
|
||||
setDensity(x, y, z, densityVal);
|
||||
if (densityVal > 0) setMaterial(x, y, z, matId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Serialisierung ────────────────────────────────────────────────────────
|
||||
|
||||
public byte[] serialize() throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
|
||||
try (DataOutputStream out = new DataOutputStream(
|
||||
new BufferedOutputStream(new GZIPOutputStream(baos)))) {
|
||||
out.writeInt(MAGIC);
|
||||
out.writeInt(VERSION);
|
||||
out.writeInt(cx); out.writeInt(cy); out.writeInt(cz);
|
||||
boolean hasDensity = density != null;
|
||||
boolean hasMaterial = material != null;
|
||||
out.writeBoolean(hasDensity);
|
||||
if (hasDensity) out.write(density);
|
||||
out.writeBoolean(hasMaterial);
|
||||
if (hasMaterial) out.write(material);
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
public static VoxelChunk deserialize(byte[] data, int cx, int cy, int cz) throws IOException {
|
||||
VoxelChunk c = new VoxelChunk(cx, cy, cz);
|
||||
try (DataInputStream in = new DataInputStream(
|
||||
new BufferedInputStream(new GZIPInputStream(
|
||||
new ByteArrayInputStream(data))))) {
|
||||
if (in.readInt() != MAGIC) throw new IOException("Kein VoxelChunk-Magic");
|
||||
int ver = in.readInt();
|
||||
if (ver != VERSION) throw new IOException("Unbekannte Chunk-Version: " + ver);
|
||||
in.readInt(); in.readInt(); in.readInt(); // cx,cy,cz (aus Dateiname)
|
||||
if (in.readBoolean()) {
|
||||
c.density = new byte[SIZE*SIZE*SIZE];
|
||||
in.readFully(c.density);
|
||||
}
|
||||
if (in.readBoolean()) {
|
||||
c.material = new byte[SIZE*SIZE*SIZE];
|
||||
in.readFully(c.material);
|
||||
}
|
||||
}
|
||||
c.dirty = false;
|
||||
return c;
|
||||
}
|
||||
|
||||
// ── Koordinaten-Hilfsmethoden ─────────────────────────────────────────────
|
||||
|
||||
/** Welt-Y des Voxels mit localY, ausgehend von diesem cy-Layer. */
|
||||
public static float toWorldY(int cy, int localY) { return cy * CELLS + localY; }
|
||||
/** Welt-X des Voxels mit localX, ausgehend von cx. */
|
||||
public static float toWorldX(int cx, int localX) { return cx * CELLS - 2048f + localX; }
|
||||
/** Welt-Z des Voxels mit localZ, ausgehend von cz. */
|
||||
public static float toWorldZ(int cz, int localZ) { return cz * CELLS - 2048f + localZ; }
|
||||
|
||||
/** cy-Layer für Welt-Y. */
|
||||
public static int worldYToCy(float worldY) { return Math.floorDiv((int)worldY, CELLS); }
|
||||
/** Lokales Y innerhalb des cy-Layers. */
|
||||
public static float worldYToLocal(float worldY, int cy) { return worldY - cy * (float)CELLS; }
|
||||
/** cx für Welt-X. */
|
||||
public static int worldXToCx(float worldX) { return (int)((worldX + 2048f) / CELLS); }
|
||||
/** Lokales X im cx-Chunk. */
|
||||
public static float worldXToLocal(float worldX, int cx){ return worldX - (cx * CELLS - 2048f); }
|
||||
/** cz für Welt-Z. */
|
||||
public static int worldZToCz(float worldZ) { return (int)((worldZ + 2048f) / CELLS); }
|
||||
/** Lokales Z im cz-Chunk. */
|
||||
public static float worldZToLocal(float worldZ, int cz){ return worldZ - (cz * CELLS - 2048f); }
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package de.blight.common;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Laden und Speichern von {@link VoxelChunk}s als {@code .blvc}-Dateien
|
||||
* im selben Verzeichnis wie die Terrain-Chunks.
|
||||
*
|
||||
* Dateiname: {@code voxel_CX_CY_CZ.blvc} (CY kann negativ sein → z.B. voxel_16_m1_16.blvc)
|
||||
*/
|
||||
public final class VoxelChunkIO {
|
||||
|
||||
private VoxelChunkIO() {}
|
||||
|
||||
public static Path getPath(int cx, int cy, int cz) {
|
||||
// Negative cy mit 'm' kodieren: voxel_16_m1_16.blvc
|
||||
String cyStr = cy < 0 ? "m" + (-cy) : String.valueOf(cy);
|
||||
return ChunkTerrainIO.chunksDir()
|
||||
.resolve(String.format("voxel_%02d_%s_%02d.blvc", cx, cyStr, cz));
|
||||
}
|
||||
|
||||
public static boolean exists(int cx, int cy, int cz) {
|
||||
return Files.exists(getPath(cx, cy, cz));
|
||||
}
|
||||
|
||||
public static void save(VoxelChunk chunk) throws IOException {
|
||||
Path p = getPath(chunk.cx, chunk.cy, chunk.cz);
|
||||
Path tmp = p.resolveSibling(p.getFileName() + ".tmp");
|
||||
Files.createDirectories(p.getParent());
|
||||
Files.write(tmp, chunk.serialize());
|
||||
try {
|
||||
Files.move(tmp, p, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.move(tmp, p, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
chunk.dirty = false;
|
||||
}
|
||||
|
||||
public static VoxelChunk load(int cx, int cy, int cz) throws IOException {
|
||||
return VoxelChunk.deserialize(Files.readAllBytes(getPath(cx, cy, cz)), cx, cy, cz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest alle vorhandenen VoxelChunks aus dem Chunks-Verzeichnis.
|
||||
* Gibt leere Liste zurück wenn kein Chunks-Verzeichnis existiert.
|
||||
*/
|
||||
public static List<VoxelChunk> loadAll() {
|
||||
List<VoxelChunk> result = new ArrayList<>();
|
||||
Path dir = ChunkTerrainIO.chunksDir();
|
||||
if (!Files.isDirectory(dir)) return result;
|
||||
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir, "voxel_*.blvc")) {
|
||||
for (Path p : ds) {
|
||||
String name = p.getFileName().toString()
|
||||
.replace("voxel_", "").replace(".blvc", "");
|
||||
String[] parts = name.split("_");
|
||||
if (parts.length != 3) continue;
|
||||
try {
|
||||
int cx = Integer.parseInt(parts[0]);
|
||||
int cy = parts[1].startsWith("m")
|
||||
? -Integer.parseInt(parts[1].substring(1))
|
||||
: Integer.parseInt(parts[1]);
|
||||
int cz = Integer.parseInt(parts[2]);
|
||||
result.add(VoxelChunk.deserialize(Files.readAllBytes(p), cx, cy, cz));
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
public enum CharacterStat {
|
||||
|
||||
CURRENT_HP ("HP (aktuell)"),
|
||||
CURRENT_STAMINA ("Stamina (aktuell)"),
|
||||
CURRENT_MANA ("Mana (aktuell)"),
|
||||
MAX_HP ("HP (maximal)"),
|
||||
MAX_STAMINA ("Stamina (maximal)"),
|
||||
MAX_MANA ("Mana (maximal)"),
|
||||
OPEN_HP_REGENERATION ("HP-Regeneration"),
|
||||
OPEN_MANA_REGENERATION ("Mana-Regeneration"),
|
||||
OPEN_STAMINA_REGENERATION("Stamina-Regeneration");
|
||||
|
||||
private final String displayName;
|
||||
|
||||
CharacterStat(String displayName) { this.displayName = displayName; }
|
||||
|
||||
public void apply(MainCharacter c, int value) {
|
||||
switch (this) {
|
||||
case CURRENT_HP -> c.heal(value);
|
||||
case CURRENT_STAMINA -> c.restoreStamina(value);
|
||||
case CURRENT_MANA -> c.restoreMana(value);
|
||||
case MAX_HP -> c.setMaxHp(c.getMaxHp() + value);
|
||||
case MAX_STAMINA -> c.setMaxStamina(c.getMaxStamina() + value);
|
||||
case MAX_MANA -> c.setMaxMana(c.getMaxMana() + value);
|
||||
case OPEN_HP_REGENERATION -> c.setOpenHpRegeneration(c.getOpenHpRegeneration() + value);
|
||||
case OPEN_MANA_REGENERATION -> c.setOpenManaRegeneration(c.getOpenManaRegeneration() + value);
|
||||
case OPEN_STAMINA_REGENERATION -> c.setOpenStaminaRegeneration(c.getOpenStaminaRegeneration() + value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() { return displayName; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ConsumableEffect {
|
||||
private CharacterStat stat;
|
||||
private int value;
|
||||
}
|
||||
@@ -1,12 +1,19 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
public class Inventar {
|
||||
|
||||
private HashMap<Item, Integer> items = new HashMap<Item, Integer>();
|
||||
|
||||
/** Gibt eine unveränderliche Sicht auf alle Items und ihre Anzahlen zurück. */
|
||||
public Map<Item, Integer> getItems() {
|
||||
return Collections.unmodifiableMap(items);
|
||||
}
|
||||
|
||||
public void collect(Item item) {
|
||||
add(item, 1);
|
||||
}
|
||||
|
||||
@@ -3,19 +3,27 @@ package de.blight.common.model;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class Item implements Interactable {
|
||||
|
||||
private String itemId;
|
||||
private ItemCategory category;
|
||||
private ItemSubCategory subCategory;
|
||||
private TextReference name;
|
||||
private TextReference description;
|
||||
private int worthGold;
|
||||
private ObjectReference modelRef;
|
||||
private boolean consumable;
|
||||
private List<ConsumableEffect> effects;
|
||||
|
||||
public void use(MainCharacter character) {
|
||||
|
||||
if (!consumable || effects == null) return;
|
||||
for (ConsumableEffect effect : effects) {
|
||||
if (effect.getStat() != null) effect.getStat().apply(character, effect.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,6 +6,6 @@ public enum ItemCategory {
|
||||
GEAR,
|
||||
CONSUMABLES,
|
||||
QUEST_ITEMS,
|
||||
USABLES,
|
||||
MISC;
|
||||
CRAFTING_ITEMS,
|
||||
MISC_ITEMS;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
import static de.blight.common.model.ItemCategory.CONSUMABLES;
|
||||
import static de.blight.common.model.ItemCategory.CRAFTING_ITEMS;
|
||||
import static de.blight.common.model.ItemCategory.GEAR;
|
||||
import static de.blight.common.model.ItemCategory.MISC_ITEMS;
|
||||
import static de.blight.common.model.ItemCategory.QUEST_ITEMS;
|
||||
import static de.blight.common.model.ItemCategory.WEAPON;
|
||||
import static de.blight.common.model.ItemSubCategory.ARMOR;
|
||||
import static de.blight.common.model.ItemSubCategory.AXE;
|
||||
import static de.blight.common.model.ItemSubCategory.FOOD;
|
||||
import static de.blight.common.model.ItemSubCategory.HALBERD;
|
||||
import static de.blight.common.model.ItemSubCategory.HELM;
|
||||
import static de.blight.common.model.ItemSubCategory.MAGICAL;
|
||||
import static de.blight.common.model.ItemSubCategory.MAGICAL_ITEM;
|
||||
import static de.blight.common.model.ItemSubCategory.MISC;
|
||||
import static de.blight.common.model.ItemSubCategory.NECKLACE;
|
||||
import static de.blight.common.model.ItemSubCategory.PERMANENT_POTION;
|
||||
import static de.blight.common.model.ItemSubCategory.PLANT;
|
||||
import static de.blight.common.model.ItemSubCategory.POTION;
|
||||
import static de.blight.common.model.ItemSubCategory.QUEST_ITEM;
|
||||
import static de.blight.common.model.ItemSubCategory.RING;
|
||||
import static de.blight.common.model.ItemSubCategory.SHIELD;
|
||||
import static de.blight.common.model.ItemSubCategory.STAFF;
|
||||
import static de.blight.common.model.ItemSubCategory.SWORD;
|
||||
import static de.blight.common.model.ItemSubCategory.TECHNICAL;
|
||||
import static de.blight.common.model.ItemSubCategory.TWO_HANDED_SWORD;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class ItemCategoryManager {
|
||||
|
||||
private static final Map<ItemCategory, List<ItemSubCategory>> mapping = new EnumMap<ItemCategory, List<ItemSubCategory>>(ItemCategory.class);
|
||||
|
||||
static {
|
||||
mapping.put(WEAPON, List.of(SWORD, TWO_HANDED_SWORD, STAFF, HALBERD, AXE));
|
||||
mapping.put(GEAR, List.of(RING, NECKLACE, ARMOR, HELM, SHIELD));
|
||||
mapping.put(CONSUMABLES, List.of(POTION, PERMANENT_POTION, FOOD, MAGICAL_ITEM));
|
||||
mapping.put(QUEST_ITEMS, List.of(QUEST_ITEM));
|
||||
mapping.put(CRAFTING_ITEMS, List.of(PLANT, TECHNICAL, MAGICAL));
|
||||
mapping.put(MISC_ITEMS, List.of(MISC));
|
||||
}
|
||||
|
||||
/** Gibt alle gültigen SubKategorien für eine Kategorie zurück (nie null, ggf. leer). */
|
||||
public static List<ItemSubCategory> getSubCategories(ItemCategory cat) {
|
||||
return mapping.getOrDefault(cat, List.of());
|
||||
}
|
||||
|
||||
public static boolean isValid(ItemCategory cat, ItemSubCategory sub) {
|
||||
return mapping.get(cat).contains(sub);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
public enum ItemSubCategory {
|
||||
|
||||
SWORD, TWO_HANDED_SWORD, STAFF, HALBERD, AXE, //WEAPONS
|
||||
RING, NECKLACE, ARMOR, HELM, SHIELD, //GEAR
|
||||
POTION, PERMANENT_POTION, FOOD, MAGICAL_ITEM,
|
||||
QUEST_ITEM,
|
||||
PLANT, TECHNICAL, MAGICAL,
|
||||
MISC;
|
||||
}
|
||||
@@ -18,13 +18,18 @@ public class MainCharacter extends GameCharacter {
|
||||
private int xp;
|
||||
|
||||
private int currentHp;
|
||||
private int maxHp;
|
||||
|
||||
private int currentStamina;
|
||||
private int maxStamina;
|
||||
|
||||
private int currentMana;
|
||||
private int myMana;
|
||||
|
||||
private int maxHp;
|
||||
private int maxStamina;
|
||||
private int maxMana;
|
||||
|
||||
private int openHpRegeneration;
|
||||
private int openManaRegeneration;
|
||||
private int openStaminaRegeneration;
|
||||
|
||||
private int restorationValue = 10;
|
||||
|
||||
private List<Quest> openQuests;
|
||||
private List<Quest> completedQuests;
|
||||
@@ -80,6 +85,40 @@ public class MainCharacter extends GameCharacter {
|
||||
return !openQuests.contains(quest) && !completedQuests.contains(quest) && !abortedQuests.contains(quest);
|
||||
}
|
||||
|
||||
public void checkRegeneration() {
|
||||
if (openHpRegeneration > 0 && currentHp < maxHp) {
|
||||
heal(restorationValue);
|
||||
}
|
||||
if (openManaRegeneration > 0 && currentMana < maxMana) {
|
||||
restoreMana(restorationValue);
|
||||
}
|
||||
if (openStaminaRegeneration > 0 && currentStamina < maxStamina) {
|
||||
restoreStamina(restorationValue);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void heal(int value) {
|
||||
currentHp += value;
|
||||
if (currentHp > maxHp) {
|
||||
currentHp = maxHp;
|
||||
}
|
||||
}
|
||||
|
||||
public void restoreMana(int value) {
|
||||
currentMana += value;
|
||||
if (currentMana > maxMana) {
|
||||
currentMana = maxMana;
|
||||
}
|
||||
}
|
||||
|
||||
public void restoreStamina(int value) {
|
||||
currentStamina += value;
|
||||
if (currentStamina > maxStamina) {
|
||||
currentStamina = maxStamina;
|
||||
}
|
||||
}
|
||||
|
||||
public void removeListener(CharacterListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
@@ -16,4 +16,14 @@ public class ObjectReference {
|
||||
|
||||
/** Relativer Asset-Pfad (z. B. {@code Models/Items/sword.j3o}). */
|
||||
private String path;
|
||||
|
||||
/**
|
||||
* Relativer Asset-Pfad zum zugehörigen Thumbnail.
|
||||
* Format: {@code .thumbnails/<path>.thumb.png} – erzeugt vom ThumbnailRenderer.
|
||||
* Gibt {@code null} zurück wenn kein Pfad gesetzt ist.
|
||||
*/
|
||||
public String getThumbnailAssetPath() {
|
||||
if (path == null || path.isEmpty()) return null;
|
||||
return ".thumbnails/" + path + ".thumb.png";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package de.blight.editor;
|
||||
|
||||
import de.blight.common.BlightHome;
|
||||
import de.blight.editor.tool.ChoiceToolParameter;
|
||||
import de.blight.editor.tool.EditorTool;
|
||||
import de.blight.editor.tool.GrassTool;
|
||||
import de.blight.editor.tool.ToolParameter;
|
||||
import de.blight.editor.tree.PalmOptions;
|
||||
import de.blight.editor.tree.TreeParams;
|
||||
import de.blight.editor.ui.MapObjectsView;
|
||||
import de.blight.editor.ui.TextureChooser;
|
||||
import de.blight.eztree.Billboard;
|
||||
import de.blight.eztree.TreeOptions;
|
||||
@@ -62,6 +64,7 @@ public class EditorApp extends Application {
|
||||
private java.nio.file.attribute.FileTime lastLivePosTime =
|
||||
java.nio.file.attribute.FileTime.fromMillis(0);
|
||||
private VBox assetPanel;
|
||||
private MapObjectsView mapObjectsView;
|
||||
private StackPane worldViewport;
|
||||
private VBox topBar; // MenuBar + aktuelle Toolbar
|
||||
private ToolBar worldToolBar; // Welt-Editor-Toolbar (Layer-Buttons)
|
||||
@@ -229,6 +232,9 @@ public class EditorApp extends Application {
|
||||
private ToggleButton areaBtn;
|
||||
private ToggleButton locationZoneBtn;
|
||||
private ToggleButton playToolBtn;
|
||||
private ToggleButton voxelBtn;
|
||||
private ToggleButton camOrbitBtn;
|
||||
private ToggleButton camFreeBtn;
|
||||
|
||||
// "Objekt"-Button in der Selektionsleiste (zum Zurückschalten bei importierten Objekten)
|
||||
private ToggleButton selModeObjectBtn;
|
||||
@@ -324,6 +330,16 @@ public class EditorApp extends Application {
|
||||
|
||||
primaryStage = stage;
|
||||
input.scanSkeletalRequested = true; // Skelett-Scan beim Start anstoßen
|
||||
|
||||
// Alle ungefangenen Exceptions auf dem FX-Thread loggen – hilft beim Finden des Zahnrad-Auslösers
|
||||
Thread.currentThread().setUncaughtExceptionHandler((t, ex) ->
|
||||
org.slf4j.LoggerFactory.getLogger(EditorApp.class)
|
||||
.error("[FX-Thread] Ungefangene Exception", ex));
|
||||
// Auch andere Threads absichern (JME3-Thread, Worker-Threads)
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, ex) ->
|
||||
org.slf4j.LoggerFactory.getLogger(EditorApp.class)
|
||||
.error("[Thread '{}'] Ungefangene Exception", t.getName(), ex));
|
||||
|
||||
stage.setTitle("Blight World Editor");
|
||||
try (var is = getClass().getResourceAsStream("/icon_editor.png")) {
|
||||
if (is != null) stage.getIcons().add(new Image(is));
|
||||
@@ -932,6 +948,9 @@ public class EditorApp extends Application {
|
||||
areaBtn = new ToggleButton("🗺 Bereiche");
|
||||
locationZoneBtn = new ToggleButton("📍 Locations");
|
||||
playToolBtn = new ToggleButton("🎮 Spielen");
|
||||
voxelBtn = new ToggleButton("⬡ Voxel");
|
||||
camOrbitBtn = new ToggleButton("⊙ Orbit");
|
||||
camFreeBtn = new ToggleButton("✈ FreeFly");
|
||||
baseBtn.setStyle("-fx-font-weight:bold;");
|
||||
grassBtn.setStyle("-fx-font-weight:bold;");
|
||||
grassVertexBtn.setStyle("-fx-font-weight:bold;");
|
||||
@@ -946,6 +965,9 @@ public class EditorApp extends Application {
|
||||
areaBtn.setStyle("-fx-font-weight:bold;");
|
||||
locationZoneBtn.setStyle("-fx-font-weight:bold;");
|
||||
playToolBtn.setStyle("-fx-font-weight:bold;");
|
||||
voxelBtn.setStyle("-fx-font-weight:bold;");
|
||||
camOrbitBtn.setStyle("-fx-font-weight:bold;");
|
||||
camFreeBtn.setStyle("-fx-font-weight:bold;");
|
||||
|
||||
ToggleGroup layerGroup = new ToggleGroup();
|
||||
baseBtn.setToggleGroup(layerGroup);
|
||||
@@ -962,6 +984,7 @@ public class EditorApp extends Application {
|
||||
areaBtn.setToggleGroup(layerGroup);
|
||||
locationZoneBtn.setToggleGroup(layerGroup);
|
||||
playToolBtn.setToggleGroup(layerGroup);
|
||||
voxelBtn.setToggleGroup(layerGroup);
|
||||
baseBtn.setSelected(true);
|
||||
|
||||
baseBtn.setOnAction(e -> {
|
||||
@@ -1030,6 +1053,19 @@ public class EditorApp extends Application {
|
||||
input.activeLayer = SharedInput.LAYER_PLAY_TOOL;
|
||||
root.setRight(buildPlayToolPanel());
|
||||
});
|
||||
voxelBtn.setOnAction(e -> {
|
||||
input.activeLayer = SharedInput.LAYER_VOXEL;
|
||||
input.activeTool = input.voxelTool;
|
||||
root.setRight(toolPanel);
|
||||
showToolParameters(toolPanel, input.activeTool);
|
||||
});
|
||||
|
||||
ToggleGroup camModeGroup = new ToggleGroup();
|
||||
camOrbitBtn.setToggleGroup(camModeGroup);
|
||||
camFreeBtn.setToggleGroup(camModeGroup);
|
||||
camOrbitBtn.setSelected(true);
|
||||
camOrbitBtn.setOnAction(e -> input.camMode = SharedInput.CAM_ORBIT);
|
||||
camFreeBtn.setOnAction(e -> input.camMode = SharedInput.CAM_FREEFLY);
|
||||
|
||||
Label hint = new Label("WASD/QE: Kamera | Mitte-Drag / L+R-Drag: Drehen | L-Klick: hoch | R-Klick: tief");
|
||||
hint.setStyle("-fx-text-fill: #555;");
|
||||
@@ -1042,6 +1078,8 @@ public class EditorApp extends Application {
|
||||
new Separator(Orientation.VERTICAL), riverBtn,
|
||||
new Separator(Orientation.VERTICAL), soundAreaBtn, areaBtn, locationZoneBtn,
|
||||
new Separator(Orientation.VERTICAL), playToolBtn,
|
||||
new Separator(Orientation.VERTICAL), voxelBtn,
|
||||
new Separator(Orientation.VERTICAL), camOrbitBtn, camFreeBtn,
|
||||
new Separator(Orientation.VERTICAL), hint);
|
||||
|
||||
worldToolBar = toolBar;
|
||||
@@ -2775,6 +2813,12 @@ public class EditorApp extends Application {
|
||||
if (tool instanceof de.blight.editor.tool.TextureTool
|
||||
&& param == ((de.blight.editor.tool.TextureTool) tool).textureIndex) {
|
||||
panel.getChildren().addAll(nameLabel, buildTextureChoiceUI(param));
|
||||
} else if (tool instanceof de.blight.editor.tool.VoxelTool vt
|
||||
&& param == vt.textureSlot) {
|
||||
panel.getChildren().addAll(nameLabel, buildVoxelTextureChoiceUI(param));
|
||||
} else if (tool instanceof de.blight.editor.tool.VoxelTool vt2
|
||||
&& param == vt2.mode) {
|
||||
panel.getChildren().addAll(nameLabel, buildVoxelModeUI(param));
|
||||
} else if (param.getImagePaths() != null) {
|
||||
String[] paths = param.getImagePaths();
|
||||
String[] labels = param.getChoices();
|
||||
@@ -3045,6 +3089,132 @@ public class EditorApp extends Application {
|
||||
return tile;
|
||||
}
|
||||
|
||||
private javafx.scene.Node buildVoxelModeUI(ChoiceToolParameter param) {
|
||||
String[] labels = param.getChoices();
|
||||
String[] imgPaths = param.getImagePaths();
|
||||
ToggleGroup tg = new ToggleGroup();
|
||||
javafx.scene.layout.TilePane tile = new javafx.scene.layout.TilePane();
|
||||
tile.setHgap(4);
|
||||
tile.setVgap(4);
|
||||
tile.setPrefColumns(6);
|
||||
|
||||
for (int j = 0; j < labels.length; j++) {
|
||||
final int idx = j;
|
||||
String path = (imgPaths != null && j < imgPaths.length) ? imgPaths[j] : null;
|
||||
|
||||
ToggleButton btn = new ToggleButton();
|
||||
btn.setToggleGroup(tg);
|
||||
btn.setPrefSize(44, 56);
|
||||
btn.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
|
||||
btn.setTooltip(new Tooltip(labels[j]));
|
||||
|
||||
VBox content = new VBox(2);
|
||||
content.setAlignment(Pos.CENTER);
|
||||
|
||||
boolean hasImage = false;
|
||||
if (path != null) {
|
||||
URL resUrl = EditorApp.class.getResource("/" + path);
|
||||
String imgUrl = resUrl != null ? resUrl.toString() : null;
|
||||
if (imgUrl == null) {
|
||||
File f = new File(path).getAbsoluteFile();
|
||||
if (f.exists()) imgUrl = f.toURI().toString();
|
||||
}
|
||||
if (imgUrl != null) {
|
||||
Image img = new Image(imgUrl, 32, 32, true, true);
|
||||
if (!img.isError()) {
|
||||
ImageView iv = new ImageView(img);
|
||||
iv.setFitWidth(32); iv.setFitHeight(32);
|
||||
content.getChildren().add(iv);
|
||||
hasImage = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasImage) {
|
||||
boolean isRemove = labels[j].equals("Entfernen");
|
||||
Label icon = new Label(isRemove ? "✕" : "+");
|
||||
icon.setStyle("-fx-font-size: 16; -fx-text-fill: " + (isRemove ? "#cc2222" : "#228822") + ";");
|
||||
content.getChildren().add(icon);
|
||||
}
|
||||
Label lbl = new Label(labels[j]);
|
||||
lbl.setStyle("-fx-font-size: 9; -fx-text-fill: #222;");
|
||||
content.getChildren().add(lbl);
|
||||
btn.setGraphic(content);
|
||||
|
||||
boolean initially = (j == param.getSelectedIndex());
|
||||
btn.setSelected(initially);
|
||||
applyModeButtonStyle(btn, initially);
|
||||
btn.selectedProperty().addListener((obs, was, isNow) -> {
|
||||
applyModeButtonStyle(btn, isNow);
|
||||
if (isNow) param.setSelectedIndex(idx);
|
||||
});
|
||||
tile.getChildren().add(btn);
|
||||
}
|
||||
tg.selectedToggleProperty().addListener((obs, oldT, newT) -> {
|
||||
if (newT == null) tg.selectToggle(oldT);
|
||||
});
|
||||
return tile;
|
||||
}
|
||||
|
||||
private javafx.scene.Node buildVoxelTextureChoiceUI(ChoiceToolParameter param) {
|
||||
ToggleGroup tg = new ToggleGroup();
|
||||
javafx.scene.layout.TilePane tile = new javafx.scene.layout.TilePane();
|
||||
tile.setHgap(4);
|
||||
tile.setVgap(4);
|
||||
tile.setPrefColumns(4);
|
||||
|
||||
for (int j = 0; j < 4; j++) {
|
||||
final int idx = j;
|
||||
String path = (j < input.terrainTexturePaths.length
|
||||
&& input.terrainTexturePaths[j] != null
|
||||
&& !input.terrainTexturePaths[j].isEmpty())
|
||||
? input.terrainTexturePaths[j] : "";
|
||||
|
||||
ToggleButton btn = new ToggleButton();
|
||||
btn.setToggleGroup(tg);
|
||||
btn.setPrefSize(56, 76);
|
||||
btn.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
|
||||
btn.setTooltip(new Tooltip("Slot " + (j + 1) + ": " + (path.isEmpty() ? "(leer)" : path)));
|
||||
|
||||
VBox content = new VBox(2);
|
||||
content.setAlignment(Pos.CENTER);
|
||||
|
||||
if (!path.isEmpty()) {
|
||||
String imgUrl = resolveImageUrl(path);
|
||||
if (imgUrl != null) {
|
||||
Image img = new Image(imgUrl, 44, 44, true, true);
|
||||
ImageView iv = new ImageView(img.isError() ? null : img);
|
||||
iv.setFitWidth(44);
|
||||
iv.setFitHeight(44);
|
||||
content.getChildren().add(img.isError() ? emptySlotPlaceholder(44) : iv);
|
||||
} else {
|
||||
content.getChildren().add(emptySlotPlaceholder(44));
|
||||
}
|
||||
} else {
|
||||
content.getChildren().add(emptySlotPlaceholder(44));
|
||||
}
|
||||
|
||||
Label lbl = new Label("S" + (j + 1) + " " + labelFromPath(path));
|
||||
lbl.setStyle("-fx-font-size: 9; -fx-text-fill: #222;");
|
||||
lbl.setMaxWidth(54);
|
||||
content.getChildren().add(lbl);
|
||||
|
||||
btn.setGraphic(content);
|
||||
boolean initially = (j == param.getSelectedIndex());
|
||||
btn.setSelected(initially);
|
||||
applyModeButtonStyle(btn, initially);
|
||||
btn.selectedProperty().addListener((obs, was, isNow) -> {
|
||||
applyModeButtonStyle(btn, isNow);
|
||||
if (isNow) param.setSelectedIndex(idx);
|
||||
});
|
||||
tile.getChildren().add(btn);
|
||||
}
|
||||
|
||||
tg.selectedToggleProperty().addListener((obs, oldT, newT) -> {
|
||||
if (newT == null) tg.selectToggle(oldT);
|
||||
});
|
||||
return tile;
|
||||
}
|
||||
|
||||
private javafx.scene.layout.Region emptySlotPlaceholder(double size) {
|
||||
javafx.scene.layout.Region r = new javafx.scene.layout.Region();
|
||||
r.setPrefSize(size, size);
|
||||
@@ -3172,10 +3342,10 @@ public class EditorApp extends Application {
|
||||
// ── Linke Seite: Asset-Panel (Welteneditor) ──────────────────────────────
|
||||
|
||||
private VBox buildAssetPanel() {
|
||||
VBox panel = new VBox(6);
|
||||
panel.setPadding(new Insets(8));
|
||||
panel.setPrefWidth(420);
|
||||
panel.setStyle("-fx-background-color: #f0f0f0;");
|
||||
// ── Tab 1: Asset-Baum ─────────────────────────────────────────────────
|
||||
VBox assetContent = new VBox(6);
|
||||
assetContent.setPadding(new Insets(8));
|
||||
assetContent.setStyle("-fx-background-color: #f0f0f0;");
|
||||
|
||||
Label title = new Label("Assets");
|
||||
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||
@@ -3283,7 +3453,106 @@ public class EditorApp extends Application {
|
||||
HBox.setHgrow(importBtn, Priority.ALWAYS);
|
||||
bottomBar.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
panel.getChildren().addAll(titleBar, tree, bottomBar);
|
||||
assetContent.getChildren().addAll(titleBar, tree, bottomBar);
|
||||
|
||||
// ── Tab 2: Karte (platzierte Objekte) ─────────────────────────────────
|
||||
mapObjectsView = new MapObjectsView(
|
||||
(x, y, z, toolHint) -> {
|
||||
input.pendingGotoX = x;
|
||||
input.pendingGotoY = y;
|
||||
input.pendingGotoZ = z;
|
||||
input.pendingGotoPitch = (float) (-Math.PI / 3);
|
||||
switch (toolHint) {
|
||||
case "model" -> { input.activeLayer = SharedInput.LAYER_OBJECTS_EDIT; root.setRight(buildObjectEditPanel()); }
|
||||
case "item" -> { input.activeLayer = SharedInput.LAYER_ITEMS; root.setRight(buildItemPlacePanel(null)); }
|
||||
case "light" -> { input.activeLayer = SharedInput.LAYER_LIGHTS; root.setRight(buildLightPanel()); }
|
||||
case "emitter" -> { input.activeLayer = SharedInput.LAYER_EMITTERS; root.setRight(buildEmitterPanel()); }
|
||||
case "water" -> { input.activeLayer = SharedInput.LAYER_WATER; root.setRight(buildWaterPanel()); }
|
||||
case "soundarea" -> { input.activeLayer = SharedInput.LAYER_SOUND_AREAS; root.setRight(buildSoundAreaPanel()); }
|
||||
case "area" -> { input.activeLayer = SharedInput.LAYER_AREAS; root.setRight(buildAreaPanel()); }
|
||||
case "locationzone" -> { input.activeLayer = SharedInput.LAYER_LOCATION_ZONES; root.setRight(buildLocationZonePanel()); }
|
||||
case "waterfall" -> { input.activeLayer = SharedInput.LAYER_WATERFALL; root.setRight(buildWaterfallPanel()); }
|
||||
}
|
||||
},
|
||||
(obj, index, toolHint) -> {
|
||||
switch (toolHint) {
|
||||
case "model" -> {
|
||||
java.util.List<de.blight.common.PlacedModel> list = new java.util.ArrayList<>(de.blight.common.PlacedModelIO.load());
|
||||
if (index >= 0 && index < list.size()) list.remove(index);
|
||||
de.blight.common.PlacedModelIO.save(list);
|
||||
input.reloadPlacedModels = true;
|
||||
}
|
||||
case "item" -> {
|
||||
de.blight.common.PlacedItem pi = (de.blight.common.PlacedItem) obj;
|
||||
java.util.List<de.blight.common.PlacedItem> list = new java.util.ArrayList<>(de.blight.common.PlacedItemIO.load());
|
||||
list.removeIf(it -> it.uuid().equals(pi.uuid()));
|
||||
de.blight.common.PlacedItemIO.save(list);
|
||||
input.reloadPlacedItems = true;
|
||||
}
|
||||
case "light" -> {
|
||||
java.util.List<de.blight.common.PlacedLight> list = new java.util.ArrayList<>(de.blight.common.LightIO.load());
|
||||
if (index >= 0 && index < list.size()) list.remove(index);
|
||||
de.blight.common.LightIO.save(list);
|
||||
input.reloadPlacedOther = true;
|
||||
}
|
||||
case "emitter" -> {
|
||||
java.util.List<de.blight.common.PlacedEmitter> list = new java.util.ArrayList<>(de.blight.common.EmitterIO.load());
|
||||
if (index >= 0 && index < list.size()) list.remove(index);
|
||||
de.blight.common.EmitterIO.save(list);
|
||||
input.reloadPlacedOther = true;
|
||||
}
|
||||
case "water" -> {
|
||||
java.util.List<de.blight.common.PlacedWater> list = new java.util.ArrayList<>(de.blight.common.WaterBodyIO.load());
|
||||
if (index >= 0 && index < list.size()) list.remove(index);
|
||||
de.blight.common.WaterBodyIO.save(list);
|
||||
input.reloadPlacedOther = true;
|
||||
}
|
||||
case "soundarea" -> {
|
||||
java.util.List<de.blight.common.PlacedSoundArea> list = new java.util.ArrayList<>(de.blight.common.SoundAreaIO.load());
|
||||
if (index >= 0 && index < list.size()) list.remove(index);
|
||||
de.blight.common.SoundAreaIO.save(list);
|
||||
input.reloadPlacedOther = true;
|
||||
}
|
||||
case "area" -> {
|
||||
java.util.List<de.blight.common.PlacedArea> list = new java.util.ArrayList<>(de.blight.common.AreaIO.load());
|
||||
if (index >= 0 && index < list.size()) list.remove(index);
|
||||
de.blight.common.AreaIO.save(list);
|
||||
input.reloadPlacedOther = true;
|
||||
}
|
||||
case "locationzone" -> {
|
||||
java.util.List<de.blight.common.PlacedLocationZone> list = new java.util.ArrayList<>(de.blight.common.LocationZoneIO.load());
|
||||
if (index >= 0 && index < list.size()) list.remove(index);
|
||||
de.blight.common.LocationZoneIO.save(list);
|
||||
input.reloadPlacedOther = true;
|
||||
}
|
||||
case "waterfall" -> {
|
||||
java.util.List<java.util.List<de.blight.common.RiverPoint>> list = new java.util.ArrayList<>(de.blight.common.RiverIO.load());
|
||||
if (index >= 0 && index < list.size()) list.remove(index);
|
||||
de.blight.common.RiverIO.save(list);
|
||||
input.reloadPlacedOther = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ── TabPane ───────────────────────────────────────────────────────────
|
||||
Tab assetsTab = new Tab("Assets", assetContent);
|
||||
assetsTab.setClosable(false);
|
||||
|
||||
Tab karteTab = new Tab("Karte", mapObjectsView);
|
||||
karteTab.setClosable(false);
|
||||
karteTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
|
||||
if (isSelected) mapObjectsView.refresh();
|
||||
});
|
||||
|
||||
TabPane tabPane = new TabPane(assetsTab, karteTab);
|
||||
tabPane.setStyle("-fx-background-color: #e8e8e8;");
|
||||
VBox.setVgrow(tabPane, Priority.ALWAYS);
|
||||
|
||||
VBox panel = new VBox(tabPane);
|
||||
panel.setPrefWidth(420);
|
||||
panel.setStyle("-fx-background-color: #e8e8e8;");
|
||||
VBox.setVgrow(tabPane, Priority.ALWAYS);
|
||||
return panel;
|
||||
}
|
||||
|
||||
@@ -4082,10 +4351,10 @@ public class EditorApp extends Application {
|
||||
new FileChooser.ExtensionFilter("Alle unterstützten Dateien",
|
||||
"*.j3o","*.obj","*.gltf","*.glb",
|
||||
"*.png","*.jpg","*.jpeg","*.bmp","*.tga","*.dds",
|
||||
"*.ogg","*.wav"),
|
||||
"*.ogg","*.wav","*.mp3"),
|
||||
new FileChooser.ExtensionFilter("Modelle", "*.j3o","*.obj","*.gltf","*.glb"),
|
||||
new FileChooser.ExtensionFilter("Texturen", "*.png","*.jpg","*.jpeg","*.bmp","*.tga","*.dds"),
|
||||
new FileChooser.ExtensionFilter("Audio (OGG, WAV)", "*.ogg","*.wav")
|
||||
new FileChooser.ExtensionFilter("Audio (OGG, WAV, MP3)", "*.ogg","*.wav","*.mp3")
|
||||
);
|
||||
var files = fc.showOpenMultipleDialog(owner);
|
||||
if (files == null) return;
|
||||
@@ -4094,7 +4363,7 @@ public class EditorApp extends Application {
|
||||
String name = file.getName().toLowerCase();
|
||||
boolean isNativeModel = name.matches(".*\\.(obj|fbx|gltf|glb)");
|
||||
boolean isJ3o = name.endsWith(".j3o");
|
||||
boolean isAudio = name.matches(".*\\.(ogg|wav)");
|
||||
boolean isAudio = name.matches(".*\\.(ogg|wav|mp3)");
|
||||
boolean isModel = isNativeModel || isJ3o;
|
||||
|
||||
String subDir = isModel ? "Models" : isAudio ? "audio" : "Textures";
|
||||
@@ -4743,6 +5012,7 @@ public class EditorApp extends Application {
|
||||
|
||||
viewport.setOnScroll(e -> {
|
||||
int steps = (int) Math.signum(e.getDeltaY());
|
||||
if (e.isControlDown()) steps *= 10;
|
||||
input.scrollAccum.addAndGet(steps);
|
||||
});
|
||||
|
||||
@@ -4796,6 +5066,8 @@ public class EditorApp extends Application {
|
||||
if (action > 0) // only left-click sets spawn
|
||||
input.playToolClickQueue.offer(new SharedInput.PlayToolClick((float) x, (float) y));
|
||||
}
|
||||
case SharedInput.LAYER_VOXEL ->
|
||||
input.voxelEditQueue.offer(new SharedInput.VoxelEdit((float) x, (float) y));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4839,12 +5111,24 @@ public class EditorApp extends Application {
|
||||
String libPath = System.getProperty("java.library.path", "");
|
||||
String projRoot = ProjectRoot.PATH.toString();
|
||||
|
||||
// Karte in run/session-<ts>/ kopieren – dort gitignored, aber im Projekt sichtbar.
|
||||
// Spielaktionen (Item-Aufheben etc.) verändern so nie die Editor-Karte.
|
||||
Path srcMapDir = ProjectRoot.PATH.resolve(
|
||||
Paths.get("blight-map", "src", "main", "map"));
|
||||
Path sessionDir = ProjectRoot.PATH.resolve("run")
|
||||
.resolve("session-" + System.currentTimeMillis());
|
||||
copyDirectory(srcMapDir, sessionDir);
|
||||
String sessionMapPath = sessionDir.resolve("blight_map.blm").toString();
|
||||
|
||||
java.util.List<String> cmd = new java.util.ArrayList<>(List.of(
|
||||
javaExe,
|
||||
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
|
||||
"--add-opens", "java.desktop/sun.awt=ALL-UNNAMED",
|
||||
"-Djava.library.path=" + libPath,
|
||||
"-Dblight.project.root=" + projRoot));
|
||||
"-Dblight.project.root=" + projRoot,
|
||||
"-D" + de.blight.common.MapIO.PROP_SESSION_MAP + "=" + sessionMapPath,
|
||||
// Vom Editor gestartet: Hauptmenü überspringen, letzten Stand fortsetzen
|
||||
"-Dblight.autostart=true"));
|
||||
|
||||
if (!Float.isNaN(input.tempSpawnX) && !Float.isNaN(input.tempSpawnZ)) {
|
||||
cmd.add("-Dblight.temp.spawn.x=" + input.tempSpawnX);
|
||||
@@ -4896,6 +5180,19 @@ public class EditorApp extends Application {
|
||||
}, "game-launcher").start();
|
||||
}
|
||||
|
||||
private static void copyDirectory(Path src, Path dst) throws IOException {
|
||||
try (java.util.stream.Stream<Path> walk = java.nio.file.Files.walk(src)) {
|
||||
for (Path s : (Iterable<Path>) walk::iterator) {
|
||||
Path d = dst.resolve(src.relativize(s));
|
||||
if (java.nio.file.Files.isDirectory(s)) {
|
||||
java.nio.file.Files.createDirectories(d);
|
||||
} else {
|
||||
java.nio.file.Files.copy(s, d, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void openGameConsole() {
|
||||
if (gameConsoleStage == null) {
|
||||
gameConsoleArea = new TextArea();
|
||||
@@ -4962,8 +5259,8 @@ public class EditorApp extends Application {
|
||||
p.setProperty("cam.yaw", String.valueOf(input.camYaw));
|
||||
p.setProperty("cam.pitch", String.valueOf(input.camPitch));
|
||||
try {
|
||||
Files.createDirectories(ProjectRoot.resolve("config"));
|
||||
try (java.io.Writer w = Files.newBufferedWriter(ProjectRoot.resolve("config", "editor.prefs"))) {
|
||||
Files.createDirectories(BlightHome.resolve("config"));
|
||||
try (java.io.Writer w = Files.newBufferedWriter(BlightHome.resolve("config", "editor.prefs"))) {
|
||||
p.store(w, "Blight Editor – Kamera-Einstellungen");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@@ -5131,7 +5428,7 @@ public class EditorApp extends Application {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle("Sound-Datei wählen");
|
||||
fc.getExtensionFilters().add(
|
||||
new FileChooser.ExtensionFilter("Audio (OGG, WAV)", "*.ogg", "*.wav"));
|
||||
new FileChooser.ExtensionFilter("Audio (OGG, WAV, MP3)", "*.ogg", "*.wav", "*.mp3"));
|
||||
Path assetRoot = ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||
if (java.nio.file.Files.isDirectory(assetRoot))
|
||||
fc.setInitialDirectory(assetRoot.toFile());
|
||||
@@ -5268,7 +5565,7 @@ public class EditorApp extends Application {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle(trackLabels[tIdx] + " wählen");
|
||||
fc.getExtensionFilters().add(
|
||||
new FileChooser.ExtensionFilter("Audio (OGG, WAV)", "*.ogg", "*.wav"));
|
||||
new FileChooser.ExtensionFilter("Audio (OGG, WAV, MP3)", "*.ogg", "*.wav", "*.mp3"));
|
||||
Path assetRoot = ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||
if (java.nio.file.Files.isDirectory(assetRoot))
|
||||
fc.setInitialDirectory(assetRoot.toFile());
|
||||
@@ -5873,17 +6170,7 @@ public class EditorApp extends Application {
|
||||
animSetClipListView.getItems().addAll(animSet.getClips());
|
||||
animSetClipListView.setPrefHeight(180);
|
||||
|
||||
// alle verfügbaren Clips aus animations/clips/ (unabhängig ob bereits im Set)
|
||||
java.util.List<String> availableClips = new java.util.ArrayList<>();
|
||||
Path clipsDir = ASSET_ROOT.resolve("animations").resolve("clips");
|
||||
if (java.nio.file.Files.isDirectory(clipsDir)) {
|
||||
try (var walk = java.nio.file.Files.walk(clipsDir, 1)) {
|
||||
walk.filter(cp -> cp.toString().endsWith(".j3o"))
|
||||
.map(cp -> cp.getFileName().toString().replaceFirst("\\.j3o$", ""))
|
||||
.sorted()
|
||||
.forEach(availableClips::add);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
Path animRootDir = ASSET_ROOT.resolve("animations");
|
||||
|
||||
Button addClipBtn = new Button("+ Hinzufügen…");
|
||||
Button removeClipBtn = new Button("- Entfernen");
|
||||
@@ -5895,27 +6182,73 @@ public class EditorApp extends Application {
|
||||
.addListener((obs, ov, nv) -> removeClipBtn.setDisable(nv == null));
|
||||
|
||||
addClipBtn.setOnAction(e -> {
|
||||
ComboBox<String> combo = new ComboBox<>();
|
||||
// alle verfügbaren Clips anzeigen, nicht nur unbenutzte
|
||||
combo.getItems().addAll(availableClips);
|
||||
combo.setEditable(true);
|
||||
combo.setMaxWidth(Double.MAX_VALUE);
|
||||
combo.getSelectionModel().selectFirst();
|
||||
org.slf4j.Logger _log = org.slf4j.LoggerFactory.getLogger("AnimSetClipScan");
|
||||
_log.debug("[ClipScan] animRootDir={} exists={}", animRootDir, java.nio.file.Files.isDirectory(animRootDir));
|
||||
|
||||
javafx.scene.control.Dialog<String> dlg = new javafx.scene.control.Dialog<>();
|
||||
dlg.setTitle("Clip hinzufügen");
|
||||
dlg.setHeaderText("Clip aus animations/clips/ wählen:");
|
||||
dlg.getDialogPane().setContent(combo);
|
||||
// Scan bei jedem Klick frisch – gesamter animations/-Ordner, sets/ ausgenommen
|
||||
java.util.List<String> allClips = new java.util.ArrayList<>();
|
||||
if (java.nio.file.Files.isDirectory(animRootDir)) {
|
||||
try (var walk = java.nio.file.Files.walk(animRootDir)) {
|
||||
walk.filter(cp -> {
|
||||
String name = cp.getFileName().toString();
|
||||
boolean isAnim = name.endsWith(".j3o") || name.endsWith(".glb")
|
||||
|| name.endsWith(".gltf") || name.endsWith(".fbx");
|
||||
boolean notSets = !animRootDir.relativize(cp).startsWith("sets");
|
||||
_log.debug("[ClipScan] {} | isAnim={} notSets={}", cp, isAnim, notSets);
|
||||
return isAnim && notSets;
|
||||
})
|
||||
.map(cp -> cp.getFileName().toString().replaceFirst("\\.[^.]+$", ""))
|
||||
.distinct()
|
||||
.sorted()
|
||||
.forEach(allClips::add);
|
||||
} catch (IOException ex) {
|
||||
_log.error("[ClipScan] Walk fehlgeschlagen", ex);
|
||||
allClips.clear();
|
||||
}
|
||||
}
|
||||
_log.debug("[ClipScan] allClips={}", allClips);
|
||||
_log.debug("[ClipScan] bereits im Set={}", animSetClipListView.getItems());
|
||||
|
||||
// Nur Clips anbieten, die noch nicht im Set enthalten sind
|
||||
java.util.List<String> notYetAdded = allClips.stream()
|
||||
.filter(c -> !animSetClipListView.getItems().contains(c))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
_log.debug("[ClipScan] notYetAdded={}", notYetAdded);
|
||||
|
||||
if (notYetAdded.isEmpty()) {
|
||||
javafx.scene.control.Alert info = new javafx.scene.control.Alert(
|
||||
javafx.scene.control.Alert.AlertType.INFORMATION);
|
||||
info.setTitle("Keine Clips verfügbar");
|
||||
info.setHeaderText(null);
|
||||
info.setContentText("Alle verfügbaren Clips sind bereits im Set enthalten.");
|
||||
info.showAndWait();
|
||||
return;
|
||||
}
|
||||
|
||||
ListView<String> list = new ListView<>();
|
||||
list.getItems().addAll(notYetAdded);
|
||||
list.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
|
||||
list.setPrefHeight(Math.min(notYetAdded.size() * 26 + 4, 320));
|
||||
list.getSelectionModel().selectFirst();
|
||||
|
||||
javafx.scene.control.Dialog<java.util.List<String>> dlg = new javafx.scene.control.Dialog<>();
|
||||
dlg.setTitle("Animation(en) hinzufügen");
|
||||
dlg.setHeaderText("Verfügbare Clips (noch nicht im Set) — Mehrfachauswahl möglich:");
|
||||
dlg.getDialogPane().setContent(list);
|
||||
dlg.getDialogPane().setPrefWidth(360);
|
||||
javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType("Hinzufügen",
|
||||
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
|
||||
dlg.getDialogPane().getButtonTypes().addAll(ok, javafx.scene.control.ButtonType.CANCEL);
|
||||
dlg.setResultConverter(bt -> bt == ok ? combo.getValue() : null);
|
||||
dlg.showAndWait().ifPresent(clip -> {
|
||||
if (clip != null && !clip.isBlank()
|
||||
&& !animSetClipListView.getItems().contains(clip)) {
|
||||
dlg.setResultConverter(bt -> bt == ok
|
||||
? new java.util.ArrayList<>(list.getSelectionModel().getSelectedItems())
|
||||
: null);
|
||||
dlg.showAndWait().ifPresent(selected -> {
|
||||
for (String clip : selected) {
|
||||
if (!animSetClipListView.getItems().contains(clip)) {
|
||||
animSetClipListView.getItems().add(clip);
|
||||
animSetDirty = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6026,9 +6359,17 @@ public class EditorApp extends Application {
|
||||
if (animSetClipListView == null) return;
|
||||
String clip = animSetClipListView.getSelectionModel().getSelectedItem();
|
||||
if (clip == null) return;
|
||||
if (animCurrentModelPath == null || animCurrentModelPath.isBlank()) {
|
||||
if (animPreviewStatusLabel != null)
|
||||
animPreviewStatusLabel.setText("Erst ein Charakter-Modell laden.");
|
||||
return;
|
||||
}
|
||||
animSetPendingPlayClip = clip;
|
||||
input.animPreviewLoadPath = "animations/clips/" + clip + ".j3o";
|
||||
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade " + clip + "…");
|
||||
// Clip zur aktuell geladenen Figur hinzufügen (nicht als Modell laden).
|
||||
// Nach Abschluss setzt AnimPreviewState animPreviewClips, das den
|
||||
// animSetPendingPlayClip-Trigger auslöst und den Clip abspielt.
|
||||
input.animPreviewAddAnimPath = "animations/clips/" + clip + ".j3o";
|
||||
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade Clip " + clip + "…");
|
||||
}
|
||||
|
||||
private void showAddActionToSetDialog() {
|
||||
|
||||
@@ -24,6 +24,7 @@ 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.editor.state.VoxelEditorState;
|
||||
import de.blight.game.console.JmeConsole;
|
||||
import de.blight.game.state.DayNightState;
|
||||
import javafx.scene.image.WritableImage;
|
||||
@@ -105,6 +106,7 @@ public class JmeEditorApp extends SimpleApplication {
|
||||
stateManager.attach(new AnimPreviewState(input));
|
||||
stateManager.attach(new ModelEditorState(input));
|
||||
stateManager.attach(new ItemPlacementState(input));
|
||||
stateManager.attach(new VoxelEditorState(input));
|
||||
|
||||
input.loadingStatus = "Initialisiere Konsole...";
|
||||
jmeConsole = new JmeConsole(false);
|
||||
|
||||
@@ -7,6 +7,7 @@ import de.blight.editor.tool.HeightTool;
|
||||
import de.blight.editor.tool.HoleTool;
|
||||
import de.blight.editor.tool.TextureTool;
|
||||
import de.blight.editor.tool.UpperHeightTool;
|
||||
import de.blight.editor.tool.VoxelTool;
|
||||
import de.blight.editor.tree.PalmOptions;
|
||||
import de.blight.editor.tree.TreeParams;
|
||||
import javafx.scene.image.WritableImage;
|
||||
@@ -25,6 +26,7 @@ public class SharedInput {
|
||||
public final GrassVertexTool grassVertexTool = new GrassVertexTool();
|
||||
public final TextureTool textureTool = new TextureTool();
|
||||
public final HoleTool holeTool = new HoleTool();
|
||||
public final VoxelTool voxelTool = new VoxelTool();
|
||||
public volatile EditorTool activeTool = heightTool;
|
||||
|
||||
// ── Initialisierungs-Status ───────────────────────────────────────────────
|
||||
@@ -37,6 +39,12 @@ public class SharedInput {
|
||||
// ── Upper-Layer-Sichtbarkeit ─────────────────────────────────────────────
|
||||
public volatile boolean upperLayerVisible = true;
|
||||
|
||||
// ── Kameramodus ───────────────────────────────────────────────────────────
|
||||
/** 0 = Orbit-Terrain-Kamera (Standard), 1 = FreeFly. */
|
||||
public volatile int camMode = 0;
|
||||
public static final int CAM_ORBIT = 0;
|
||||
public static final int CAM_FREEFLY = 1;
|
||||
|
||||
// ── Kamerabewegung (WASD + QE) ──────────────────────────────────────────
|
||||
public volatile boolean forward, backward, left, right, up, down;
|
||||
|
||||
@@ -277,6 +285,19 @@ public class SharedInput {
|
||||
|
||||
// ── Kamera-Info (JME3-Thread schreibt, JavaFX-Thread liest) ─────────────
|
||||
public volatile float camX = 0f, camY = 0f, camZ = 0f;
|
||||
|
||||
// ── Kamera-Teleport (JavaFX schreibt, JME3-Thread liest und setzt zurück) ─
|
||||
/** NaN = kein Teleport angefordert. Y=NaN → Terrain-Höhe + Offset verwenden. */
|
||||
public volatile float pendingGotoX = Float.NaN;
|
||||
public volatile float pendingGotoY = Float.NaN;
|
||||
public volatile float pendingGotoZ = Float.NaN;
|
||||
/** NaN = Kamera-Pitch unverändert lassen. */
|
||||
public volatile float pendingGotoPitch = Float.NaN;
|
||||
|
||||
// ── Reload-Signale nach Löschung aus MapObjectsView ──────────────────────
|
||||
public volatile boolean reloadPlacedModels = false;
|
||||
public volatile boolean reloadPlacedItems = false;
|
||||
public volatile boolean reloadPlacedOther = false; // Lichter, Emitter, Wasser, Bereiche, Zonen
|
||||
/** Yaw in Grad: 0° = Süden (−Z), 90° = Westen (−X), ±180° = Norden (+Z). */
|
||||
public volatile float camYaw = 0f;
|
||||
/** Pitch in Grad: positiv = Blick nach oben, negativ = nach unten. */
|
||||
@@ -602,6 +623,14 @@ public class SharedInput {
|
||||
public volatile String modelEditorLod2Path = "";
|
||||
public volatile boolean modelEditorLodChanged = false;
|
||||
|
||||
// ── Voxel-Werkzeug ────────────────────────────────────────────────────────
|
||||
/** activeLayer==16 → Voxel-Klippen/Höhlen bearbeiten */
|
||||
public static final int LAYER_VOXEL = 16;
|
||||
|
||||
/** Klick/Drag im Viewport im Voxel-Modus. */
|
||||
public record VoxelEdit(float screenX, float screenY) {}
|
||||
public final ConcurrentLinkedQueue<VoxelEdit> voxelEditQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
// ── Item-Platzierung ──────────────────────────────────────────────────────
|
||||
/** activeLayer==21 → Item-Pickup auf die Karte platzieren */
|
||||
public static final int LAYER_ITEMS = 21;
|
||||
|
||||
@@ -82,25 +82,26 @@ public class ItemPlacementState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
reloadFromDisk();
|
||||
rootNode.attachChild(itemRoot);
|
||||
rootNode.attachChild(previewNode);
|
||||
}
|
||||
|
||||
public void reloadFromDisk() {
|
||||
items.clear();
|
||||
nodes.clear();
|
||||
itemRoot.detachAllChildren();
|
||||
|
||||
try {
|
||||
items.addAll(PlacedItemIO.load());
|
||||
} catch (Exception e) {
|
||||
log.warn("[ItemPlacement] Laden fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
|
||||
for (PlacedItem pi : items) {
|
||||
Node n = buildItemNode(pi.itemId());
|
||||
n.setLocalTranslation(pi.x(), pi.y() + 0.25f, pi.z());
|
||||
itemRoot.attachChild(n);
|
||||
nodes.add(n);
|
||||
}
|
||||
|
||||
rootNode.attachChild(itemRoot);
|
||||
rootNode.attachChild(previewNode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -116,6 +117,10 @@ public class ItemPlacementState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (input.reloadPlacedItems) {
|
||||
input.reloadPlacedItems = false;
|
||||
reloadFromDisk();
|
||||
}
|
||||
updatePreview();
|
||||
|
||||
SharedInput.ObjectClick click;
|
||||
@@ -161,7 +166,7 @@ public class ItemPlacementState extends BaseAppState {
|
||||
if (hits.size() == 0) return;
|
||||
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
PlacedItem pi = new PlacedItem(input.pendingItemId, pt.x, pt.y, pt.z);
|
||||
PlacedItem pi = PlacedItem.create(input.pendingItemId, pt.x, pt.y, pt.z);
|
||||
items.add(pi);
|
||||
|
||||
Node n = buildItemNode(pi.itemId());
|
||||
|
||||
@@ -317,6 +317,11 @@ public class SceneObjectState extends BaseAppState {
|
||||
deleteSelected();
|
||||
}
|
||||
|
||||
if (input.reloadPlacedModels) {
|
||||
input.reloadPlacedModels = false;
|
||||
try { loadPlacedModels(de.blight.common.PlacedModelIO.load()); } catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
// Zusammenfassen
|
||||
if (input.mergeSelectedRequested) {
|
||||
input.mergeSelectedRequested = false;
|
||||
|
||||
@@ -41,6 +41,7 @@ import de.blight.common.PlacedWater;
|
||||
import de.blight.common.RiverPoint;
|
||||
import de.blight.common.SoundAreaIO;
|
||||
import de.blight.common.WaterBodyIO;
|
||||
import de.blight.common.BlightHome;
|
||||
import de.blight.common.MapData;
|
||||
import de.blight.common.MapIO;
|
||||
import de.blight.common.PlacedModelIO;
|
||||
@@ -130,7 +131,7 @@ public class TerrainEditorState extends BaseAppState {
|
||||
private Texture2D upperSplatTex;
|
||||
|
||||
// ── Kameraposition ────────────────────────────────────────────────────────
|
||||
private static final Path EDITOR_PREFS = de.blight.editor.ProjectRoot.resolve("config", "editor.prefs");
|
||||
private static final Path EDITOR_PREFS = BlightHome.resolve("config", "editor.prefs");
|
||||
private static final float DEFAULT_CAM_Y = 50f;
|
||||
private static final float DEFAULT_PITCH = (float) (-Math.PI / 4); // -45°
|
||||
|
||||
@@ -695,6 +696,22 @@ public class TerrainEditorState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
float pgx = input.pendingGotoX;
|
||||
if (!Float.isNaN(pgx)) {
|
||||
input.pendingGotoX = Float.NaN;
|
||||
float pgz = input.pendingGotoZ;
|
||||
float pgy = input.pendingGotoY;
|
||||
if (Float.isNaN(pgy)) {
|
||||
Float h = terrain != null ? terrain.getHeight(new com.jme3.math.Vector2f(pgx, pgz)) : null;
|
||||
pgy = h != null ? h + 20f : camPos.y;
|
||||
}
|
||||
camPos.set(pgx, pgy, pgz);
|
||||
float pp = input.pendingGotoPitch;
|
||||
if (!Float.isNaN(pp)) {
|
||||
input.pendingGotoPitch = Float.NaN;
|
||||
camPitch = FastMath.clamp(pp, -FastMath.HALF_PI + 0.05f, FastMath.HALF_PI - 0.05f);
|
||||
}
|
||||
}
|
||||
updateCamera(tpf);
|
||||
processEdits();
|
||||
processTextureEdits();
|
||||
@@ -729,6 +746,17 @@ public class TerrainEditorState extends BaseAppState {
|
||||
terrain.setMaterial(buildTerrainMaterial());
|
||||
}
|
||||
|
||||
if (input.reloadPlacedOther) {
|
||||
input.reloadPlacedOther = false;
|
||||
try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {}
|
||||
try { if (emitterState != null) emitterState.loadPlacedEmitters(de.blight.common.EmitterIO.load()); } catch (Exception ignored) {}
|
||||
try { if (waterBodyState != null) waterBodyState.loadPlacedBodies(de.blight.common.WaterBodyIO.load()); } catch (Exception ignored) {}
|
||||
try { if (soundAreaState != null) soundAreaState.loadAreas(de.blight.common.SoundAreaIO.load()); } catch (Exception ignored) {}
|
||||
try { if (areaState != null) areaState.loadAreas(de.blight.common.AreaIO.load()); } catch (Exception ignored) {}
|
||||
try { if (locationZoneState != null) locationZoneState.loadZones(de.blight.common.LocationZoneIO.load()); } catch (Exception ignored) {}
|
||||
try { if (riverEditorState != null) riverEditorState.loadPlacedRivers(de.blight.common.RiverIO.load()); } catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
if (input.saveRequested) {
|
||||
input.saveRequested = false;
|
||||
performSave();
|
||||
@@ -814,6 +842,9 @@ public class TerrainEditorState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
/** Gibt den TerrainQuad-Node zurück (z.B. für Voxel-Raycasts). */
|
||||
public TerrainQuad getTerrainNode() { return terrain; }
|
||||
|
||||
/** Gibt die Terrain-Höhe (Welt-Y) an der angegebenen Welt-XZ-Position zurück. */
|
||||
public float getTerrainHeightAt(float worldX, float worldZ) {
|
||||
if (terrain == null) return 0f;
|
||||
@@ -859,7 +890,19 @@ public class TerrainEditorState extends BaseAppState {
|
||||
final List<PlacedArea> areas = areaState != null ? areaState.getPlacedAreas() : null;
|
||||
final List<PlacedLocationZone> locationZones = locationZoneState != null ? locationZoneState.getPlacedZones() : null;
|
||||
|
||||
// ── Schwere Arbeit (Upsample + Datei-I/O) auf Hintergrund-Thread ─────
|
||||
// ── Platzierte Objekte synchron speichern (kleine Textdateien) ──────────
|
||||
// Muss synchron im JME-Thread erfolgen, damit kein Race mit asynchronen
|
||||
// Löschoperationen aus dem JavaFX-Thread entsteht.
|
||||
try { if (models != null) PlacedModelIO.save(models); } catch (IOException e) { log.error("Modelle speichern", e); }
|
||||
try { if (lights != null) LightIO.save(lights); } catch (IOException e) { log.error("Lichter speichern", e); }
|
||||
try { if (emitters != null) EmitterIO.save(emitters); } catch (IOException e) { log.error("Emitter speichern", e); }
|
||||
try { if (waters != null) WaterBodyIO.save(waters); } catch (IOException e) { log.error("Wasser speichern", e); }
|
||||
try { if (rivers != null) de.blight.common.RiverIO.save(rivers); } catch (IOException e) { log.error("Flüsse speichern", e); }
|
||||
try { if (soundAreas != null) SoundAreaIO.save(soundAreas); } catch (IOException e) { log.error("Soundbereiche speichern", e); }
|
||||
try { if (areas != null) AreaIO.save(areas); } catch (IOException e) { log.error("Bereiche speichern", e); }
|
||||
try { if (locationZones != null) LocationZoneIO.save(locationZones); } catch (IOException e) { log.error("Zonen speichern", e); }
|
||||
|
||||
// ── Schwere Arbeit (Terrain-Upsample + Datei-I/O) auf Hintergrund-Thread ─
|
||||
saveExecutor.submit(() -> {
|
||||
try {
|
||||
MapData data = new MapData();
|
||||
@@ -905,14 +948,6 @@ public class TerrainEditorState extends BaseAppState {
|
||||
log.error("Chunk-Export fehlgeschlagen", e);
|
||||
}
|
||||
}
|
||||
if (models != null) PlacedModelIO.save(models);
|
||||
if (lights != null) LightIO.save(lights);
|
||||
if (emitters != null) EmitterIO.save(emitters);
|
||||
if (waters != null) WaterBodyIO.save(waters);
|
||||
if (rivers != null) de.blight.common.RiverIO.save(rivers);
|
||||
if (soundAreas != null) SoundAreaIO.save(soundAreas);
|
||||
if (areas != null) AreaIO.save(areas);
|
||||
if (locationZones != null) LocationZoneIO.save(locationZones);
|
||||
|
||||
input.saveStatusMsg = "Gespeichert: " + MapIO.getMapPath();
|
||||
log.info("{}", input.saveStatusMsg);
|
||||
@@ -941,7 +976,8 @@ public class TerrainEditorState extends BaseAppState {
|
||||
|| layer == SharedInput.LAYER_WATER
|
||||
|| layer == SharedInput.LAYER_SOUND_AREAS || layer == SharedInput.LAYER_AREAS
|
||||
|| layer == SharedInput.LAYER_LOCATION_ZONES
|
||||
|| layer == SharedInput.LAYER_PLAY_TOOL || mx < 0) {
|
||||
|| layer == SharedInput.LAYER_PLAY_TOOL
|
||||
|| layer == SharedInput.LAYER_VOXEL || mx < 0) {
|
||||
brushIndicator.setCullHint(Spatial.CullHint.Always);
|
||||
return;
|
||||
}
|
||||
@@ -1235,6 +1271,16 @@ public class TerrainEditorState extends BaseAppState {
|
||||
float terrainDist = terrainDistBelow();
|
||||
float speed = FastMath.clamp(terrainDist, 5f, CAM_SPEED) * tpf;
|
||||
|
||||
if (input.camMode == SharedInput.CAM_FREEFLY) {
|
||||
Vector3f fwd = cam.getDirection().clone();
|
||||
Vector3f lft = cam.getLeft().clone();
|
||||
if (input.forward) camPos.addLocal(fwd.mult(speed));
|
||||
if (input.backward) camPos.subtractLocal(fwd.mult(speed));
|
||||
if (input.left) camPos.addLocal(lft.mult(speed));
|
||||
if (input.right) camPos.subtractLocal(lft.mult(speed));
|
||||
if (input.up) camPos.y += speed;
|
||||
if (input.down) camPos.y -= speed;
|
||||
} else {
|
||||
float hFwdX = -FastMath.sin(camYaw);
|
||||
float hFwdZ = -FastMath.cos(camYaw);
|
||||
if (input.forward) { camPos.x -= hFwdX * speed; camPos.z -= hFwdZ * speed; }
|
||||
@@ -1247,6 +1293,7 @@ public class TerrainEditorState extends BaseAppState {
|
||||
|
||||
if (input.up) orbitAroundTerrain( ORBIT_SPEED * tpf);
|
||||
if (input.down) orbitAroundTerrain(-ORBIT_SPEED * tpf);
|
||||
}
|
||||
|
||||
int scroll = input.scrollAccum.getAndSet(0);
|
||||
if (scroll != 0)
|
||||
|
||||
@@ -0,0 +1,603 @@
|
||||
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.CollisionResult;
|
||||
import com.jme3.collision.CollisionResults;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.math.FastMath;
|
||||
import com.jme3.math.Ray;
|
||||
import com.jme3.math.Vector2f;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.Camera;
|
||||
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.terrain.geomipmap.TerrainQuad;
|
||||
import com.jme3.texture.Texture;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.common.VoxelChunk;
|
||||
import de.blight.common.VoxelChunkIO;
|
||||
import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.state.TerrainEditorState;
|
||||
import de.blight.game.state.MarchingCubes;
|
||||
import de.blight.game.state.VoxelChunkNode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* Editor-AppState für das Voxel-Werkzeug.
|
||||
*
|
||||
* Verwaltet alle im Editor geladenen VoxelChunks (in-memory HashMap +
|
||||
* gerenderte VoxelChunkNodes), verarbeitet Edit-Events aus der SharedInput-Queue
|
||||
* und kümmert sich um Auto-Save und Hintergrund-LOD-Berechnung.
|
||||
*/
|
||||
public class VoxelEditorState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(VoxelEditorState.class);
|
||||
|
||||
/** Maximale Edits pro Frame. */
|
||||
private static final int MAX_EDITS_PER_FRAME = 4;
|
||||
/** Idle-Zeit in Sekunden, nach der LOD1/2 im Hintergrund gebaut werden. */
|
||||
private static final float LOD_REBUILD_IDLE_S = 0.5f;
|
||||
/** Idle-Zeit in Sekunden für Auto-Save. */
|
||||
private static final float AUTO_SAVE_IDLE_S = 3.0f;
|
||||
|
||||
// ── JME-Zustand ──────────────────────────────────────────────────────────
|
||||
|
||||
private SimpleApplication app;
|
||||
private AssetManager assets;
|
||||
private Camera cam;
|
||||
|
||||
/** Root-Node für alle VoxelChunkNodes. */
|
||||
private Node voxelRoot;
|
||||
/** Wird von TerrainEditorState gesetzt; wird als Raycast-Ziel genutzt. */
|
||||
private Node terrainNode;
|
||||
private TerrainQuad terrainQuad;
|
||||
|
||||
private final SharedInput input;
|
||||
|
||||
// ── Chunk-Verwaltung ─────────────────────────────────────────────────────
|
||||
|
||||
/** key → VoxelChunk (in-memory, ggf. dirty). */
|
||||
private final Map<Long, VoxelChunk> chunks = new HashMap<>();
|
||||
/** key → zugehöriger VoxelChunkNode in der Szene. */
|
||||
private final Map<Long, VoxelChunkNode> nodes = new HashMap<>();
|
||||
|
||||
// ── Timers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/** Sekunden seit letztem Edit. */
|
||||
private float idleSinceEdit = 0f;
|
||||
/** Sekunden seit letztem Save. */
|
||||
private float idleSinceSave = 0f;
|
||||
/** LOD-Rebuild ist für diesen Frame angefordert. */
|
||||
private boolean lodRebuildPending = false;
|
||||
|
||||
// ── Hintergrund-Executor ─────────────────────────────────────────────────
|
||||
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r, "VoxelEditor-BG");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
// ── Material ─────────────────────────────────────────────────────────────
|
||||
|
||||
private Material voxelMaterial;
|
||||
|
||||
// ── Brush-Indikator ───────────────────────────────────────────────────────
|
||||
|
||||
private Geometry brushIndicator;
|
||||
|
||||
// ── LOD-Rebuild-Queue ─────────────────────────────────────────────────────
|
||||
|
||||
/** Chunks, die LOD1/2 neu brauchen. Wird im Hintergrund-Thread abgearbeitet. */
|
||||
private final ConcurrentLinkedQueue<Long> lodRebuildQueue = new ConcurrentLinkedQueue<>();
|
||||
/** Chunks mit fertigen LOD1/2-Meshes, die im JME-Thread übernommen werden. */
|
||||
private final ConcurrentLinkedQueue<Runnable> lodResultQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||
|
||||
public VoxelEditorState(SharedInput input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
// ── AppState-Lifecycle ────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
this.app = (SimpleApplication) application;
|
||||
this.assets = app.getAssetManager();
|
||||
this.cam = app.getCamera();
|
||||
|
||||
voxelRoot = new Node("voxelEditorRoot");
|
||||
app.getRootNode().attachChild(voxelRoot);
|
||||
|
||||
// TerrainEditorState wurde vor uns attached und ist zu diesem Zeitpunkt initialisiert
|
||||
TerrainEditorState tes = app.getStateManager().getState(TerrainEditorState.class);
|
||||
if (tes != null) {
|
||||
terrainNode = tes.getTerrainNode();
|
||||
terrainQuad = terrainNode instanceof TerrainQuad tq ? tq : null;
|
||||
}
|
||||
|
||||
voxelMaterial = buildMaterial();
|
||||
|
||||
brushIndicator = buildBrushIndicator();
|
||||
app.getRootNode().attachChild(brushIndicator);
|
||||
|
||||
// Alle vorhandenen .blvc-Dateien laden
|
||||
List<VoxelChunk> loaded = VoxelChunkIO.loadAll();
|
||||
for (VoxelChunk chunk : loaded) {
|
||||
long key = chunkKey(chunk.cx, chunk.cy, chunk.cz);
|
||||
chunks.put(key, chunk);
|
||||
addNodeForChunk(key, chunk);
|
||||
}
|
||||
log.info("VoxelEditorState: {} Chunks geladen.", loaded.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application app) {
|
||||
executor.shutdownNow();
|
||||
voxelRoot.removeFromParent();
|
||||
if (brushIndicator != null) brushIndicator.removeFromParent();
|
||||
nodes.clear();
|
||||
chunks.clear();
|
||||
}
|
||||
|
||||
@Override protected void onEnable() {}
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
// LOD-Ergebnisse aus dem Hintergrund-Thread übernehmen
|
||||
Runnable r;
|
||||
while ((r = lodResultQueue.poll()) != null) r.run();
|
||||
|
||||
// Brush-Indikator immer aktualisieren (zeigen/verstecken je nach Layer)
|
||||
updateBrushIndicator();
|
||||
|
||||
// Nur aktiv wenn LAYER_VOXEL gesetzt
|
||||
if (input.activeLayer != SharedInput.LAYER_VOXEL) {
|
||||
idleSinceEdit = 0f;
|
||||
idleSinceSave += tpf;
|
||||
checkAutoSave();
|
||||
return;
|
||||
}
|
||||
|
||||
// Edit-Queue verarbeiten (max. MAX_EDITS_PER_FRAME)
|
||||
int processed = 0;
|
||||
SharedInput.VoxelEdit edit;
|
||||
while (processed < MAX_EDITS_PER_FRAME
|
||||
&& (edit = input.voxelEditQueue.poll()) != null) {
|
||||
handleEdit(edit);
|
||||
processed++;
|
||||
idleSinceEdit = 0f;
|
||||
idleSinceSave = 0f;
|
||||
}
|
||||
|
||||
if (processed == 0) {
|
||||
idleSinceEdit += tpf;
|
||||
idleSinceSave += tpf;
|
||||
}
|
||||
|
||||
// LOD1/2 nach Idle-Zeit im Hintergrund bauen
|
||||
if (idleSinceEdit >= LOD_REBUILD_IDLE_S && !lodRebuildPending) {
|
||||
lodRebuildPending = true;
|
||||
scheduleLodRebuild();
|
||||
}
|
||||
|
||||
checkAutoSave();
|
||||
}
|
||||
|
||||
// ── Öffentliche API ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Wird von TerrainEditorState gesetzt, damit VoxelEditorState Raycasts
|
||||
* gegen das Terrain durchführen kann.
|
||||
*/
|
||||
public void setTerrainNode(Node terrain) {
|
||||
this.terrainNode = terrain;
|
||||
this.terrainQuad = terrain instanceof TerrainQuad tq ? tq : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert alle dirty Chunks synchron.
|
||||
* Kann von außen aufgerufen werden (z.B. beim globalen Speichern).
|
||||
*/
|
||||
public void saveAll() {
|
||||
for (VoxelChunk chunk : chunks.values()) {
|
||||
if (!chunk.dirty) continue;
|
||||
try {
|
||||
VoxelChunkIO.save(chunk);
|
||||
} catch (IOException e) {
|
||||
log.error("Fehler beim Speichern von VoxelChunk ({},{},{}): {}",
|
||||
chunk.cx, chunk.cy, chunk.cz, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Intern: Edit verarbeiten ───────────────────────────────────────────────
|
||||
|
||||
private void handleEdit(SharedInput.VoxelEdit edit) {
|
||||
float jmeX = edit.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - edit.screenY() * (float) input.viewportScaleY;
|
||||
Vector3f worldPos = raycast(jmeX, jmeY);
|
||||
if (worldPos == null) return;
|
||||
applyEdit(worldPos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt einen Raycast durch und gibt den nächstgelegenen Treffpunkt zurück,
|
||||
* oder null wenn kein Treffer.
|
||||
*/
|
||||
private Vector3f raycast(float screenX, float screenY) {
|
||||
Vector2f click2d = new Vector2f(screenX, screenY);
|
||||
Vector3f origin = cam.getWorldCoordinates(click2d, 0f);
|
||||
Vector3f target = cam.getWorldCoordinates(click2d, 1f);
|
||||
Vector3f dir = target.subtract(origin).normalizeLocal();
|
||||
Ray ray = new Ray(origin, dir);
|
||||
|
||||
CollisionResults results = new CollisionResults();
|
||||
float bestDist = Float.MAX_VALUE;
|
||||
Vector3f best = null;
|
||||
|
||||
// Terrain
|
||||
if (terrainNode != null) {
|
||||
terrainNode.collideWith(ray, results);
|
||||
if (results.size() > 0) {
|
||||
CollisionResult cr = results.getClosestCollision();
|
||||
if (cr.getDistance() < bestDist) {
|
||||
bestDist = cr.getDistance();
|
||||
best = cr.getContactPoint();
|
||||
}
|
||||
results.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Voxel-Geometrie
|
||||
voxelRoot.collideWith(ray, results);
|
||||
if (results.size() > 0) {
|
||||
CollisionResult cr = results.getClosestCollision();
|
||||
if (cr.getDistance() < bestDist) {
|
||||
best = cr.getContactPoint();
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wendet den Pinsel auf alle betroffenen Chunks an.
|
||||
*
|
||||
* Modi 0-3: Solid-Voxel hinzufügen (verschiedene Falloff-Kurven), danach alle Voxel
|
||||
* unterhalb der Terrain-Oberfläche in der Pinselfläche entfernen.
|
||||
* Modus 4 (Entfernen): Voxel löschen – für Höhlen unterhalb des Terrains.
|
||||
*/
|
||||
private void applyEdit(Vector3f worldPos) {
|
||||
float radius = (float) input.voxelTool.brushRadius.getValue();
|
||||
float strength = (float) input.voxelTool.brushStrength.getValue();
|
||||
int modeIdx = input.voxelTool.mode.getSelectedIndex();
|
||||
int texSlot = input.voxelTool.textureSlot.getSelectedIndex();
|
||||
byte matId = (byte)(texSlot & 3);
|
||||
boolean remove = (modeIdx == de.blight.editor.tool.VoxelTool.MODE_REMOVE);
|
||||
boolean addFlat = (modeIdx == de.blight.editor.tool.VoxelTool.MODE_ADD);
|
||||
|
||||
float wx = worldPos.x, wy = worldPos.y, wz = worldPos.z;
|
||||
|
||||
int cxMin = VoxelChunk.worldXToCx(wx - radius);
|
||||
int cxMax = VoxelChunk.worldXToCx(wx + radius);
|
||||
int czMin = VoxelChunk.worldZToCz(wz - radius);
|
||||
int czMax = VoxelChunk.worldZToCz(wz + radius);
|
||||
// Spalten-Modi bauen nach oben bis zu strength Voxel → Y-Range entsprechend erweitern
|
||||
int cyMin = VoxelChunk.worldYToCy(wy - radius);
|
||||
int cyMax = (!remove && !addFlat)
|
||||
? VoxelChunk.worldYToCy(wy + strength)
|
||||
: VoxelChunk.worldYToCy(wy + radius);
|
||||
|
||||
for (int cz = czMin; cz <= czMax; cz++) {
|
||||
for (int cx = cxMin; cx <= cxMax; cx++) {
|
||||
for (int cy = cyMin; cy <= cyMax; cy++) {
|
||||
VoxelChunkNode node = getOrCreateNode(cx, cy, cz);
|
||||
VoxelChunk chunk = node.getChunk();
|
||||
|
||||
float lx = VoxelChunk.worldXToLocal(wx, cx);
|
||||
float ly = VoxelChunk.worldYToLocal(wy, cy);
|
||||
float lz = VoxelChunk.worldZToLocal(wz, cz);
|
||||
|
||||
if (remove) {
|
||||
chunk.applyBrush(lx, ly, lz, radius, Byte.MIN_VALUE, (byte)0);
|
||||
} else if (addFlat) {
|
||||
byte d = (byte) Math.min(127, (int) strength);
|
||||
chunk.applyBrush(lx, ly, lz, radius, d, matId);
|
||||
} else {
|
||||
applyAddBrush(chunk, lx, ly, lz, radius, strength, modeIdx, matId);
|
||||
clearVoxelsBelowTerrain(chunk, cx, cy, cz, wx, wz, radius);
|
||||
}
|
||||
|
||||
node.rebuildMesh(0);
|
||||
node.setActiveLod(0);
|
||||
|
||||
long key = chunkKey(cx, cy, cz);
|
||||
if (!lodRebuildQueue.contains(key)) lodRebuildQueue.add(key);
|
||||
lodRebuildPending = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spalten-Pinsel: Für jede XZ-Position im Pinselradius wird eine Voxel-Säule
|
||||
* nach oben aufgebaut. Die Säulenhöhe ist proportional zum mode-spezifischen Falloff:
|
||||
* Spike → steile Spitze in der Mitte
|
||||
* Plateau → gleichmäßige flache Erhöhung
|
||||
* Sinus → sanfte Kuppel
|
||||
* Smooth → weiche raised-cosine Kuppel
|
||||
*/
|
||||
private void applyAddBrush(VoxelChunk chunk,
|
||||
float lx, float ly, float lz,
|
||||
float radius, float strength,
|
||||
int mode, byte matId) {
|
||||
int x0 = Math.max(0, (int)(lx - radius));
|
||||
int x1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lx + radius));
|
||||
int z0 = Math.max(0, (int)(lz - radius));
|
||||
int z1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lz + radius));
|
||||
float r2 = radius * radius;
|
||||
byte d = (byte) Math.min(127, (int) strength);
|
||||
// Basiszeile innerhalb dieses Chunks (kann negativ sein für höhere Chunks)
|
||||
int baseY = (int) ly;
|
||||
|
||||
for (int z = z0; z <= z1; z++) {
|
||||
float dz = z - lz;
|
||||
for (int x = x0; x <= x1; x++) {
|
||||
float dx = x - lx;
|
||||
float d2 = dx*dx + dz*dz;
|
||||
if (d2 > r2) continue;
|
||||
|
||||
float t = (float) Math.sqrt(d2) / radius; // 0=Mitte, 1=Rand
|
||||
float falloff;
|
||||
switch (mode) {
|
||||
case de.blight.editor.tool.VoxelTool.MODE_SINUS ->
|
||||
falloff = (float) Math.cos(t * Math.PI / 2);
|
||||
case de.blight.editor.tool.VoxelTool.MODE_SPIKE ->
|
||||
falloff = (1f - t) * (1f - t);
|
||||
case de.blight.editor.tool.VoxelTool.MODE_SMOOTH ->
|
||||
falloff = (float)(0.5 * (1 + Math.cos(t * Math.PI)));
|
||||
default -> // PLATEAU
|
||||
falloff = 1f;
|
||||
}
|
||||
|
||||
int colHeight = (int)(strength * falloff);
|
||||
if (colHeight < 1) continue;
|
||||
|
||||
// Säule von baseY bis baseY+colHeight, geclampt auf Chunk-Grenzen
|
||||
int yBottom = Math.max(0, baseY);
|
||||
int yTop = Math.min(VoxelChunk.SIZE - 1, baseY + colHeight);
|
||||
for (int y = yBottom; y <= yTop; y++) {
|
||||
if (chunk.getDensity(x, y, z) <= 0) {
|
||||
chunk.setDensity(x, y, z, d);
|
||||
chunk.setMaterial(x, y, z, matId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt alle Solid-Voxel in der X/Z-Fußfläche des Pinsels, die unterhalb der
|
||||
* aktuellen Terrain-Oberfläche liegen.
|
||||
*/
|
||||
private void clearVoxelsBelowTerrain(VoxelChunk chunk, int cx, int cy, int cz,
|
||||
float brushWx, float brushWz, float radius) {
|
||||
if (terrainQuad == null || chunk.isEmpty()) return;
|
||||
|
||||
float lxC = VoxelChunk.worldXToLocal(brushWx, cx);
|
||||
float lzC = VoxelChunk.worldZToLocal(brushWz, cz);
|
||||
int x0 = Math.max(0, (int)(lxC - radius));
|
||||
int x1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lxC + radius));
|
||||
int z0 = Math.max(0, (int)(lzC - radius));
|
||||
int z1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lzC + radius));
|
||||
|
||||
for (int lz = z0; lz <= z1; lz++) {
|
||||
float worldZ = VoxelChunk.toWorldZ(cz, lz);
|
||||
for (int lx = x0; lx <= x1; lx++) {
|
||||
float worldX = VoxelChunk.toWorldX(cx, lx);
|
||||
Float h = terrainQuad.getHeight(new Vector2f(worldX, worldZ));
|
||||
if (h == null) continue;
|
||||
|
||||
for (int ly = 0; ly < VoxelChunk.SIZE; ly++) {
|
||||
if (chunk.getDensity(lx, ly, lz) <= 0) continue;
|
||||
if (VoxelChunk.toWorldY(cy, ly) <= h) {
|
||||
chunk.setDensity(lx, ly, lz, Byte.MIN_VALUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Intern: Chunk/Node-Verwaltung ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Gibt den VoxelChunkNode für (cx,cy,cz) zurück.
|
||||
* Legt neuen Chunk und Node an, falls noch nicht vorhanden.
|
||||
*/
|
||||
private VoxelChunkNode getOrCreateNode(int cx, int cy, int cz) {
|
||||
long key = chunkKey(cx, cy, cz);
|
||||
VoxelChunkNode node = nodes.get(key);
|
||||
if (node != null) return node;
|
||||
|
||||
// Ggf. aus Datei laden
|
||||
VoxelChunk chunk;
|
||||
if (VoxelChunkIO.exists(cx, cy, cz)) {
|
||||
try {
|
||||
chunk = VoxelChunkIO.load(cx, cy, cz);
|
||||
} catch (IOException e) {
|
||||
log.warn("Chunk ({},{},{}) laden fehlgeschlagen, neu erstellt: {}",
|
||||
cx, cy, cz, e.getMessage());
|
||||
chunk = new VoxelChunk(cx, cy, cz);
|
||||
}
|
||||
} else {
|
||||
chunk = new VoxelChunk(cx, cy, cz);
|
||||
}
|
||||
|
||||
chunks.put(key, chunk);
|
||||
node = addNodeForChunk(key, chunk);
|
||||
return node;
|
||||
}
|
||||
|
||||
/** Erstellt den VoxelChunkNode und hängt ihn in den Szenen-Graph ein. */
|
||||
private VoxelChunkNode addNodeForChunk(long key, VoxelChunk chunk) {
|
||||
VoxelChunkNode node = new VoxelChunkNode(chunk, voxelMaterial);
|
||||
// LOD0 bauen
|
||||
node.rebuildMesh(0);
|
||||
node.setActiveLod(0);
|
||||
voxelRoot.attachChild(node);
|
||||
nodes.put(key, node);
|
||||
// LOD1/2 für Hintergrund vormerken
|
||||
lodRebuildQueue.add(key);
|
||||
return node;
|
||||
}
|
||||
|
||||
// ── Intern: Hintergrund-LOD ───────────────────────────────────────────────
|
||||
|
||||
private void scheduleLodRebuild() {
|
||||
executor.submit(() -> {
|
||||
Long key;
|
||||
while ((key = lodRebuildQueue.poll()) != null) {
|
||||
VoxelChunk chunk = chunks.get(key);
|
||||
if (chunk == null) continue;
|
||||
// LOD1 und LOD2 berechnen (kein JME-Context nötig – nur reine Berechnungen)
|
||||
var mesh1 = MarchingCubes.build(chunk, 4);
|
||||
var mesh2 = MarchingCubes.build(chunk, 16);
|
||||
final Long fKey = key;
|
||||
// Fertige Meshes an JME-Thread übergeben (kein zweites Build!)
|
||||
lodResultQueue.add(() -> {
|
||||
VoxelChunkNode node = nodes.get(fKey);
|
||||
if (node == null) return;
|
||||
if (mesh1 != null) node.setLodMesh(1, mesh1);
|
||||
if (mesh2 != null) node.setLodMesh(2, mesh2);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Intern: Auto-Save ─────────────────────────────────────────────────────
|
||||
|
||||
private void checkAutoSave() {
|
||||
if (idleSinceSave < AUTO_SAVE_IDLE_S) return;
|
||||
boolean anyDirty = false;
|
||||
for (VoxelChunk c : chunks.values()) { if (c.dirty) { anyDirty = true; break; } }
|
||||
if (!anyDirty) return;
|
||||
|
||||
idleSinceSave = 0f;
|
||||
executor.submit(() -> {
|
||||
for (VoxelChunk chunk : chunks.values()) {
|
||||
if (!chunk.dirty) continue;
|
||||
try {
|
||||
VoxelChunkIO.save(chunk);
|
||||
} catch (IOException e) {
|
||||
log.error("Auto-Save VoxelChunk ({},{},{}): {}",
|
||||
chunk.cx, chunk.cy, chunk.cz, e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Intern: Material ─────────────────────────────────────────────────────
|
||||
|
||||
private Material buildMaterial() {
|
||||
Material mat = new Material(assets, "MatDefs/Voxel.j3md");
|
||||
mat.setFloat("TexScale", 4f);
|
||||
|
||||
String[] texPaths = input.terrainTexturePaths;
|
||||
String[] slotNames = {"Tex0", "Tex1", "Tex2", "Tex3"};
|
||||
for (int i = 0; i < slotNames.length; i++) {
|
||||
String path = (i < texPaths.length && texPaths[i] != null && !texPaths[i].isEmpty())
|
||||
? texPaths[i] : "Common/Textures/MissingTexture.png";
|
||||
try {
|
||||
Texture t = assets.loadTexture(path);
|
||||
t.setWrap(Texture.WrapMode.Repeat);
|
||||
mat.setTexture(slotNames[i], t);
|
||||
} catch (Exception e) {
|
||||
log.warn("VoxelEditorState: Textur {} ({}) nicht ladbar: {}", slotNames[i], path, e.getMessage());
|
||||
}
|
||||
}
|
||||
return mat;
|
||||
}
|
||||
|
||||
// ── Intern: Brush-Indikator ───────────────────────────────────────────────
|
||||
|
||||
private void updateBrushIndicator() {
|
||||
if (brushIndicator == null) return;
|
||||
if (input.activeLayer != SharedInput.LAYER_VOXEL) {
|
||||
brushIndicator.setCullHint(Spatial.CullHint.Always);
|
||||
return;
|
||||
}
|
||||
float mx = input.mouseScreenX;
|
||||
float my = input.mouseScreenY;
|
||||
if (mx < 0) {
|
||||
brushIndicator.setCullHint(Spatial.CullHint.Always);
|
||||
return;
|
||||
}
|
||||
float jmeX = mx * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - my * (float) input.viewportScaleY;
|
||||
Vector3f pos = raycast(jmeX, jmeY);
|
||||
if (pos != null) {
|
||||
float r = (float) input.voxelTool.brushRadius.getValue();
|
||||
brushIndicator.setLocalTranslation(pos.x, pos.y + 0.3f, pos.z);
|
||||
brushIndicator.setLocalScale(r, 1f, r);
|
||||
brushIndicator.setCullHint(Spatial.CullHint.Inherit);
|
||||
} else {
|
||||
brushIndicator.setCullHint(Spatial.CullHint.Always);
|
||||
}
|
||||
}
|
||||
|
||||
private Geometry buildBrushIndicator() {
|
||||
int segments = 32;
|
||||
FloatBuffer pos = BufferUtils.createFloatBuffer((segments + 1) * 3);
|
||||
pos.put(0f).put(0f).put(0f);
|
||||
for (int i = 0; i < segments; i++) {
|
||||
float a = FastMath.TWO_PI * i / segments;
|
||||
pos.put(FastMath.cos(a)).put(0f).put(FastMath.sin(a));
|
||||
}
|
||||
IntBuffer idx = BufferUtils.createIntBuffer(segments * 3);
|
||||
for (int i = 0; i < segments; i++) {
|
||||
idx.put(0);
|
||||
idx.put(1 + i);
|
||||
idx.put(1 + (i + 1) % segments);
|
||||
}
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
|
||||
mesh.setBuffer(VertexBuffer.Type.Index, 3, idx);
|
||||
mesh.updateBound();
|
||||
|
||||
Geometry geo = new Geometry("voxelBrushIndicator", mesh);
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(0.2f, 0.6f, 1f, 0.4f));
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
mat.getAdditionalRenderState().setDepthTest(false);
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||
geo.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
geo.setMaterial(mat);
|
||||
geo.setCullHint(Spatial.CullHint.Always);
|
||||
return geo;
|
||||
}
|
||||
|
||||
// ── Hilfsmethode ─────────────────────────────────────────────────────────
|
||||
|
||||
private static long chunkKey(int cx, int cy, int cz) {
|
||||
return ((long)(cx & 0xFFFF)) | (((long)(cy & 0xFFFF)) << 16) | (((long)(cz & 0xFFFF)) << 32);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package de.blight.editor.tool;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Voxel-Klippen/Höhlen-Werkzeug.
|
||||
*
|
||||
* Modi 0-3 fügen Solid-Voxel oberhalb des Terrains hinzu (gleiche Pinselformen wie das Höhen-Tool).
|
||||
* Voxel, die unterhalb der Terrain-Oberfläche landen, werden automatisch entfernt.
|
||||
* Modus 4 entfernt Voxel – zum Aushöhlen von Höhlen unterhalb des Terrains.
|
||||
*/
|
||||
public class VoxelTool extends EditorTool {
|
||||
|
||||
public static final int MODE_SINUS = 0;
|
||||
public static final int MODE_SPIKE = 1;
|
||||
public static final int MODE_PLATEAU = 2;
|
||||
public static final int MODE_SMOOTH = 3;
|
||||
public static final int MODE_REMOVE = 4;
|
||||
/** Einfaches Hinzufügen: gleichmäßige Dichte überall im Pinsel, kein Terrain-Check. */
|
||||
public static final int MODE_ADD = 5;
|
||||
|
||||
public final ChoiceToolParameter mode = new ChoiceToolParameter(
|
||||
"Modus",
|
||||
new String[]{"Sinus", "Spike", "Plateau", "Smooth", "Entfernen", "Hinzufügen"},
|
||||
MODE_SPIKE,
|
||||
new String[]{
|
||||
"img/editor/terraintool_sinus.png",
|
||||
"img/editor/terraintool_spike.png",
|
||||
"img/editor/terraintool_plateau.png",
|
||||
"img/editor/terraintool_smooth.png",
|
||||
null,
|
||||
null
|
||||
}
|
||||
);
|
||||
|
||||
public final ChoiceToolParameter textureSlot = new ChoiceToolParameter(
|
||||
"Textur", new String[]{"S1", "S2", "S3", "S4"}, 0
|
||||
);
|
||||
|
||||
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 5.0, 0.5, 30.0);
|
||||
public final ToolParameter brushStrength = new ToolParameter("Stärke", 20.0, 1.0, 80.0);
|
||||
|
||||
@Override public String getName() { return "Voxel"; }
|
||||
|
||||
@Override
|
||||
public List<ChoiceToolParameter> getChoiceParameters() {
|
||||
return List.of(mode, textureSlot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ToolParameter> getParameters() {
|
||||
return List.of(brushRadius, brushStrength);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.model.CharacterStat;
|
||||
import de.blight.common.model.ConsumableEffect;
|
||||
import de.blight.common.model.Item;
|
||||
import de.blight.common.model.ItemCategory;
|
||||
import de.blight.common.model.ItemCategoryManager;
|
||||
import de.blight.common.model.ItemIO;
|
||||
import de.blight.common.model.ItemSubCategory;
|
||||
import de.blight.common.model.ObjectReference;
|
||||
import de.blight.common.model.TextReference;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -17,6 +21,7 @@ import javafx.scene.paint.Color;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -35,12 +40,16 @@ public class ItemEditorView extends BorderPane {
|
||||
// Form-Felder
|
||||
private TextField idField;
|
||||
private ComboBox<ItemCategory> catCombo;
|
||||
private ComboBox<ItemSubCategory> subCatCombo;
|
||||
private TextField nameField;
|
||||
private TextField descField;
|
||||
private Spinner<Integer> goldSpinner;
|
||||
private TextField modelRefField;
|
||||
private VBox formContainer;
|
||||
private Button deleteBtn;
|
||||
private CheckBox consumableCheck;
|
||||
private VBox consumablesSection;
|
||||
private VBox effectsRows;
|
||||
|
||||
public ItemEditorView(Path itemDir) {
|
||||
this.itemDir = itemDir;
|
||||
@@ -146,8 +155,8 @@ public class ItemEditorView extends BorderPane {
|
||||
case GEAR -> "#5588cc";
|
||||
case CONSUMABLES -> "#55aa55";
|
||||
case QUEST_ITEMS -> "#ccaa33";
|
||||
case USABLES -> "#aa55cc";
|
||||
case MISC -> "#778899";
|
||||
case CRAFTING_ITEMS -> "#aa55cc";
|
||||
case MISC_ITEMS -> "#778899";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -200,6 +209,36 @@ public class ItemEditorView extends BorderPane {
|
||||
}
|
||||
});
|
||||
|
||||
subCatCombo = new ComboBox<>();
|
||||
subCatCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
subCatCombo.setDisable(true);
|
||||
subCatCombo.setPromptText("Erst Kategorie wählen");
|
||||
subCatCombo.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(ItemSubCategory sub, boolean empty) {
|
||||
super.updateItem(sub, empty);
|
||||
setText(empty || sub == null ? null : sub.name());
|
||||
}
|
||||
});
|
||||
subCatCombo.setButtonCell(new ListCell<>() {
|
||||
@Override protected void updateItem(ItemSubCategory sub, boolean empty) {
|
||||
super.updateItem(sub, empty);
|
||||
setText(empty || sub == null ? null : sub.name());
|
||||
}
|
||||
});
|
||||
|
||||
// Wenn Kategorie wechselt: SubKategorie-Liste aktualisieren
|
||||
catCombo.valueProperty().addListener((obs, oldCat, newCat) -> {
|
||||
ItemSubCategory prevSub = subCatCombo.getValue();
|
||||
subCatCombo.getItems().setAll(
|
||||
newCat != null ? ItemCategoryManager.getSubCategories(newCat) : java.util.List.of());
|
||||
subCatCombo.setDisable(newCat == null || subCatCombo.getItems().isEmpty());
|
||||
if (prevSub != null && subCatCombo.getItems().contains(prevSub)) {
|
||||
subCatCombo.setValue(prevSub);
|
||||
} else {
|
||||
subCatCombo.setValue(null);
|
||||
}
|
||||
});
|
||||
|
||||
nameField = new TextField();
|
||||
nameField.setPromptText("TextReference-Schlüssel");
|
||||
descField = new TextField();
|
||||
@@ -230,6 +269,26 @@ public class ItemEditorView extends BorderPane {
|
||||
HBox.setHgrow(modelRefField, Priority.ALWAYS);
|
||||
modelFullRow.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
// ── Consumables-Sektion ───────────────────────────────────────────────
|
||||
consumableCheck = new CheckBox("Konsumierbar");
|
||||
consumableCheck.setStyle("-fx-text-fill: #aaa;");
|
||||
|
||||
effectsRows = new VBox(4);
|
||||
Button addEffectBtn = new Button("+ Effekt hinzufügen");
|
||||
addEffectBtn.setStyle("-fx-background-color: #2a4a2a; -fx-text-fill: #aaffaa; -fx-cursor: hand;");
|
||||
addEffectBtn.setOnAction(e -> addEffectRow(null, 0));
|
||||
consumablesSection = new VBox(6,
|
||||
sectionTitle("Effekte beim Benutzen"),
|
||||
effectsRows,
|
||||
addEffectBtn
|
||||
);
|
||||
consumablesSection.setVisible(false);
|
||||
consumablesSection.setManaged(false);
|
||||
consumableCheck.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
|
||||
consumablesSection.setVisible(isSelected);
|
||||
consumablesSection.setManaged(isSelected);
|
||||
});
|
||||
|
||||
Button saveBtn = new Button("Item speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
@@ -240,6 +299,7 @@ public class ItemEditorView extends BorderPane {
|
||||
new Separator(),
|
||||
row("Item-ID:", idField),
|
||||
row("Kategorie:", catCombo),
|
||||
row("SubKategorie:", subCatCombo),
|
||||
new Separator(),
|
||||
sectionTitle("Texte"),
|
||||
row("Name:", nameField),
|
||||
@@ -249,6 +309,9 @@ public class ItemEditorView extends BorderPane {
|
||||
row("Wert (Gold):", goldSpinner),
|
||||
row("Modell:", modelFullRow),
|
||||
new Separator(),
|
||||
consumableCheck,
|
||||
consumablesSection,
|
||||
new Separator(),
|
||||
saveBtn
|
||||
);
|
||||
return form;
|
||||
@@ -285,31 +348,54 @@ public class ItemEditorView extends BorderPane {
|
||||
|
||||
private void loadFormFromItem(Item item) {
|
||||
idField.setText(safe(item.getItemId()));
|
||||
catCombo.setValue(item.getCategory());
|
||||
catCombo.setValue(item.getCategory()); // löst Listener aus → subCatCombo befüllt
|
||||
subCatCombo.setValue(item.getSubCategory());
|
||||
nameField.setText(item.getName() != null ? item.getName().id() : "");
|
||||
descField.setText(item.getDescription() != null ? item.getDescription().id() : "");
|
||||
goldSpinner.getValueFactory().setValue(item.getWorthGold());
|
||||
ObjectReference ref = item.getModelRef();
|
||||
modelRefField.setText(ref != null && ref.getPath() != null ? ref.getPath() : "");
|
||||
consumableCheck.setSelected(item.isConsumable());
|
||||
effectsRows.getChildren().clear();
|
||||
if (item.getEffects() != null) {
|
||||
for (ConsumableEffect effect : item.getEffects()) {
|
||||
addEffectRow(effect.getStat(), effect.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveFormToItem(Item item) {
|
||||
item.setItemId(idField.getText().trim());
|
||||
item.setCategory(catCombo.getValue());
|
||||
item.setSubCategory(subCatCombo.getValue());
|
||||
item.setName(ref(nameField));
|
||||
item.setDescription(ref(descField));
|
||||
item.setWorthGold(goldSpinner.getValue());
|
||||
String mr = modelRefField.getText().trim();
|
||||
item.setModelRef(mr.isBlank() ? null : new ObjectReference(mr));
|
||||
item.setConsumable(consumableCheck.isSelected());
|
||||
List<ConsumableEffect> effects = new ArrayList<>();
|
||||
for (Node node : effectsRows.getChildren()) {
|
||||
if (!(node instanceof HBox row)) continue;
|
||||
@SuppressWarnings("unchecked")
|
||||
ComboBox<CharacterStat> sc = (ComboBox<CharacterStat>) row.getChildren().get(0);
|
||||
@SuppressWarnings("unchecked")
|
||||
Spinner<Integer> sp = (Spinner<Integer>) row.getChildren().get(1);
|
||||
if (sc.getValue() != null) effects.add(new ConsumableEffect(sc.getValue(), sp.getValue()));
|
||||
}
|
||||
item.setEffects(effects.isEmpty() ? null : effects);
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
idField.clear();
|
||||
catCombo.setValue(null);
|
||||
catCombo.setValue(null); // setzt subCatCombo via Listener zurück
|
||||
subCatCombo.setValue(null);
|
||||
nameField.clear();
|
||||
descField.clear();
|
||||
goldSpinner.getValueFactory().setValue(0);
|
||||
modelRefField.clear();
|
||||
consumableCheck.setSelected(false);
|
||||
effectsRows.getChildren().clear();
|
||||
}
|
||||
|
||||
// ── List operations ───────────────────────────────────────────────────────
|
||||
@@ -317,7 +403,7 @@ public class ItemEditorView extends BorderPane {
|
||||
private void createItem() {
|
||||
Item item = new Item();
|
||||
item.setItemId("neues_item_" + System.currentTimeMillis());
|
||||
item.setCategory(ItemCategory.MISC);
|
||||
item.setCategory(ItemCategory.MISC_ITEMS);
|
||||
items.add(item);
|
||||
listView.getSelectionModel().select(item);
|
||||
}
|
||||
@@ -356,6 +442,28 @@ public class ItemEditorView extends BorderPane {
|
||||
selectItem(savedId);
|
||||
}
|
||||
|
||||
// ── Effect rows ───────────────────────────────────────────────────────────
|
||||
|
||||
private void addEffectRow(CharacterStat stat, int value) {
|
||||
ComboBox<CharacterStat> statCombo = new ComboBox<>();
|
||||
statCombo.getItems().addAll(CharacterStat.values());
|
||||
statCombo.setValue(stat);
|
||||
statCombo.setMinWidth(210);
|
||||
statCombo.setPromptText("Stat wählen");
|
||||
|
||||
Spinner<Integer> valSpinner = new Spinner<>(-99999, 99999, value);
|
||||
valSpinner.setEditable(true);
|
||||
valSpinner.setPrefWidth(100);
|
||||
|
||||
Button removeBtn = new Button("✕");
|
||||
removeBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #cc4444; -fx-cursor: hand;");
|
||||
|
||||
HBox row = new HBox(6, statCombo, valSpinner, removeBtn);
|
||||
row.setAlignment(Pos.CENTER_LEFT);
|
||||
removeBtn.setOnAction(e -> effectsRows.getChildren().remove(row));
|
||||
effectsRows.getChildren().add(row);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static Label sectionTitle(String text) {
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.*;
|
||||
import de.blight.common.RiverPoint;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.layout.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Übersicht aller auf der Karte platzierten Objekte.
|
||||
* Doppelklick teleportiert die Kamera 5 m über das Objekt und aktiviert das passende Werkzeug.
|
||||
* Entf-Taste löscht den markierten Eintrag nach Sicherheitsabfrage.
|
||||
*/
|
||||
public class MapObjectsView extends VBox {
|
||||
|
||||
@FunctionalInterface
|
||||
public interface JumpAction {
|
||||
void execute(float x, float y, float z, String toolHint);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface DeleteAction {
|
||||
void execute(Object placedObj, int index, String toolHint) throws Exception;
|
||||
}
|
||||
|
||||
private record Entry(float x, float y, float z, String toolHint, Object placedObj, int index) {}
|
||||
|
||||
private final JumpAction onJump;
|
||||
private final DeleteAction onDelete;
|
||||
private final TreeItem<String> treeRoot = new TreeItem<>("Karte");
|
||||
private final TreeView<String> tree;
|
||||
private final Map<TreeItem<String>, Entry> entryMap = new HashMap<>();
|
||||
|
||||
public MapObjectsView(JumpAction onJump, DeleteAction onDelete) {
|
||||
this.onJump = onJump;
|
||||
this.onDelete = onDelete;
|
||||
setStyle("-fx-background-color: #f0f0f0;");
|
||||
setPadding(new Insets(8));
|
||||
setSpacing(6);
|
||||
|
||||
Label titleLbl = new Label("Platzierte Objekte");
|
||||
titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #222;");
|
||||
|
||||
Button refreshBtn = new Button("↺");
|
||||
refreshBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #555; -fx-cursor: hand;");
|
||||
refreshBtn.setTooltip(new Tooltip("Neu laden"));
|
||||
refreshBtn.setOnAction(e -> refresh());
|
||||
|
||||
HBox header = new HBox(8, titleLbl, refreshBtn);
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
treeRoot.setExpanded(true);
|
||||
tree = new TreeView<>(treeRoot);
|
||||
tree.setShowRoot(false);
|
||||
tree.setStyle("-fx-background-color: #fafafa;");
|
||||
VBox.setVgrow(tree, Priority.ALWAYS);
|
||||
|
||||
tree.setCellFactory(tv -> new TreeCell<>() {
|
||||
@Override
|
||||
protected void updateItem(String item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) { setText(null); setStyle(""); return; }
|
||||
setText(item);
|
||||
boolean isLeaf = getTreeItem() != null && getTreeItem().isLeaf();
|
||||
setStyle(isLeaf
|
||||
? "-fx-text-fill: #333; -fx-padding: 1 4 1 4;"
|
||||
: "-fx-text-fill: #1a4a8a; -fx-font-weight: bold; -fx-padding: 2 4 2 0;");
|
||||
}
|
||||
});
|
||||
|
||||
tree.setOnMouseClicked(e -> {
|
||||
if (e.getClickCount() != 2) return;
|
||||
TreeItem<String> sel = tree.getSelectionModel().getSelectedItem();
|
||||
if (sel == null || !sel.isLeaf()) return;
|
||||
Entry entry = entryMap.get(sel);
|
||||
if (entry != null) onJump.execute(entry.x(), entry.y(), entry.z(), entry.toolHint());
|
||||
});
|
||||
|
||||
tree.setOnKeyPressed(e -> {
|
||||
if (e.getCode() != KeyCode.DELETE) return;
|
||||
TreeItem<String> sel = tree.getSelectionModel().getSelectedItem();
|
||||
if (sel == null || !sel.isLeaf()) return;
|
||||
Entry entry = entryMap.get(sel);
|
||||
if (entry == null) return;
|
||||
confirmAndDelete(sel.getValue(), entry);
|
||||
e.consume();
|
||||
});
|
||||
|
||||
getChildren().addAll(header, tree);
|
||||
refresh();
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
treeRoot.getChildren().clear();
|
||||
entryMap.clear();
|
||||
|
||||
loadModels();
|
||||
loadItems();
|
||||
loadLights();
|
||||
loadEmitters();
|
||||
loadWaterBodies();
|
||||
loadRivers();
|
||||
loadSoundAreas();
|
||||
loadAreas();
|
||||
loadLocationZones();
|
||||
}
|
||||
|
||||
// ── Löschen ───────────────────────────────────────────────────────────────
|
||||
|
||||
private void confirmAndDelete(String label, Entry entry) {
|
||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||
alert.setTitle("Objekt löschen");
|
||||
alert.setHeaderText("Objekt wirklich löschen?");
|
||||
alert.setContentText(label);
|
||||
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO);
|
||||
|
||||
Optional<ButtonType> result = alert.showAndWait();
|
||||
if (result.isEmpty() || result.get() != ButtonType.YES) return;
|
||||
|
||||
try {
|
||||
onDelete.execute(entry.placedObj(), entry.index(), entry.toolHint());
|
||||
refresh();
|
||||
} catch (Exception ex) {
|
||||
Alert err = new Alert(Alert.AlertType.ERROR, "Fehler: " + ex.getMessage(), ButtonType.OK);
|
||||
err.showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Loader ────────────────────────────────────────────────────────────────
|
||||
|
||||
private void loadModels() {
|
||||
try {
|
||||
List<PlacedModel> list = PlacedModelIO.load();
|
||||
TreeItem<String> group = group("Modelle", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
PlacedModel m = list.get(idx);
|
||||
TreeItem<String> item = leaf(lastName(m.modelPath()) + " " + pos(m.x(), m.y(), m.z()));
|
||||
entryMap.put(item, new Entry(m.x(), m.y() + 5f, m.z(), "model", m, idx));
|
||||
group.getChildren().add(item);
|
||||
}
|
||||
treeRoot.getChildren().add(group);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private void loadItems() {
|
||||
try {
|
||||
List<PlacedItem> list = PlacedItemIO.load();
|
||||
TreeItem<String> group = group("Items", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
PlacedItem i = list.get(idx);
|
||||
TreeItem<String> item = leaf(i.itemId() + " " + pos(i.x(), i.y(), i.z()));
|
||||
entryMap.put(item, new Entry(i.x(), i.y() + 5f, i.z(), "item", i, idx));
|
||||
group.getChildren().add(item);
|
||||
}
|
||||
treeRoot.getChildren().add(group);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private void loadLights() {
|
||||
try {
|
||||
List<PlacedLight> list = LightIO.load();
|
||||
TreeItem<String> group = group("Lichter", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
PlacedLight l = list.get(idx);
|
||||
TreeItem<String> item = leaf("Licht " + pos(l.x(), l.y(), l.z()));
|
||||
entryMap.put(item, new Entry(l.x(), l.y() + 5f, l.z(), "light", l, idx));
|
||||
group.getChildren().add(item);
|
||||
}
|
||||
treeRoot.getChildren().add(group);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private void loadEmitters() {
|
||||
try {
|
||||
List<PlacedEmitter> list = EmitterIO.load();
|
||||
TreeItem<String> group = group("Emitter", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
PlacedEmitter e = list.get(idx);
|
||||
TreeItem<String> item = leaf(lastName(e.texturePath()) + " " + pos(e.x(), e.y(), e.z()));
|
||||
entryMap.put(item, new Entry(e.x(), e.y() + 5f, e.z(), "emitter", e, idx));
|
||||
group.getChildren().add(item);
|
||||
}
|
||||
treeRoot.getChildren().add(group);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private void loadWaterBodies() {
|
||||
try {
|
||||
List<PlacedWater> list = WaterBodyIO.load();
|
||||
TreeItem<String> group = group("Wasser", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
PlacedWater w = list.get(idx);
|
||||
float cx = centroid(w.pointsX()), cz = centroid(w.pointsZ());
|
||||
TreeItem<String> item = leaf("Wasser #" + (idx + 1) + " " + posXZ(cx, cz));
|
||||
entryMap.put(item, new Entry(cx, Float.NaN, cz, "water", w, idx));
|
||||
group.getChildren().add(item);
|
||||
}
|
||||
treeRoot.getChildren().add(group);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private void loadRivers() {
|
||||
try {
|
||||
List<List<RiverPoint>> list = RiverIO.load();
|
||||
TreeItem<String> group = group("Wasserfälle", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
List<RiverPoint> river = list.get(idx);
|
||||
float cx = 0f, cy = 0f, cz = 0f;
|
||||
for (RiverPoint pt : river) { cx += pt.x(); cy += pt.y(); cz += pt.z(); }
|
||||
if (!river.isEmpty()) { cx /= river.size(); cy /= river.size(); cz /= river.size(); }
|
||||
TreeItem<String> item = leaf("Wasserfall #" + (idx + 1) + " " + pos(cx, cy, cz));
|
||||
entryMap.put(item, new Entry(cx, cy + 5f, cz, "waterfall", river, idx));
|
||||
group.getChildren().add(item);
|
||||
}
|
||||
treeRoot.getChildren().add(group);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private void loadSoundAreas() {
|
||||
try {
|
||||
List<PlacedSoundArea> list = SoundAreaIO.load();
|
||||
TreeItem<String> group = group("Soundbereiche", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
PlacedSoundArea s = list.get(idx);
|
||||
float cx = centroid(s.pointsX()), cz = centroid(s.pointsZ());
|
||||
String label = s.soundPath() != null && !s.soundPath().isBlank()
|
||||
? lastName(s.soundPath()) : "Soundbereich #" + (idx + 1);
|
||||
TreeItem<String> item = leaf(label + " " + posXZ(cx, cz));
|
||||
entryMap.put(item, new Entry(cx, Float.NaN, cz, "soundarea", s, idx));
|
||||
group.getChildren().add(item);
|
||||
}
|
||||
treeRoot.getChildren().add(group);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private void loadAreas() {
|
||||
try {
|
||||
List<PlacedArea> list = AreaIO.load();
|
||||
TreeItem<String> group = group("Bereiche", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
PlacedArea a = list.get(idx);
|
||||
float cx = centroid(a.pointsX()), cz = centroid(a.pointsZ());
|
||||
String label = a.nameId() != null && !a.nameId().isBlank()
|
||||
? a.nameId() : "Bereich #" + (idx + 1);
|
||||
TreeItem<String> item = leaf(label + " " + posXZ(cx, cz));
|
||||
entryMap.put(item, new Entry(cx, Float.NaN, cz, "area", a, idx));
|
||||
group.getChildren().add(item);
|
||||
}
|
||||
treeRoot.getChildren().add(group);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private void loadLocationZones() {
|
||||
try {
|
||||
List<PlacedLocationZone> list = LocationZoneIO.load();
|
||||
TreeItem<String> group = group("Zonen", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
PlacedLocationZone z = list.get(idx);
|
||||
float cx = centroid(z.pointsX()), cz = centroid(z.pointsZ());
|
||||
String label = z.nameId() != null && !z.nameId().isBlank()
|
||||
? z.nameId() : "Zone #" + (idx + 1);
|
||||
TreeItem<String> item = leaf(label + " " + posXZ(cx, cz));
|
||||
entryMap.put(item, new Entry(cx, Float.NaN, cz, "locationzone", z, idx));
|
||||
group.getChildren().add(item);
|
||||
}
|
||||
treeRoot.getChildren().add(group);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static TreeItem<String> group(String name, int count) {
|
||||
TreeItem<String> g = new TreeItem<>(name + " (" + count + ")");
|
||||
g.setExpanded(true);
|
||||
return g;
|
||||
}
|
||||
|
||||
private static TreeItem<String> leaf(String label) {
|
||||
return new TreeItem<>(label);
|
||||
}
|
||||
|
||||
private static String pos(float x, float y, float z) {
|
||||
return String.format("(%.0f, %.0f, %.0f)", x, y, z);
|
||||
}
|
||||
|
||||
private static String posXZ(float x, float z) {
|
||||
return String.format("(%.0f, ?, %.0f)", x, z);
|
||||
}
|
||||
|
||||
private static float centroid(float[] arr) {
|
||||
if (arr == null || arr.length == 0) return 0f;
|
||||
float sum = 0f;
|
||||
for (float v : arr) sum += v;
|
||||
return sum / arr.length;
|
||||
}
|
||||
|
||||
private static String lastName(String path) {
|
||||
if (path == null || path.isBlank()) return "—";
|
||||
int slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
|
||||
return slash >= 0 ? path.substring(slash + 1) : path;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,10 @@ import com.jme3.input.KeyInput;
|
||||
import com.jme3.input.controls.ActionListener;
|
||||
import com.jme3.input.controls.KeyTrigger;
|
||||
import com.jme3.system.AppSettings;
|
||||
import de.blight.common.BlightHome;
|
||||
import de.blight.common.SaveGameIO;
|
||||
import de.blight.game.config.*;
|
||||
import de.blight.game.state.SaveGameState;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.bridge.SLF4JBridgeHandler;
|
||||
@@ -20,7 +23,6 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class BlightGame extends SimpleApplication {
|
||||
|
||||
@@ -33,6 +35,10 @@ public class BlightGame extends SimpleApplication {
|
||||
private ConfigScreen configScreen;
|
||||
private GraphicsScreen graphicsScreen;
|
||||
private PauseMenu pauseMenu;
|
||||
private MainMenuState mainMenuState;
|
||||
|
||||
/** Routed durch onClose-Callbacks, damit Config/Grafik-Screen zurück zum richtigen Menü springen. */
|
||||
private Runnable screenCloseTarget;
|
||||
|
||||
private JWindow splashWindow;
|
||||
|
||||
@@ -74,13 +80,11 @@ public class BlightGame extends SimpleApplication {
|
||||
|
||||
private static JWindow showSplash() {
|
||||
try {
|
||||
// Logo skaliert auf 700 px Breite (Seitenverhältnis beibehalten)
|
||||
BufferedImage img = ImageIO.read(BlightGame.class.getResourceAsStream("/logo.png"));
|
||||
int tw = 700;
|
||||
int th = (int) (img.getHeight() * (tw / (double) img.getWidth()));
|
||||
Image scaled = img.getScaledInstance(tw, th, Image.SCALE_SMOOTH);
|
||||
|
||||
// Dunkles Panel
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
panel.setBackground(new Color(0x1c1c1c));
|
||||
panel.setOpaque(true);
|
||||
@@ -103,7 +107,6 @@ public class BlightGame extends SimpleApplication {
|
||||
win.setLocationRelativeTo(null);
|
||||
win.setVisible(true);
|
||||
|
||||
// Pollt loadingStatus alle 100 ms – analog zum Editor-Splash
|
||||
splashTimer = new Timer(100, null);
|
||||
splashTimer.addActionListener(e -> {
|
||||
splashLabel.setText(loadingStatus);
|
||||
@@ -131,29 +134,55 @@ public class BlightGame extends SimpleApplication {
|
||||
keyBindings = KeyBindingStore.load();
|
||||
graphicsSettings = GraphicsStore.load();
|
||||
|
||||
status("Baue Spielwelt...");
|
||||
status("Lade Spielstand...");
|
||||
stateManager.attach(new SaveGameState());
|
||||
|
||||
// WorldScene erst vorbereiten, aber nicht aktivieren (onEnable lädt die Welt)
|
||||
status("Initialisiere Welt...");
|
||||
worldScene = new WorldScene(keyBindings);
|
||||
worldScene.setEnabled(false);
|
||||
stateManager.attach(worldScene);
|
||||
|
||||
// screenCloseTarget: dynamisch gesetzt, je nachdem ob Config/Grafik vom Hauptmenü
|
||||
// oder vom Pausemenü geöffnet wurde.
|
||||
screenCloseTarget = () -> {};
|
||||
|
||||
configScreen = new ConfigScreen(keyBindings, () -> worldScene.reloadBindings(keyBindings));
|
||||
configScreen.setOnClose(() -> pauseMenu.setEnabled(true));
|
||||
configScreen.setOnClose(() -> {
|
||||
configScreen.setEnabled(false);
|
||||
screenCloseTarget.run();
|
||||
});
|
||||
stateManager.attach(configScreen);
|
||||
configScreen.setEnabled(false);
|
||||
|
||||
graphicsScreen = new GraphicsScreen(graphicsSettings, () -> pauseMenu.setEnabled(true));
|
||||
graphicsScreen = new GraphicsScreen(graphicsSettings, () -> {
|
||||
graphicsScreen.setEnabled(false);
|
||||
screenCloseTarget.run();
|
||||
});
|
||||
stateManager.attach(graphicsScreen);
|
||||
graphicsScreen.setEnabled(false);
|
||||
|
||||
SaveGameState saveState = stateManager.getState(SaveGameState.class);
|
||||
|
||||
pauseMenu = new PauseMenu(
|
||||
() -> { pauseMenu.setEnabled(false); graphicsScreen.setEnabled(true); },
|
||||
() -> { pauseMenu.setEnabled(false); configScreen.setEnabled(true); }
|
||||
() -> { if (saveState != null) saveState.persist(); },
|
||||
() -> {
|
||||
screenCloseTarget = () -> pauseMenu.setEnabled(true);
|
||||
pauseMenu.setEnabled(false);
|
||||
graphicsScreen.setEnabled(true);
|
||||
},
|
||||
() -> {
|
||||
screenCloseTarget = () -> pauseMenu.setEnabled(true);
|
||||
pauseMenu.setEnabled(false);
|
||||
configScreen.setEnabled(true);
|
||||
}
|
||||
);
|
||||
stateManager.attach(pauseMenu);
|
||||
pauseMenu.setEnabled(false);
|
||||
|
||||
// ── Screenshot (Druck-Taste) ───────────────────────────────────────────
|
||||
// ── Screenshot (F12) ─────────────────────────────────────────────────────
|
||||
try {
|
||||
Path screenshotDir = findProjectRoot().resolve("screenshots");
|
||||
Path screenshotDir = BlightHome.resolve("screenshots");
|
||||
Files.createDirectories(screenshotDir);
|
||||
screenshotState = new ScreenshotAppState(screenshotDir + File.separator, "screenshot");
|
||||
stateManager.attach(screenshotState);
|
||||
@@ -161,55 +190,109 @@ public class BlightGame extends SimpleApplication {
|
||||
} catch (IOException e) {
|
||||
log.warn("Screenshot-Verzeichnis konnte nicht angelegt werden", e);
|
||||
}
|
||||
inputManager.addMapping("Screenshot", new KeyTrigger(KeyInput.KEY_SYSRQ));
|
||||
inputManager.addMapping("Screenshot", new KeyTrigger(KeyInput.KEY_F12));
|
||||
inputManager.addListener((ActionListener) (name, isPressed, tpf) -> {
|
||||
if (isPressed && screenshotState != null) screenshotState.takeScreenshot();
|
||||
}, "Screenshot");
|
||||
|
||||
// ── Schnellspeichern (F5, konfigurierbar) ────────────────────────────
|
||||
inputManager.addMapping("QuickSave", new KeyTrigger(keyBindings.quicksave));
|
||||
inputManager.addListener((ActionListener) (name, isPressed, tpf) -> {
|
||||
if (!isPressed || mainMenuState != null && mainMenuState.isEnabled()) return;
|
||||
SaveGameState sgs = stateManager.getState(SaveGameState.class);
|
||||
if (sgs != null) sgs.persist();
|
||||
log.info("[Save] Schnellspeichern ausgelöst.");
|
||||
}, "QuickSave");
|
||||
|
||||
// ── ESC-Handler ───────────────────────────────────────────────────────
|
||||
inputManager.addMapping("ToggleMenu", new KeyTrigger(KeyInput.KEY_ESCAPE));
|
||||
inputManager.addListener((ActionListener) (name, isPressed, tpf) -> {
|
||||
if (!isPressed) return;
|
||||
|
||||
if (graphicsScreen.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
// Im Hauptmenü: ESC ignorieren
|
||||
if (mainMenuState != null && mainMenuState.isEnabled()) return;
|
||||
|
||||
if (graphicsScreen.isEnabled()) return;
|
||||
|
||||
if (configScreen.isEnabled()) {
|
||||
if (configScreen.isWaiting()) {
|
||||
configScreen.cancelWaiting();
|
||||
} else {
|
||||
configScreen.setEnabled(false);
|
||||
pauseMenu.setEnabled(true);
|
||||
screenCloseTarget.run();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
de.blight.game.state.InventoryState inv = worldScene.getInventoryState();
|
||||
if (inv != null && inv.isEnabled()) {
|
||||
inv.setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pauseMenu.isEnabled()) {
|
||||
pauseMenu.setEnabled(false);
|
||||
worldScene.setPaused(false);
|
||||
return;
|
||||
}
|
||||
|
||||
pauseMenu.setEnabled(true);
|
||||
worldScene.setPaused(true);
|
||||
}, "ToggleMenu");
|
||||
|
||||
// ── Startentscheidung: Hauptmenü oder direkt ins Spiel (Editor-Start) ─
|
||||
boolean autostart = Boolean.getBoolean("blight.autostart");
|
||||
if (autostart) {
|
||||
startWorld();
|
||||
} else {
|
||||
showMainMenu();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Start-Flows ──────────────────────────────────────────────────────────
|
||||
|
||||
private void showMainMenu() {
|
||||
boolean hasSave = SaveGameIO.exists();
|
||||
mainMenuState = new MainMenuState(
|
||||
this::onNewGame,
|
||||
hasSave ? this::onContinue : null,
|
||||
hasSave ? this::onContinue : null, // Spiel laden = gleiche Logik (ein Slot)
|
||||
() -> {
|
||||
screenCloseTarget = () -> mainMenuState.setEnabled(true);
|
||||
mainMenuState.setEnabled(false);
|
||||
configScreen.setEnabled(true);
|
||||
},
|
||||
this::stop
|
||||
);
|
||||
stateManager.attach(mainMenuState);
|
||||
inputManager.setCursorVisible(true);
|
||||
}
|
||||
|
||||
private void onNewGame() {
|
||||
SaveGameState sgs = stateManager.getState(SaveGameState.class);
|
||||
if (sgs != null) sgs.resetForNewGame();
|
||||
startWorld();
|
||||
}
|
||||
|
||||
private void onContinue() {
|
||||
startWorld();
|
||||
}
|
||||
|
||||
private void startWorld() {
|
||||
if (mainMenuState != null) {
|
||||
mainMenuState.setEnabled(false);
|
||||
}
|
||||
worldScene.setEnabled(true);
|
||||
inputManager.setCursorVisible(false);
|
||||
}
|
||||
|
||||
// ── Render-Loop ──────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void simpleUpdate(float tpf) {
|
||||
if (!gameReady) {
|
||||
gameReady = true; // erstes gerendtertes Frame → Splash schließen
|
||||
gameReady = true;
|
||||
status("Bereit");
|
||||
}
|
||||
}
|
||||
|
||||
private static Path findProjectRoot() {
|
||||
String prop = System.getProperty("blight.project.root");
|
||||
if (prop != null) return Paths.get(prop);
|
||||
File dir = Paths.get(".").toAbsolutePath().normalize().toFile();
|
||||
while (dir != null) {
|
||||
if (new File(dir, "blight-editor").isDirectory()
|
||||
&& new File(dir, "blight-game").isDirectory())
|
||||
return dir.toPath();
|
||||
dir = dir.getParentFile();
|
||||
}
|
||||
return Paths.get(".").toAbsolutePath().normalize();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@ package de.blight.game.config;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import de.blight.common.BlightHome;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
|
||||
public class GraphicsStore {
|
||||
|
||||
private static final Path FILE = Paths.get("config", "graphics.json");
|
||||
private static final Path FILE = BlightHome.resolve("config", "graphics.json");
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
public static GraphicsSettings load() {
|
||||
|
||||
@@ -2,13 +2,14 @@ package de.blight.game.config;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import de.blight.common.BlightHome;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
|
||||
public class KeyBindingStore {
|
||||
|
||||
private static final Path FILE = Paths.get("config", "keybindings.json");
|
||||
private static final Path FILE = BlightHome.resolve("config", "keybindings.json");
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
public static KeyBindings load() {
|
||||
|
||||
@@ -13,6 +13,8 @@ public class KeyBindings {
|
||||
public int sprint = KeyInput.KEY_LSHIFT;
|
||||
public int walk = KeyInput.KEY_LMENU;
|
||||
public int interact = KeyInput.KEY_E;
|
||||
public int inventory = KeyInput.KEY_I;
|
||||
public int quicksave = KeyInput.KEY_F5;
|
||||
|
||||
/** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */
|
||||
public static final String[][] ENTRIES = {
|
||||
@@ -24,6 +26,8 @@ public class KeyBindings {
|
||||
{"sprint", "Rennen"},
|
||||
{"walk", "Gehen"},
|
||||
{"interact", "Interagieren"},
|
||||
{"inventory", "Inventar"},
|
||||
{"quicksave", "Schnellspeichern"},
|
||||
};
|
||||
|
||||
public int get(String fieldName) {
|
||||
|
||||
@@ -31,7 +31,10 @@ public class PauseMenu extends BaseAppState {
|
||||
private static final int BTN_GRAFIK = 0;
|
||||
private static final int BTN_AUDIO = 1;
|
||||
private static final int BTN_STEUERUNG = 2;
|
||||
private static final int BTN_BEENDEN = 3;
|
||||
private static final int BTN_SPEICHERN = 3;
|
||||
private static final int BTN_BEENDEN = 4;
|
||||
|
||||
private static final ColorRGBA COL_BTN_SAVE = new ColorRGBA(0.12f, 0.28f, 0.14f, 1.00f);
|
||||
|
||||
private SimpleApplication app;
|
||||
private Node guiNode;
|
||||
@@ -40,11 +43,13 @@ public class PauseMenu extends BaseAppState {
|
||||
|
||||
private Runnable onGraphics;
|
||||
private Runnable onControls;
|
||||
private Runnable onSave;
|
||||
|
||||
// [x, y, w, h] per button
|
||||
private final float[][] btnBounds = new float[4][4];
|
||||
private final float[][] btnBounds = new float[5][4];
|
||||
|
||||
public PauseMenu(Runnable onGraphics, Runnable onControls) {
|
||||
public PauseMenu(Runnable onSave, Runnable onGraphics, Runnable onControls) {
|
||||
this.onSave = onSave;
|
||||
this.onGraphics = onGraphics;
|
||||
this.onControls = onControls;
|
||||
}
|
||||
@@ -79,7 +84,7 @@ public class PauseMenu extends BaseAppState {
|
||||
panel = new Node("pause-panel");
|
||||
addQuad(panel, 0, 0, sw, sh, COL_BG, -2);
|
||||
|
||||
float pw = 320, ph = 360;
|
||||
float pw = 320, ph = 430;
|
||||
float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
|
||||
addQuad(panel, px, py, pw, ph, COL_PANEL, -1);
|
||||
|
||||
@@ -87,23 +92,23 @@ public class PauseMenu extends BaseAppState {
|
||||
centerText(title, px, py + ph - 48, pw);
|
||||
panel.attachChild(title);
|
||||
|
||||
String[] labels = {"Grafik", "Audio", "Steuerung", "Beenden"};
|
||||
boolean[] enabled = {true, false, true, true};
|
||||
String[] labels = {"Grafik", "Audio", "Steuerung", "Speichern", "Beenden"};
|
||||
boolean[] enabled = {true, false, true, true, true};
|
||||
ColorRGBA[] bgCols = {COL_BTN, COL_BTN_DIS, COL_BTN, COL_BTN_SAVE, COL_BTN_QUIT};
|
||||
|
||||
float bw = 260, bh = 52;
|
||||
float bx = px + (pw - bw) / 2f;
|
||||
float startY = py + ph - 112;
|
||||
float step = 62;
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
float by = startY - i * step;
|
||||
ColorRGBA bgCol = !enabled[i] ? COL_BTN_DIS : (i == BTN_BEENDEN ? COL_BTN_QUIT : COL_BTN);
|
||||
ColorRGBA txCol = enabled[i] ? COL_TEXT : COL_TEXT_DIS;
|
||||
|
||||
addQuad(panel, bx, by, bw, bh, bgCol, 0);
|
||||
addQuad(panel, bx, by, bw, bh, bgCols[i], 0);
|
||||
|
||||
BitmapText lbl = txt(labels[i], 18, txCol);
|
||||
if (!enabled[i]) {
|
||||
// Center label in upper portion, show hint below
|
||||
lbl.setLocalTranslation(bx + (bw - lbl.getLineWidth()) / 2f, by + bh - 12, 1);
|
||||
BitmapText hint = txt("Bald verfügbar", 12, COL_TEXT_SUB);
|
||||
hint.setLocalTranslation(bx + (bw - hint.getLineWidth()) / 2f, by + 14, 1);
|
||||
@@ -124,12 +129,13 @@ public class PauseMenu extends BaseAppState {
|
||||
if (!isPressed) return;
|
||||
Vector2f c = app.getInputManager().getCursorPosition();
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (!hits(c, btnBounds[i][0], btnBounds[i][1], btnBounds[i][2], btnBounds[i][3])) continue;
|
||||
switch (i) {
|
||||
case BTN_GRAFIK -> { if (onGraphics != null) onGraphics.run(); }
|
||||
case BTN_AUDIO -> { /* Bald verfügbar */ }
|
||||
case BTN_STEUERUNG -> { if (onControls != null) onControls.run(); }
|
||||
case BTN_SPEICHERN -> { if (onSave != null) onSave.run(); }
|
||||
case BTN_BEENDEN -> app.stop();
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -43,14 +43,14 @@ public class PlayerInputControl {
|
||||
private boolean animCtxLogged = false;
|
||||
|
||||
private AnimComposer animComposer;
|
||||
/** Letzter gestarteter Clip (nur für tryPlay-Deduplizierung genutzt). */
|
||||
private String runningClip;
|
||||
/** Frames, für die JUMP erzwungen wird (überbrückt onGround()-Lag). */
|
||||
private int jumpFrames = 0;
|
||||
/** Läuft gerade eine Pickup-Animation? */
|
||||
private boolean pickupActive = false;
|
||||
private float pickupRemaining = 0f;
|
||||
|
||||
/** Autopilot: wenn gesetzt, geht der Charakter automatisch in diese (normalisierte) Richtung. */
|
||||
private Vector3f autopilotDir = null;
|
||||
|
||||
private final ActionListener actionListener = (name, isPressed, tpf) -> {
|
||||
if (paused) return;
|
||||
switch (name) {
|
||||
@@ -94,10 +94,13 @@ public class PlayerInputControl {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isPaused() { return paused; }
|
||||
|
||||
public void setPaused(boolean paused) {
|
||||
this.paused = paused;
|
||||
if (paused) {
|
||||
forward = backward = left = right = sprint = walk = false;
|
||||
autopilotDir = null;
|
||||
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
}
|
||||
}
|
||||
@@ -109,6 +112,31 @@ public class PlayerInputControl {
|
||||
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktiviert den Autopilot-Modus. Der Charakter läuft mit Walk-Geschwindigkeit
|
||||
* in die angegebene (normalisierte) Richtung und spielt die WALK-Animation.
|
||||
* {@code null} deaktiviert den Autopilot.
|
||||
*/
|
||||
public void setAutopilotDirection(Vector3f dir) {
|
||||
this.autopilotDir = dir;
|
||||
if (dir == null && physicsChar != null && !paused && !pickupActive) {
|
||||
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spielt die PICK_UP-Animation einmalig ab und blockiert Bewegung für {@code duration} Sekunden.
|
||||
*/
|
||||
public void requestPickup(float duration) {
|
||||
pickupActive = true;
|
||||
pickupRemaining = duration;
|
||||
autopilotDir = null;
|
||||
forward = backward = left = right = false;
|
||||
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
playAction(AnimationAction.PICK_UP);
|
||||
currentAnim = AnimationAction.PICK_UP;
|
||||
}
|
||||
|
||||
private void registerMappings(KeyBindings kb) {
|
||||
inputManager.addMapping("Forward", new KeyTrigger(kb.forward));
|
||||
inputManager.addMapping("Backward", new KeyTrigger(kb.backward));
|
||||
@@ -120,33 +148,48 @@ public class PlayerInputControl {
|
||||
inputManager.addListener(actionListener, ACTION_NAMES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spielt die PICK_UP-Animation einmalig ab und blockiert Bewegung für {@code duration} Sekunden.
|
||||
* Wird von WorldItemsState aufgerufen, sobald der Spieler ein Item aufnimmt.
|
||||
*/
|
||||
public void requestPickup(float duration) {
|
||||
pickupActive = true;
|
||||
pickupRemaining = duration;
|
||||
forward = backward = left = right = false;
|
||||
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
playAction(AnimationAction.PICK_UP);
|
||||
currentAnim = AnimationAction.PICK_UP;
|
||||
public void update(float tpf) {
|
||||
if (physicsChar == null) return;
|
||||
|
||||
if (paused) {
|
||||
// Autopilot bei Pause sofort beenden
|
||||
if (autopilotDir != null) {
|
||||
autopilotDir = null;
|
||||
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
public void update(float tpf) {
|
||||
if (physicsChar == null || paused) return;
|
||||
|
||||
// Pickup-Animation hat höchste Priorität
|
||||
if (pickupActive) {
|
||||
pickupRemaining -= tpf;
|
||||
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
if (pickupRemaining <= 0f) {
|
||||
pickupActive = false;
|
||||
currentAnim = null; // erzwingt Neubewertung im nächsten Frame
|
||||
currentAnim = null;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Autopilot: Charakter läuft automatisch in eine vorgegebene Richtung (WALK-Animation)
|
||||
if (autopilotDir != null) {
|
||||
physicsChar.setWalkDirection(autopilotDir.mult(MOVE_SPEED * WALK_MULT));
|
||||
if (visual != null) {
|
||||
Quaternion targetRot = new Quaternion();
|
||||
targetRot.lookAt(autopilotDir, Vector3f.UNIT_Y);
|
||||
Quaternion current = visual.getLocalRotation().clone();
|
||||
current.slerp(targetRot, ROTATE_SPEED * tpf);
|
||||
visual.setLocalRotation(current);
|
||||
}
|
||||
if (AnimationAction.WALK != currentAnim) {
|
||||
playAction(AnimationAction.WALK);
|
||||
currentAnim = AnimationAction.WALK;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Normale Spielereingabe (WASD)
|
||||
Vector3f camDir = cam.getDirection().clone().setY(0).normalizeLocal();
|
||||
Vector3f camLeft = cam.getLeft().clone().setY(0).normalizeLocal();
|
||||
|
||||
@@ -176,7 +219,7 @@ public class PlayerInputControl {
|
||||
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
}
|
||||
|
||||
// Animation
|
||||
// Animations-Auswahl
|
||||
if (jumpFrames > 0) jumpFrames--;
|
||||
|
||||
AnimationAction target;
|
||||
|
||||
@@ -44,6 +44,7 @@ import de.blight.game.state.TerrainChunkState;
|
||||
import de.blight.game.state.WaterBodyState;
|
||||
import de.blight.game.state.WeatherState;
|
||||
import de.blight.game.state.InteractionHudState;
|
||||
import de.blight.game.state.InventoryState;
|
||||
import de.blight.game.state.WorldItemsState;
|
||||
import de.blight.game.state.WorldObjectsState;
|
||||
|
||||
@@ -65,6 +66,7 @@ public class WorldScene extends BaseAppState {
|
||||
private final KeyBindings keyBindings;
|
||||
private ThirdPersonCamera thirdPersonCam;
|
||||
private PlayerInputControl playerInput;
|
||||
private InventoryState inventoryState;
|
||||
private AnimationLibrary animLib;
|
||||
private Node character;
|
||||
private Spatial characterVisual;
|
||||
@@ -78,6 +80,8 @@ public class WorldScene extends BaseAppState {
|
||||
this.keyBindings = keyBindings;
|
||||
}
|
||||
|
||||
public InventoryState getInventoryState() { return inventoryState; }
|
||||
|
||||
/** Wird von ConfigScreen nach dem Speichern aufgerufen. */
|
||||
public void reloadBindings(KeyBindings kb) {
|
||||
if (playerInput != null) playerInput.reloadBindings(kb);
|
||||
@@ -156,10 +160,18 @@ public class WorldScene extends BaseAppState {
|
||||
|
||||
MainCharacter mc = findMainCharacter();
|
||||
if (mc != null) {
|
||||
// SaveGameState mit Charakter und Positions-Lieferant verknüpfen
|
||||
de.blight.game.state.SaveGameState saveState =
|
||||
app.getStateManager().getState(de.blight.game.state.SaveGameState.class);
|
||||
if (saveState != null) saveState.bind(mc, physicsChar::getPhysicsLocation);
|
||||
|
||||
app.getStateManager().attach(new LocationState(mc, character));
|
||||
app.getStateManager().attach(
|
||||
new WorldItemsState(keyBindings, physicsChar, mc, playerInput));
|
||||
app.getStateManager().attach(new InteractionHudState());
|
||||
inventoryState = new InventoryState(mc, keyBindings);
|
||||
inventoryState.setEnabled(false);
|
||||
app.getStateManager().attach(inventoryState);
|
||||
}
|
||||
|
||||
// Maus einfangen – keine Klick-Pflicht für Kamerasteuerung
|
||||
@@ -352,12 +364,23 @@ public class WorldScene extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn aus Map oder Editor-Property
|
||||
// Spawn-Priorität: 1) Editor-Property 2) Spielstand 3) Karten-Default
|
||||
String propX = System.getProperty("blight.temp.spawn.x");
|
||||
String propZ = System.getProperty("blight.temp.spawn.z");
|
||||
if (loadedMapData != null) {
|
||||
spawnX = propX != null ? Float.parseFloat(propX) : loadedMapData.spawnX;
|
||||
spawnZ = propZ != null ? Float.parseFloat(propZ) : loadedMapData.spawnZ;
|
||||
if (propX != null) {
|
||||
spawnX = Float.parseFloat(propX);
|
||||
spawnZ = propZ != null ? Float.parseFloat(propZ) : (loadedMapData != null ? loadedMapData.spawnZ : 0f);
|
||||
} else {
|
||||
de.blight.game.state.SaveGameState saveState =
|
||||
app.getStateManager().getState(de.blight.game.state.SaveGameState.class);
|
||||
if (saveState != null && saveState.getSave().character.positionSaved) {
|
||||
spawnX = saveState.getSave().character.x;
|
||||
spawnY = saveState.getSave().character.y;
|
||||
spawnZ = saveState.getSave().character.z;
|
||||
} else if (loadedMapData != null) {
|
||||
spawnX = loadedMapData.spawnX;
|
||||
spawnZ = loadedMapData.spawnZ;
|
||||
}
|
||||
}
|
||||
System.out.println("[WorldScene] SpawnXZ: X=" + spawnX + " Z=" + spawnZ);
|
||||
|
||||
@@ -366,9 +389,15 @@ public class WorldScene extends BaseAppState {
|
||||
terrainChunkState = new TerrainChunkState(bulletAppState, mat, loadedMapData);
|
||||
app.getStateManager().attach(terrainChunkState);
|
||||
|
||||
// Spawn-Höhe aus Chunk-Daten
|
||||
// Spawn-Höhe: aus gespeicherter Position oder aus Terrain berechnen
|
||||
de.blight.game.state.SaveGameState _sv =
|
||||
app.getStateManager().getState(de.blight.game.state.SaveGameState.class);
|
||||
boolean hasSavedY = _sv != null && _sv.getSave().character.positionSaved
|
||||
&& System.getProperty("blight.temp.spawn.x") == null;
|
||||
if (!hasSavedY) {
|
||||
float terrainH = terrainChunkState.getHeightAt(spawnX, spawnZ);
|
||||
spawnY = terrainH + 10f;
|
||||
}
|
||||
System.out.println("[WorldScene] SpawnXYZ=(" + spawnX + ", " + spawnY + ", " + spawnZ + ")");
|
||||
}
|
||||
|
||||
|
||||
@@ -6,26 +6,21 @@ import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.font.BitmapFont;
|
||||
import com.jme3.font.BitmapText;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.math.Vector2f;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.Spatial;
|
||||
import de.blight.common.model.Item;
|
||||
import de.blight.common.model.TextRegistry;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Zeigt oberhalb von nahegelegenen Item-Pickups den Namen an,
|
||||
* wenn der Spieler darauf zielt.
|
||||
* Zeigt den Namen eines anvisierten Item-Pickups in Weltkoordinaten oberhalb des Objekts an.
|
||||
* Die Erkennung (welches Item angezielt wird) übernimmt WorldItemsState.
|
||||
*/
|
||||
public class InteractionHudState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(InteractionHudState.class);
|
||||
|
||||
private static final float SHOW_RANGE = 3.5f;
|
||||
private static final float DOT_THRESH = 0.65f;
|
||||
private static final float Y_OFFSET = 0.6f;
|
||||
|
||||
private Camera cam;
|
||||
@@ -76,60 +71,37 @@ public class InteractionHudState extends BaseAppState {
|
||||
return;
|
||||
}
|
||||
|
||||
int idx = worldItems.getHoveredIdx();
|
||||
if (idx < 0) {
|
||||
labelText.setCullHint(Spatial.CullHint.Always);
|
||||
return;
|
||||
}
|
||||
|
||||
String name = worldItems.getHoveredItemName();
|
||||
if (name == null) {
|
||||
labelText.setCullHint(Spatial.CullHint.Always);
|
||||
return;
|
||||
}
|
||||
|
||||
// Weltposition des Items → Bildschirmkoordinate
|
||||
Node itemsRoot = worldItems.getItemsRoot();
|
||||
if (itemsRoot == null || itemsRoot.getQuantity() == 0) {
|
||||
if (idx >= itemsRoot.getQuantity()) {
|
||||
labelText.setCullHint(Spatial.CullHint.Always);
|
||||
return;
|
||||
}
|
||||
Spatial target = itemsRoot.getChild(idx);
|
||||
Vector3f worldPos = target.getWorldTranslation().add(0f, Y_OFFSET, 0f);
|
||||
Vector3f screenV = cam.getScreenCoordinates(worldPos);
|
||||
|
||||
// Hinter der Kamera → nicht anzeigen
|
||||
if (screenV.z >= 1f) {
|
||||
labelText.setCullHint(Spatial.CullHint.Always);
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3f playerPos = worldItems.getPhysicsChar() != null
|
||||
? worldItems.getPhysicsChar().getPhysicsLocation()
|
||||
: cam.getLocation();
|
||||
Vector3f camDir = cam.getDirection().normalizeLocal();
|
||||
|
||||
Spatial bestTarget = null;
|
||||
float bestDot = -1f;
|
||||
|
||||
for (Spatial s : itemsRoot.getChildren()) {
|
||||
Vector3f itemPos = s.getWorldTranslation();
|
||||
float dx = itemPos.x - playerPos.x;
|
||||
float dz = itemPos.z - playerPos.z;
|
||||
float dist = (float) Math.sqrt(dx * dx + dz * dz);
|
||||
if (dist > SHOW_RANGE) continue;
|
||||
|
||||
Vector3f toItem = itemPos.subtract(cam.getLocation()).normalizeLocal();
|
||||
float dot = camDir.dot(toItem);
|
||||
if (dot > DOT_THRESH && dot > bestDot) {
|
||||
bestDot = dot;
|
||||
bestTarget = s;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestTarget == null) {
|
||||
labelText.setCullHint(Spatial.CullHint.Always);
|
||||
return;
|
||||
}
|
||||
|
||||
String label = resolveLabel(bestTarget);
|
||||
labelText.setText(label);
|
||||
|
||||
Vector3f worldPos = bestTarget.getWorldTranslation().add(0f, Y_OFFSET, 0f);
|
||||
Vector3f screenV3 = cam.getScreenCoordinates(worldPos);
|
||||
Vector2f screen = new Vector2f(screenV3.x, screenV3.y);
|
||||
|
||||
labelText.setText(name);
|
||||
float textW = labelText.getLineWidth();
|
||||
labelText.setLocalTranslation(screen.x - textW * 0.5f, screen.y, 1f);
|
||||
labelText.setLocalTranslation(screenV.x - textW * 0.5f, screenV.y, 1f);
|
||||
labelText.setCullHint(Spatial.CullHint.Inherit);
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
||||
|
||||
private String resolveLabel(Spatial s) {
|
||||
String itemId = s.getUserData("itemId");
|
||||
if (itemId == null) return "?";
|
||||
WorldItemsState state = worldItems;
|
||||
// Resolve via TextRegistry if a full Item definition is available
|
||||
// For now fall back to itemId
|
||||
return itemId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.font.BitmapFont;
|
||||
import com.jme3.font.BitmapText;
|
||||
import com.jme3.input.MouseInput;
|
||||
import com.jme3.input.controls.ActionListener;
|
||||
import com.jme3.input.controls.AnalogListener;
|
||||
import com.jme3.input.controls.KeyTrigger;
|
||||
import com.jme3.input.controls.MouseAxisTrigger;
|
||||
import com.jme3.input.controls.MouseButtonTrigger;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.math.Vector2f;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.shape.Quad;
|
||||
import com.jme3.texture.Texture;
|
||||
import de.blight.common.model.*;
|
||||
import de.blight.game.config.KeyBindings;
|
||||
import de.blight.game.scene.WorldScene;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
/**
|
||||
* In-Game-Inventaransicht: öffnet/schließt sich mit der 'I'-Taste.
|
||||
* Items werden nach Kategorie (Tabs) und innerhalb der Kategorie nach
|
||||
* SubKategorie und dann nach Preis sortiert angezeigt.
|
||||
* Thumbnails werden aus {@code ObjectReference.getThumbnailAssetPath()} geladen.
|
||||
*/
|
||||
public class InventoryState extends BaseAppState {
|
||||
|
||||
// ── Farben ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static final ColorRGBA COL_OVERLAY = new ColorRGBA(0f, 0f, 0f, 0.72f);
|
||||
private static final ColorRGBA COL_PANEL = new ColorRGBA(0.09f, 0.09f, 0.14f, 0.97f);
|
||||
private static final ColorRGBA COL_HDR = new ColorRGBA(0.05f, 0.05f, 0.09f, 1.00f);
|
||||
private static final ColorRGBA COL_TAB_ON = new ColorRGBA(0.22f, 0.22f, 0.42f, 1.00f);
|
||||
private static final ColorRGBA COL_TAB_OFF = new ColorRGBA(0.12f, 0.12f, 0.20f, 1.00f);
|
||||
private static final ColorRGBA COL_CELL = new ColorRGBA(0.14f, 0.14f, 0.22f, 1.00f);
|
||||
private static final ColorRGBA COL_CELL_FRAME = new ColorRGBA(0.22f, 0.22f, 0.35f, 1.00f);
|
||||
private static final ColorRGBA COL_THUMB_BG = new ColorRGBA(0.20f, 0.20f, 0.28f, 1.00f);
|
||||
private static final ColorRGBA COL_BADGE = new ColorRGBA(0.04f, 0.04f, 0.07f, 0.92f);
|
||||
private static final ColorRGBA COL_SUBHDR = new ColorRGBA(0.10f, 0.10f, 0.18f, 0.80f);
|
||||
private static final ColorRGBA COL_WHITE = ColorRGBA.White;
|
||||
private static final ColorRGBA COL_MUTED = new ColorRGBA(0.55f, 0.55f, 0.60f, 1.00f);
|
||||
private static final ColorRGBA COL_GOLD = new ColorRGBA(1.00f, 0.85f, 0.20f, 1.00f);
|
||||
private static final ColorRGBA COL_SUBCAT_TXT = new ColorRGBA(0.55f, 0.78f, 1.00f, 1.00f);
|
||||
|
||||
// ── Layout ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static final int COLS = 5;
|
||||
private static final int CELL_W = 155;
|
||||
private static final int CELL_H = 195; // 128 thumb + 67 text
|
||||
private static final int CELL_GAP = 8;
|
||||
private static final int THUMB_SZ = 128;
|
||||
private static final int HDR_H = 42;
|
||||
private static final int TAB_H = 32;
|
||||
private static final int PAD = 16; // panel inner padding
|
||||
private static final int SUBHDR_H = 26;
|
||||
private static final int SUBHDR_GAP = 4;
|
||||
|
||||
// ── Input-Mapping-Namen ───────────────────────────────────────────────────
|
||||
|
||||
private static final String MAP_TOGGLE = "_InvToggle";
|
||||
private static final String MAP_SCROLL_UP = "_InvScrollUp";
|
||||
private static final String MAP_SCROLL_DN = "_InvScrollDn";
|
||||
private static final String MAP_CLICK = "_InvClick";
|
||||
|
||||
// ── JME-Zustand ───────────────────────────────────────────────────────────
|
||||
|
||||
private SimpleApplication app;
|
||||
private AssetManager assetManager;
|
||||
private BitmapFont font;
|
||||
private Node guiNode;
|
||||
private Node panel;
|
||||
private Node gridNode;
|
||||
|
||||
// ── Daten ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private final MainCharacter mc;
|
||||
private final KeyBindings keyBindings;
|
||||
|
||||
// ── UI-Zustand ────────────────────────────────────────────────────────────
|
||||
|
||||
private ItemCategory activeTab = null;
|
||||
private float scrollY = 0f; // Pixel-Scroll-Offset (nach unten = positiv)
|
||||
private float maxScrollY = 0f;
|
||||
|
||||
// Für Tab-Klick-Erkennung: parallele Arrays
|
||||
private ItemCategory[] tabOrder;
|
||||
private float[][] tabBounds; // [tabIdx] = {x, y, w, h}
|
||||
|
||||
// Content-Bereich in Screen-Koordinaten (JME3 Y-up)
|
||||
private float contentLeft;
|
||||
private float contentBottom;
|
||||
private float contentTop;
|
||||
|
||||
// Sortierte Item-Liste des aktiven Tabs
|
||||
private List<Map.Entry<Item, Integer>> activeItems = new ArrayList<>();
|
||||
|
||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||
|
||||
public InventoryState(MainCharacter mc, KeyBindings keyBindings) {
|
||||
this.mc = mc;
|
||||
this.keyBindings = keyBindings;
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application app) {
|
||||
this.app = (SimpleApplication) app;
|
||||
this.assetManager = app.getAssetManager();
|
||||
this.guiNode = this.app.getGuiNode();
|
||||
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
|
||||
|
||||
app.getInputManager().addMapping(MAP_TOGGLE, new KeyTrigger(keyBindings.inventory));
|
||||
app.getInputManager().addMapping(MAP_SCROLL_UP, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
|
||||
app.getInputManager().addMapping(MAP_SCROLL_DN, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
|
||||
app.getInputManager().addMapping(MAP_CLICK, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
|
||||
app.getInputManager().addListener(toggleListener, MAP_TOGGLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
scrollY = 0f;
|
||||
buildPanel();
|
||||
app.getInputManager().setCursorVisible(true);
|
||||
app.getInputManager().addListener(scrollListener, MAP_SCROLL_UP, MAP_SCROLL_DN);
|
||||
app.getInputManager().addListener(clickListener, MAP_CLICK);
|
||||
WorldScene ws = app.getStateManager().getState(WorldScene.class);
|
||||
if (ws != null) ws.setPaused(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisable() {
|
||||
destroyPanel();
|
||||
app.getInputManager().removeListener(scrollListener);
|
||||
app.getInputManager().removeListener(clickListener);
|
||||
app.getInputManager().setCursorVisible(false);
|
||||
WorldScene ws = app.getStateManager().getState(WorldScene.class);
|
||||
if (ws != null) ws.setPaused(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application app) {
|
||||
app.getInputManager().deleteMapping(MAP_TOGGLE);
|
||||
app.getInputManager().deleteMapping(MAP_SCROLL_UP);
|
||||
app.getInputManager().deleteMapping(MAP_SCROLL_DN);
|
||||
app.getInputManager().deleteMapping(MAP_CLICK);
|
||||
}
|
||||
|
||||
// ── Listener ──────────────────────────────────────────────────────────────
|
||||
|
||||
private final ActionListener toggleListener = (name, pressed, tpf) -> {
|
||||
if (pressed && MAP_TOGGLE.equals(name)) setEnabled(!isEnabled());
|
||||
};
|
||||
|
||||
private final AnalogListener scrollListener = (name, value, tpf) -> {
|
||||
float step = 60f * value;
|
||||
if (MAP_SCROLL_UP.equals(name)) scrollY = Math.max(0f, scrollY - step);
|
||||
else scrollY = Math.min(maxScrollY, scrollY + step);
|
||||
rebuildGrid();
|
||||
};
|
||||
|
||||
private final ActionListener clickListener = (name, pressed, tpf) -> {
|
||||
if (!pressed || panel == null || tabBounds == null) return;
|
||||
Vector2f c = app.getInputManager().getCursorPosition();
|
||||
for (int i = 0; i < tabBounds.length; i++) {
|
||||
float[] b = tabBounds[i];
|
||||
if (c.x >= b[0] && c.x <= b[0] + b[2] && c.y >= b[1] && c.y <= b[1] + b[3]) {
|
||||
switchTab(tabOrder[i]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ── Haupt-Panel aufbauen ──────────────────────────────────────────────────
|
||||
|
||||
private void buildPanel() {
|
||||
float sw = app.getCamera().getWidth();
|
||||
float sh = app.getCamera().getHeight();
|
||||
|
||||
// Panelgröße: 5 Spalten + Ränder
|
||||
float pw = COLS * (CELL_W + CELL_GAP) + CELL_GAP + 2 * PAD;
|
||||
float ph = HDR_H + TAB_H + 4 + 450 + PAD; // header + tabs + gap + content + bottom
|
||||
float px = (sw - pw) / 2f;
|
||||
float py = (sh - ph) / 2f;
|
||||
|
||||
panel = new Node("inv-panel");
|
||||
quad(panel, 0, 0, sw, sh, COL_OVERLAY, -20); // Verdunkelung
|
||||
quad(panel, px, py, pw, ph, COL_PANEL, -19); // Hauptpanel
|
||||
quad(panel, px, py + ph - HDR_H, pw, HDR_H, COL_HDR, -18); // Header-Balken
|
||||
|
||||
// Titel
|
||||
BitmapText title = txt("Inventar", 22, COL_WHITE);
|
||||
title.setLocalTranslation(px + PAD, py + ph - 12, -17);
|
||||
panel.attachChild(title);
|
||||
|
||||
// Content-Bereich (für Scroll-Berechnungen)
|
||||
contentLeft = px + PAD;
|
||||
contentBottom = py + PAD;
|
||||
contentTop = py + ph - HDR_H - TAB_H - 8;
|
||||
|
||||
// Tabs aufbauen
|
||||
buildTabs(px, py, pw, ph);
|
||||
|
||||
guiNode.attachChild(panel);
|
||||
}
|
||||
|
||||
private void buildTabs(float px, float py, float pw, float ph) {
|
||||
tabOrder = ItemCategory.values();
|
||||
tabBounds = new float[tabOrder.length][4];
|
||||
|
||||
if (activeTab == null) activeTab = tabOrder[0];
|
||||
|
||||
float tabY = py + ph - HDR_H - TAB_H;
|
||||
float tabW = (pw - 8) / tabOrder.length;
|
||||
float tabX0 = px + 4;
|
||||
|
||||
for (int i = 0; i < tabOrder.length; i++) {
|
||||
boolean active = tabOrder[i] == activeTab;
|
||||
float tx = tabX0 + i * tabW;
|
||||
quad(panel, tx, tabY, tabW - 4, TAB_H, active ? COL_TAB_ON : COL_TAB_OFF, -18);
|
||||
BitmapText lbl = txt(catLabel(tabOrder[i]), 13, active ? COL_WHITE : COL_MUTED);
|
||||
lbl.setLocalTranslation(tx + (tabW - 4 - lbl.getLineWidth()) / 2f, tabY + TAB_H - 8, -17);
|
||||
panel.attachChild(lbl);
|
||||
tabBounds[i] = new float[]{ tx, tabY, tabW - 4, TAB_H };
|
||||
}
|
||||
|
||||
activeItems = sortedItems(activeTab);
|
||||
maxScrollY = computeMaxScroll();
|
||||
scrollY = 0f;
|
||||
buildGrid();
|
||||
}
|
||||
|
||||
private void switchTab(ItemCategory cat) {
|
||||
if (cat == activeTab) return;
|
||||
activeTab = cat;
|
||||
scrollY = 0f;
|
||||
activeItems = sortedItems(cat);
|
||||
destroyPanel();
|
||||
buildPanel();
|
||||
}
|
||||
|
||||
// ── Item-Grid ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Erstellt den Grid-Node und hängt ihn ans Panel. */
|
||||
private void buildGrid() {
|
||||
gridNode = new Node("inv-grid");
|
||||
panel.attachChild(gridNode);
|
||||
|
||||
if (activeItems.isEmpty()) {
|
||||
BitmapText empty = txt("Keine Items vorhanden", 15, COL_MUTED);
|
||||
float ey = contentBottom + (contentTop - contentBottom) / 2f + 10;
|
||||
float ex = contentLeft + (COLS * (CELL_W + CELL_GAP) - CELL_GAP - empty.getLineWidth()) / 2f;
|
||||
empty.setLocalTranslation(ex, ey, -17);
|
||||
gridNode.attachChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
// Items nach SubKategorie gruppiert (Reihenfolge aus sortedItems beibehalten)
|
||||
Map<ItemSubCategory, List<Map.Entry<Item, Integer>>> groups = new LinkedHashMap<>();
|
||||
for (Map.Entry<Item, Integer> e : activeItems) {
|
||||
groups.computeIfAbsent(e.getKey().getSubCategory(), k -> new ArrayList<>()).add(e);
|
||||
}
|
||||
|
||||
// Virtual Y: beginnt am Top des Content-Bereichs, läuft nach unten (Y nimmt ab)
|
||||
float virtY = contentTop - scrollY;
|
||||
|
||||
for (Map.Entry<ItemSubCategory, List<Map.Entry<Item, Integer>>> group : groups.entrySet()) {
|
||||
// SubKategorie-Header
|
||||
float subHdrY = virtY - SUBHDR_H;
|
||||
if (subHdrY + SUBHDR_H >= contentBottom && subHdrY <= contentTop) {
|
||||
String subLabel = group.getKey() != null ? subCatLabel(group.getKey()) : "Sonstiges";
|
||||
float subBgW = COLS * (CELL_W + CELL_GAP) - CELL_GAP;
|
||||
quad(gridNode, contentLeft, subHdrY, subBgW, SUBHDR_H, COL_SUBHDR, -18);
|
||||
BitmapText sLbl = txt(" " + subLabel, 12, COL_SUBCAT_TXT);
|
||||
sLbl.setLocalTranslation(contentLeft + 6, subHdrY + SUBHDR_H - 7, -17);
|
||||
gridNode.attachChild(sLbl);
|
||||
}
|
||||
virtY -= SUBHDR_H + SUBHDR_GAP;
|
||||
|
||||
// Item-Zellen zeilenweise
|
||||
List<Map.Entry<Item, Integer>> groupItems = group.getValue();
|
||||
int col = 0;
|
||||
for (Map.Entry<Item, Integer> entry : groupItems) {
|
||||
if (col == 0) virtY -= CELL_H;
|
||||
|
||||
float cellX = contentLeft + col * (CELL_W + CELL_GAP);
|
||||
float cellY = virtY;
|
||||
|
||||
// Nur rendern wenn vollständig im sichtbaren Content-Bereich
|
||||
if (cellY >= contentBottom && cellY + CELL_H <= contentTop) {
|
||||
buildCell(gridNode, entry.getKey(), entry.getValue(), cellX, cellY);
|
||||
}
|
||||
|
||||
col++;
|
||||
if (col >= COLS) {
|
||||
col = 0;
|
||||
virtY -= CELL_GAP;
|
||||
}
|
||||
}
|
||||
// Wenn letzte Zeile nicht voll war, Y-Schritt nachholen
|
||||
if (col != 0) virtY -= CELL_GAP;
|
||||
virtY -= CELL_GAP; // Abstand zwischen Gruppen
|
||||
}
|
||||
}
|
||||
|
||||
/** Entfernt den Grid-Node und erstellt ihn neu (bei Scroll). */
|
||||
private void rebuildGrid() {
|
||||
if (panel == null) return;
|
||||
if (gridNode != null) panel.detachChild(gridNode);
|
||||
buildGrid();
|
||||
}
|
||||
|
||||
/** Berechnet maximalen Scroll-Offset in Pixel. */
|
||||
private float computeMaxScroll() {
|
||||
Map<ItemSubCategory, List<Map.Entry<Item, Integer>>> groups = new LinkedHashMap<>();
|
||||
for (Map.Entry<Item, Integer> e : activeItems) {
|
||||
groups.computeIfAbsent(e.getKey().getSubCategory(), k -> new ArrayList<>()).add(e);
|
||||
}
|
||||
float totalH = 0f;
|
||||
for (List<Map.Entry<Item, Integer>> g : groups.values()) {
|
||||
totalH += SUBHDR_H + SUBHDR_GAP;
|
||||
int rows = (int) Math.ceil(g.size() / (double) COLS);
|
||||
totalH += rows * (CELL_H + CELL_GAP);
|
||||
}
|
||||
float contentH = contentTop - contentBottom;
|
||||
return Math.max(0f, totalH - contentH);
|
||||
}
|
||||
|
||||
// ── Einzel-Zelle ──────────────────────────────────────────────────────────
|
||||
|
||||
private void buildCell(Node parent, Item item, int count, float x, float y) {
|
||||
// Zell-Hintergrund + Rand
|
||||
quad(parent, x, y, CELL_W, CELL_H, COL_CELL_FRAME, -18);
|
||||
quad(parent, x + 1, y + 1, CELL_W - 2, CELL_H - 2, COL_CELL, -17);
|
||||
|
||||
// Thumbnail
|
||||
float thumbX = x + (CELL_W - THUMB_SZ) / 2f;
|
||||
float thumbY = y + CELL_H - THUMB_SZ - 6;
|
||||
|
||||
Texture thumb = null;
|
||||
if (item.getModelRef() != null) {
|
||||
String tp = item.getModelRef().getThumbnailAssetPath();
|
||||
if (tp != null) thumb = loadThumb(tp);
|
||||
}
|
||||
|
||||
if (thumb != null) {
|
||||
Geometry tg = new Geometry("thumb", new Quad(THUMB_SZ, THUMB_SZ));
|
||||
Material tm = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
tm.setTexture("ColorMap", thumb);
|
||||
tm.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
tg.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
tg.setMaterial(tm);
|
||||
tg.setLocalTranslation(thumbX, thumbY, -16);
|
||||
parent.attachChild(tg);
|
||||
} else {
|
||||
quad(parent, thumbX, thumbY, THUMB_SZ, THUMB_SZ, COL_THUMB_BG, -16);
|
||||
}
|
||||
|
||||
// Anzahl-Badge unten rechts (nur wenn > 1)
|
||||
if (count > 1) {
|
||||
String cntStr = count > 999 ? "999+" : String.valueOf(count);
|
||||
BitmapText cntTxt = txt(cntStr, 13, COL_WHITE);
|
||||
float bw = Math.max(26, cntTxt.getLineWidth() + 8);
|
||||
float bh = 18;
|
||||
float bx = x + CELL_W - bw - 5;
|
||||
float by = thumbY + 4;
|
||||
quad(parent, bx, by, bw, bh, COL_BADGE, -15);
|
||||
cntTxt.setLocalTranslation(bx + (bw - cntTxt.getLineWidth()) / 2f, by + bh - 3, -14);
|
||||
parent.attachChild(cntTxt);
|
||||
}
|
||||
|
||||
// Name
|
||||
BitmapText nameTxt = txt(clip(item.getDisplayText(), 17), 13, COL_WHITE);
|
||||
nameTxt.setLocalTranslation(x + (CELL_W - nameTxt.getLineWidth()) / 2f, y + 42, -16);
|
||||
parent.attachChild(nameTxt);
|
||||
|
||||
// Goldwert
|
||||
BitmapText goldTxt = txt(item.getWorthGold() + " G", 12, COL_GOLD);
|
||||
goldTxt.setLocalTranslation(x + (CELL_W - goldTxt.getLineWidth()) / 2f, y + 22, -16);
|
||||
parent.attachChild(goldTxt);
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Lädt Texture oder gibt null zurück wenn nicht vorhanden. */
|
||||
private Texture loadThumb(String assetPath) {
|
||||
try { return assetManager.loadTexture(assetPath); }
|
||||
catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
/** Items des Tabs sortiert nach SubKategorie-Ordinal, dann Preis. */
|
||||
private List<Map.Entry<Item, Integer>> sortedItems(ItemCategory cat) {
|
||||
Inventar inv = mc.getInventar();
|
||||
if (inv == null) return List.of();
|
||||
return inv.getItems().entrySet().stream()
|
||||
.filter(e -> e.getKey().getCategory() == cat)
|
||||
.sorted(Comparator
|
||||
.<Map.Entry<Item, Integer>>comparingInt(e ->
|
||||
e.getKey().getSubCategory() == null
|
||||
? Integer.MAX_VALUE
|
||||
: e.getKey().getSubCategory().ordinal())
|
||||
.thenComparingInt(e -> e.getKey().getWorthGold()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void destroyPanel() {
|
||||
if (panel != null) { guiNode.detachChild(panel); panel = null; gridNode = null; }
|
||||
}
|
||||
|
||||
private Geometry quad(Node parent, float x, float y, float w, float h, ColorRGBA col, float z) {
|
||||
Geometry g = new Geometry("q", new Quad(w, h));
|
||||
Material m = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
m.setColor("Color", col.clone());
|
||||
if (col.a < 1f) {
|
||||
m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
g.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
}
|
||||
g.setMaterial(m);
|
||||
g.setLocalTranslation(x, y, z);
|
||||
parent.attachChild(g);
|
||||
return g;
|
||||
}
|
||||
|
||||
private BitmapText txt(String s, int size, ColorRGBA col) {
|
||||
BitmapText t = new BitmapText(font, false);
|
||||
t.setSize(size); t.setColor(col); t.setText(s);
|
||||
return t;
|
||||
}
|
||||
|
||||
private static String clip(String s, int max) {
|
||||
return s.length() <= max ? s : s.substring(0, max - 1) + "…";
|
||||
}
|
||||
|
||||
private static String catLabel(ItemCategory c) {
|
||||
return switch (c) {
|
||||
case WEAPON -> "Waffen";
|
||||
case GEAR -> "Ausrüstung";
|
||||
case CONSUMABLES -> "Verbrauchbar";
|
||||
case QUEST_ITEMS -> "Quest";
|
||||
case CRAFTING_ITEMS -> "Handwerk";
|
||||
case MISC_ITEMS -> "Sonstiges";
|
||||
};
|
||||
}
|
||||
|
||||
private static String subCatLabel(ItemSubCategory s) {
|
||||
return switch (s) {
|
||||
case SWORD -> "Schwerter";
|
||||
case TWO_HANDED_SWORD -> "Zweihandschwerter";
|
||||
case STAFF -> "Stäbe";
|
||||
case HALBERD -> "Hellebarden";
|
||||
case AXE -> "Äxte";
|
||||
case RING -> "Ringe";
|
||||
case NECKLACE -> "Halsketten";
|
||||
case ARMOR -> "Rüstungen";
|
||||
case HELM -> "Helme";
|
||||
case SHIELD -> "Schilde";
|
||||
case POTION -> "Tränke";
|
||||
case PERMANENT_POTION -> "Dauerhafte Tränke";
|
||||
case FOOD -> "Nahrung";
|
||||
case MAGICAL_ITEM -> "Magische Items";
|
||||
case QUEST_ITEM -> "Quest-Items";
|
||||
case PLANT -> "Pflanzen";
|
||||
case TECHNICAL -> "Technisches";
|
||||
case MAGICAL -> "Magisches";
|
||||
case MISC -> "Sonstiges";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,542 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.scene.Mesh;
|
||||
import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.common.VoxelChunk;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Marching-Cubes Mesh-Generator für {@link VoxelChunk}.
|
||||
*
|
||||
* Vertex-Nummerierung einer MC-Zelle:
|
||||
* v0=(x,y,z), v1=(x+s,y,z), v2=(x+s,y,z+s), v3=(x,y,z+s)
|
||||
* v4=(x,y+s,z), v5=(x+s,y+s,z), v6=(x+s,y+s,z+s), v7=(x,y+s,z+s)
|
||||
*
|
||||
* 12 Kanten (von-nach):
|
||||
* 0:v0-v1, 1:v1-v2, 2:v3-v2, 3:v0-v3
|
||||
* 4:v4-v5, 5:v5-v6, 6:v7-v6, 7:v4-v7
|
||||
* 8:v0-v4, 9:v1-v5, 10:v2-v6, 11:v3-v7
|
||||
*/
|
||||
public final class MarchingCubes {
|
||||
|
||||
private MarchingCubes() {}
|
||||
|
||||
// ── Vollständige Standard-Lookup-Tabellen (Lorensen/Cline) ─────────────────
|
||||
|
||||
/** Für jeden der 256 Cube-Zustände: Bitmask der 12 geschnittenen Kanten. */
|
||||
private static final int[] EDGE_TABLE = {
|
||||
0x000, 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
|
||||
0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
|
||||
0x190, 0x099, 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
|
||||
0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90,
|
||||
0x230, 0x339, 0x033, 0x13a, 0x636, 0x73f, 0x435, 0x53c,
|
||||
0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30,
|
||||
0x3a0, 0x2a9, 0x1a3, 0x0aa, 0x7a6, 0x6af, 0x5a5, 0x4ac,
|
||||
0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0,
|
||||
0x460, 0x569, 0x663, 0x76a, 0x066, 0x16f, 0x265, 0x36c,
|
||||
0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60,
|
||||
0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0x0ff, 0x3f5, 0x2fc,
|
||||
0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0,
|
||||
0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x055, 0x15c,
|
||||
0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950,
|
||||
0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0x0cc,
|
||||
0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0,
|
||||
0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc,
|
||||
0x0cc, 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0,
|
||||
0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c,
|
||||
0x15c, 0x055, 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650,
|
||||
0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc,
|
||||
0x2fc, 0x3f5, 0x0ff, 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0,
|
||||
0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c,
|
||||
0x36c, 0x265, 0x16f, 0x066, 0x76a, 0x663, 0x569, 0x460,
|
||||
0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac,
|
||||
0x4ac, 0x5a5, 0x6af, 0x7a6, 0x0aa, 0x1a3, 0x2a9, 0x3a0,
|
||||
0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x835, 0xb3f, 0xa36, // NOTE: 0x835 = 0x83f corrected below
|
||||
0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x033, 0x339, 0x230,
|
||||
0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
|
||||
0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x099, 0x190,
|
||||
0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
|
||||
0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x000
|
||||
};
|
||||
|
||||
/**
|
||||
* TRI_TABLE[cubeIndex] = Liste von Kanten-Indizes (0–11) in Dreiergruppen,
|
||||
* terminiert durch -1. Maximal 16 Einträge pro Zeile.
|
||||
* Standard Paul Bourke / Lorensen-Cline Tabelle.
|
||||
*/
|
||||
private static final int[][] TRI_TABLE = {
|
||||
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 8, 3, 9, 8, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 8, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 2, 10, 0, 2, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 2, 8, 3, 2, 10, 8, 10, 9, 8, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 11, 2, 8, 11, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 9, 0, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 11, 2, 1, 9, 11, 9, 8, 11, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 3, 10, 1, 11, 10, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 10, 1, 0, 8, 10, 8, 11, 10, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 3, 9, 0, 3, 11, 9, 11, 10, 9, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 8, 11, 9, 11, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 4, 3, 0, 7, 3, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 1, 9, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 4, 1, 9, 4, 7, 1, 7, 3, 1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 2, 10, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 3, 4, 7, 3, 0, 4, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 2, 10, 9, 0, 2, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4, -1, -1, -1, -1},
|
||||
{ 8, 4, 7, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{11, 4, 7, 11, 2, 4, 2, 0, 4, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 0, 1, 8, 4, 7, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1, -1, -1, -1, -1},
|
||||
{ 3, 10, 1, 3, 11, 10, 7, 8, 4, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4, -1, -1, -1, -1},
|
||||
{ 4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3, -1, -1, -1, -1},
|
||||
{ 4, 7, 11, 4, 11, 9, 9, 11, 10, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 5, 4, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 5, 4, 1, 5, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 8, 5, 4, 8, 3, 5, 3, 1, 5, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 2, 10, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 3, 0, 8, 1, 2, 10, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 5, 2, 10, 5, 4, 2, 4, 0, 2, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8, -1, -1, -1, -1},
|
||||
{ 9, 5, 4, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 11, 2, 0, 8, 11, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 5, 4, 0, 1, 5, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5, -1, -1, -1, -1},
|
||||
{10, 3, 11, 10, 1, 3, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10, -1, -1, -1, -1},
|
||||
{ 5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3, -1, -1, -1, -1},
|
||||
{ 5, 4, 8, 5, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 7, 8, 5, 7, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 3, 0, 9, 5, 3, 5, 7, 3, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 7, 8, 0, 1, 7, 1, 5, 7, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 7, 8, 9, 5, 7, 10, 1, 2, -1, -1, -1, -1, -1, -1, -1},
|
||||
{10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3, -1, -1, -1, -1},
|
||||
{ 8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2, -1, -1, -1, -1},
|
||||
{ 2, 10, 5, 2, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 7, 9, 5, 7, 8, 9, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11, -1, -1, -1, -1},
|
||||
{ 2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7, -1, -1, -1, -1},
|
||||
{11, 2, 1, 11, 1, 7, 7, 1, 5, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11, -1, -1, -1, -1},
|
||||
{ 5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0, -1},
|
||||
{11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0, -1},
|
||||
{11, 10, 5, 7, 11, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 8, 3, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 0, 1, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 8, 3, 1, 9, 8, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 6, 5, 2, 6, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 6, 5, 1, 2, 6, 3, 0, 8, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 6, 5, 9, 0, 6, 0, 2, 6, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8, -1, -1, -1, -1},
|
||||
{ 2, 3, 11, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{11, 0, 8, 11, 2, 0, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 1, 9, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11, -1, -1, -1, -1},
|
||||
{ 6, 3, 11, 6, 5, 3, 5, 1, 3, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6, -1, -1, -1, -1},
|
||||
{ 3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9, -1, -1, -1, -1},
|
||||
{ 6, 5, 9, 6, 9, 11, 11, 9, 8, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 5, 10, 6, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 4, 3, 0, 4, 7, 3, 6, 5, 10, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 9, 0, 5, 10, 6, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1},
|
||||
{10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4, -1, -1, -1, -1},
|
||||
{ 6, 1, 2, 6, 5, 1, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7, -1, -1, -1, -1},
|
||||
{ 8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6, -1, -1, -1, -1},
|
||||
{ 7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9, -1},
|
||||
{ 3, 11, 2, 7, 8, 4, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11, -1, -1, -1, -1},
|
||||
{ 0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1},
|
||||
{ 9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6, -1},
|
||||
{ 8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6, -1, -1, -1, -1},
|
||||
{ 5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11, -1},
|
||||
{ 0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7, -1},
|
||||
{ 6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9, -1, -1, -1, -1},
|
||||
{10, 4, 9, 6, 4, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 4, 10, 6, 4, 9, 10, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1},
|
||||
{10, 0, 1, 10, 6, 0, 6, 4, 0, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10, -1, -1, -1, -1},
|
||||
{ 1, 4, 9, 1, 2, 4, 2, 6, 4, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4, -1, -1, -1, -1},
|
||||
{ 0, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 8, 3, 2, 8, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1},
|
||||
{10, 4, 9, 10, 6, 4, 11, 2, 3, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6, -1, -1, -1, -1},
|
||||
{ 3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10, -1, -1, -1, -1},
|
||||
{ 6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1, -1},
|
||||
{ 9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3, -1, -1, -1, -1},
|
||||
{ 8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1, -1},
|
||||
{ 3, 11, 6, 3, 6, 0, 0, 6, 4, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 6, 4, 8, 11, 6, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 7, 10, 6, 7, 8, 10, 8, 9, 10, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10, -1, -1, -1, -1},
|
||||
{10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0, -1, -1, -1, -1},
|
||||
{10, 6, 7, 10, 7, 1, 1, 7, 3, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7, -1, -1, -1, -1},
|
||||
{ 2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9, -1},
|
||||
{ 7, 8, 0, 7, 0, 6, 6, 0, 2, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 7, 3, 2, 6, 7, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7, -1, -1, -1, -1},
|
||||
{ 2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7, -1},
|
||||
{ 1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11, -1},
|
||||
{11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1, -1, -1, -1, -1},
|
||||
{ 8, 9, 1, 8, 1, 3, 9, 6, 1, 11, 1, 7, 6, 7, 1, -1},
|
||||
{ 0, 9, 1, 11, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0, -1, -1, -1, -1},
|
||||
{ 7, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 3, 0, 8, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 1, 9, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 8, 1, 9, 8, 3, 1, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1},
|
||||
{10, 1, 2, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 2, 10, 3, 0, 8, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 2, 9, 0, 2, 10, 9, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8, -1, -1, -1, -1},
|
||||
{ 7, 2, 3, 6, 2, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 7, 0, 8, 7, 6, 0, 6, 2, 0, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 2, 7, 6, 2, 3, 7, 0, 1, 9, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6, -1, -1, -1, -1},
|
||||
{10, 7, 6, 10, 1, 7, 1, 3, 7, -1, -1, -1, -1, -1, -1, -1},
|
||||
{10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8, -1, -1, -1, -1},
|
||||
{ 0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7, -1, -1, -1, -1},
|
||||
{ 7, 6, 10, 7, 10, 8, 8, 10, 9, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 6, 8, 4, 11, 8, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 3, 6, 11, 3, 0, 6, 0, 4, 6, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 8, 6, 11, 8, 4, 6, 9, 0, 1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6, -1, -1, -1, -1},
|
||||
{ 6, 8, 4, 6, 11, 8, 2, 10, 1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6, -1, -1, -1, -1},
|
||||
{ 4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9, -1, -1, -1, -1},
|
||||
{10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3, -1},
|
||||
{ 8, 2, 3, 8, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8, -1, -1, -1, -1},
|
||||
{ 1, 9, 4, 1, 4, 2, 2, 4, 6, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1, -1, -1, -1, -1},
|
||||
{10, 1, 0, 10, 0, 6, 6, 0, 4, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3, -1},
|
||||
{10, 9, 4, 6, 10, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 4, 9, 5, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 8, 3, 4, 9, 5, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 5, 0, 1, 5, 4, 0, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1},
|
||||
{11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5, -1, -1, -1, -1},
|
||||
{ 9, 5, 4, 10, 1, 2, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5, -1, -1, -1, -1},
|
||||
{ 7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2, -1, -1, -1, -1},
|
||||
{ 3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6, -1},
|
||||
{ 7, 2, 3, 7, 6, 2, 5, 4, 9, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7, -1, -1, -1, -1},
|
||||
{ 3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0, -1, -1, -1, -1},
|
||||
{ 6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8, -1},
|
||||
{ 9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7, -1, -1, -1, -1},
|
||||
{ 1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4, -1},
|
||||
{ 4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10, -1},
|
||||
{ 7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10, -1, -1, -1, -1},
|
||||
{ 6, 9, 5, 6, 11, 9, 11, 8, 9, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5, -1, -1, -1, -1},
|
||||
{ 0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11, -1, -1, -1, -1},
|
||||
{ 6, 11, 3, 6, 3, 5, 5, 3, 1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6, -1, -1, -1, -1},
|
||||
{ 0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10, -1},
|
||||
{11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5, -1},
|
||||
{ 6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3, -1, -1, -1, -1},
|
||||
{ 5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2, -1, -1, -1, -1},
|
||||
{ 9, 5, 6, 9, 6, 0, 0, 6, 2, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8, -1},
|
||||
{ 1, 5, 6, 2, 1, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6, -1},
|
||||
{10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0, -1, -1, -1, -1},
|
||||
{ 0, 3, 8, 5, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{10, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{11, 5, 10, 7, 5, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{11, 5, 10, 11, 7, 5, 8, 3, 0, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 5, 11, 7, 5, 10, 11, 1, 9, 0, -1, -1, -1, -1, -1, -1, -1},
|
||||
{10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1, -1, -1, -1, -1},
|
||||
{11, 1, 2, 11, 7, 1, 7, 5, 1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11, -1, -1, -1, -1},
|
||||
{ 9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7, -1, -1, -1, -1},
|
||||
{ 7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2, -1},
|
||||
{ 2, 5, 10, 2, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5, -1, -1, -1, -1},
|
||||
{ 9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2, -1, -1, -1, -1},
|
||||
{ 9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2, -1},
|
||||
{ 1, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 8, 7, 0, 7, 1, 1, 7, 5, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 0, 3, 9, 3, 5, 5, 3, 7, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 8, 7, 5, 9, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 5, 8, 4, 5, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0, -1, -1, -1, -1},
|
||||
{ 0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5, -1, -1, -1, -1},
|
||||
{10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4, -1},
|
||||
{ 2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8, -1, -1, -1, -1},
|
||||
{ 0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11, -1},
|
||||
{ 0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5, -1},
|
||||
{ 9, 4, 5, 2, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4, -1, -1, -1, -1},
|
||||
{ 5, 10, 2, 5, 2, 4, 4, 2, 0, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9, -1},
|
||||
{ 5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2, -1, -1, -1, -1},
|
||||
{ 8, 4, 5, 8, 5, 3, 3, 5, 1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 4, 5, 1, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5, -1, -1, -1, -1},
|
||||
{ 9, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 4, 11, 7, 4, 9, 11, 9, 10, 11, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11, -1, -1, -1, -1},
|
||||
{ 1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11, -1, -1, -1, -1},
|
||||
{ 3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4, -1},
|
||||
{ 4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2, -1, -1, -1, -1},
|
||||
{ 9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3, -1},
|
||||
{11, 7, 4, 11, 4, 2, 2, 4, 0, -1, -1, -1, -1, -1, -1, -1},
|
||||
{11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4, -1, -1, -1, -1},
|
||||
{ 2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9, -1, -1, -1, -1},
|
||||
{ 9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7, -1},
|
||||
{ 3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10, -1},
|
||||
{ 1, 10, 2, 8, 7, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 4, 9, 1, 4, 1, 7, 7, 1, 3, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1, -1, -1, -1, -1},
|
||||
{ 4, 0, 3, 7, 4, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 4, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 3, 0, 9, 3, 9, 11, 11, 9, 10, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 1, 10, 0, 10, 8, 8, 10, 11, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 3, 1, 10, 11, 3, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 2, 11, 1, 11, 9, 9, 11, 8, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9, -1, -1, -1, -1},
|
||||
{ 0, 2, 11, 8, 0, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 3, 2, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 2, 3, 8, 2, 8, 10, 10, 8, 9, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 9, 10, 2, 0, 9, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8, -1, -1, -1, -1},
|
||||
{ 1, 10, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 1, 3, 8, 9, 1, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{ 0, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}
|
||||
};
|
||||
|
||||
// ── Öffentliche Methode ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Erzeugt ein JME-Mesh aus den Voxel-Daten mit dem angegebenen LOD-Schritt.
|
||||
*
|
||||
* @param chunk Quelldaten
|
||||
* @param lodStep 1=LOD0 (voll), 4=LOD1, 16=LOD2
|
||||
* @return JME-Mesh oder null wenn keine Oberfläche vorhanden
|
||||
*/
|
||||
public static Mesh build(VoxelChunk chunk, int lodStep) {
|
||||
if (chunk.isEmpty()) return null;
|
||||
|
||||
// Flache Listen für Positions-, Normal- und Color-Daten
|
||||
List<Float> positions = new ArrayList<>(4096);
|
||||
List<Float> normals = new ArrayList<>(4096);
|
||||
List<Float> colors = new ArrayList<>(4096);
|
||||
|
||||
int cells = VoxelChunk.CELLS; // 128
|
||||
int size = VoxelChunk.SIZE; // 129
|
||||
|
||||
// Schrittzahl berechnen – Anzahl Zellen = cells / lodStep
|
||||
int steps = cells / lodStep;
|
||||
|
||||
for (int iy = 0; iy < steps; iy++) {
|
||||
for (int iz = 0; iz < steps; iz++) {
|
||||
for (int ix = 0; ix < steps; ix++) {
|
||||
|
||||
// Eckpunkt-Koordinaten (lokaler Voxel-Raum, 0..128)
|
||||
int x0 = ix * lodStep;
|
||||
int y0 = iy * lodStep;
|
||||
int z0 = iz * lodStep;
|
||||
int x1 = x0 + lodStep;
|
||||
int y1 = y0 + lodStep;
|
||||
int z1 = z0 + lodStep;
|
||||
|
||||
// 8 Dichte-Werte der Eckpunkte
|
||||
float d0 = chunk.getDensity(x0, y0, z0);
|
||||
float d1 = chunk.getDensity(x1, y0, z0);
|
||||
float d2 = chunk.getDensity(x1, y0, z1);
|
||||
float d3 = chunk.getDensity(x0, y0, z1);
|
||||
float d4 = chunk.getDensity(x0, y1, z0);
|
||||
float d5 = chunk.getDensity(x1, y1, z0);
|
||||
float d6 = chunk.getDensity(x1, y1, z1);
|
||||
float d7 = chunk.getDensity(x0, y1, z1);
|
||||
|
||||
// cubeIndex aufbauen (Bit gesetzt = Vertex > 0 = solid)
|
||||
int cubeIndex = 0;
|
||||
if (d0 > 0) cubeIndex |= 1;
|
||||
if (d1 > 0) cubeIndex |= 2;
|
||||
if (d2 > 0) cubeIndex |= 4;
|
||||
if (d3 > 0) cubeIndex |= 8;
|
||||
if (d4 > 0) cubeIndex |= 16;
|
||||
if (d5 > 0) cubeIndex |= 32;
|
||||
if (d6 > 0) cubeIndex |= 64;
|
||||
if (d7 > 0) cubeIndex |= 128;
|
||||
|
||||
if (EDGE_TABLE[cubeIndex] == 0) continue;
|
||||
|
||||
// Materialen der 8 Eckpunkte
|
||||
int m0 = chunk.getMaterial(x0, y0, z0) & 0xFF;
|
||||
int m1 = chunk.getMaterial(x1, y0, z0) & 0xFF;
|
||||
int m2 = chunk.getMaterial(x1, y0, z1) & 0xFF;
|
||||
int m3 = chunk.getMaterial(x0, y0, z1) & 0xFF;
|
||||
int m4 = chunk.getMaterial(x0, y1, z0) & 0xFF;
|
||||
int m5 = chunk.getMaterial(x1, y1, z0) & 0xFF;
|
||||
int m6 = chunk.getMaterial(x1, y1, z1) & 0xFF;
|
||||
int m7 = chunk.getMaterial(x0, y1, z1) & 0xFF;
|
||||
|
||||
// Positionen der 8 Eckpunkte als float[]
|
||||
float[] vx = { x0, x1, x1, x0, x0, x1, x1, x0 };
|
||||
float[] vy = { y0, y0, y0, y0, y1, y1, y1, y1 };
|
||||
float[] vz = { z0, z0, z1, z1, z0, z0, z1, z1 };
|
||||
float[] dd = { d0, d1, d2, d3, d4, d5, d6, d7 };
|
||||
int[] mm = { m0, m1, m2, m3, m4, m5, m6, m7 };
|
||||
|
||||
// 12 Kantenpunkte berechnen
|
||||
float[] epx = new float[12];
|
||||
float[] epy = new float[12];
|
||||
float[] epz = new float[12];
|
||||
float[] enx = new float[12];
|
||||
float[] eny = new float[12];
|
||||
float[] enz = new float[12];
|
||||
float[] ecR = new float[12];
|
||||
float[] ecG = new float[12];
|
||||
float[] ecB = new float[12];
|
||||
float[] ecA = new float[12];
|
||||
|
||||
// Kante i verbindet Vertex A mit Vertex B
|
||||
int[][] edgeVerts = {
|
||||
{0,1}, {1,2}, {3,2}, {0,3},
|
||||
{4,5}, {5,6}, {7,6}, {4,7},
|
||||
{0,4}, {1,5}, {2,6}, {3,7}
|
||||
};
|
||||
|
||||
int edgeMask = EDGE_TABLE[cubeIndex];
|
||||
for (int e = 0; e < 12; e++) {
|
||||
if ((edgeMask & (1 << e)) == 0) continue;
|
||||
int a = edgeVerts[e][0];
|
||||
int b = edgeVerts[e][1];
|
||||
float dA = dd[a], dB = dd[b];
|
||||
float t;
|
||||
if (Math.abs(dB - dA) < 0.001f) {
|
||||
t = 0.5f;
|
||||
} else {
|
||||
t = (0f - dA) / (dB - dA);
|
||||
if (t < 1e-6f) t = 1e-6f;
|
||||
if (t > 1f - 1e-6f) t = 1f - 1e-6f;
|
||||
}
|
||||
epx[e] = vx[a] + t * (vx[b] - vx[a]);
|
||||
epy[e] = vy[a] + t * (vy[b] - vy[a]);
|
||||
epz[e] = vz[a] + t * (vz[b] - vz[a]);
|
||||
|
||||
// Normalen via Gradient
|
||||
float[] gA = gradient(chunk, (int)vx[a], (int)vy[a], (int)vz[a]);
|
||||
float[] gB = gradient(chunk, (int)vx[b], (int)vy[b], (int)vz[b]);
|
||||
enx[e] = gA[0] + t * (gB[0] - gA[0]);
|
||||
eny[e] = gA[1] + t * (gB[1] - gA[1]);
|
||||
enz[e] = gA[2] + t * (gB[2] - gA[2]);
|
||||
float nlen = (float)Math.sqrt(enx[e]*enx[e] + eny[e]*eny[e] + enz[e]*enz[e]);
|
||||
if (nlen > 1e-6f) { enx[e] /= nlen; eny[e] /= nlen; enz[e] /= nlen; }
|
||||
|
||||
// Material-Blend
|
||||
float[] wA = matWeights(mm[a]);
|
||||
float[] wB = matWeights(mm[b]);
|
||||
ecR[e] = wA[0] + t * (wB[0] - wA[0]);
|
||||
ecG[e] = wA[1] + t * (wB[1] - wA[1]);
|
||||
ecB[e] = wA[2] + t * (wB[2] - wA[2]);
|
||||
ecA[e] = wA[3] + t * (wB[3] - wA[3]);
|
||||
}
|
||||
|
||||
// Dreiecke ausgeben
|
||||
int[] tri = TRI_TABLE[cubeIndex];
|
||||
for (int t = 0; t < 16; t += 3) {
|
||||
if (tri[t] < 0) break;
|
||||
int e0 = tri[t], e1 = tri[t+1], e2 = tri[t+2];
|
||||
// Vertex 0
|
||||
positions.add(epx[e0]); positions.add(epy[e0]); positions.add(epz[e0]);
|
||||
normals.add(enx[e0]); normals.add(eny[e0]); normals.add(enz[e0]);
|
||||
colors.add(ecR[e0]); colors.add(ecG[e0]); colors.add(ecB[e0]); colors.add(ecA[e0]);
|
||||
// Vertex 1
|
||||
positions.add(epx[e1]); positions.add(epy[e1]); positions.add(epz[e1]);
|
||||
normals.add(enx[e1]); normals.add(eny[e1]); normals.add(enz[e1]);
|
||||
colors.add(ecR[e1]); colors.add(ecG[e1]); colors.add(ecB[e1]); colors.add(ecA[e1]);
|
||||
// Vertex 2
|
||||
positions.add(epx[e2]); positions.add(epy[e2]); positions.add(epz[e2]);
|
||||
normals.add(enx[e2]); normals.add(eny[e2]); normals.add(enz[e2]);
|
||||
colors.add(ecR[e2]); colors.add(ecG[e2]); colors.add(ecB[e2]); colors.add(ecA[e2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (positions.isEmpty()) return null;
|
||||
|
||||
int vertCount = positions.size() / 3;
|
||||
|
||||
FloatBuffer posBuf = BufferUtils.createFloatBuffer(positions.size());
|
||||
FloatBuffer normBuf = BufferUtils.createFloatBuffer(normals.size());
|
||||
FloatBuffer colBuf = BufferUtils.createFloatBuffer(colors.size());
|
||||
IntBuffer idxBuf = BufferUtils.createIntBuffer(vertCount);
|
||||
|
||||
for (float v : positions) posBuf.put(v);
|
||||
for (float v : normals) normBuf.put(v);
|
||||
for (float v : colors) colBuf.put(v);
|
||||
for (int i = 0; i < vertCount; i++) idxBuf.put(i);
|
||||
|
||||
posBuf.rewind(); normBuf.rewind(); colBuf.rewind(); idxBuf.rewind();
|
||||
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.setBuffer(VertexBuffer.Type.Position, 3, posBuf);
|
||||
mesh.setBuffer(VertexBuffer.Type.Normal, 3, normBuf);
|
||||
mesh.setBuffer(VertexBuffer.Type.Color, 4, colBuf);
|
||||
mesh.setBuffer(VertexBuffer.Type.Index, 3, idxBuf);
|
||||
mesh.updateBound();
|
||||
return mesh;
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Berechnet den negierten Gradienten (zeigt von Solid weg) an Position (ix,iy,iz). */
|
||||
private static float[] gradient(VoxelChunk chunk, int ix, int iy, int iz) {
|
||||
float nx = getDensityClamped(chunk, ix+1, iy, iz )
|
||||
- getDensityClamped(chunk, ix-1, iy, iz );
|
||||
float ny = getDensityClamped(chunk, ix, iy+1, iz )
|
||||
- getDensityClamped(chunk, ix, iy-1, iz );
|
||||
float nz = getDensityClamped(chunk, ix, iy, iz+1)
|
||||
- getDensityClamped(chunk, ix, iy, iz-1);
|
||||
// negieren: Gradient zeigt vom Solid weg (nach außen)
|
||||
float len = (float)Math.sqrt(nx*nx + ny*ny + nz*nz);
|
||||
if (len < 1e-6f) return new float[]{ 0f, 1f, 0f };
|
||||
return new float[]{ -nx/len, -ny/len, -nz/len };
|
||||
}
|
||||
|
||||
/** Liest Dichte mit Klemmen an Chunk-Grenzen. */
|
||||
private static float getDensityClamped(VoxelChunk chunk, int x, int y, int z) {
|
||||
int s = VoxelChunk.SIZE - 1; // 128
|
||||
x = Math.max(0, Math.min(s, x));
|
||||
y = Math.max(0, Math.min(s, y));
|
||||
z = Math.max(0, Math.min(s, z));
|
||||
return chunk.getDensity(x, y, z);
|
||||
}
|
||||
|
||||
/** Gibt vec4-Gewichte für ein Material zurück: 0→(1,0,0,0), 1→(0,1,0,0), usw. */
|
||||
private static float[] matWeights(int matId) {
|
||||
switch (matId & 3) {
|
||||
case 0: return new float[]{ 1f, 0f, 0f, 0f };
|
||||
case 1: return new float[]{ 0f, 1f, 0f, 0f };
|
||||
case 2: return new float[]{ 0f, 0f, 1f, 0f };
|
||||
default: return new float[]{ 0f, 0f, 0f, 1f };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.math.Vector3f;
|
||||
import de.blight.common.SaveGame;
|
||||
import de.blight.common.SaveGameIO;
|
||||
import de.blight.common.model.Inventar;
|
||||
import de.blight.common.model.Item;
|
||||
import de.blight.common.model.MainCharacter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Verwaltet den persistenten Spielstand (Delta-Save).
|
||||
* Wird von {@link de.blight.game.BlightGame} früh eingehängt, damit alle anderen
|
||||
* States beim Starten bereits auf den geladenen Save zugreifen können.
|
||||
*/
|
||||
public class SaveGameState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SaveGameState.class);
|
||||
|
||||
private SaveGame save;
|
||||
private MainCharacter mc;
|
||||
private Supplier<Vector3f> positionProvider;
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application app) {
|
||||
if (SaveGameIO.exists()) {
|
||||
try {
|
||||
save = SaveGameIO.load();
|
||||
log.info("[Save] Spielstand geladen (gespeichert: {})", save.savedAt);
|
||||
} catch (IOException e) {
|
||||
log.warn("[Save] Laden fehlgeschlagen, starte neu: {}", e.getMessage());
|
||||
save = new SaveGame();
|
||||
}
|
||||
} else {
|
||||
log.info("[Save] Kein Spielstand gefunden, starte neues Spiel.");
|
||||
save = new SaveGame();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application app) {
|
||||
persist();
|
||||
}
|
||||
|
||||
@Override protected void onEnable() {}
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
public SaveGame getSave() { return save; }
|
||||
|
||||
/**
|
||||
* Aufgerufen von {@link de.blight.game.scene.WorldScene}, sobald Charakter
|
||||
* und Physik-Position bereitstehen.
|
||||
*/
|
||||
public void bind(MainCharacter mc, Supplier<Vector3f> positionProvider) {
|
||||
this.mc = mc;
|
||||
this.positionProvider = positionProvider;
|
||||
}
|
||||
|
||||
/** Meldet ein aufgesammeltes Item und löst sofort eine Sicherung aus. */
|
||||
public void reportItemPickedUp(String uuid) {
|
||||
save.world.pickedUpItems.add(uuid);
|
||||
persist();
|
||||
}
|
||||
|
||||
/** Meldet einen besiegten Gegner (für zukünftige Implementierung). */
|
||||
public void reportEnemyDefeated(String enemyId) {
|
||||
save.world.defeatedEnemies.add(enemyId);
|
||||
persist();
|
||||
}
|
||||
|
||||
/** Verwertet den aktuellen Spielstand und startet ein neues Spiel. */
|
||||
public void resetForNewGame() {
|
||||
try { java.nio.file.Files.deleteIfExists(de.blight.common.SaveGameIO.getSavePath()); }
|
||||
catch (java.io.IOException ignored) {}
|
||||
save = new de.blight.common.SaveGame();
|
||||
mc = null;
|
||||
positionProvider = null;
|
||||
log.info("[Save] Neues Spiel gestartet – Spielstand gelöscht.");
|
||||
}
|
||||
|
||||
// ── Sicherung ─────────────────────────────────────────────────────────────
|
||||
|
||||
public void persist() {
|
||||
if (mc == null) return;
|
||||
|
||||
// Spieler-Position
|
||||
if (positionProvider != null) {
|
||||
Vector3f pos = positionProvider.get();
|
||||
save.character.positionSaved = true;
|
||||
save.character.x = pos.x;
|
||||
save.character.y = pos.y;
|
||||
save.character.z = pos.z;
|
||||
}
|
||||
|
||||
// Inventar
|
||||
save.character.inventory.clear();
|
||||
Inventar inv = mc.getInventar();
|
||||
if (inv != null) {
|
||||
for (Map.Entry<Item, Integer> e : inv.getItems().entrySet()) {
|
||||
String id = e.getKey().getItemId();
|
||||
if (id != null) save.character.inventory.put(id, e.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
SaveGameIO.save(save);
|
||||
} catch (IOException e) {
|
||||
log.error("[Save] Speichern fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.bullet.BulletAppState;
|
||||
import com.jme3.bullet.collision.shapes.MeshCollisionShape;
|
||||
import com.jme3.bullet.control.RigidBodyControl;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Mesh;
|
||||
import com.jme3.scene.Node;
|
||||
import de.blight.common.VoxelChunk;
|
||||
|
||||
/**
|
||||
* JME-Node für einen VoxelChunk mit 3 LOD-Geometrien.
|
||||
* Wird von VoxelChunkState und VoxelEditorState verwaltet.
|
||||
*
|
||||
* Position im Weltraum: Translation = (cx*128-2048, cy*128, cz*128-2048).
|
||||
*/
|
||||
public class VoxelChunkNode extends Node {
|
||||
|
||||
private static final int[] LOD_STEPS = {1, 4, 16};
|
||||
|
||||
private final VoxelChunk chunk;
|
||||
private final Material material;
|
||||
|
||||
private final Geometry[] lodGeos = new Geometry[3];
|
||||
private int currentLod = -1;
|
||||
|
||||
private RigidBodyControl physics;
|
||||
private BulletAppState bulletState;
|
||||
|
||||
public VoxelChunkNode(VoxelChunk chunk, Material material) {
|
||||
super("voxel_" + chunk.cx + "_" + chunk.cy + "_" + chunk.cz);
|
||||
this.chunk = chunk;
|
||||
this.material = material;
|
||||
|
||||
// Welt-Translation setzen
|
||||
float wx = chunk.cx * VoxelChunk.CELLS - 2048f;
|
||||
float wy = chunk.cy * (float) VoxelChunk.CELLS;
|
||||
float wz = chunk.cz * VoxelChunk.CELLS - 2048f;
|
||||
setLocalTranslation(wx, wy, wz);
|
||||
}
|
||||
|
||||
/** Baut das Mesh für den angegebenen LOD-Level neu. lod: 0/1/2. */
|
||||
public void rebuildMesh(int lod) {
|
||||
if (lod < 0 || lod > 2) return;
|
||||
Mesh mesh = MarchingCubes.build(chunk, LOD_STEPS[lod]);
|
||||
if (mesh == null) {
|
||||
// Keine Oberfläche: evtl. vorherige Geo entfernen
|
||||
if (lodGeos[lod] != null) { detachChild(lodGeos[lod]); lodGeos[lod] = null; }
|
||||
return;
|
||||
}
|
||||
if (lodGeos[lod] == null) {
|
||||
lodGeos[lod] = new Geometry("lod" + lod, mesh);
|
||||
lodGeos[lod].setMaterial(material);
|
||||
// Wenn dieser LOD gerade aktiv ist, sofort einhängen
|
||||
if (lod == currentLod) attachChild(lodGeos[lod]);
|
||||
} else {
|
||||
lodGeos[lod].setMesh(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt ein bereits fertig berechnetes Mesh für einen LOD-Level.
|
||||
* Gedacht für den Hintergrund-Thread: Mesh dort bauen, dann hier übergeben.
|
||||
*/
|
||||
public void setLodMesh(int lod, Mesh mesh) {
|
||||
if (lod < 0 || lod > 2 || mesh == null) return;
|
||||
if (lodGeos[lod] == null) {
|
||||
lodGeos[lod] = new Geometry("lod" + lod, mesh);
|
||||
lodGeos[lod].setMaterial(material);
|
||||
} else {
|
||||
lodGeos[lod].setMesh(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
/** Schaltet auf den angegebenen LOD um (blendet andere aus). */
|
||||
public void setActiveLod(int lod) {
|
||||
if (lod == currentLod) return;
|
||||
currentLod = lod;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (lodGeos[i] != null) detachChild(lodGeos[i]);
|
||||
}
|
||||
if (lodGeos[lod] != null) {
|
||||
attachChild(lodGeos[lod]);
|
||||
}
|
||||
}
|
||||
|
||||
/** Erzeugt / aktualisiert die Physik-Kollision (LOD0-Mesh). */
|
||||
public void updatePhysics(BulletAppState bullet) {
|
||||
this.bulletState = bullet;
|
||||
if (physics != null) {
|
||||
bullet.getPhysicsSpace().remove(physics);
|
||||
removeControl(physics);
|
||||
}
|
||||
if (lodGeos[0] == null || lodGeos[0].getMesh() == null) return;
|
||||
MeshCollisionShape shape = new MeshCollisionShape(lodGeos[0].getMesh());
|
||||
physics = new RigidBodyControl(shape, 0f);
|
||||
addControl(physics);
|
||||
bullet.getPhysicsSpace().add(physics);
|
||||
}
|
||||
|
||||
public void removePhysics() {
|
||||
if (physics == null || bulletState == null) return;
|
||||
bulletState.getPhysicsSpace().remove(physics);
|
||||
removeControl(physics);
|
||||
physics = null;
|
||||
}
|
||||
|
||||
public VoxelChunk getChunk() { return chunk; }
|
||||
|
||||
/** Gibt true zurück wenn mindestens ein LOD ein Mesh hat. */
|
||||
public boolean hasMesh() {
|
||||
for (Geometry g : lodGeos) if (g != null) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.bullet.BulletAppState;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.texture.Image;
|
||||
import com.jme3.texture.Texture;
|
||||
import com.jme3.texture.TextureArray;
|
||||
import de.blight.common.VoxelChunk;
|
||||
import de.blight.common.VoxelChunkIO;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Verwaltet die Voxel-Geometrie im Spiel als {@link TerrainChunkState.ChunkListener}.
|
||||
*
|
||||
* - Lädt VoxelChunks bei Sichtbarkeit (onChunkVisible)
|
||||
* - Baut LOD-Meshes gemäß TerrainChunk-LOD
|
||||
* - Aktiviert Physik-Collider bei LOD0 (PHYSICS_RANGE)
|
||||
* - Nutzt Texture2DArray für 4 Voxel-Texturen (aus terrainTexturePaths)
|
||||
*/
|
||||
public class VoxelChunkState extends BaseAppState
|
||||
implements TerrainChunkState.ChunkListener {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(VoxelChunkState.class);
|
||||
|
||||
private final BulletAppState bulletState;
|
||||
private final String[] texturePaths; // 4 Pfade der unteren Terrain-Texturen
|
||||
|
||||
private SimpleApplication app;
|
||||
private AssetManager assets;
|
||||
private Node voxelRoot;
|
||||
private Material voxelMaterial;
|
||||
|
||||
// key = cx | ((long)cy << 16) | ((long)cz << 32)
|
||||
private final Map<Long, VoxelChunkNode> nodes = new HashMap<>();
|
||||
|
||||
public VoxelChunkState(BulletAppState bulletState, String[] texturePaths) {
|
||||
this.bulletState = bulletState;
|
||||
this.texturePaths = texturePaths;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
this.app = (SimpleApplication) application;
|
||||
this.assets = app.getAssetManager();
|
||||
voxelRoot = new Node("voxelRoot");
|
||||
app.getRootNode().attachChild(voxelRoot);
|
||||
voxelMaterial = buildMaterial();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application app) {
|
||||
voxelRoot.removeFromParent();
|
||||
for (VoxelChunkNode n : nodes.values()) n.removePhysics();
|
||||
nodes.clear();
|
||||
}
|
||||
|
||||
@Override protected void onEnable() {}
|
||||
@Override protected void onDisable() {}
|
||||
@Override public void update(float tpf) {}
|
||||
|
||||
// ── ChunkListener ─────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void onChunkVisible(int cx, int cz, int lod) {
|
||||
// Alle cy-Layers für diesen cx/cz laden
|
||||
loadLayersForXZ(cx, cz, lod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkHidden(int cx, int cz) {
|
||||
// Alle cy-Layers für diesen cx/cz entfernen
|
||||
List<Long> toRemove = new ArrayList<>();
|
||||
for (Map.Entry<Long, VoxelChunkNode> e : nodes.entrySet()) {
|
||||
VoxelChunkNode n = e.getValue();
|
||||
if (n.getChunk().cx == cx && n.getChunk().cz == cz) toRemove.add(e.getKey());
|
||||
}
|
||||
for (Long key : toRemove) removeNode(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkLodChanged(int cx, int cz, int oldLod, int newLod) {
|
||||
for (VoxelChunkNode n : nodes.values()) {
|
||||
VoxelChunk c = n.getChunk();
|
||||
if (c.cx != cx || c.cz != cz) continue;
|
||||
n.setActiveLod(newLod);
|
||||
// Physik nur bei LOD0 (nahes Terrain)
|
||||
if (newLod == 0) {
|
||||
n.updatePhysics(bulletState);
|
||||
} else {
|
||||
n.removePhysics();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Intern ────────────────────────────────────────────────────────────────
|
||||
|
||||
private void loadLayersForXZ(int cx, int cz, int lod) {
|
||||
// Suche alle vorhandenen .blvc-Dateien für diesen cx/cz
|
||||
// Einfachste Lösung: bekannte cy-Range scannen (-8..+8)
|
||||
for (int cy = -8; cy <= 8; cy++) {
|
||||
if (!VoxelChunkIO.exists(cx, cy, cz)) continue;
|
||||
long key = chunkKey(cx, cy, cz);
|
||||
if (nodes.containsKey(key)) continue; // bereits geladen
|
||||
try {
|
||||
VoxelChunk chunk = VoxelChunkIO.load(cx, cy, cz);
|
||||
addNode(key, chunk, lod);
|
||||
} catch (IOException e) {
|
||||
log.warn("Voxel-Chunk laden fehlgeschlagen ({},{},{}): {}", cx, cy, cz, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addNode(long key, VoxelChunk chunk, int lod) {
|
||||
VoxelChunkNode node = new VoxelChunkNode(chunk, voxelMaterial);
|
||||
for (int l = 0; l < 3; l++) node.rebuildMesh(l);
|
||||
node.setActiveLod(lod);
|
||||
if (lod == 0) node.updatePhysics(bulletState);
|
||||
voxelRoot.attachChild(node);
|
||||
nodes.put(key, node);
|
||||
}
|
||||
|
||||
private void removeNode(long key) {
|
||||
VoxelChunkNode n = nodes.remove(key);
|
||||
if (n == null) return;
|
||||
n.removePhysics();
|
||||
n.removeFromParent();
|
||||
}
|
||||
|
||||
/** Fügt einen extern geladenen VoxelChunk zur Szene hinzu (z.B. aus dem Editor). */
|
||||
public void addOrUpdateChunk(VoxelChunk chunk, int lod) {
|
||||
long key = chunkKey(chunk.cx, chunk.cy, chunk.cz);
|
||||
VoxelChunkNode existing = nodes.get(key);
|
||||
if (existing != null) {
|
||||
for (int l = 0; l < 3; l++) existing.rebuildMesh(l);
|
||||
existing.setActiveLod(lod);
|
||||
if (lod == 0) existing.updatePhysics(bulletState);
|
||||
} else {
|
||||
addNode(key, chunk, lod);
|
||||
}
|
||||
}
|
||||
|
||||
private Material buildMaterial() {
|
||||
Material mat = new Material(assets, "MatDefs/Voxel.j3md");
|
||||
mat.setFloat("TexScale", 4f);
|
||||
|
||||
// Texture2DArray aus 4 Terrain-Textur-Pfaden aufbauen
|
||||
try {
|
||||
List<Image> images = new ArrayList<>();
|
||||
for (String path : texturePaths) {
|
||||
Texture t = (path != null && !path.isEmpty())
|
||||
? assets.loadTexture(path)
|
||||
: assets.loadTexture("Common/Textures/MissingTexture.png");
|
||||
images.add(t.getImage());
|
||||
}
|
||||
TextureArray texArray = new TextureArray(images);
|
||||
texArray.setMinFilter(Texture.MinFilter.Trilinear);
|
||||
texArray.setMagFilter(Texture.MagFilter.Bilinear);
|
||||
mat.setParam("TexArray", com.jme3.shader.VarType.TextureArray, texArray);
|
||||
} catch (Exception e) {
|
||||
log.warn("Voxel Texture2DArray Aufbau fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
return mat;
|
||||
}
|
||||
|
||||
public static long chunkKey(int cx, int cy, int cz) {
|
||||
return ((long)(cx & 0xFFFF)) | (((long)(cy & 0xFFFF)) << 16) | (((long)(cz & 0xFFFF)) << 32);
|
||||
}
|
||||
}
|
||||
@@ -5,19 +5,28 @@ import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.asset.plugins.FileLocator;
|
||||
import com.jme3.audio.AudioData;
|
||||
import com.jme3.audio.AudioNode;
|
||||
import com.jme3.bullet.control.CharacterControl;
|
||||
import com.jme3.collision.CollisionResult;
|
||||
import com.jme3.collision.CollisionResults;
|
||||
import com.jme3.input.InputManager;
|
||||
import com.jme3.input.MouseInput;
|
||||
import com.jme3.input.controls.ActionListener;
|
||||
import com.jme3.input.controls.KeyTrigger;
|
||||
import com.jme3.input.controls.MouseButtonTrigger;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.math.Ray;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.Spatial;
|
||||
import com.jme3.scene.shape.Box;
|
||||
import de.blight.common.PlacedItem;
|
||||
import de.blight.common.PlacedItemIO;
|
||||
import de.blight.common.SaveGame;
|
||||
import de.blight.common.model.Inventar;
|
||||
import de.blight.common.model.Item;
|
||||
import de.blight.common.model.ItemIO;
|
||||
@@ -35,17 +44,19 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Lädt alle auf der Karte platzierten Items, stellt sie als 3D-Objekte dar und
|
||||
* verarbeitet das Aufheben (E-Taste) mit PICK_UP-Animation.
|
||||
*/
|
||||
public class WorldItemsState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WorldItemsState.class);
|
||||
|
||||
private static final float PICKUP_RANGE = 2.5f;
|
||||
private static final float INTERACT_RANGE = 5f;
|
||||
private static final float REACH_DIST = 0.5f;
|
||||
private static final float PICKUP_ANIM_DURATION = 0.8f;
|
||||
private static final float WALK_TIMEOUT = 5.0f;
|
||||
|
||||
private static final String INTERACT_ACTION = "Interact";
|
||||
private static final String SECONDARY_ATTACK_ACTION = "SecondaryAttack";
|
||||
|
||||
// ── Abhängigkeiten ────────────────────────────────────────────────────────
|
||||
|
||||
private final KeyBindings keyBindings;
|
||||
private final CharacterControl physicsChar;
|
||||
@@ -55,6 +66,7 @@ public class WorldItemsState extends BaseAppState {
|
||||
private SimpleApplication app;
|
||||
private AssetManager assets;
|
||||
private InputManager inputManager;
|
||||
private Camera cam;
|
||||
private Node rootNode;
|
||||
private Node itemsRoot;
|
||||
|
||||
@@ -62,6 +74,25 @@ public class WorldItemsState extends BaseAppState {
|
||||
private final List<Spatial> visuals = new ArrayList<>();
|
||||
private final Map<String, Item> itemDefs = new HashMap<>();
|
||||
|
||||
// ── Hover-Zustand ─────────────────────────────────────────────────────────
|
||||
|
||||
private int hoveredIdx = -1;
|
||||
private float hoverLogTimer = 0f;
|
||||
|
||||
// ── Pickup-Sequenz-Zustand ────────────────────────────────────────────────
|
||||
|
||||
private enum Phase { IDLE, WALKING, ANIMATING }
|
||||
|
||||
private Phase phase = Phase.IDLE;
|
||||
private int targetIdx = -1;
|
||||
private float walkTimer = 0f;
|
||||
private float animTimer = 0f;
|
||||
private boolean halfwayDone = false;
|
||||
|
||||
private AudioNode pickupSound = null;
|
||||
|
||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||
|
||||
public WorldItemsState(KeyBindings keyBindings, CharacterControl physicsChar,
|
||||
MainCharacter mainCharacter, PlayerInputControl playerInput) {
|
||||
this.keyBindings = keyBindings;
|
||||
@@ -77,6 +108,7 @@ public class WorldItemsState extends BaseAppState {
|
||||
this.app = (SimpleApplication) app;
|
||||
this.assets = app.getAssetManager();
|
||||
this.inputManager = app.getInputManager();
|
||||
this.cam = app.getCamera();
|
||||
this.rootNode = this.app.getRootNode();
|
||||
this.itemsRoot = new Node("worldItemsRoot");
|
||||
|
||||
@@ -91,6 +123,16 @@ public class WorldItemsState extends BaseAppState {
|
||||
if (it.getItemId() != null) itemDefs.put(it.getItemId(), it);
|
||||
}
|
||||
log.info("[WorldItems] {} Item-Definitionen geladen.", itemDefs.size());
|
||||
|
||||
try {
|
||||
pickupSound = new AudioNode(assets, "audio/static/pickup.ogg", AudioData.DataType.Buffer);
|
||||
pickupSound.setPositional(false);
|
||||
pickupSound.setLooping(false);
|
||||
pickupSound.setVolume(1f);
|
||||
} catch (Exception e) {
|
||||
log.warn("[WorldItems] Pickup-Sound nicht ladbar: {}", e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -103,6 +145,11 @@ public class WorldItemsState extends BaseAppState {
|
||||
log.warn("[WorldItems] Laden fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
|
||||
SaveGameState saveState = app.getStateManager().getState(SaveGameState.class);
|
||||
if (saveState != null && !saveState.getSave().world.pickedUpItems.isEmpty()) {
|
||||
items.removeIf(pi -> saveState.getSave().world.pickedUpItems.contains(pi.uuid()));
|
||||
}
|
||||
|
||||
for (PlacedItem pi : items) {
|
||||
Spatial s = buildVisual(pi);
|
||||
s.setLocalTranslation(pi.x(), pi.y() + 0.25f, pi.z());
|
||||
@@ -113,64 +160,302 @@ public class WorldItemsState extends BaseAppState {
|
||||
rootNode.attachChild(itemsRoot);
|
||||
log.info("[WorldItems] {} Item-Pickups geladen.", items.size());
|
||||
|
||||
inputManager.addMapping(INTERACT_ACTION, new KeyTrigger(keyBindings.interact));
|
||||
if (saveState != null && mainCharacter != null) {
|
||||
SaveGame.CharacterSave cs = saveState.getSave().character;
|
||||
if (!cs.inventory.isEmpty()) {
|
||||
Inventar inv = mainCharacter.getInventar();
|
||||
if (inv == null) { inv = new Inventar(); mainCharacter.setInventar(inv); }
|
||||
for (Map.Entry<String, Integer> e : cs.inventory.entrySet()) {
|
||||
Item def = itemDefs.get(e.getKey());
|
||||
if (def != null) {
|
||||
for (int i = 0; i < e.getValue(); i++) inv.collect(def);
|
||||
}
|
||||
}
|
||||
log.info("[WorldItems] Inventar wiederhergestellt ({} Einträge).", cs.inventory.size());
|
||||
}
|
||||
}
|
||||
|
||||
inputManager.addMapping(INTERACT_ACTION,
|
||||
new KeyTrigger(keyBindings.interact));
|
||||
inputManager.addMapping(SECONDARY_ATTACK_ACTION,
|
||||
new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
|
||||
inputManager.addListener(interactListener, INTERACT_ACTION);
|
||||
inputManager.addListener(secondaryAttackListener, SECONDARY_ATTACK_ACTION);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisable() {
|
||||
cancelApproach();
|
||||
hoveredIdx = -1;
|
||||
itemsRoot.detachAllChildren();
|
||||
itemsRoot.removeFromParent();
|
||||
visuals.clear();
|
||||
items.clear();
|
||||
inputManager.removeListener(interactListener);
|
||||
inputManager.removeListener(secondaryAttackListener);
|
||||
try { inputManager.deleteMapping(INTERACT_ACTION); } catch (Exception ignored) {}
|
||||
try { inputManager.deleteMapping(SECONDARY_ATTACK_ACTION); } catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application app) {}
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {}
|
||||
public void update(float tpf) {
|
||||
hoverLogTimer += tpf;
|
||||
updateHover();
|
||||
switch (phase) {
|
||||
case WALKING -> updateWalking(tpf);
|
||||
case ANIMATING -> updateAnimating(tpf);
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Interaktion ───────────────────────────────────────────────────────────
|
||||
// ── Hover / Zielen ────────────────────────────────────────────────────────
|
||||
|
||||
private final ActionListener interactListener = (name, isPressed, tpf) -> {
|
||||
if (isPressed) tryPickup();
|
||||
};
|
||||
private void updateHover() {
|
||||
if (phase != Phase.IDLE) {
|
||||
setHovered(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
private void tryPickup() {
|
||||
if (physicsChar == null || items.isEmpty()) return;
|
||||
Vector3f playerPos = physicsChar.getPhysicsLocation();
|
||||
float centerX = cam.getWidth() * 0.5f;
|
||||
float hMargin = cam.getWidth() * 0.10f;
|
||||
float screenH = cam.getHeight();
|
||||
|
||||
int nearest = -1;
|
||||
boolean doLog = hoverLogTimer >= 1f;
|
||||
if (doLog) {
|
||||
hoverLogTimer = 0f;
|
||||
log.info("[Hover] items={} screenW={} screenH={} centerX={} hMargin={}",
|
||||
items.size(), cam.getWidth(), cam.getHeight(), centerX, hMargin);
|
||||
}
|
||||
|
||||
int bestIdx = -1;
|
||||
float bestDist = Float.MAX_VALUE;
|
||||
|
||||
Vector3f charPos = physicsChar != null ? physicsChar.getPhysicsLocation() : cam.getLocation();
|
||||
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
PlacedItem pi = items.get(i);
|
||||
Vector3f worldPos = new Vector3f(pi.x(), pi.y() + 0.25f, pi.z());
|
||||
|
||||
// Distanz vom Charakter (nicht von der Kamera)
|
||||
float charDist = worldPos.distance(charPos);
|
||||
if (doLog) log.info("[Hover] item[{}] id={} charDist={}", i, pi.itemId(), charDist);
|
||||
|
||||
if (charDist > INTERACT_RANGE) {
|
||||
if (doLog) log.info("[Hover] → skip: außerhalb Reichweite ({} > {})", charDist, INTERACT_RANGE);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Muss vor der Kamera liegen
|
||||
Vector3f camToItem = worldPos.subtract(cam.getLocation());
|
||||
if (camToItem.dot(cam.getDirection()) <= 0f) {
|
||||
if (doLog) log.info("[Hover] → skip: hinter der Kamera");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bildschirmprojektion (von der Kamera aus)
|
||||
Vector3f screen = cam.getScreenCoordinates(worldPos);
|
||||
float hDiff = Math.abs(screen.x - centerX);
|
||||
if (doLog) log.info("[Hover] screen=({},{},{}) hDiff={} hMargin={}",
|
||||
(int)screen.x, (int)screen.y, screen.z, (int)hDiff, (int)hMargin);
|
||||
|
||||
if (hDiff > hMargin) {
|
||||
if (doLog) log.info("[Hover] → skip: horizontal außerhalb Mitte");
|
||||
continue;
|
||||
}
|
||||
if (screen.y < 0f || screen.y > screenH) {
|
||||
if (doLog) log.info("[Hover] → skip: vertikal außerhalb Bildschirm");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verdeckungsprüfung: Strahl von Kamera zum Item
|
||||
float camDist = camToItem.length();
|
||||
if (isOccluded(camToItem.normalize(), camDist, i, doLog)) {
|
||||
if (doLog) log.info("[Hover] → skip: verdeckt");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (charDist < bestDist) {
|
||||
bestDist = charDist;
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (doLog) log.info("[Hover] → hoveredIdx={}", bestIdx);
|
||||
setHovered(bestIdx);
|
||||
}
|
||||
|
||||
private boolean isOccluded(Vector3f direction, float itemDist, int itemIdx, boolean doLog) {
|
||||
Ray ray = new Ray(cam.getLocation(), direction);
|
||||
CollisionResults results = new CollisionResults();
|
||||
rootNode.collideWith(ray, results);
|
||||
|
||||
if (doLog) log.info("[Hover] occlusion: {} Treffer, itemDist={:.2f}", results.size(), itemDist);
|
||||
|
||||
for (int j = 0; j < results.size(); j++) {
|
||||
CollisionResult cr = results.getCollision(j);
|
||||
float d = cr.getDistance();
|
||||
if (d < 1.5f) {
|
||||
if (doLog) log.info("[Hover] [{}] d={:.2f} → Nah-Clip skip ({})", j, d, cr.getGeometry().getName());
|
||||
continue;
|
||||
}
|
||||
if (d > itemDist - 0.15f) {
|
||||
if (doLog) log.info("[Hover] [{}] d={:.2f} → am/hinter Item, kein Blocker ({})", j, d, cr.getGeometry().getName());
|
||||
continue;
|
||||
}
|
||||
int vidx = findVisualIndex(cr.getGeometry());
|
||||
if (vidx == itemIdx) {
|
||||
if (doLog) log.info("[Hover] [{}] d={:.2f} → Item selbst ({})", j, d, cr.getGeometry().getName());
|
||||
continue;
|
||||
}
|
||||
if (doLog) log.info("[Hover] [{}] d={:.2f} → BLOCKIERT durch '{}'", j, d, cr.getGeometry().getName());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private int findVisualIndex(Geometry g) {
|
||||
Spatial s = g;
|
||||
while (s != null) {
|
||||
int idx = visuals.indexOf(s);
|
||||
if (idx >= 0) return idx;
|
||||
if (s == itemsRoot) return -1;
|
||||
s = s.getParent();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void setHovered(int idx) {
|
||||
hoveredIdx = idx;
|
||||
}
|
||||
|
||||
public int getHoveredIdx() { return hoveredIdx; }
|
||||
|
||||
public String getHoveredItemName() {
|
||||
if (hoveredIdx < 0 || hoveredIdx >= items.size()) return null;
|
||||
PlacedItem pi = items.get(hoveredIdx);
|
||||
Item def = itemDefs.get(pi.itemId());
|
||||
return def != null ? def.getDisplayText() : pi.itemId();
|
||||
}
|
||||
|
||||
// ── Pickup-Sequenz ────────────────────────────────────────────────────────
|
||||
|
||||
private void updateWalking(float tpf) {
|
||||
if (playerInput.isPaused()) {
|
||||
cancelApproach();
|
||||
return;
|
||||
}
|
||||
|
||||
walkTimer += tpf;
|
||||
if (walkTimer > WALK_TIMEOUT) {
|
||||
log.info("[WorldItems] Pickup-Annäherung abgebrochen (Timeout).");
|
||||
cancelApproach();
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetIdx < 0 || targetIdx >= items.size()) {
|
||||
cancelApproach();
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3f playerPos = physicsChar.getPhysicsLocation();
|
||||
PlacedItem target = items.get(targetIdx);
|
||||
float dx = target.x() - playerPos.x;
|
||||
float dz = target.z() - playerPos.z;
|
||||
float distSq = dx * dx + dz * dz;
|
||||
|
||||
if (distSq <= REACH_DIST * REACH_DIST) {
|
||||
startPickupAnim();
|
||||
} else {
|
||||
float dist = (float) Math.sqrt(distSq);
|
||||
playerInput.setAutopilotDirection(new Vector3f(dx / dist, 0f, dz / dist));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAnimating(float tpf) {
|
||||
animTimer += tpf;
|
||||
if (!halfwayDone && animTimer >= PICKUP_ANIM_DURATION * 0.5f) {
|
||||
halfwayDone = true;
|
||||
executePickup();
|
||||
}
|
||||
if (animTimer >= PICKUP_ANIM_DURATION) {
|
||||
phase = Phase.IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Listener ──────────────────────────────────────────────────────────────
|
||||
|
||||
private final ActionListener interactListener = (name, isPressed, tpf) -> {
|
||||
if (isPressed) {
|
||||
log.info("[WorldItems] Interact gedrückt: phase={} hoveredIdx={} items={}",
|
||||
phase, hoveredIdx, items.size());
|
||||
if (phase == Phase.IDLE) tryPickup();
|
||||
}
|
||||
};
|
||||
|
||||
private final ActionListener secondaryAttackListener = (name, isPressed, tpf) -> {
|
||||
if (isPressed && phase == Phase.WALKING) {
|
||||
log.info("[WorldItems] Pickup-Annäherung abgebrochen (Sekundärangriff).");
|
||||
cancelApproach();
|
||||
}
|
||||
};
|
||||
|
||||
// ── Pickup-Logik ──────────────────────────────────────────────────────────
|
||||
|
||||
private void tryPickup() {
|
||||
log.info("[WorldItems] tryPickup: hoveredIdx={} physicsChar={}", hoveredIdx, physicsChar != null ? "ok" : "NULL");
|
||||
if (hoveredIdx < 0 || physicsChar == null) return;
|
||||
|
||||
targetIdx = hoveredIdx;
|
||||
PlacedItem pi = items.get(targetIdx);
|
||||
|
||||
Vector3f playerPos = physicsChar.getPhysicsLocation();
|
||||
float dx = pi.x() - playerPos.x;
|
||||
float dz = pi.z() - playerPos.z;
|
||||
float dist = (float) Math.sqrt(dx * dx + dz * dz);
|
||||
if (dist < PICKUP_RANGE && dist < bestDist) {
|
||||
bestDist = dist;
|
||||
nearest = i;
|
||||
float distSq = dx * dx + dz * dz;
|
||||
|
||||
log.info("[WorldItems] → Ziel='{}' playerDist={}", pi.itemId(), Math.sqrt(distSq));
|
||||
if (distSq <= REACH_DIST * REACH_DIST) {
|
||||
startPickupAnim();
|
||||
} else {
|
||||
walkTimer = 0f;
|
||||
phase = Phase.WALKING;
|
||||
log.info("[WorldItems] → WALKING gestartet");
|
||||
}
|
||||
}
|
||||
if (nearest < 0) return;
|
||||
|
||||
PlacedItem picked = items.get(nearest);
|
||||
Spatial pickedSpatial = visuals.get(nearest);
|
||||
pickedSpatial.removeFromParent();
|
||||
items.remove(nearest);
|
||||
visuals.remove(nearest);
|
||||
private void startPickupAnim() {
|
||||
playerInput.setAutopilotDirection(null);
|
||||
playerInput.requestPickup(PICKUP_ANIM_DURATION);
|
||||
if (pickupSound != null) pickupSound.playInstance();
|
||||
animTimer = 0f;
|
||||
halfwayDone = false;
|
||||
phase = Phase.ANIMATING;
|
||||
}
|
||||
|
||||
private void executePickup() {
|
||||
if (targetIdx < 0 || targetIdx >= items.size()) {
|
||||
phase = Phase.IDLE;
|
||||
return;
|
||||
}
|
||||
|
||||
PlacedItem picked = items.get(targetIdx);
|
||||
Spatial pickedVisual = visuals.get(targetIdx);
|
||||
pickedVisual.removeFromParent();
|
||||
items.remove(targetIdx);
|
||||
visuals.remove(targetIdx);
|
||||
targetIdx = -1;
|
||||
|
||||
// Kartendatei sofort aktualisieren
|
||||
try {
|
||||
PlacedItemIO.save(items);
|
||||
} catch (IOException e) {
|
||||
log.warn("[WorldItems] Speichern fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// Ins Inventar legen
|
||||
Item def = itemDefs.get(picked.itemId());
|
||||
if (mainCharacter != null) {
|
||||
if (mainCharacter.getInventar() == null) {
|
||||
@@ -184,12 +469,19 @@ public class WorldItemsState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
// PICK_UP-Animation spielen und Bewegung sperren
|
||||
if (playerInput != null) {
|
||||
playerInput.requestPickup(PICKUP_ANIM_DURATION);
|
||||
SaveGameState saveState = getApplication().getStateManager().getState(SaveGameState.class);
|
||||
if (saveState != null) {
|
||||
saveState.reportItemPickedUp(picked.uuid());
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelApproach() {
|
||||
if (phase == Phase.IDLE) return;
|
||||
playerInput.setAutopilotDirection(null);
|
||||
phase = Phase.IDLE;
|
||||
targetIdx = -1;
|
||||
}
|
||||
|
||||
// ── Zugriff ───────────────────────────────────────────────────────────────
|
||||
|
||||
public Node getItemsRoot() { return itemsRoot; }
|
||||
@@ -210,7 +502,6 @@ public class WorldItemsState extends BaseAppState {
|
||||
log.warn("[WorldItems] Modell für '{}' nicht ladbar: {}", pi.itemId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
// Fallback: goldener Würfel
|
||||
Geometry g = new Geometry("item_" + pi.itemId(), new Box(0.15f, 0.15f, 0.15f));
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(1f, 0.82f, 0.1f, 1f));
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
# x y z activationRadius texturePath imagesX imagesY startR startG startB startA endR endG endB endA startSize endSize velX velY velZ velVariation gravX gravY gravZ lowLife highLife maxParticles emitRate
|
||||
-350.58115 165.22334 72.31232 20.00000 Effects/Explosion/flame.png 2 2 1.0000 0.7000 0.1000 1.0000 1.0000 0.1000 0.0000 0.0000 0.5000 1.2000 0.0000 2.5000 0.0000 0.5000 0.0000 -0.1000 0.0000 1.0000 3.0000 60 25.0000
|
||||
-67.57600 3.71900 -36.32000 20.00000 Effects/Explosion/flame.png 2 2 1.0000 0.7000 0.1000 1.0000 1.0000 0.1000 0.0000 0.0000 0.5000 1.2000 0.0000 2.5000 0.0000 0.5000 0.0000 -0.1000 0.0000 1.0000 3.0000 60 25.0000
|
||||
|
||||
@@ -1,40 +1 @@
|
||||
# x y z r g b intensity radius
|
||||
-351.99496 165.72334 72.11122 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-351.55142 165.72334 72.58471 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-351.43768 165.72334 72.54273 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-351.29898 165.72334 72.48790 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-351.17831 165.72334 72.50105 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-351.04108 165.72334 72.53436 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-350.62204 165.72334 72.48065 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-350.53287 165.72334 72.41996 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-350.38269 165.72334 72.27039 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-350.20273 165.72334 72.12134 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-350.06265 165.72334 72.04984 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-349.92236 165.72334 72.02018 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-349.76706 165.72334 72.01057 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-349.61569 165.72334 71.96039 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-349.47086 165.72334 71.83027 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-349.36353 165.72334 71.73398 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-349.20959 165.72334 71.58717 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-348.90756 165.72334 71.23451 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-348.79315 165.72334 71.05963 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-348.72208 165.72334 70.80517 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-348.57535 165.72334 70.41891 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-348.47836 165.72334 69.83525 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-348.56183 165.72334 68.33922 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-348.92676 165.72334 66.96924 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-349.46359 165.72334 65.80831 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-350.16031 165.72334 64.75426 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-350.81577 165.72334 63.87869 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-351.78735 165.72334 63.35125 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-352.91010 165.72334 62.80974 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-353.19580 165.72334 62.27907 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-353.43668 165.72334 61.80404 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-353.18146 165.72334 61.10947 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-352.84583 165.72334 60.76453 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-352.58203 165.72334 60.53225 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-352.27545 165.72334 60.25900 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-351.78003 165.72334 60.08908 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-350.62851 165.72334 59.92413 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-349.55182 165.72334 59.88406 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
-67.83228 3.73790 -36.36091 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||
|
||||
Binary file not shown.
@@ -29,10 +29,8 @@ Models/Buggy/Buggy.j3o 296.60590 1.00000 201.24153 0.00000 1.00000 0.00000 0.000
|
||||
Models/Jaime/Jaime.j3o 304.02374 1.00000 199.25398 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||
Models/tree.j3o 221.98000 1.00000 130.25000 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000
|
||||
Models/Palm_Palme1_20260522_075134.j3o 240.61151 1.00000 97.48973 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||
Models/Palm_Palme1_20260522_075134.j3o 150.18289 0.96490 16.69994 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||
Models/FernPlantV2.j3o 336.18240 1.00000 -164.72426 0.00000 0.00200 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||
Models/plants/fern/fern_20260608_165631.j3o 158.98721 0.96388 25.31449 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||
Models/plants/fern/fern_20260608_165628.j3o 157.71002 0.96395 20.47514 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||
Models/plants/fern/fern_20260608_165628.j3o 159.09314 0.95784 17.07811 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||
Models/plants/fern/fern_20260608_165628.j3o 160.70380 0.93929 19.18060 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||
Models/plants/fern/fern_20260608_165628.j3o 161.39258 0.90349 21.80369 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||
|
||||
@@ -1 +1,9 @@
|
||||
# itemId x y z
|
||||
# uuid itemId x y z
|
||||
8daba5c4-5a5a-474f-b92f-d537026cae22 blutagave 154.93541 0.96067 31.66673
|
||||
452d861c-f1f3-4891-8d7d-bf63426c06f5 blutagave 150.51953 0.97004 29.83217
|
||||
ea972904-a271-4ef0-9fee-ee449207941f blutagave 151.96960 0.97972 35.17637
|
||||
b458fea6-8394-4315-8874-a35725bffa73 erzmoss 140.69641 0.99656 36.09897
|
||||
d9f6c1a8-4b77-4131-b105-0c1373322e12 erzmoss 143.09398 0.98996 30.72424
|
||||
fd7d3852-04a1-482b-a00a-641b4de333a4 erzmoss 147.13496 0.97094 24.37807
|
||||
c6f52659-2a97-4178-890c-2fb6f2216d2c erzmoss 156.07599 0.96447 26.57461
|
||||
b5e3decf-e7a7-4531-8cb3-a55d0a965a54 erzmoss 163.04799 0.93061 29.08921
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
# blight_rivers.blr – Flussdaten
|
||||
# Format: x,y,z,width,uvSpeed | x,y,z,width,uvSpeed | ...
|
||||
83.70156,11.20734,-377.98270,8.00000,0.40000|53.36767,11.15734,-386.29489,8.00000,0.40000|19.23992,11.10734,-376.33185,8.00000,0.40000|-22.25724,11.05734,-322.99374,8.00000,0.40000
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
# polygon soundPath volume crossfade
|
||||
67.278,-201.343;67.278,-201.343;57.509,-236.901;57.509,-236.901 1.0000 false
|
||||
-4.282,-291.410;-4.282,-291.410;-4.282,-291.410 1.0000 false
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_03_0_09.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_03_0_09.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_03_m1_09.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_03_m1_09.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_04_0_10.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_04_0_10.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_04_m1_10.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_04_m1_10.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_05_0_10.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_05_0_10.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_05_m1_10.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_05_m1_10.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_06_0_10.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_06_0_10.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_06_m1_10.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_06_m1_10.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_08_0_11.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_08_0_11.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_08_0_12.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_08_0_12.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_08_m1_11.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_08_m1_11.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_08_m1_12.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_08_m1_12.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_09_0_11.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_09_0_11.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_09_0_12.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_09_0_12.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_09_m1_11.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_09_m1_11.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_09_m1_12.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_09_m1_12.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_10_0_12.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_10_0_12.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_10_0_13.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_10_0_13.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_10_m1_12.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_10_m1_12.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_10_m1_13.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_10_m1_13.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_11_0_12.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_11_0_12.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_11_0_13.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_11_0_13.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_11_m1_12.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_11_m1_12.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_11_m1_13.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_11_m1_13.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_12_0_10.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_12_0_10.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_12_0_11.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_12_0_11.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_12_0_13.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_12_0_13.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_12_0_14.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_12_0_14.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_12_m1_10.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_12_m1_10.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_12_m1_11.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_12_m1_11.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_12_m1_13.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_12_m1_13.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_12_m1_14.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_12_m1_14.blvc
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user