diff --git a/.gitignore b/.gitignore index 089e052..4d4ffe5 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,9 @@ bin/ # Lokale Downloads (nicht ins Repo) downloads/ + +# Ingame-Karten-Sessions (vom Editor beim Spielstart erzeugt) +run/ + +# Spielstände +saves/ diff --git a/blight-assets/src/main/resources/MatDefs/Voxel.j3md b/blight-assets/src/main/resources/MatDefs/Voxel.j3md new file mode 100644 index 0000000..70a3d1b --- /dev/null +++ b/blight-assets/src/main/resources/MatDefs/Voxel.j3md @@ -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 + } + } +} diff --git a/blight-assets/src/main/resources/Models/custom_mesh_0.j3o b/blight-assets/src/main/resources/Models/custom_mesh_0.j3o index 611f3b4..934683b 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_0.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_0.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_1.j3o b/blight-assets/src/main/resources/Models/custom_mesh_1.j3o index b589a1c..db100e3 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_1.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_1.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_2.j3o b/blight-assets/src/main/resources/Models/custom_mesh_2.j3o index fe75734..28e0d0b 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_2.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_2.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_3.j3o b/blight-assets/src/main/resources/Models/custom_mesh_3.j3o index d02bf01..0130e51 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_3.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_3.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_4.j3o b/blight-assets/src/main/resources/Models/custom_mesh_4.j3o index bc0a9b6..da829fa 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_4.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_4.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_5.j3o b/blight-assets/src/main/resources/Models/custom_mesh_5.j3o index 8c57797..de521fe 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_5.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_5.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_6.j3o b/blight-assets/src/main/resources/Models/custom_mesh_6.j3o index 3c54fc3..27f7491 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_6.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_6.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_7.j3o b/blight-assets/src/main/resources/Models/custom_mesh_7.j3o index e018c86..2721b89 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_7.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_7.j3o differ diff --git a/blight-assets/src/main/resources/Shaders/Voxel.frag b/blight-assets/src/main/resources/Shaders/Voxel.frag new file mode 100644 index 0000000..f7b33b3 --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/Voxel.frag @@ -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; +} diff --git a/blight-assets/src/main/resources/Shaders/Voxel.vert b/blight-assets/src/main/resources/Shaders/Voxel.vert new file mode 100644 index 0000000..f92e564 --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/Voxel.vert @@ -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); +} diff --git a/blight-assets/src/main/resources/animations/sets/human.animset.json b/blight-assets/src/main/resources/animations/sets/human.animset.json index d3790ef..b2e4b6d 100644 --- a/blight-assets/src/main/resources/animations/sets/human.animset.json +++ b/blight-assets/src/main/resources/animations/sets/human.animset.json @@ -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" } -} +} \ No newline at end of file diff --git a/blight-assets/src/main/resources/audio/static/pickup.ogg b/blight-assets/src/main/resources/audio/static/pickup.ogg new file mode 100644 index 0000000..de6308f Binary files /dev/null and b/blight-assets/src/main/resources/audio/static/pickup.ogg differ diff --git a/blight-assets/src/main/resources/items/misc/blutagave.item b/blight-assets/src/main/resources/items/crafting_items/blutagave.item similarity index 56% rename from blight-assets/src/main/resources/items/misc/blutagave.item rename to blight-assets/src/main/resources/items/crafting_items/blutagave.item index 4f6c24b..1f96014 100644 --- a/blight-assets/src/main/resources/items/misc/blutagave.item +++ b/blight-assets/src/main/resources/items/crafting_items/blutagave.item @@ -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 + } + ] } \ No newline at end of file diff --git a/blight-assets/src/main/resources/items/misc/erzmoss.item b/blight-assets/src/main/resources/items/crafting_items/erzmoss.item similarity index 56% rename from blight-assets/src/main/resources/items/misc/erzmoss.item rename to blight-assets/src/main/resources/items/crafting_items/erzmoss.item index f28f5b3..329dffc 100644 --- a/blight-assets/src/main/resources/items/misc/erzmoss.item +++ b/blight-assets/src/main/resources/items/crafting_items/erzmoss.item @@ -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 + } + ] } \ No newline at end of file diff --git a/blight-common/src/main/java/de/blight/common/BlightHome.java b/blight-common/src/main/java/de/blight/common/BlightHome.java new file mode 100644 index 0000000..71cfdca --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/BlightHome.java @@ -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)); + } +} diff --git a/blight-common/src/main/java/de/blight/common/MapIO.java b/blight-common/src/main/java/de/blight/common/MapIO.java index 51fc656..4b828a4 100644 --- a/blight-common/src/main/java/de/blight/common/MapIO.java +++ b/blight-common/src/main/java/de/blight/common/MapIO.java @@ -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(); } diff --git a/blight-common/src/main/java/de/blight/common/PlacedItem.java b/blight-common/src/main/java/de/blight/common/PlacedItem.java index aa50872..37c5c5b 100644 --- a/blight-common/src/main/java/de/blight/common/PlacedItem.java +++ b/blight-common/src/main/java/de/blight/common/PlacedItem.java @@ -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); + } +} diff --git a/blight-common/src/main/java/de/blight/common/PlacedItemIO.java b/blight-common/src/main/java/de/blight/common/PlacedItemIO.java index e3fe832..4922b46 100644 --- a/blight-common/src/main/java/de/blight/common/PlacedItemIO.java +++ b/blight-common/src/main/java/de/blight/common/PlacedItemIO.java @@ -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 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; diff --git a/blight-common/src/main/java/de/blight/common/SaveGame.java b/blight-common/src/main/java/de/blight/common/SaveGame.java new file mode 100644 index 0000000..1280549 --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/SaveGame.java @@ -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 inventory = new LinkedHashMap<>(); + } + + public static class WorldSave { + /** UUIDs der PlacedItems, die der Spieler aufgesammelt hat. */ + public Set pickedUpItems = new HashSet<>(); + /** IDs besiegter Gegner (für zukünftige Implementierung). */ + public Set defeatedEnemies = new HashSet<>(); + } +} diff --git a/blight-common/src/main/java/de/blight/common/SaveGameIO.java b/blight-common/src/main/java/de/blight/common/SaveGameIO.java new file mode 100644 index 0000000..579e99a --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/SaveGameIO.java @@ -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); + } +} diff --git a/blight-common/src/main/java/de/blight/common/VoxelChunk.java b/blight-common/src/main/java/de/blight/common/VoxelChunk.java new file mode 100644 index 0000000..eb48dea --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/VoxelChunk.java @@ -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); } +} diff --git a/blight-common/src/main/java/de/blight/common/VoxelChunkIO.java b/blight-common/src/main/java/de/blight/common/VoxelChunkIO.java new file mode 100644 index 0000000..cb9ffef --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/VoxelChunkIO.java @@ -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 loadAll() { + List result = new ArrayList<>(); + Path dir = ChunkTerrainIO.chunksDir(); + if (!Files.isDirectory(dir)) return result; + try (DirectoryStream 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; + } +} diff --git a/blight-common/src/main/java/de/blight/common/model/CharacterStat.java b/blight-common/src/main/java/de/blight/common/model/CharacterStat.java new file mode 100644 index 0000000..7cd43a5 --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/model/CharacterStat.java @@ -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; } +} diff --git a/blight-common/src/main/java/de/blight/common/model/ConsumableEffect.java b/blight-common/src/main/java/de/blight/common/model/ConsumableEffect.java new file mode 100644 index 0000000..ff6d507 --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/model/ConsumableEffect.java @@ -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; +} diff --git a/blight-common/src/main/java/de/blight/common/model/Inventar.java b/blight-common/src/main/java/de/blight/common/model/Inventar.java index 15c7a90..0e6ce08 100644 --- a/blight-common/src/main/java/de/blight/common/model/Inventar.java +++ b/blight-common/src/main/java/de/blight/common/model/Inventar.java @@ -1,11 +1,18 @@ 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 items = new HashMap(); + + /** Gibt eine unveränderliche Sicht auf alle Items und ihre Anzahlen zurück. */ + public Map getItems() { + return Collections.unmodifiableMap(items); + } public void collect(Item item) { add(item, 1); diff --git a/blight-common/src/main/java/de/blight/common/model/Item.java b/blight-common/src/main/java/de/blight/common/model/Item.java index 431fc7b..dab00f7 100644 --- a/blight-common/src/main/java/de/blight/common/model/Item.java +++ b/blight-common/src/main/java/de/blight/common/model/Item.java @@ -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 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 diff --git a/blight-common/src/main/java/de/blight/common/model/ItemCategory.java b/blight-common/src/main/java/de/blight/common/model/ItemCategory.java index 9ae4b1f..0f64e4b 100644 --- a/blight-common/src/main/java/de/blight/common/model/ItemCategory.java +++ b/blight-common/src/main/java/de/blight/common/model/ItemCategory.java @@ -6,6 +6,6 @@ public enum ItemCategory { GEAR, CONSUMABLES, QUEST_ITEMS, - USABLES, - MISC; + CRAFTING_ITEMS, + MISC_ITEMS; } diff --git a/blight-common/src/main/java/de/blight/common/model/ItemCategoryManager.java b/blight-common/src/main/java/de/blight/common/model/ItemCategoryManager.java new file mode 100644 index 0000000..aa0ec3d --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/model/ItemCategoryManager.java @@ -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> mapping = new EnumMap>(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 getSubCategories(ItemCategory cat) { + return mapping.getOrDefault(cat, List.of()); + } + + public static boolean isValid(ItemCategory cat, ItemSubCategory sub) { + return mapping.get(cat).contains(sub); + } +} diff --git a/blight-common/src/main/java/de/blight/common/model/ItemSubCategory.java b/blight-common/src/main/java/de/blight/common/model/ItemSubCategory.java new file mode 100644 index 0000000..66ecf26 --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/model/ItemSubCategory.java @@ -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; +} diff --git a/blight-common/src/main/java/de/blight/common/model/MainCharacter.java b/blight-common/src/main/java/de/blight/common/model/MainCharacter.java index 538a475..29a41d7 100644 --- a/blight-common/src/main/java/de/blight/common/model/MainCharacter.java +++ b/blight-common/src/main/java/de/blight/common/model/MainCharacter.java @@ -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 openQuests; private List 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); } diff --git a/blight-common/src/main/java/de/blight/common/model/ObjectReference.java b/blight-common/src/main/java/de/blight/common/model/ObjectReference.java index 717d64c..ccbe4f5 100644 --- a/blight-common/src/main/java/de/blight/common/model/ObjectReference.java +++ b/blight-common/src/main/java/de/blight/common/model/ObjectReference.java @@ -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/.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"; + } } diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index bebc82f..c32ea24 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -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 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 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 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 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 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 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 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 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> 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-/ 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 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 walk = java.nio.file.Files.walk(src)) { + for (Path s : (Iterable) 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 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,26 +6182,72 @@ public class EditorApp extends Application { .addListener((obs, ov, nv) -> removeClipBtn.setDisable(nv == null)); addClipBtn.setOnAction(e -> { - ComboBox 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 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 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 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 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> 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)) { - animSetClipListView.getItems().add(clip); - animSetDirty = true; + 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() { diff --git a/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java b/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java index 1b30b78..0b0d051 100644 --- a/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java @@ -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); diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index 79ecc35..a26f880 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -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 voxelEditQueue = new ConcurrentLinkedQueue<>(); + // ── Item-Platzierung ────────────────────────────────────────────────────── /** activeLayer==21 → Item-Pickup auf die Karte platzieren */ public static final int LAYER_ITEMS = 21; diff --git a/blight-editor/src/main/java/de/blight/editor/state/ItemPlacementState.java b/blight-editor/src/main/java/de/blight/editor/state/ItemPlacementState.java index 36c0e6e..9c7c650 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/ItemPlacementState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/ItemPlacementState.java @@ -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()); diff --git a/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java index 71ae508..4cc89f6 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java @@ -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; diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java index a7bc5e8..a26f34f 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java @@ -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 areas = areaState != null ? areaState.getPlacedAreas() : null; final List 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,18 +1271,29 @@ public class TerrainEditorState extends BaseAppState { float terrainDist = terrainDistBelow(); float speed = FastMath.clamp(terrainDist, 5f, CAM_SPEED) * tpf; - float hFwdX = -FastMath.sin(camYaw); - float hFwdZ = -FastMath.cos(camYaw); - if (input.forward) { camPos.x -= hFwdX * speed; camPos.z -= hFwdZ * speed; } - if (input.backward) { camPos.x += hFwdX * speed; camPos.z += hFwdZ * speed; } + 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; } + if (input.backward) { camPos.x += hFwdX * speed; camPos.z += hFwdZ * speed; } - Vector3f lft = cam.getLeft().clone().setY(0); - if (lft.lengthSquared() > 0.001f) lft.normalizeLocal(); - if (input.left) camPos.addLocal(lft.mult(speed)); - if (input.right) camPos.subtractLocal(lft.mult(speed)); + Vector3f lft = cam.getLeft().clone().setY(0); + if (lft.lengthSquared() > 0.001f) lft.normalizeLocal(); + if (input.left) camPos.addLocal(lft.mult(speed)); + if (input.right) camPos.subtractLocal(lft.mult(speed)); - if (input.up) orbitAroundTerrain( ORBIT_SPEED * tpf); - if (input.down) orbitAroundTerrain(-ORBIT_SPEED * tpf); + if (input.up) orbitAroundTerrain( ORBIT_SPEED * tpf); + if (input.down) orbitAroundTerrain(-ORBIT_SPEED * tpf); + } int scroll = input.scrollAccum.getAndSet(0); if (scroll != 0) diff --git a/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java new file mode 100644 index 0000000..3bce4b9 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java @@ -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 chunks = new HashMap<>(); + /** key → zugehöriger VoxelChunkNode in der Szene. */ + private final Map 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 lodRebuildQueue = new ConcurrentLinkedQueue<>(); + /** Chunks mit fertigen LOD1/2-Meshes, die im JME-Thread übernommen werden. */ + private final ConcurrentLinkedQueue 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 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); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tool/VoxelTool.java b/blight-editor/src/main/java/de/blight/editor/tool/VoxelTool.java new file mode 100644 index 0000000..ca7425c --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tool/VoxelTool.java @@ -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 getChoiceParameters() { + return List.of(mode, textureSlot); + } + + @Override + public List getParameters() { + return List.of(brushRadius, brushStrength); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/ui/ItemEditorView.java b/blight-editor/src/main/java/de/blight/editor/ui/ItemEditorView.java index 1e324c3..946eee2 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/ItemEditorView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/ItemEditorView.java @@ -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; /** @@ -33,14 +38,18 @@ public class ItemEditorView extends BorderPane { private Item current = null; // Form-Felder - private TextField idField; - private ComboBox catCombo; + private TextField idField; + private ComboBox catCombo; + private ComboBox subCatCombo; private TextField nameField; private TextField descField; private Spinner 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 effects = new ArrayList<>(); + for (Node node : effectsRows.getChildren()) { + if (!(node instanceof HBox row)) continue; + @SuppressWarnings("unchecked") + ComboBox sc = (ComboBox) row.getChildren().get(0); + @SuppressWarnings("unchecked") + Spinner sp = (Spinner) 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 statCombo = new ComboBox<>(); + statCombo.getItems().addAll(CharacterStat.values()); + statCombo.setValue(stat); + statCombo.setMinWidth(210); + statCombo.setPromptText("Stat wählen"); + + Spinner 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) { diff --git a/blight-editor/src/main/java/de/blight/editor/ui/MapObjectsView.java b/blight-editor/src/main/java/de/blight/editor/ui/MapObjectsView.java new file mode 100644 index 0000000..2514920 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/ui/MapObjectsView.java @@ -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 treeRoot = new TreeItem<>("Karte"); + private final TreeView tree; + private final Map, 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 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 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 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 list = PlacedModelIO.load(); + TreeItem group = group("Modelle", list.size()); + for (int idx = 0; idx < list.size(); idx++) { + PlacedModel m = list.get(idx); + TreeItem 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 list = PlacedItemIO.load(); + TreeItem group = group("Items", list.size()); + for (int idx = 0; idx < list.size(); idx++) { + PlacedItem i = list.get(idx); + TreeItem 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 list = LightIO.load(); + TreeItem group = group("Lichter", list.size()); + for (int idx = 0; idx < list.size(); idx++) { + PlacedLight l = list.get(idx); + TreeItem 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 list = EmitterIO.load(); + TreeItem group = group("Emitter", list.size()); + for (int idx = 0; idx < list.size(); idx++) { + PlacedEmitter e = list.get(idx); + TreeItem 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 list = WaterBodyIO.load(); + TreeItem 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 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 = RiverIO.load(); + TreeItem group = group("Wasserfälle", list.size()); + for (int idx = 0; idx < list.size(); idx++) { + List 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 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 list = SoundAreaIO.load(); + TreeItem 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 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 list = AreaIO.load(); + TreeItem 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 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 list = LocationZoneIO.load(); + TreeItem 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 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 group(String name, int count) { + TreeItem g = new TreeItem<>(name + " (" + count + ")"); + g.setExpanded(true); + return g; + } + + private static TreeItem 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; + } +} diff --git a/blight-game/src/main/java/de/blight/game/BlightGame.java b/blight-game/src/main/java/de/blight/game/BlightGame.java index a249965..aabea57 100644 --- a/blight-game/src/main/java/de/blight/game/BlightGame.java +++ b/blight-game/src/main/java/de/blight/game/BlightGame.java @@ -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 { @@ -29,10 +31,14 @@ public class BlightGame extends SimpleApplication { private KeyBindings keyBindings; private GraphicsSettings graphicsSettings; private ScreenshotAppState screenshotState; - private WorldScene worldScene; - private ConfigScreen configScreen; - private GraphicsScreen graphicsScreen; - private PauseMenu pauseMenu; + private WorldScene worldScene; + 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(); - } } diff --git a/blight-game/src/main/java/de/blight/game/config/GraphicsStore.java b/blight-game/src/main/java/de/blight/game/config/GraphicsStore.java index f1377bc..79a2b3b 100644 --- a/blight-game/src/main/java/de/blight/game/config/GraphicsStore.java +++ b/blight-game/src/main/java/de/blight/game/config/GraphicsStore.java @@ -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() { diff --git a/blight-game/src/main/java/de/blight/game/config/KeyBindingStore.java b/blight-game/src/main/java/de/blight/game/config/KeyBindingStore.java index 23cfbdb..9c17d8a 100644 --- a/blight-game/src/main/java/de/blight/game/config/KeyBindingStore.java +++ b/blight-game/src/main/java/de/blight/game/config/KeyBindingStore.java @@ -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() { diff --git a/blight-game/src/main/java/de/blight/game/config/KeyBindings.java b/blight-game/src/main/java/de/blight/game/config/KeyBindings.java index d59aa4f..1589abb 100644 --- a/blight-game/src/main/java/de/blight/game/config/KeyBindings.java +++ b/blight-game/src/main/java/de/blight/game/config/KeyBindings.java @@ -5,25 +5,29 @@ import com.jme3.input.KeyInput; /** Speichert alle konfigurierbaren Tastenbelegungen als plain int-Felder (KeyInput-Codes). */ public class KeyBindings { - public int forward = KeyInput.KEY_W; - public int backward = KeyInput.KEY_S; - public int left = KeyInput.KEY_A; - public int right = KeyInput.KEY_D; - public int jump = KeyInput.KEY_SPACE; - public int sprint = KeyInput.KEY_LSHIFT; - public int walk = KeyInput.KEY_LMENU; - public int interact = KeyInput.KEY_E; + public int forward = KeyInput.KEY_W; + public int backward = KeyInput.KEY_S; + public int left = KeyInput.KEY_A; + public int right = KeyInput.KEY_D; + public int jump = KeyInput.KEY_SPACE; + 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 = { - {"forward", "Vorwärts"}, - {"backward", "Rückwärts"}, - {"left", "Links"}, - {"right", "Rechts"}, - {"jump", "Springen"}, - {"sprint", "Rennen"}, - {"walk", "Gehen"}, - {"interact", "Interagieren"}, + {"forward", "Vorwärts"}, + {"backward", "Rückwärts"}, + {"left", "Links"}, + {"right", "Rechts"}, + {"jump", "Springen"}, + {"sprint", "Rennen"}, + {"walk", "Gehen"}, + {"interact", "Interagieren"}, + {"inventory", "Inventar"}, + {"quicksave", "Schnellspeichern"}, }; public int get(String fieldName) { diff --git a/blight-game/src/main/java/de/blight/game/config/PauseMenu.java b/blight-game/src/main/java/de/blight/game/config/PauseMenu.java index 6b26941..77b88fd 100644 --- a/blight-game/src/main/java/de/blight/game/config/PauseMenu.java +++ b/blight-game/src/main/java/de/blight/game/config/PauseMenu.java @@ -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; diff --git a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java index 8eed6ab..081e149 100644 --- a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java +++ b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java @@ -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 || paused) return; + if (physicsChar == null) return; + if (paused) { + // Autopilot bei Pause sofort beenden + if (autopilotDir != null) { + autopilotDir = null; + physicsChar.setWalkDirection(Vector3f.ZERO); + } + 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; diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index c3763f3..4563427 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -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 - float terrainH = terrainChunkState.getHeightAt(spawnX, spawnZ); - spawnY = terrainH + 10f; + // 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 + ")"); } diff --git a/blight-game/src/main/java/de/blight/game/state/InteractionHudState.java b/blight-game/src/main/java/de/blight/game/state/InteractionHudState.java index 5c56f67..831035a 100644 --- a/blight-game/src/main/java/de/blight/game/state/InteractionHudState.java +++ b/blight-game/src/main/java/de/blight/game/state/InteractionHudState.java @@ -6,30 +6,25 @@ 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 static final float Y_OFFSET = 0.6f; - private Camera cam; - private Node guiNode; + private Camera cam; + private Node guiNode; private WorldItemsState worldItems; private BitmapText labelText; @@ -39,8 +34,8 @@ public class InteractionHudState extends BaseAppState { @Override protected void initialize(Application app) { SimpleApplication sapp = (SimpleApplication) app; - this.cam = app.getCamera(); - this.guiNode = sapp.getGuiNode(); + this.cam = app.getCamera(); + this.guiNode = sapp.getGuiNode(); BitmapFont font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt"); labelText = new BitmapText(font, false); @@ -76,60 +71,37 @@ public class InteractionHudState extends BaseAppState { return; } - Node itemsRoot = worldItems.getItemsRoot(); - if (itemsRoot == null || itemsRoot.getQuantity() == 0) { + int idx = worldItems.getHoveredIdx(); + if (idx < 0) { 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) { + String name = worldItems.getHoveredItemName(); + if (name == null) { labelText.setCullHint(Spatial.CullHint.Always); return; } - String label = resolveLabel(bestTarget); - labelText.setText(label); + // Weltposition des Items → Bildschirmkoordinate + Node itemsRoot = worldItems.getItemsRoot(); + 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); - Vector3f worldPos = bestTarget.getWorldTranslation().add(0f, Y_OFFSET, 0f); - Vector3f screenV3 = cam.getScreenCoordinates(worldPos); - Vector2f screen = new Vector2f(screenV3.x, screenV3.y); + // Hinter der Kamera → nicht anzeigen + if (screenV.z >= 1f) { + labelText.setCullHint(Spatial.CullHint.Always); + return; + } + 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; - } } diff --git a/blight-game/src/main/java/de/blight/game/state/InventoryState.java b/blight-game/src/main/java/de/blight/game/state/InventoryState.java new file mode 100644 index 0000000..1818655 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/InventoryState.java @@ -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> 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>> groups = new LinkedHashMap<>(); + for (Map.Entry 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>> 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> groupItems = group.getValue(); + int col = 0; + for (Map.Entry 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>> groups = new LinkedHashMap<>(); + for (Map.Entry e : activeItems) { + groups.computeIfAbsent(e.getKey().getSubCategory(), k -> new ArrayList<>()).add(e); + } + float totalH = 0f; + for (List> 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> 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 + .>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"; + }; + } +} diff --git a/blight-game/src/main/java/de/blight/game/state/MarchingCubes.java b/blight-game/src/main/java/de/blight/game/state/MarchingCubes.java new file mode 100644 index 0000000..9e8b82f --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/MarchingCubes.java @@ -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 positions = new ArrayList<>(4096); + List normals = new ArrayList<>(4096); + List 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 }; + } + } +} diff --git a/blight-game/src/main/java/de/blight/game/state/SaveGameState.java b/blight-game/src/main/java/de/blight/game/state/SaveGameState.java new file mode 100644 index 0000000..81b513f --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/SaveGameState.java @@ -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 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 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 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()); + } + } +} diff --git a/blight-game/src/main/java/de/blight/game/state/VoxelChunkNode.java b/blight-game/src/main/java/de/blight/game/state/VoxelChunkNode.java new file mode 100644 index 0000000..ca01dc2 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/VoxelChunkNode.java @@ -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; + } +} diff --git a/blight-game/src/main/java/de/blight/game/state/VoxelChunkState.java b/blight-game/src/main/java/de/blight/game/state/VoxelChunkState.java new file mode 100644 index 0000000..2320792 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/VoxelChunkState.java @@ -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 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 toRemove = new ArrayList<>(); + for (Map.Entry 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 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); + } +} diff --git a/blight-game/src/main/java/de/blight/game/state/WorldItemsState.java b/blight-game/src/main/java/de/blight/game/state/WorldItemsState.java index 1e7ec24..0e4f30c 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldItemsState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldItemsState.java @@ -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 String INTERACT_ACTION = "Interact"; + 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,12 +66,32 @@ public class WorldItemsState extends BaseAppState { private SimpleApplication app; private AssetManager assets; private InputManager inputManager; + private Camera cam; private Node rootNode; private Node itemsRoot; - private final List items = new ArrayList<>(); - private final List visuals = new ArrayList<>(); - private final Map itemDefs = new HashMap<>(); + private final List items = new ArrayList<>(); + private final List visuals = new ArrayList<>(); + private final Map 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) { @@ -74,11 +105,12 @@ public class WorldItemsState extends BaseAppState { @Override protected void initialize(Application app) { - this.app = (SimpleApplication) app; - this.assets = app.getAssetManager(); + this.app = (SimpleApplication) app; + this.assets = app.getAssetManager(); this.inputManager = app.getInputManager(); - this.rootNode = this.app.getRootNode(); - this.itemsRoot = new Node("worldItemsRoot"); + this.cam = app.getCamera(); + this.rootNode = this.app.getRootNode(); + this.itemsRoot = new Node("worldItemsRoot"); try { assets.registerLocator( @@ -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 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); - try { inputManager.deleteMapping(INTERACT_ACTION); } catch (Exception ignored) {} + 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); - 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; + 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 (nearest < 0) return; - PlacedItem picked = items.get(nearest); - Spatial pickedSpatial = visuals.get(nearest); - pickedSpatial.removeFromParent(); - items.remove(nearest); - visuals.remove(nearest); + 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 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"); + } + } + + 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,15 +469,22 @@ 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; } + public Node getItemsRoot() { return itemsRoot; } public CharacterControl getPhysicsChar() { return physicsChar; } // ── Visuelles ───────────────────────────────────────────────────────────── @@ -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)); diff --git a/blight-map/src/main/map/blight_emitters.bpe b/blight-map/src/main/map/blight_emitters.bpe index 0bfe7c2..0f2c5cb 100644 --- a/blight-map/src/main/map/blight_emitters.bpe +++ b/blight-map/src/main/map/blight_emitters.bpe @@ -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 diff --git a/blight-map/src/main/map/blight_lights.bll b/blight-map/src/main/map/blight_lights.bll index 1220553..36a87bd 100644 --- a/blight-map/src/main/map/blight_lights.bll +++ b/blight-map/src/main/map/blight_lights.bll @@ -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 diff --git a/blight-map/src/main/map/blight_map.blm b/blight-map/src/main/map/blight_map.blm index e02e8c3..9c29247 100644 Binary files a/blight-map/src/main/map/blight_map.blm and b/blight-map/src/main/map/blight_map.blm differ diff --git a/blight-map/src/main/map/blight_objects.blo b/blight-map/src/main/map/blight_objects.blo index 7618d26..ce9e369 100644 --- a/blight-map/src/main/map/blight_objects.blo +++ b/blight-map/src/main/map/blight_objects.blo @@ -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 diff --git a/blight-map/src/main/map/blight_placed_items.bpi b/blight-map/src/main/map/blight_placed_items.bpi index 3320a13..cf09131 100644 --- a/blight-map/src/main/map/blight_placed_items.bpi +++ b/blight-map/src/main/map/blight_placed_items.bpi @@ -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 diff --git a/blight-map/src/main/map/blight_rivers.blr b/blight-map/src/main/map/blight_rivers.blr index ad9fbca..257b263 100644 --- a/blight-map/src/main/map/blight_rivers.blr +++ b/blight-map/src/main/map/blight_rivers.blr @@ -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 diff --git a/blight-map/src/main/map/blight_sound_areas.bsa b/blight-map/src/main/map/blight_sound_areas.bsa index 283c465..40258fa 100644 --- a/blight-map/src/main/map/blight_sound_areas.bsa +++ b/blight-map/src/main/map/blight_sound_areas.bsa @@ -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 diff --git a/blight-map/src/main/map/chunks/chunk_15_11.blc b/blight-map/src/main/map/chunks/chunk_15_11.blc index 5912ae3..ef04412 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_11.blc and b/blight-map/src/main/map/chunks/chunk_15_11.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_12.blc b/blight-map/src/main/map/chunks/chunk_15_12.blc index 6ed5d9c..761f89a 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_12.blc and b/blight-map/src/main/map/chunks/chunk_15_12.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_11.blc b/blight-map/src/main/map/chunks/chunk_16_11.blc index 3ad4529..7d2258c 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_11.blc and b/blight-map/src/main/map/chunks/chunk_16_11.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_12.blc b/blight-map/src/main/map/chunks/chunk_16_12.blc index 542f447..77957d0 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_12.blc and b/blight-map/src/main/map/chunks/chunk_16_12.blc differ diff --git a/blight-map/src/main/map/chunks/voxel_03_0_09.blvc b/blight-map/src/main/map/chunks/voxel_03_0_09.blvc new file mode 100644 index 0000000..fd25b59 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_03_0_09.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_03_m1_09.blvc b/blight-map/src/main/map/chunks/voxel_03_m1_09.blvc new file mode 100644 index 0000000..0317d5d Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_03_m1_09.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_04_0_10.blvc b/blight-map/src/main/map/chunks/voxel_04_0_10.blvc new file mode 100644 index 0000000..0337620 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_04_0_10.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_04_m1_10.blvc b/blight-map/src/main/map/chunks/voxel_04_m1_10.blvc new file mode 100644 index 0000000..1bea1cc Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_04_m1_10.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_05_0_10.blvc b/blight-map/src/main/map/chunks/voxel_05_0_10.blvc new file mode 100644 index 0000000..28ef251 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_05_0_10.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_05_m1_10.blvc b/blight-map/src/main/map/chunks/voxel_05_m1_10.blvc new file mode 100644 index 0000000..e0af981 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_05_m1_10.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_06_0_10.blvc b/blight-map/src/main/map/chunks/voxel_06_0_10.blvc new file mode 100644 index 0000000..8b5f08a Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_06_0_10.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_06_m1_10.blvc b/blight-map/src/main/map/chunks/voxel_06_m1_10.blvc new file mode 100644 index 0000000..e88d69c Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_06_m1_10.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_08_0_11.blvc b/blight-map/src/main/map/chunks/voxel_08_0_11.blvc new file mode 100644 index 0000000..681440c Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_08_0_11.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_08_0_12.blvc b/blight-map/src/main/map/chunks/voxel_08_0_12.blvc new file mode 100644 index 0000000..07c1dc4 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_08_0_12.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_08_m1_11.blvc b/blight-map/src/main/map/chunks/voxel_08_m1_11.blvc new file mode 100644 index 0000000..28b3cb0 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_08_m1_11.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_08_m1_12.blvc b/blight-map/src/main/map/chunks/voxel_08_m1_12.blvc new file mode 100644 index 0000000..1d841ad Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_08_m1_12.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_09_0_11.blvc b/blight-map/src/main/map/chunks/voxel_09_0_11.blvc new file mode 100644 index 0000000..f927cc4 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_09_0_11.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_09_0_12.blvc b/blight-map/src/main/map/chunks/voxel_09_0_12.blvc new file mode 100644 index 0000000..4056663 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_09_0_12.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_09_m1_11.blvc b/blight-map/src/main/map/chunks/voxel_09_m1_11.blvc new file mode 100644 index 0000000..467319d Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_09_m1_11.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_09_m1_12.blvc b/blight-map/src/main/map/chunks/voxel_09_m1_12.blvc new file mode 100644 index 0000000..3c4da69 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_09_m1_12.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_10_0_12.blvc b/blight-map/src/main/map/chunks/voxel_10_0_12.blvc new file mode 100644 index 0000000..ce1175b Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_10_0_12.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_10_0_13.blvc b/blight-map/src/main/map/chunks/voxel_10_0_13.blvc new file mode 100644 index 0000000..0087f1e Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_10_0_13.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_10_m1_12.blvc b/blight-map/src/main/map/chunks/voxel_10_m1_12.blvc new file mode 100644 index 0000000..53ed017 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_10_m1_12.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_10_m1_13.blvc b/blight-map/src/main/map/chunks/voxel_10_m1_13.blvc new file mode 100644 index 0000000..ee1215e Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_10_m1_13.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_11_0_12.blvc b/blight-map/src/main/map/chunks/voxel_11_0_12.blvc new file mode 100644 index 0000000..e671591 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_11_0_12.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_11_0_13.blvc b/blight-map/src/main/map/chunks/voxel_11_0_13.blvc new file mode 100644 index 0000000..9d6cee3 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_11_0_13.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_11_m1_12.blvc b/blight-map/src/main/map/chunks/voxel_11_m1_12.blvc new file mode 100644 index 0000000..1b87513 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_11_m1_12.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_11_m1_13.blvc b/blight-map/src/main/map/chunks/voxel_11_m1_13.blvc new file mode 100644 index 0000000..ad9615b Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_11_m1_13.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_12_0_10.blvc b/blight-map/src/main/map/chunks/voxel_12_0_10.blvc new file mode 100644 index 0000000..f6d31bc Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_12_0_10.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_12_0_11.blvc b/blight-map/src/main/map/chunks/voxel_12_0_11.blvc new file mode 100644 index 0000000..e42d9cd Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_12_0_11.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_12_0_13.blvc b/blight-map/src/main/map/chunks/voxel_12_0_13.blvc new file mode 100644 index 0000000..e11476f Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_12_0_13.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_12_0_14.blvc b/blight-map/src/main/map/chunks/voxel_12_0_14.blvc new file mode 100644 index 0000000..d345a1f Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_12_0_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_12_m1_10.blvc b/blight-map/src/main/map/chunks/voxel_12_m1_10.blvc new file mode 100644 index 0000000..dbf8cdb Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_12_m1_10.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_12_m1_11.blvc b/blight-map/src/main/map/chunks/voxel_12_m1_11.blvc new file mode 100644 index 0000000..74c486f Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_12_m1_11.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_12_m1_13.blvc b/blight-map/src/main/map/chunks/voxel_12_m1_13.blvc new file mode 100644 index 0000000..8555ef7 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_12_m1_13.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_12_m1_14.blvc b/blight-map/src/main/map/chunks/voxel_12_m1_14.blvc new file mode 100644 index 0000000..fac2396 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_12_m1_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_13_0_12.blvc b/blight-map/src/main/map/chunks/voxel_13_0_12.blvc new file mode 100644 index 0000000..3b3da86 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_13_0_12.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_13_0_13.blvc b/blight-map/src/main/map/chunks/voxel_13_0_13.blvc new file mode 100644 index 0000000..69cef8d Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_13_0_13.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_13_0_14.blvc b/blight-map/src/main/map/chunks/voxel_13_0_14.blvc new file mode 100644 index 0000000..2e38026 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_13_0_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_13_m1_12.blvc b/blight-map/src/main/map/chunks/voxel_13_m1_12.blvc new file mode 100644 index 0000000..db8c6e8 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_13_m1_12.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_13_m1_13.blvc b/blight-map/src/main/map/chunks/voxel_13_m1_13.blvc new file mode 100644 index 0000000..db98b95 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_13_m1_13.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_13_m1_14.blvc b/blight-map/src/main/map/chunks/voxel_13_m1_14.blvc new file mode 100644 index 0000000..f18c8cf Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_13_m1_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_12.blvc b/blight-map/src/main/map/chunks/voxel_14_0_12.blvc new file mode 100644 index 0000000..a29f4a4 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_14_0_12.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_13.blvc b/blight-map/src/main/map/chunks/voxel_14_0_13.blvc new file mode 100644 index 0000000..c50578c Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_14_0_13.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_14.blvc b/blight-map/src/main/map/chunks/voxel_14_0_14.blvc new file mode 100644 index 0000000..c80fed8 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_14_0_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_14_m1_12.blvc b/blight-map/src/main/map/chunks/voxel_14_m1_12.blvc new file mode 100644 index 0000000..164a60c Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_14_m1_12.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_14_m1_13.blvc b/blight-map/src/main/map/chunks/voxel_14_m1_13.blvc new file mode 100644 index 0000000..d54ec56 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_14_m1_13.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_14_m1_14.blvc b/blight-map/src/main/map/chunks/voxel_14_m1_14.blvc new file mode 100644 index 0000000..52e5246 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_14_m1_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_12.blvc b/blight-map/src/main/map/chunks/voxel_15_0_12.blvc new file mode 100644 index 0000000..39ffbf6 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_12.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_13.blvc b/blight-map/src/main/map/chunks/voxel_15_0_13.blvc new file mode 100644 index 0000000..7c8273c Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_13.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_14.blvc b/blight-map/src/main/map/chunks/voxel_15_0_14.blvc new file mode 100644 index 0000000..3f3b512 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_15.blvc b/blight-map/src/main/map/chunks/voxel_15_0_15.blvc new file mode 100644 index 0000000..d318286 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_16.blvc b/blight-map/src/main/map/chunks/voxel_15_0_16.blvc new file mode 100644 index 0000000..e0b540f Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_16.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_m1_12.blvc b/blight-map/src/main/map/chunks/voxel_15_m1_12.blvc new file mode 100644 index 0000000..363642e Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_m1_12.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_m1_13.blvc b/blight-map/src/main/map/chunks/voxel_15_m1_13.blvc new file mode 100644 index 0000000..78e2a03 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_m1_13.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_m1_14.blvc b/blight-map/src/main/map/chunks/voxel_15_m1_14.blvc new file mode 100644 index 0000000..5ea65cc Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_m1_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_m1_15.blvc b/blight-map/src/main/map/chunks/voxel_15_m1_15.blvc new file mode 100644 index 0000000..341ded1 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_m1_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_m1_16.blvc b/blight-map/src/main/map/chunks/voxel_15_m1_16.blvc new file mode 100644 index 0000000..28eef63 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_m1_16.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_12.blvc b/blight-map/src/main/map/chunks/voxel_16_0_12.blvc new file mode 100644 index 0000000..be95ae1 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_12.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_13.blvc b/blight-map/src/main/map/chunks/voxel_16_0_13.blvc new file mode 100644 index 0000000..033d616 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_13.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_14.blvc b/blight-map/src/main/map/chunks/voxel_16_0_14.blvc new file mode 100644 index 0000000..0a5ad66 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_15.blvc b/blight-map/src/main/map/chunks/voxel_16_0_15.blvc new file mode 100644 index 0000000..4b38f16 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_16.blvc b/blight-map/src/main/map/chunks/voxel_16_0_16.blvc new file mode 100644 index 0000000..25a74e1 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_16.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_1_15.blvc b/blight-map/src/main/map/chunks/voxel_16_1_15.blvc new file mode 100644 index 0000000..93368db Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_1_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_12.blvc b/blight-map/src/main/map/chunks/voxel_16_m1_12.blvc new file mode 100644 index 0000000..f6ac8c4 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_12.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_13.blvc b/blight-map/src/main/map/chunks/voxel_16_m1_13.blvc new file mode 100644 index 0000000..9c00561 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_13.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_14.blvc b/blight-map/src/main/map/chunks/voxel_16_m1_14.blvc new file mode 100644 index 0000000..80cbd8a Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_15.blvc b/blight-map/src/main/map/chunks/voxel_16_m1_15.blvc new file mode 100644 index 0000000..634fcb0 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_16.blvc b/blight-map/src/main/map/chunks/voxel_16_m1_16.blvc new file mode 100644 index 0000000..43db38f Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_16.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_13.blvc b/blight-map/src/main/map/chunks/voxel_17_0_13.blvc new file mode 100644 index 0000000..c017e6b Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_13.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_14.blvc b/blight-map/src/main/map/chunks/voxel_17_0_14.blvc new file mode 100644 index 0000000..45e182e Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_15.blvc b/blight-map/src/main/map/chunks/voxel_17_0_15.blvc new file mode 100644 index 0000000..33f61b4 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_16.blvc b/blight-map/src/main/map/chunks/voxel_17_0_16.blvc new file mode 100644 index 0000000..4480b38 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_16.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_13.blvc b/blight-map/src/main/map/chunks/voxel_17_m1_13.blvc new file mode 100644 index 0000000..2a98f70 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_13.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_14.blvc b/blight-map/src/main/map/chunks/voxel_17_m1_14.blvc new file mode 100644 index 0000000..93ee8eb Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_14.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_15.blvc b/blight-map/src/main/map/chunks/voxel_17_m1_15.blvc new file mode 100644 index 0000000..5e8e245 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_16.blvc b/blight-map/src/main/map/chunks/voxel_17_m1_16.blvc new file mode 100644 index 0000000..9830c13 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_16.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_19_0_09.blvc b/blight-map/src/main/map/chunks/voxel_19_0_09.blvc new file mode 100644 index 0000000..5f607a3 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_19_0_09.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_19_m1_09.blvc b/blight-map/src/main/map/chunks/voxel_19_m1_09.blvc new file mode 100644 index 0000000..24832a1 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_19_m1_09.blvc differ