diff --git a/blight-assets/src/main/resources/Textures/hud/minimap_world.png b/blight-assets/src/main/resources/Textures/hud/minimap_world.png index b7677cb..0ba1932 100644 Binary files a/blight-assets/src/main/resources/Textures/hud/minimap_world.png and b/blight-assets/src/main/resources/Textures/hud/minimap_world.png differ diff --git a/blight-common/src/main/java/de/blight/common/map/WorldMapRenderModel.java b/blight-common/src/main/java/de/blight/common/map/WorldMapRenderModel.java new file mode 100644 index 0000000..945c0a3 --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/map/WorldMapRenderModel.java @@ -0,0 +1,27 @@ +package de.blight.common.map; + +import de.blight.common.*; +import de.blight.common.model.Location; + +import java.util.List; + +/** + * Fertig vorberechnetes Karten-Modell: enthält alle Rohdaten + die im Editor + * vorcompilierten Overlay-Informationen (SeaMask, Küstenpfade, Baum-Cluster). + * WorldMapRenderer.render() arbeitet nur noch gegen dieses Modell. + */ +public record WorldMapRenderModel( + // Terrain-Daten + Polygon-Objekte (werden vom Renderer für das PNG genutzt) + MapData mapData, + List areas, + List zones, + List locations, + List waters, + List models, + int[] slotColorsRGB, + // Vorberechnete Overlay-Daten (werden vom Canvas genutzt, nicht vom PNG-Renderer) + boolean[] seaMask, // Wasser-Pixel-Maske bei SEA_MASK_SIZE-Auflösung + float[] terrainSamples, // Höhenwerte bei SEA_MASK_SIZE (für Wellen-Prüfung) + List coastPaths, // geglättete Marching-Squares Küstenpfade (Weltkoord.) + List> treeClusters +) {} diff --git a/blight-common/src/main/java/de/blight/common/map/WorldMapRenderer.java b/blight-common/src/main/java/de/blight/common/map/WorldMapRenderer.java index 022f922..536304f 100644 --- a/blight-common/src/main/java/de/blight/common/map/WorldMapRenderer.java +++ b/blight-common/src/main/java/de/blight/common/map/WorldMapRenderer.java @@ -62,17 +62,25 @@ public final class WorldMapRenderer { public static boolean[] buildSeaMask(MapData m, int size) { int TV = MapData.TERRAIN_VERTS; + int UV = MapData.UPPER_VERTS; boolean[] mask = new boolean[size * size]; for (int py = 0; py < size; py++) { for (int px = 0; px < size; px++) { int hx = Math.min((int)((float) px / (size - 1) * (TV - 1)), TV - 1); int hz = Math.min((int)((float) py / (size - 1) * (TV - 1)), TV - 1); - mask[py * size + px] = m.terrainHeight[hz * TV + hx] < 0f; + int ux = Math.min((int)((float) px / (size - 1) * (UV - 1)), UV - 1); + int uz = Math.min((int)((float) py / (size - 1) * (UV - 1)), UV - 1); + float h = m.terrainHeight[hz * TV + hx]; + float upper = m.upperTop[uz * UV + ux]; + if (upper > 0f && upper > h) { h = upper; } + mask[py * size + px] = h < 0f; } } return mask; } + private static final int WATER_COLOR = 0xFF_ADD8E6; + // Default-Slot-Farben für Slots 1-8 (Base-Layer 1-4 + Upper-Layer 5-8) private static final int[] DEF_SLOT_R = { 71, 115, 140, 204, 90, 130, 110, 180 }; private static final int[] DEF_SLOT_G = { 148, 82, 115, 184, 80, 90, 60, 100 }; @@ -80,15 +88,17 @@ public final class WorldMapRenderer { private WorldMapRenderer() {} - public static BufferedImage render(RenderInput input, int targetSize, RenderOptions opts) { - MapData m = input.mapData(); + /** Hauptmethode: rendert das Hintergrund-PNG aus einem vorberechneten Modell. */ + public static BufferedImage render(WorldMapRenderModel model, int targetSize, RenderOptions opts) { + MapData m = model.mapData(); int TV = MapData.TERRAIN_VERTS; int SS = MapData.SPLAT_SIZE; - int[] slotR = slotChannel(input, 0); - int[] slotG = slotChannel(input, 1); - int[] slotB = slotChannel(input, 2); + int[] slotR = slotChannel(model.slotColorsRGB(), 0); + int[] slotG = slotChannel(model.slotColorsRGB(), 1); + int[] slotB = slotChannel(model.slotColorsRGB(), 2); // ── 1. Heightmap auf Zielauflösung samplen ──────────────────────────── + int UV = MapData.UPPER_VERTS; float[] heights = new float[targetSize * targetSize]; float minH = Float.MAX_VALUE, maxH = -Float.MAX_VALUE; @@ -96,7 +106,11 @@ public final class WorldMapRenderer { for (int px = 0; px < targetSize; px++) { int hx = Math.min((int)((float) px / (targetSize - 1) * (TV - 1)), TV - 1); int hz = Math.min((int)((float) py / (targetSize - 1) * (TV - 1)), TV - 1); + int ux = Math.min((int)((float) px / (targetSize - 1) * (UV - 1)), UV - 1); + int uz = Math.min((int)((float) py / (targetSize - 1) * (UV - 1)), UV - 1); float h = m.terrainHeight[hz * TV + hx]; + float upper = m.upperTop[uz * UV + ux]; + if (upper > 0f && upper > h) { h = upper; } heights[py * targetSize + px] = h; if (h < minH) minH = h; if (h > maxH) maxH = h; @@ -161,6 +175,9 @@ public final class WorldMapRenderer { } } + // Kuwahara-Filter: lässt das Terrain wie gemalt wirken + if (opts.showSplatColors()) { applyKuwahara(img, 3); } + // ── 3. Vektor-Overlays ──────────────────────────────────────────────── Graphics2D gfx = img.createGraphics(); gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); @@ -171,10 +188,10 @@ public final class WorldMapRenderer { // ── Weißfüllung ─────────────────────────────────────────────────────── for (int py = 0; py < targetSize; py++) { for (int px = 0; px < targetSize; px++) { - if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, 0xFFFFFFFF); + if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, WATER_COLOR); } } - for (PlacedWater w : input.waters()) { + for (PlacedWater w : model.waters()) { int[] xs = worldToPixels(w.pointsX(), targetSize); int[] ys = worldToPixels(w.pointsZ(), targetSize); Polygon poly = new Polygon(xs, ys, xs.length); @@ -186,7 +203,7 @@ public final class WorldMapRenderer { for (int py = y0; py <= y1; py++) { for (int px = x0; px <= x1; px++) { if (poly.contains(px, py) && heights[py * targetSize + px] < wh) - img.setRGB(px, py, 0xFFFFFFFF); + img.setRGB(px, py, WATER_COLOR); } } } @@ -219,7 +236,7 @@ public final class WorldMapRenderer { } } // Wasserflächen: nur wenn groß genug - for (PlacedWater w : input.waters()) { + for (PlacedWater w : model.waters()) { int[] xs = worldToPixels(w.pointsX(), targetSize); int[] ys = worldToPixels(w.pointsZ(), targetSize); Polygon poly = new Polygon(xs, ys, xs.length); @@ -250,7 +267,7 @@ public final class WorldMapRenderer { gfx.setColor(Color.BLACK); gfx.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); // Wasserflächen: Polygon-Umriss - for (PlacedWater w : input.waters()) { + for (PlacedWater w : model.waters()) { int[] xs = worldToPixels(w.pointsX(), targetSize); int[] ys = worldToPixels(w.pointsZ(), targetSize); gfx.drawPolygon(xs, ys, xs.length); @@ -293,7 +310,7 @@ public final class WorldMapRenderer { 10f, new float[]{dash, gap}, 0f); int aFontSize = Math.max(8, targetSize / 160); gfx.setFont(new Font("SansSerif", Font.BOLD, aFontSize)); - for (PlacedArea a : input.areas()) { + for (PlacedArea a : model.areas()) { int[] xs = worldToPixels(a.pointsX(), targetSize); int[] ys = worldToPixels(a.pointsZ(), targetSize); gfx.setStroke(dashed); @@ -314,7 +331,7 @@ public final class WorldMapRenderer { // Location-Zonen if (opts.showZones()) { gfx.setStroke(new BasicStroke(lineW)); - for (PlacedLocationZone z : input.zones()) { + for (PlacedLocationZone z : model.zones()) { int[] xs = worldToPixels(z.pointsX(), targetSize); int[] ys = worldToPixels(z.pointsZ(), targetSize); gfx.setColor(new Color(240, 190, 40, 70)); @@ -324,13 +341,13 @@ public final class WorldMapRenderer { } } - // Modell-Punkte if (opts.showModels()) { int dotR = Math.max(1, targetSize / 600); gfx.setColor(new Color(160, 80, 20, 200)); - for (PlacedModel model : input.models()) { - int mx = worldToPixel(model.x(), targetSize); - int mz = worldToPixel(model.z(), targetSize); + for (PlacedModel pm : model.models()) { + if (isTree(pm)) { continue; } + int mx = worldToPixel(pm.x(), targetSize); + int mz = worldToPixel(pm.z(), targetSize); gfx.fillRect(mx - dotR, mz - dotR, dotR * 2 + 1, dotR * 2 + 1); } } @@ -339,7 +356,7 @@ public final class WorldMapRenderer { if (opts.showLocations()) { int fontSize = Math.max(8, targetSize / 140); gfx.setFont(new Font("SansSerif", Font.BOLD, fontSize)); - for (Location loc : input.locations()) { + for (Location loc : model.locations()) { if (!loc.isShowOnMap()) continue; if (loc.getId() == null || loc.getId().isEmpty()) continue; float wx = Float.isNaN(loc.getLabelX()) ? loc.getCenterX() : loc.getLabelX(); @@ -354,6 +371,15 @@ public final class WorldMapRenderer { return img; } + /** Rückwärts-kompatibel: baut ein minimales Modell (ohne Overlay-Daten) und delegiert. */ + public static BufferedImage render(RenderInput input, int targetSize, RenderOptions opts) { + return render(new WorldMapRenderModel( + input.mapData(), input.areas(), input.zones(), input.locations(), + input.waters(), input.models(), input.slotColorsRGB(), + null, null, null, null + ), targetSize, opts); + } + /** * Rendert einen rechteckigen Weltausschnitt als {@link BufferedImage}. * Koordinatenursprung und Skalierung passen sich dem Ausschnitt an, @@ -383,16 +409,21 @@ public final class WorldMapRenderer { float minH = 0f, maxH = 1f; if (opts.showTerrain() || opts.showWater()) { + int UV2 = MapData.UPPER_VERTS; heights = new float[targetSize * targetSize]; minH = Float.MAX_VALUE; maxH = -Float.MAX_VALUE; for (int py = 0; py < targetSize; py++) { float wz = wz0 + (float) py / (targetSize - 1) * rSize; - int hz = iclamp((int) ((wz + WORLD_HALF) / WORLD_SIZE * (TV - 1)), 0, TV - 1); + int hz = iclamp((int) ((wz + WORLD_HALF) / WORLD_SIZE * (TV - 1)), 0, TV - 1); + int uz = iclamp((int) ((wz + WORLD_HALF) / WORLD_SIZE * (UV2 - 1)), 0, UV2 - 1); for (int px = 0; px < targetSize; px++) { float wx = wx0 + (float) px / (targetSize - 1) * rSize; - int hx = iclamp((int) ((wx + WORLD_HALF) / WORLD_SIZE * (TV - 1)), 0, TV - 1); + int hx = iclamp((int) ((wx + WORLD_HALF) / WORLD_SIZE * (TV - 1)), 0, TV - 1); + int ux = iclamp((int) ((wx + WORLD_HALF) / WORLD_SIZE * (UV2 - 1)), 0, UV2 - 1); float h = m.terrainHeight[hz * TV + hx]; + float upper = m.upperTop[uz * UV2 + ux]; + if (upper > 0f && upper > h) { h = upper; } heights[py * targetSize + px] = h; if (h < minH) { minH = h; } if (h > maxH) { maxH = h; } @@ -403,9 +434,9 @@ public final class WorldMapRenderer { // ── Terrain (optional) ──────────────────────────────────────────────── if (opts.showTerrain()) { float heightRange = Math.max(0.01f, maxH - minH); - int[] sR = slotChannel(input, 0); - int[] sG = slotChannel(input, 1); - int[] sB = slotChannel(input, 2); + int[] sR = slotChannel(input.slotColorsRGB(), 0); + int[] sG = slotChannel(input.slotColorsRGB(), 1); + int[] sB = slotChannel(input.slotColorsRGB(), 2); for (int py = 0; py < targetSize; py++) { for (int px = 0; px < targetSize; px++) { @@ -465,6 +496,8 @@ public final class WorldMapRenderer { } } + if (opts.showSplatColors()) { applyKuwahara(img, 3); } + // ── Vektor-Overlays ─────────────────────────────────────────────────── Graphics2D gfx = img.createGraphics(); gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); @@ -474,7 +507,7 @@ public final class WorldMapRenderer { // ── Weißfüllung ─────────────────────────────────────────────────────── for (int py = 0; py < targetSize; py++) { for (int px = 0; px < targetSize; px++) { - if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, 0xFFFFFFFF); + if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, WATER_COLOR); } } for (PlacedWater w : input.waters()) { @@ -489,7 +522,7 @@ public final class WorldMapRenderer { for (int py = y0; py <= y1; py++) { for (int px = x0; px <= x1; px++) { if (poly.contains(px, py) && heights[py * targetSize + px] < wh) - img.setRGB(px, py, 0xFFFFFFFF); + img.setRGB(px, py, WATER_COLOR); } } } @@ -629,6 +662,7 @@ public final class WorldMapRenderer { int dotR = Math.max(1, targetSize / 600); gfx.setColor(new Color(160, 80, 20, 200)); for (PlacedModel model : input.models()) { + if (isTree(model)) { continue; } int mx = wrp1(model.x(), wx0, rSize, targetSize); int mz = wrp1(model.z(), wz0, rSize, targetSize); gfx.fillRect(mx - dotR, mz - dotR, dotR * 2 + 1, dotR * 2 + 1); @@ -680,13 +714,12 @@ public final class WorldMapRenderer { // Gibt den R-, G- oder B-Kanal (channel=0/1/2) aller 8 Splatmap-Slots zurück. // slotColorsRGB: 24 Werte (8 Slots × 3), 12 Werte (4 Slots, Upper-Layer = Defaults) oder null. - private static int[] slotChannel(RenderInput input, int channel) { - int[] rgb = input.slotColorsRGB(); + private static int[] slotChannel(int[] slotColorsRGB, int channel) { int[] def = channel == 0 ? DEF_SLOT_R : (channel == 1 ? DEF_SLOT_G : DEF_SLOT_B); - if (rgb == null || rgb.length < 12) { return def; } + if (slotColorsRGB == null || slotColorsRGB.length < 12) { return def; } int[] out = new int[8]; for (int s = 0; s < 8; s++) { - out[s] = (rgb.length >= (s + 1) * 3) ? rgb[s * 3 + channel] : def[s]; + out[s] = (slotColorsRGB.length >= (s + 1) * 3) ? slotColorsRGB[s * 3 + channel] : def[s]; } return out; } @@ -802,4 +835,53 @@ public final class WorldMapRenderer { float v01 = (arr[i01] & 0xFF) / 255f, v11 = (arr[i11] & 0xFF) / 255f; return (v00*(1-tx) + v10*tx)*(1-tz) + (v01*(1-tx) + v11*tx)*tz; } + + public static boolean isTree(PlacedModel m) { + return m.modelPath().replace('\\', '/').toLowerCase().contains("/trees/"); + } + + /** + * Kuwahara-Filter: Für jeden Pixel das Quadranten-Fenster mit der kleinsten + * Varianz wählen und dessen Mittelwert setzen → Ölgemälde-/gemalt-Effekt. + * Radius r=3 → Fenster 7×7, 4 Quadranten je 4×4. + */ + private static void applyKuwahara(BufferedImage img, int r) { + int w = img.getWidth(), h = img.getHeight(); + int[] src = img.getRGB(0, 0, w, h, null, 0, w); + int[] dst = new int[src.length]; + + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + float bestVar = Float.MAX_VALUE; + int bestPacked = src[y * w + x]; + + // 4 Quadranten: [xOff0..xOff1] × [yOff0..yOff1] + int[][] quads = {{-r,-r,0,0},{0,-r,r,0},{-r,0,0,r},{0,0,r,r}}; + for (int[] q : quads) { + int x0 = Math.max(0, x+q[0]), y0 = Math.max(0, y+q[1]); + int x1 = Math.min(w-1, x+q[2]), y1 = Math.min(h-1, y+q[3]); + float sumR=0,sumG=0,sumB=0, sum2R=0,sum2G=0,sum2B=0; + int cnt = 0; + for (int qy = y0; qy <= y1; qy++) { + for (int qx = x0; qx <= x1; qx++) { + int p = src[qy * w + qx]; + float pr = (p>>16)&0xFF, pg = (p>>8)&0xFF, pb = p&0xFF; + sumR+=pr; sumG+=pg; sumB+=pb; + sum2R+=pr*pr; sum2G+=pg*pg; sum2B+=pb*pb; + cnt++; + } + } + float inv = 1f / cnt; + float mR=sumR*inv, mG=sumG*inv, mB=sumB*inv; + float var = (sum2R*inv - mR*mR) + (sum2G*inv - mG*mG) + (sum2B*inv - mB*mB); + if (var < bestVar) { + bestVar = var; + bestPacked = (clamp((int)mR)<<16) | (clamp((int)mG)<<8) | clamp((int)mB); + } + } + dst[y * w + x] = bestPacked | 0xFF000000; + } + } + img.setRGB(0, 0, w, h, dst, 0, w); + } } 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 c202160..361db19 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -5639,11 +5639,36 @@ public class EditorApp extends Application { }); worldMapView = new de.blight.editor.ui.WorldMapView(() -> primaryStage); - Tab weltkartTab = new Tab("Weltkarte", worldMapView); + worldMapView.setVoxelChunkSupplier(() -> { + de.blight.editor.state.VoxelEditorState ves = jmeApp == null ? null + : jmeApp.getStateManager().getState(de.blight.editor.state.VoxelEditorState.class); + return ves != null ? ves.getChunksSnapshot() : de.blight.common.VoxelChunkIO.loadAll(); + }); + worldMapView.setCameraInfoSupplier(() -> + new de.blight.editor.ui.WorldMapView.CameraInfo( + input.camX, input.camY, input.camZ, input.camYaw)); + worldMapView.setTeleportCallback(pos -> { + input.pendingGotoX = pos[0]; + input.pendingGotoY = pos[1]; + input.pendingGotoZ = pos[2]; + }); + + final Tab weltkartTab = new Tab("Weltkarte", worldMapView); weltkartTab.setClosable(false); weltkartTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> { if (isSelected && !worldMapView.isLoaded()) worldMapView.loadAndRender(); }); + worldMapView.setFullscreenCallbacks( + () -> { + // Erst aus Tab lösen, dann in centerStack einsetzen + weltkartTab.setContent(new javafx.scene.control.Label("")); + setCenterView(worldMapView); + }, + () -> { + // Erst aus centerStack lösen (worldViewport zurück), dann in Tab setzen + setCenterView(worldViewport); + weltkartTab.setContent(worldMapView); + }); TabPane tabPane = new TabPane(assetsTab, karteTab, weltkartTab); tabPane.setStyle("-fx-background-color: #e8e8e8;"); diff --git a/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java b/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java index cbeef17..702d8ff 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java @@ -3,11 +3,15 @@ package de.blight.editor.ui; import de.blight.common.*; import de.blight.common.model.Location; import de.blight.common.map.WorldMapRenderer; -import de.blight.common.map.WorldMapRenderer.RenderInput; import de.blight.common.map.WorldMapRenderer.RenderOptions; +import de.blight.common.map.WorldMapRenderModel; +import javafx.animation.Animation; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; import javafx.application.Platform; import javafx.embed.swing.SwingFXUtils; +import javafx.util.Duration; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Cursor; @@ -15,6 +19,7 @@ import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.control.*; import javafx.scene.image.WritableImage; +import javafx.scene.input.MouseButton; import javafx.scene.layout.*; import javafx.scene.paint.Color; import javafx.scene.shape.StrokeLineCap; @@ -32,10 +37,21 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import javafx.geometry.VPos; +import javafx.scene.text.TextAlignment; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.stream.Collectors; /** * Interaktive 2D-Weltkarte im Editor-Tab. @@ -46,6 +62,8 @@ import java.util.function.Supplier; */ public class WorldMapView extends VBox { + private static final Logger log = LoggerFactory.getLogger(WorldMapView.class); + private static final int RENDER_SIZE = 2048; // Konstante Bildschirmgrößen für das Canvas-Overlay @@ -63,29 +81,56 @@ public class WorldMapView extends VBox { private final ProgressBar progress = new ProgressBar(-1); private final StackPane canvasPane = new StackPane(canvas); - private final ToggleButton layerTerrain = layerBtn("Gelände"); - private final ToggleButton layerWater = layerBtn("Wasser"); - private final ToggleButton layerAreas = layerBtn("Areas"); - private final ToggleButton layerZones = layerBtn("Zonen"); - private final ToggleButton layerLocations = layerBtn("Orte"); - private final ToggleButton layerModels = layerBtn("Modelle"); - private final ToggleButton labelBtn = new ToggleButton("Label"); + // Layer-Checkboxen (im MenuButton gebündelt) + private final CheckBox cbTerrain = layerCheck("Gelände"); + private final CheckBox cbWater = layerCheck("Wasser"); + private final CheckBox cbAreas = layerCheck("Areas"); + private final CheckBox cbZones = layerCheck("Zonen"); + private final CheckBox cbLocations = layerCheck("Orte"); + private final CheckBox cbModels = layerCheck("Modelle"); + private final ToggleButton labelBtn = new ToggleButton("Label"); + + private final Button fullscreenBtn = new Button("⤢ Vollbild"); + private final Button backBtn = new Button("< Zurück"); + private boolean isFullscreen = false; + + /** Kamera-Zustand (Position + Blickrichtung) für die Karten-Überlagerung. */ + public record CameraInfo(float x, float y, float z, float yawDeg) {} private WritableImage mapFxImage; private BufferedImage mapBuffered; private List cachedAreas = new ArrayList<>(); private List cachedLocs = new ArrayList<>(); - private List cachedWaters = new ArrayList<>(); - private boolean[] seaMask = null; - private float[][] seaCoastSegs = null; // [wx1,wz1,wx2,wz2] in Weltkoordinaten + private WorldMapRenderModel currentModel = null; + private boolean autoLoaded = false; private double panX = 0, panY = 0; private double scale = 1.0; private double dragStartX, dragStartY, dragStartPanX, dragStartPanY; - private final Supplier stageSupplier; - private final AtomicBoolean loading = new AtomicBoolean(false); + private final Supplier stageSupplier; + private Supplier> voxelChunkSupplier = null; + private Supplier cameraInfoSupplier = null; + private Consumer teleportCallback = null; // [worldX, worldY, worldZ] + private Runnable enterFullscreenCallback = null; + private Runnable exitFullscreenCallback = null; + private final AtomicBoolean loading = new AtomicBoolean(false); + + /** Verknüpft den Live-Voxel-Chunk-Zustand des Editors mit der Kartenansicht. */ + public void setVoxelChunkSupplier(Supplier> s) { this.voxelChunkSupplier = s; } + + /** Liefert aktuelle Kamera-Position und Blickrichtung für die Kartenanzeige. */ + public void setCameraInfoSupplier(Supplier s) { cameraInfoSupplier = s; } + + /** Callback für Kamera-Teleport per Linksklick; erhält [worldX, worldY, worldZ]. */ + public void setTeleportCallback(Consumer cb) { teleportCallback = cb; } + + /** Callbacks für Vollbild-Modus ein/aus (typisch: Eltern-Layout anpassen). */ + public void setFullscreenCallbacks(Runnable enter, Runnable exit) { + enterFullscreenCallback = enter; + exitFullscreenCallback = exit; + } private static final String[] ASSET_BASES = { "blight-assets/src/main/resources", @@ -119,7 +164,23 @@ public class WorldMapView extends VBox { } private void buildUi() { - Button refreshBtn = new Button("Aktualisieren"); + // ── Layer-Auswahl als MenuButton mit Checkboxen ─────────────────────── + cbTerrain.selectedProperty().addListener((obs, o, n) -> rerenderFromModel()); + cbWater.selectedProperty().addListener((obs, o, n) -> rerenderFromModel()); + cbZones.selectedProperty().addListener((obs, o, n) -> rerenderFromModel()); + cbModels.selectedProperty().addListener((obs, o, n) -> redraw()); + cbAreas.selectedProperty().addListener((obs, o, n) -> redraw()); + cbLocations.selectedProperty().addListener((obs, o, n) -> redraw()); + + MenuButton layerMenu = new MenuButton("Layer ▾"); + for (CheckBox cb : new CheckBox[]{cbTerrain, cbWater, cbAreas, cbZones, cbLocations, cbModels}) { + CustomMenuItem item = new CustomMenuItem(cb, false); + item.setHideOnClick(false); + layerMenu.getItems().add(item); + } + + // ── Buttons ─────────────────────────────────────────────────────────── + Button refreshBtn = new Button("↻ Modell aktualisieren"); refreshBtn.setOnAction(e -> loadAndRender()); Button exportBtn = new Button("Als PNG exportieren…"); @@ -129,15 +190,21 @@ public class WorldMapView extends VBox { labelBtn.selectedProperty().addListener((obs, o, n) -> canvas.setCursor(n ? Cursor.CROSSHAIR : Cursor.DEFAULT)); + fullscreenBtn.setOnAction(e -> enterFullscreen()); + backBtn.setOnAction(e -> exitFullscreen()); + backBtn.setVisible(false); + ToolBar toolbar = new ToolBar( - new Label("Layer:"), - layerTerrain, layerWater, layerAreas, layerZones, layerLocations, layerModels, + layerMenu, new Separator(), new Label("Bearbeiten:"), labelBtn, new Separator(), refreshBtn, - exportBtn + exportBtn, + new Separator(), + fullscreenBtn, + backBtn ); canvasPane.setStyle("-fx-background-color: #1a1a2a;"); @@ -148,16 +215,6 @@ public class WorldMapView extends VBox { canvas.widthProperty().addListener(obs -> redraw()); canvas.heightProperty().addListener(obs -> redraw()); - // Terrain/Wasser/Zonen/Modelle → PNG neu rendern - layerTerrain.setOnAction(e -> rerender()); - layerWater.setOnAction(e -> rerender()); - layerZones.setOnAction(e -> rerender()); - layerModels.setOnAction(e -> rerender()); - - // Areas und Locations → nur Canvas-Overlay neu zeichnen (kein PNG-Rerender) - layerAreas.setOnAction(e -> redraw()); - layerLocations.setOnAction(e -> redraw()); - canvas.setOnMousePressed(e -> { dragStartX = e.getX(); dragStartY = e.getY(); @@ -170,9 +227,14 @@ public class WorldMapView extends VBox { redraw(); }); canvas.setOnMouseReleased(e -> { - if (!labelBtn.isSelected()) return; - if (Math.abs(e.getX() - dragStartX) < 5 && Math.abs(e.getY() - dragStartY) < 5) { - handleLabelPlacement(e.getX(), e.getY()); + boolean wasDrag = Math.abs(e.getX() - dragStartX) >= 5 + || Math.abs(e.getY() - dragStartY) >= 5; + if (!wasDrag) { + if (labelBtn.isSelected()) { + handleLabelPlacement(e.getX(), e.getY()); + } else if (e.getButton() == MouseButton.PRIMARY) { + handleMapClick(e.getX(), e.getY()); + } } }); @@ -203,12 +265,23 @@ public class WorldMapView extends VBox { getChildren().addAll(toolbar, canvasPane, statusBar); setStyle("-fx-background-color: #1a1a2a;"); + + // Kamera-Indikator: regelmäßig neu zeichnen (unabhängig vom Karten-Rendering) + Timeline cameraRefresh = new Timeline( + new KeyFrame(Duration.millis(120), e -> { + if (cameraInfoSupplier != null && mapFxImage != null && !isFullscreen) { + redraw(); + } + })); + cameraRefresh.setCycleCount(Animation.INDEFINITE); + cameraRefresh.play(); } // ── Öffentliche API ─────────────────────────────────────────────────────── public boolean isLoaded() { return mapFxImage != null || loading.get(); } + /** Lädt alle Weltdaten neu und baut das Render-Modell (teuer: I/O + Berechnungen). */ public void loadAndRender() { if (loading.getAndSet(true)) return; progress.setVisible(true); @@ -216,93 +289,92 @@ public class WorldMapView extends VBox { Thread t = new Thread(() -> { try { + log.debug("[WorldMap] Modell-Build gestartet (I/O + Vorberechnungen)"); + long t0 = System.currentTimeMillis(); + MapData mapData = MapIO.load(); List areas = AreaIO.load(); List zones = LocationZoneIO.load(); List locs = LocationIO.load(); List waters = WaterBodyIO.load(); List models = PlacedModelIO.load(); + bakeVoxelHeights(mapData); + + Platform.runLater(() -> statusLbl.setText("Berechne Modell…")); + WorldMapRenderModel model = buildRenderModel(mapData, areas, zones, locs, waters, models); + log.debug("[WorldMap] Modell fertig – {} Areas, {} Orte, {} Wasser, {} Modelle ({} ms)", + model.areas().size(), model.locations().size(), + model.waters().size(), model.models().size(), + System.currentTimeMillis() - t0); Platform.runLater(() -> statusLbl.setText("Rendere Karte…")); - - int[] slotColors = computeSlotColors(mapData); - RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors); - BufferedImage bi = WorldMapRenderer.render(input, RENDER_SIZE, buildBackgroundOptions()); - boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE); - float[][] segs = buildSeaCoastSegs(mask, SEA_MASK_SIZE); + long t1 = System.currentTimeMillis(); + BufferedImage bi = WorldMapRenderer.render(model, RENDER_SIZE, buildBackgroundOptions()); + log.debug("[WorldMap] Hintergrund-PNG gerendert ({}×{} px, {} ms)", + RENDER_SIZE, RENDER_SIZE, System.currentTimeMillis() - t1); Platform.runLater(() -> { - cachedAreas = new ArrayList<>(areas); - cachedLocs = new ArrayList<>(locs); - cachedWaters = new ArrayList<>(waters); - seaMask = mask; - seaCoastSegs = segs; - mapBuffered = bi; - mapFxImage = SwingFXUtils.toFXImage(bi, null); + currentModel = model; + cachedAreas = new ArrayList<>(model.areas()); + cachedLocs = new ArrayList<>(model.locations()); + mapBuffered = bi; + mapFxImage = SwingFXUtils.toFXImage(bi, null); fitToView(); redraw(); progress.setVisible(false); - statusLbl.setText("Bereit – " + areas.size() + " Areas, " + - locs.size() + " Orte, " + waters.size() + " Wasser"); + statusLbl.setText("Bereit – " + model.areas().size() + " Areas, " + + model.locations().size() + " Orte, " + model.waters().size() + " Wasser"); loading.set(false); + log.debug("[WorldMap] Gesamt-Ladezeit: {} ms", System.currentTimeMillis() - t0); }); } catch (Exception ex) { + log.error("[WorldMap] Fehler beim Modell-Build", ex); Platform.runLater(() -> { progress.setVisible(false); statusLbl.setText("Fehler: " + ex.getMessage()); loading.set(false); }); } - }, "WorldMapRenderer"); + }, "WorldMapModelBuilder"); t.setDaemon(true); t.start(); } // ── Interne Methoden ────────────────────────────────────────────────────── - private void rerender() { - if (mapBuffered == null) { loadAndRender(); return; } + /** Rendert das PNG aus dem gecachten Modell neu (kein I/O, kein Modell-Rebuild). */ + private void rerenderFromModel() { + if (currentModel == null) { loadAndRender(); return; } if (loading.getAndSet(true)) return; progress.setVisible(true); statusLbl.setText("Rendere…"); + WorldMapRenderModel model = currentModel; Thread t = new Thread(() -> { - try { - MapData mapData = MapIO.load(); - List areas = AreaIO.load(); - List zones = LocationZoneIO.load(); - List locs = LocationIO.load(); - List waters = WaterBodyIO.load(); - List models = PlacedModelIO.load(); - int[] slotColors = computeSlotColors(mapData); - RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors); - BufferedImage bi = WorldMapRenderer.render(input, RENDER_SIZE, buildBackgroundOptions()); - boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE); - Platform.runLater(() -> { - cachedAreas = new ArrayList<>(areas); - cachedLocs = new ArrayList<>(locs); - cachedWaters = new ArrayList<>(waters); - seaMask = mask; - mapBuffered = bi; - mapFxImage = SwingFXUtils.toFXImage(bi, null); - redraw(); - progress.setVisible(false); - statusLbl.setText("Bereit"); - loading.set(false); - }); - } catch (Exception ex) { - Platform.runLater(() -> { - progress.setVisible(false); - statusLbl.setText("Fehler: " + ex.getMessage()); - loading.set(false); - }); - } + log.debug("[WorldMap] PNG-Rerender aus gecachtem Modell gestartet ({}×{})", RENDER_SIZE, RENDER_SIZE); + long t0 = System.currentTimeMillis(); + BufferedImage bi = WorldMapRenderer.render(model, RENDER_SIZE, buildBackgroundOptions()); + log.debug("[WorldMap] PNG-Rerender fertig ({} ms)", System.currentTimeMillis() - t0); + Platform.runLater(() -> { + mapBuffered = bi; + mapFxImage = SwingFXUtils.toFXImage(bi, null); + redraw(); + progress.setVisible(false); + statusLbl.setText("Bereit"); + loading.set(false); + }); }, "WorldMapRerender"); t.setDaemon(true); t.start(); } private void redraw() { + // Beim ersten Anzeigen der Weltkarte automatisch Modell laden + if (!autoLoaded && currentModel == null && !loading.get()) { + autoLoaded = true; + loadAndRender(); + } + GraphicsContext gc = canvas.getGraphicsContext2D(); double w = canvas.getWidth(), h = canvas.getHeight(); @@ -311,15 +383,17 @@ public class WorldMapView extends VBox { if (mapFxImage == null) { gc.setFill(Color.GRAY); - gc.fillText("Karte noch nicht geladen – 'Aktualisieren' klicken", 20, 40); + gc.fillText("Karte wird geladen…", 20, 40); return; } gc.drawImage(mapFxImage, panX, panY, mapFxImage.getWidth() * scale, mapFxImage.getHeight() * scale); - if (layerWater.isSelected()) drawWaterOverlay(gc); - if (layerAreas.isSelected()) drawAreasOverlay(gc); - if (layerLocations.isSelected()) drawLocationsOverlay(gc); + if (cbWater.isSelected()) drawWaterOverlay(gc); + if (cbAreas.isSelected()) drawAreasOverlay(gc); + if (cbLocations.isSelected()) drawLocationsOverlay(gc); + if (cbModels.isSelected()) drawTreeOverlay(gc); + if (!isFullscreen) drawCameraIndicator(gc); } // ── Canvas-Vektor-Overlays ──────────────────────────────────────────────── @@ -396,8 +470,13 @@ public class WorldMapView extends VBox { } private void drawWaterOverlay(GraphicsContext gc) { + if (currentModel == null) return; + boolean[] seaMask = currentModel.seaMask(); + float[] terrainSamples = currentModel.terrainSamples(); + List seaCoastPaths = currentModel.coastPaths(); + // ── Seewellen (Terrain < 0) ────────────────────────────────────────────── - if (seaMask != null && layerWater.isSelected()) { + if (seaMask != null && cbWater.isSelected()) { // Wellengröße in Weltkoordinaten (skaliert mit Zoom, damit Bildschirmgröße = WAVE_W) double pxPerWorld = (RENDER_SIZE - 1) * scale / WorldMapRenderer.WORLD_SIZE; double wWave = WAVE_W / pxPerWorld; @@ -445,15 +524,18 @@ public class WorldMapView extends VBox { } } - if (cachedWaters.isEmpty()) return; + List waters = currentModel.waters(); + if (waters.isEmpty()) return; final double spX = WAVE_W * 1.8, spY = WAVE_W * 1.05, wmA = WAVE_W * 0.2; - for (PlacedWater w : cachedWaters) { + for (PlacedWater w : waters) { float[] wx = w.pointsX(), wz = w.pointsZ(); int n = wx.length; if (n < 3) continue; + float wh = w.waterHeight(); + double[] cx = new double[n], cz = new double[n]; for (int i = 0; i < n; i++) { cx[i] = toCanvasX(wx[i]); cz[i] = toCanvasZ(wz[i]); } @@ -468,7 +550,7 @@ public class WorldMapView extends VBox { gc.save(); gc.beginPath(); gc.moveTo(cx[0], cz[0]); - for (int i = 1; i < n; i++) gc.lineTo(cx[i], cz[i]); + for (int i = 1; i < n; i++) { gc.lineTo(cx[i], cz[i]); } gc.closePath(); gc.clip(); @@ -481,11 +563,17 @@ public class WorldMapView extends VBox { double rowOff = (row & 1) == 1 ? spX * 0.5 : 0; int col = 0; for (double wxx = minX + rowOff; wxx + WAVE_W <= maxX; wxx += spX, col++) { - // deterministisches Jitter pro Zelle double jx = Math.abs(Math.sin(row * 73.1 + col * 157.3)) * spX * 0.25; double jy = Math.sin(row * 211.7 + col * 89.5) * spY * 0.2; double ox = wxx + jx, oy = wy + jy; - if (ox < minX || ox + WAVE_W > maxX || oy < minZ || oy > maxZ) continue; + if (ox < minX || ox + WAVE_W > maxX || oy < minZ || oy > maxZ) { continue; } + // Höhenprüfung: Terrain muss unter waterHeight liegen + if (terrainSamples != null) { + float owx = toWorldX(ox), owz = toWorldZ(oy); + int mxS = Math.max(0, Math.min(SEA_MASK_SIZE-1, (int)((owx + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE-1)))); + int mzS = Math.max(0, Math.min(SEA_MASK_SIZE-1, (int)((owz + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE-1)))); + if (terrainSamples[mzS * SEA_MASK_SIZE + mxS] >= wh) { continue; } + } gc.beginPath(); gc.moveTo(ox, oy); gc.bezierCurveTo(ox + WAVE_W*0.3, oy - wmA, ox + WAVE_W*0.7, oy + wmA, ox + WAVE_W, oy); @@ -495,44 +583,22 @@ public class WorldMapView extends VBox { gc.restore(); } - // Schwarzer Umriss (konstant 3px) - gc.setStroke(Color.BLACK); - gc.setLineWidth(3.0); - gc.setLineDashes(null); - gc.beginPath(); - gc.moveTo(cx[0], cz[0]); - for (int i = 1; i < n; i++) gc.lineTo(cx[i], cz[i]); - gc.closePath(); - gc.stroke(); } - // ── Seeküstenlinie (konstant 3px, weicher Halo gegen Treppeneffekt) ──────── - if (seaCoastSegs != null && seaCoastSegs.length > 0) { + // ── Seeküstenlinie – verbundene Pfade, runde Joins, weicher Halo ──────────── + if (seaCoastPaths != null && !seaCoastPaths.isEmpty()) { double cW = canvas.getWidth(), cH = canvas.getHeight(); gc.setLineDashes(null); gc.setLineCap(StrokeLineCap.ROUND); + gc.setLineJoin(StrokeLineJoin.ROUND); - // Halo-Pass: breiter, halbtransparent - gc.setStroke(Color.color(0, 0, 0, 0.2)); + gc.setStroke(Color.color(0, 0, 0, 0.22)); gc.setLineWidth(7.0); - for (float[] seg : seaCoastSegs) { - double sx1 = toCanvasX(seg[0]), sy1 = toCanvasZ(seg[1]); - double sx2 = toCanvasX(seg[2]), sy2 = toCanvasZ(seg[3]); - if (Math.max(sx1, sx2) < -8 || Math.min(sx1, sx2) > cW + 8) continue; - if (Math.max(sy1, sy2) < -8 || Math.min(sy1, sy2) > cH + 8) continue; - gc.strokeLine(sx1, sy1, sx2, sy2); - } + for (float[][] path : seaCoastPaths) { drawCoastPath(gc, path, cW, cH, 8); } - // Kern-Pass: 3px solid gc.setStroke(Color.BLACK); gc.setLineWidth(3.0); - for (float[] seg : seaCoastSegs) { - double sx1 = toCanvasX(seg[0]), sy1 = toCanvasZ(seg[1]); - double sx2 = toCanvasX(seg[2]), sy2 = toCanvasZ(seg[3]); - if (Math.max(sx1, sx2) < -3 || Math.min(sx1, sx2) > cW + 3) continue; - if (Math.max(sy1, sy2) < -3 || Math.min(sy1, sy2) > cH + 3) continue; - gc.strokeLine(sx1, sy1, sx2, sy2); - } + for (float[][] path : seaCoastPaths) { drawCoastPath(gc, path, cW, cH, 4); } } } @@ -603,28 +669,336 @@ public class WorldMapView extends VBox { } } - private static float[][] buildSeaCoastSegs(boolean[] mask, int size) { - List segs = new ArrayList<>(); - for (int mz = 0; mz < size - 1; mz++) { - for (int mx = 0; mx < size - 1; mx++) { - boolean c = mask[mz * size + mx]; - boolean r = mask[mz * size + (mx + 1)]; - boolean d = mask[(mz + 1) * size + mx]; - float wx0 = (float)(mx / (double)(size - 1) * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF); - float wx1 = (float)((mx + 1) / (double)(size - 1) * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF); - float wz0 = (float)(mz / (double)(size - 1) * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF); - float wz1 = (float)((mz + 1) / (double)(size - 1) * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF); - float wzM = (wz0 + wz1) * 0.5f; - float wxM = (wx0 + wx1) * 0.5f; - // Horizontale Kante zwischen (mz,mx) und (mz+1,mx) - if (c != d) segs.add(new float[]{wx0, wzM, wx1, wzM}); - // Vertikale Kante zwischen (mz,mx) und (mz,mx+1) - if (c != r) segs.add(new float[]{wxM, wz0, wxM, wz1}); + // Zoom-Schwelle: darüber Einzelbaum-Symbole, darunter Wald-Cluster-Symbol + private static final double TREE_INDIVIDUAL_SCALE = 3.0; + // Farbe der Baum-/Waldsymbole + private static final Color TREE_COLOR = Color.rgb(25, 90, 25); + + private void drawTreeOverlay(GraphicsContext gc) { + if (currentModel == null) { return; } + List> treeClusters = currentModel.treeClusters(); + if (treeClusters == null || treeClusters.isEmpty()) { return; } + double cW = canvas.getWidth(), cH = canvas.getHeight(); + + gc.save(); + gc.setFill(TREE_COLOR); + gc.setTextAlign(TextAlignment.CENTER); + gc.setTextBaseline(VPos.CENTER); + + if (scale >= TREE_INDIVIDUAL_SCALE) { + // Einzelne Bäume – ein Symbol pro Baum + gc.setFont(Font.font("SansSerif", 11)); + for (List cluster : treeClusters) { + for (PlacedModel t : cluster) { + double cx = toCanvasX(t.x()), cz = toCanvasZ(t.z()); + if (cx < -15 || cx > cW + 15 || cz < -15 || cz > cH + 15) { continue; } + gc.fillText("♣", cx, cz); + } + } + } else { + // Wald-Modus – ein Symbol pro Cluster (am Schwerpunkt) + gc.setFont(Font.font("SansSerif", FontWeight.BOLD, 14)); + for (List cluster : treeClusters) { + double sumX = 0, sumZ = 0; + for (PlacedModel t : cluster) { sumX += t.x(); sumZ += t.z(); } + double cx = toCanvasX((float)(sumX / cluster.size())); + double cz = toCanvasZ((float)(sumZ / cluster.size())); + if (cx < -20 || cx > cW + 20 || cz < -20 || cz > cH + 20) { continue; } + gc.fillText("♣", cx, cz); } } + + gc.restore(); + } + + private static List> clusterTreeModels(List trees, float radius) { + int n = trees.size(); + int[] parent = new int[n]; + for (int i = 0; i < n; i++) { parent[i] = i; } + float r2 = radius * radius; + for (int i = 0; i < n; i++) { + for (int j = i + 1; j < n; j++) { + float dx = trees.get(i).x() - trees.get(j).x(); + float dz = trees.get(i).z() - trees.get(j).z(); + if (dx*dx + dz*dz <= r2) { + int pi = ufFind(parent, i), pj = ufFind(parent, j); + if (pi != pj) { parent[pi] = pj; } + } + } + } + Map> groups = new HashMap<>(); + for (int i = 0; i < n; i++) { + groups.computeIfAbsent(ufFind(parent, i), k -> new ArrayList<>()).add(trees.get(i)); + } + return new ArrayList<>(groups.values()); + } + + private static int ufFind(int[] parent, int i) { + while (parent[i] != i) { parent[i] = parent[parent[i]]; i = parent[i]; } + return i; + } + + /** + * Liest alle VoxelChunks (aus dem Live-Supplier oder von Disk) und schreibt die + * höchsten soliden Voxel-Y-Werte in mapData.upperTop. + */ + private void bakeVoxelHeights(MapData mapData) { + List chunks = voxelChunkSupplier != null + ? voxelChunkSupplier.get() + : VoxelChunkIO.loadAll(); + if (chunks.isEmpty()) { return; } + int UV = MapData.UPPER_VERTS; + float WH = WorldMapRenderer.WORLD_HALF; + float WS = WorldMapRenderer.WORLD_SIZE; + for (VoxelChunk chunk : chunks) { + if (chunk.isEmpty()) { continue; } + for (int lz = 0; lz < VoxelChunk.SIZE; lz++) { + float worldZ = VoxelChunk.toWorldZ(chunk.cz, lz); + int uz = Math.round((worldZ + WH) / WS * (UV - 1)); + if (uz < 0 || uz >= UV) { continue; } + for (int lx = 0; lx < VoxelChunk.SIZE; lx++) { + float worldX = VoxelChunk.toWorldX(chunk.cx, lx); + int ux = Math.round((worldX + WH) / WS * (UV - 1)); + if (ux < 0 || ux >= UV) { continue; } + // Oberste solide Voxel-Y in dieser Spalte suchen + for (int ly = VoxelChunk.SIZE - 1; ly >= 0; ly--) { + if (chunk.getDensity(lx, ly, lz) > 0) { + float topY = VoxelChunk.toWorldY(chunk.cy, ly); + int idx = uz * UV + ux; + if (topY > mapData.upperTop[idx]) { mapData.upperTop[idx] = topY; } + break; + } + } + } + } + } + } + + private WorldMapRenderModel buildRenderModel( + MapData mapData, + List areas, List zones, + List locs, List waters, + List models) { + int[] slotColors = computeSlotColors(mapData); + boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE); + float[] tSamp = buildTerrainSamples(mapData, SEA_MASK_SIZE); + List paths = buildSeaCoastPaths(buildCoastSegsMS(mapData, waters, SEA_MASK_SIZE)); + List treeList = models.stream() + .filter(WorldMapRenderer::isTree).collect(Collectors.toList()); + List> tClusters = clusterTreeModels(treeList, 40f); + return new WorldMapRenderModel( + mapData, areas, zones, locs, waters, models, slotColors, + mask, tSamp, paths, tClusters); + } + + private static float[] buildTerrainSamples(MapData mapData, int size) { + int TV = MapData.TERRAIN_VERTS; + int UV = MapData.UPPER_VERTS; + float[] s = new float[size * size]; + for (int mz = 0; mz < size; mz++) { + for (int mx = 0; mx < size; mx++) { + int hx = Math.min((int)(mx / (double)(size-1) * (TV-1)), TV-1); + int hz = Math.min((int)(mz / (double)(size-1) * (TV-1)), TV-1); + int ux = Math.min((int)(mx / (double)(size-1) * (UV-1)), UV-1); + int uz = Math.min((int)(mz / (double)(size-1) * (UV-1)), UV-1); + float h = mapData.terrainHeight[hz * TV + hx]; + float upper = mapData.upperTop[uz * UV + ux]; + if (upper > 0f && upper > h) { h = upper; } + s[mz * size + mx] = h; + } + } + return s; + } + + // Erzeugt interpolierte Marching-Squares-Segmente für Meer + Wasserflächen. + // Endpunkte liegen am echten Höhen-Nulldurchgang → keine Treppenstufen auf Diagonalen. + private static float[][] buildCoastSegsMS(MapData mapData, List waters, int size) { + int TV = MapData.TERRAIN_VERTS; + int UV = MapData.UPPER_VERTS; + float[] h = new float[size * size]; + for (int mz = 0; mz < size; mz++) { + for (int mx = 0; mx < size; mx++) { + int hx = Math.min((int)(mx / (double)(size-1) * (TV-1)), TV-1); + int hz = Math.min((int)(mz / (double)(size-1) * (TV-1)), TV-1); + int ux = Math.min((int)(mx / (double)(size-1) * (UV-1)), UV-1); + int uz = Math.min((int)(mz / (double)(size-1) * (UV-1)), UV-1); + float base = mapData.terrainHeight[hz * TV + hx]; + float upper = mapData.upperTop[uz * UV + ux]; + h[mz * size + mx] = (upper > 0f && upper > base) ? upper : base; + } + } + List segs = new ArrayList<>(); + marchingSquares(h, size, 0f, null, null, segs); + for (PlacedWater w : waters) { + marchingSquares(h, size, w.waterHeight(), w.pointsX(), w.pointsZ(), segs); + } return segs.toArray(new float[0][]); } + private static void marchingSquares(float[] h, int size, float thr, + float[] polyX, float[] polyZ, List out) { + for (int mz = 0; mz < size - 1; mz++) { + for (int mx = 0; mx < size - 1; mx++) { + float hTL = h[mz*size + mx], hTR = h[mz*size + (mx+1)]; + float hBL = h[(mz+1)*size + mx], hBR = h[(mz+1)*size + (mx+1)]; + float wx0 = msW(mx, size), wx1 = msW(mx+1, size); + float wz0 = msW(mz, size), wz1 = msW(mz+1, size); + + boolean iTL, iTR, iBL, iBR; + if (polyX != null) { + iTL = hTL < thr && polyContains(polyX, polyZ, wx0, wz0); + iTR = hTR < thr && polyContains(polyX, polyZ, wx1, wz0); + iBL = hBL < thr && polyContains(polyX, polyZ, wx0, wz1); + iBR = hBR < thr && polyContains(polyX, polyZ, wx1, wz1); + } else { + iTL = hTL < thr; iTR = hTR < thr; + iBL = hBL < thr; iBR = hBR < thr; + } + + int idx = (iTL?8:0)|(iTR?4:0)|(iBR?2:0)|(iBL?1:0); + if (idx == 0 || idx == 15) { continue; } + + // Interpolierte Schnittpunkte auf den vier Zellkanten + float xTop = msLerp(wx0, wx1, thr, hTL, hTR); // obere Kante + float xBot = msLerp(wx0, wx1, thr, hBL, hBR); // untere Kante + float zLft = msLerp(wz0, wz1, thr, hTL, hBL); // linke Kante + float zRgt = msLerp(wz0, wz1, thr, hTR, hBR); // rechte Kante + + switch (idx) { + case 1: case 14: out.add(new float[]{wx0,zLft, xBot,wz1}); break; + case 2: case 13: out.add(new float[]{xBot,wz1, wx1,zRgt}); break; + case 4: case 11: out.add(new float[]{xTop,wz0, wx1,zRgt}); break; + case 8: case 7: out.add(new float[]{wx0,zLft, xTop,wz0}); break; + case 3: case 12: out.add(new float[]{wx0,zLft, wx1,zRgt}); break; + case 6: case 9: out.add(new float[]{xTop,wz0, xBot,wz1}); break; + case 5: { + float hC = (hTL+hTR+hBL+hBR)*0.25f; + if (hC >= thr) { out.add(new float[]{wx0,zLft,xTop,wz0}); out.add(new float[]{xBot,wz1,wx1,zRgt}); } + else { out.add(new float[]{wx0,zLft,xBot,wz1}); out.add(new float[]{xTop,wz0,wx1,zRgt}); } + break; + } + case 10: { + float hC = (hTL+hTR+hBL+hBR)*0.25f; + if (hC >= thr) { out.add(new float[]{xTop,wz0,wx1,zRgt}); out.add(new float[]{wx0,zLft,xBot,wz1}); } + else { out.add(new float[]{wx0,zLft,xTop,wz0}); out.add(new float[]{xBot,wz1,wx1,zRgt}); } + break; + } + } + } + } + } + + private static float msLerp(float from, float to, float thr, float hA, float hB) { + float d = hB - hA; + if (Math.abs(d) < 1e-6f) { return (from + to) * 0.5f; } + return from + Math.max(0f, Math.min(1f, (thr - hA) / d)) * (to - from); + } + + private static float msW(int idx, int size) { + return (float)(idx / (double)(size-1) * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF); + } + + // Verbindet Rohsegmente zu geschlossenen Küstenpfaden. + private static List buildSeaCoastPaths(float[][] segs) { + if (segs == null || segs.length == 0) { return Collections.emptyList(); } + + Map> adj = new HashMap<>(segs.length * 4); + for (int i = 0; i < segs.length; i++) { + adj.computeIfAbsent(coastPtKey(segs[i][0], segs[i][1]), k -> new ArrayList<>()).add(i); + adj.computeIfAbsent(coastPtKey(segs[i][2], segs[i][3]), k -> new ArrayList<>()).add(i); + } + + boolean[] used = new boolean[segs.length]; + List result = new ArrayList<>(); + + for (int start = 0; start < segs.length; start++) { + if (used[start]) { continue; } + used[start] = true; + + List fwd = new ArrayList<>(); + List bwd = new ArrayList<>(); + fwd.add(new float[]{segs[start][2], segs[start][3]}); + bwd.add(new float[]{segs[start][0], segs[start][1]}); + coastExtend(fwd, segs, adj, used); + coastExtend(bwd, segs, adj, used); + + float[][] raw = new float[bwd.size() + fwd.size()][]; + for (int i = 0; i < bwd.size(); i++) { raw[i] = bwd.get(bwd.size() - 1 - i); } + for (int i = 0; i < fwd.size(); i++) { raw[bwd.size() + i] = fwd.get(i); } + result.add(chaikin(simplifyCoastPath(raw), 3)); + } + return result; + } + + // Entfernt kollineare Zwischenpunkte (lange H/V-Läufe → ein Segment). + private static float[][] simplifyCoastPath(float[][] path) { + int n = path.length; + // Letzten Punkt entfernen wenn er gleich dem ersten ist (geschlossene Schleife) + int len = (n > 1 && Math.abs(path[0][0]-path[n-1][0]) < 0.01f + && Math.abs(path[0][1]-path[n-1][1]) < 0.01f) ? n - 1 : n; + if (len <= 2) { return path; } + List out = new ArrayList<>(len); + for (int i = 0; i < len; i++) { + float[] a = path[(i + len - 1) % len], b = path[i], c = path[(i + 1) % len]; + float cross = (b[0]-a[0])*(c[1]-a[1]) - (b[1]-a[1])*(c[0]-a[0]); + if (Math.abs(cross) > 1e-3f) { out.add(b); } + } + return out.isEmpty() ? new float[][]{path[0]} : out.toArray(new float[0][]); + } + + // Chaikin-Eckenschnitt: glättet Treppenstufen zu Kurven. + private static float[][] chaikin(float[][] path, int iterations) { + float[][] cur = path; + for (int iter = 0; iter < iterations; iter++) { + int n = cur.length; + float[][] next = new float[n * 2][]; + for (int i = 0; i < n; i++) { + float[] p0 = cur[i], p1 = cur[(i + 1) % n]; + next[i*2] = new float[]{ p0[0]*0.75f + p1[0]*0.25f, p0[1]*0.75f + p1[1]*0.25f }; + next[i*2+1] = new float[]{ p0[0]*0.25f + p1[0]*0.75f, p0[1]*0.25f + p1[1]*0.75f }; + } + cur = next; + } + return cur; + } + + private static void coastExtend(List pts, float[][] segs, Map> adj, boolean[] used) { + for (;;) { + float[] p = pts.get(pts.size() - 1); + List nb = adj.get(coastPtKey(p[0], p[1])); + int next = -1; + if (nb != null) { for (int idx : nb) { if (!used[idx]) { next = idx; break; } } } + if (next < 0) { break; } + used[next] = true; + float[] seg = segs[next]; + boolean atA = Math.abs(seg[0] - p[0]) < 0.1f && Math.abs(seg[1] - p[1]) < 0.1f; + pts.add(atA ? new float[]{seg[2], seg[3]} : new float[]{seg[0], seg[1]}); + } + } + + private static long coastPtKey(float x, float z) { + return ((long)Math.round(x * 4)) << 32 | (Math.round(z * 4) & 0xFFFFFFFFL); + } + + // Zeichnet einen Küstenpfad als einzelnen geschlossenen Canvas-Pfad. + // margin: Viewport-Puffer in Pixeln für Culling. + private void drawCoastPath(GraphicsContext gc, float[][] path, double cW, double cH, double margin) { + if (path.length < 2) { return; } + double bx0 = Double.MAX_VALUE, bx1 = -Double.MAX_VALUE; + double by0 = Double.MAX_VALUE, by1 = -Double.MAX_VALUE; + for (float[] pt : path) { + double px = toCanvasX(pt[0]), py = toCanvasZ(pt[1]); + if (px < bx0) bx0 = px; if (px > bx1) bx1 = px; + if (py < by0) by0 = py; if (py > by1) by1 = py; + } + if (bx1 < -margin || bx0 > cW + margin || by1 < -margin || by0 > cH + margin) { return; } + gc.beginPath(); + gc.moveTo(toCanvasX(path[0][0]), toCanvasZ(path[0][1])); + for (int i = 1; i < path.length; i++) { gc.lineTo(toCanvasX(path[i][0]), toCanvasZ(path[i][1])); } + gc.closePath(); + gc.stroke(); + } + // ── Hilfsmethoden ───────────────────────────────────────────────────────── private void fitToView() { @@ -681,21 +1055,22 @@ public class WorldMapView extends VBox { File file = fc.showSaveDialog(stageSupplier.get()); if (file == null) return; + if (currentModel == null) { + statusLbl.setText("Kein Modell geladen – bitte erst 'Modell aktualisieren'"); + return; + } + statusLbl.setText("Exportiere 4096×4096 PNG…"); progress.setVisible(true); + WorldMapRenderModel model = currentModel; Thread t = new Thread(() -> { try { - MapData mapData = MapIO.load(); - List areas = AreaIO.load(); - List zones = LocationZoneIO.load(); - List locs = LocationIO.load(); - List waters = WaterBodyIO.load(); - List models = PlacedModelIO.load(); - int[] slotColors = computeSlotColors(mapData); - RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors); - BufferedImage bi = WorldMapRenderer.render(input, 4096, buildExportOptions()); + log.debug("[WorldMap] Export-PNG gestartet (4096×4096, Ziel: {})", file.getName()); + long t0 = System.currentTimeMillis(); + BufferedImage bi = WorldMapRenderer.render(model, 4096, buildExportOptions()); ImageIO.write(bi, "PNG", file); + log.debug("[WorldMap] Export-PNG fertig ({} ms)", System.currentTimeMillis() - t0); Platform.runLater(() -> { progress.setVisible(false); statusLbl.setText("Exportiert: " + file.getName()); @@ -714,34 +1089,116 @@ public class WorldMapView extends VBox { /** Hintergrund-PNG: ohne Areas und Locations (die werden live auf Canvas gezeichnet). */ private RenderOptions buildBackgroundOptions() { return new RenderOptions( - layerTerrain.isSelected(), - layerTerrain.isSelected(), - layerWater.isSelected(), + cbTerrain.isSelected(), + cbTerrain.isSelected(), + cbWater.isSelected(), false, // Wellen im Editor über Canvas gezeichnet false, - layerZones.isSelected(), + cbZones.isSelected(), false, - layerModels.isSelected() + cbModels.isSelected() ); } /** Export-PNG: alle Layer gebacken. */ private RenderOptions buildExportOptions() { return new RenderOptions( - layerTerrain.isSelected(), - layerTerrain.isSelected(), - layerWater.isSelected(), + cbTerrain.isSelected(), + cbTerrain.isSelected(), + cbWater.isSelected(), true, // Wellen im Export-PNG gebacken - layerAreas.isSelected(), - layerZones.isSelected(), - layerLocations.isSelected(), - layerModels.isSelected() + cbAreas.isSelected(), + cbZones.isSelected(), + cbLocations.isSelected(), + cbModels.isSelected() ); } - private static ToggleButton layerBtn(String label) { - ToggleButton btn = new ToggleButton(label); - btn.setSelected(true); - return btn; + private static CheckBox layerCheck(String label) { + CheckBox cb = new CheckBox(label); + cb.setSelected(true); + return cb; + } + + // ── Vollbild ────────────────────────────────────────────────────────────── + + private void enterFullscreen() { + isFullscreen = true; + fullscreenBtn.setVisible(false); + backBtn.setVisible(true); + if (enterFullscreenCallback != null) { + enterFullscreenCallback.run(); + } + } + + private void exitFullscreen() { + isFullscreen = false; + backBtn.setVisible(false); + fullscreenBtn.setVisible(true); + if (exitFullscreenCallback != null) { + exitFullscreenCallback.run(); + } + } + + // ── Kamera-Indikator ────────────────────────────────────────────────────── + + private void drawCameraIndicator(GraphicsContext gc) { + if (cameraInfoSupplier == null || mapFxImage == null) return; + CameraInfo ci = cameraInfoSupplier.get(); + double cx = toCanvasX(ci.x()); + double cz = toCanvasZ(ci.z()); + // Yaw 0° = Blick in -Z. Canvas-Winkel: angle = -(yaw + 90°) in Rad + double angle = -(Math.toRadians(ci.yawDeg()) + Math.PI / 2.0); + double coneLen = 55.0; + double halfFov = Math.toRadians(22.5); // 45° FOV / 2 + + double lx = cx + coneLen * Math.cos(angle - halfFov); + double lz = cz + coneLen * Math.sin(angle - halfFov); + double rx = cx + coneLen * Math.cos(angle + halfFov); + double rz = cz + coneLen * Math.sin(angle + halfFov); + + gc.save(); + gc.setLineDashes(null); + // Kegel-Fläche + gc.setFill(Color.rgb(255, 220, 0, 0.25)); + gc.fillPolygon(new double[]{cx, lx, rx}, new double[]{cz, lz, rz}, 3); + // Kegel-Rand + gc.setStroke(Color.rgb(255, 220, 0, 0.85)); + gc.setLineWidth(1.5); + gc.strokePolygon(new double[]{cx, lx, rx}, new double[]{cz, lz, rz}, 3); + // Positions-Punkt + gc.setFill(Color.rgb(255, 220, 0)); + gc.fillOval(cx - 4.5, cz - 4.5, 9, 9); + gc.setStroke(Color.BLACK); + gc.setLineWidth(1.0); + gc.strokeOval(cx - 4.5, cz - 4.5, 9, 9); + gc.restore(); + } + + // ── Kamera-Teleport ─────────────────────────────────────────────────────── + + private void handleMapClick(double canvasX, double canvasY) { + if (teleportCallback == null) return; + float wx = toWorldX(canvasX); + float wz = toWorldZ(canvasY); + float dstH = getTerrainH(wx, wz); + float targetY = dstH + 20f; + if (cameraInfoSupplier != null) { + CameraInfo ci = cameraInfoSupplier.get(); + float srcH = getTerrainH(ci.x(), ci.z()); + targetY = dstH + (ci.y() - srcH); + } + teleportCallback.accept(new float[]{wx, targetY, wz}); + } + + private float getTerrainH(float worldX, float worldZ) { + if (currentModel == null) return 0f; + float[] samples = currentModel.terrainSamples(); + if (samples == null) return 0f; + int mx = Math.max(0, Math.min(SEA_MASK_SIZE - 1, + (int)((worldX + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE - 1)))); + int mz = Math.max(0, Math.min(SEA_MASK_SIZE - 1, + (int)((worldZ + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE - 1)))); + return samples[mz * SEA_MASK_SIZE + mx]; } } diff --git a/blight-editor/src/main/resources/logback.xml b/blight-editor/src/main/resources/logback.xml index 403bccf..a6fe9f4 100644 --- a/blight-editor/src/main/resources/logback.xml +++ b/blight-editor/src/main/resources/logback.xml @@ -17,6 +17,9 @@ + + + diff --git a/blight-game/src/main/java/de/blight/game/state/MinimapState.java b/blight-game/src/main/java/de/blight/game/state/MinimapState.java index 1de0344..19cf440 100644 --- a/blight-game/src/main/java/de/blight/game/state/MinimapState.java +++ b/blight-game/src/main/java/de/blight/game/state/MinimapState.java @@ -533,6 +533,8 @@ public class MinimapState extends BaseAppState { Files.createDirectories(dir); Path png = dir.resolve("minimap_world.png"); + log.debug("[Minimap] Lade Weltdaten für Render-Modell…"); + long t0 = System.currentTimeMillis(); MapData mapData = MapIO.load(); List areas = AreaIO.load(); List zones = LocationZoneIO.load(); @@ -541,6 +543,8 @@ public class MinimapState extends BaseAppState { List models = PlacedModelIO.load(); int[] slotColors = computeSlotColors(mapData, root); renderInput = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors); + log.debug("[Minimap] Render-Modell bereit – {} Areas, {} Orte, {} Wasser ({} ms)", + areas.size(), locs.size(), waters.size(), System.currentTimeMillis() - t0); boolean needsRender = !Files.exists(png); if (!needsRender) { @@ -553,9 +557,11 @@ public class MinimapState extends BaseAppState { } if (needsRender) { log.info("[Minimap] Rendere Weltkarte {}×{}…", TEXTURE_SIZE, TEXTURE_SIZE); + long t1 = System.currentTimeMillis(); BufferedImage bi = WorldMapRenderer.render(renderInput, TEXTURE_SIZE, RenderOptions.all()); ImageIO.write(bi, "PNG", png.toFile()); - log.info("[Minimap] Weltkarte gespeichert: {}", png); + log.debug("[Minimap] Weltkarte gerendert und gespeichert ({} ms): {}", + System.currentTimeMillis() - t1, png); } else { log.info("[Minimap] Gecachte Weltkarte: {}", png); } @@ -645,8 +651,12 @@ public class MinimapState extends BaseAppState { final float capHU = vr / WORLD_SIZE; Thread t = new Thread(() -> { + log.debug("[Minimap] Vektor-Layer neu rendern – Zentrum ({}/{}), Radius {}", + Math.round(wx), Math.round(wz), Math.round(vr)); + long t0 = System.currentTimeMillis(); RenderOptions opts = new RenderOptions(false, false, true, true, true, true, true); BufferedImage bi = WorldMapRenderer.renderRegion(renderInput, VEC_TEX_SIZE, opts, wx, wz, vr); + log.debug("[Minimap] Vektor-Layer fertig ({} ms)", System.currentTimeMillis() - t0); app.enqueue(() -> { updateVectorTexture(bi); vecRenderU = capU; diff --git a/blight-game/src/main/resources/logback.xml b/blight-game/src/main/resources/logback.xml index 290b237..f329bfa 100644 --- a/blight-game/src/main/resources/logback.xml +++ b/blight-game/src/main/resources/logback.xml @@ -19,6 +19,8 @@ + + diff --git a/blight-map/src/main/map/benches/b26f42df-7897-4840-9e7f-5f35329aa0a6.bench b/blight-map/src/main/map/benches/b26f42df-7897-4840-9e7f-5f35329aa0a6.bench new file mode 100644 index 0000000..7694398 --- /dev/null +++ b/blight-map/src/main/map/benches/b26f42df-7897-4840-9e7f-5f35329aa0a6.bench @@ -0,0 +1,9 @@ +{ + "id": "b26f42df-7897-4840-9e7f-5f35329aa0a6", + "benchType": "Simple", + "sitzX": 0.30349, + "sitzY": 2.5, + "sitzZ": -5.00573, + "sitzRotY": 1.5707964, + "sitzSet": true +} \ No newline at end of file diff --git a/blight-map/src/main/map/blight_grass_vertex.blgv b/blight-map/src/main/map/blight_grass_vertex.blgv index a9590d2..360e9ca 100644 Binary files a/blight-map/src/main/map/blight_grass_vertex.blgv and b/blight-map/src/main/map/blight_grass_vertex.blgv differ diff --git a/blight-map/src/main/map/blight_map.blm b/blight-map/src/main/map/blight_map.blm index 4d3caf0..0a08fee 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 900ed33..5976815 100644 --- a/blight-map/src/main/map/blight_objects.blo +++ b/blight-map/src/main/map/blight_objects.blo @@ -11,3 +11,5 @@ Models/trees/palm/palm_20260816_213341.j3o 270.73062 3.06689 -913.67090 -1.45828 Models/imported/bank1.j3o 236.63928 -6.31074 -888.17450 -3.22597 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH 9ac9943d-0e12-4d5c-8323-0e2b92eebdec Models/trees/willow/willow_20260823_101850.j3o 152.40488 11.49070 -888.86847 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 Models/trees/willow/willow_20260823_101856.j3o 170.68971 11.48961 -873.92236 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 +Models/imported/wolf.j3o -1.25617 2.00000 -1.86424 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 +Models/imported/bank1.j3o 0.30349 2.00000 -5.00573 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH b26f42df-7897-4840-9e7f-5f35329aa0a6 diff --git a/blight-map/src/main/map/chunks/chunk_09_01.blc b/blight-map/src/main/map/chunks/chunk_09_01.blc index 92156d0..9d013f9 100644 Binary files a/blight-map/src/main/map/chunks/chunk_09_01.blc and b/blight-map/src/main/map/chunks/chunk_09_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_09_02.blc b/blight-map/src/main/map/chunks/chunk_09_02.blc index 6208e51..b1da577 100644 Binary files a/blight-map/src/main/map/chunks/chunk_09_02.blc and b/blight-map/src/main/map/chunks/chunk_09_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_10_00.blc b/blight-map/src/main/map/chunks/chunk_10_00.blc index e66be84..1dc115c 100644 Binary files a/blight-map/src/main/map/chunks/chunk_10_00.blc and b/blight-map/src/main/map/chunks/chunk_10_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_10_01.blc b/blight-map/src/main/map/chunks/chunk_10_01.blc index 6585991..d87e9d1 100644 Binary files a/blight-map/src/main/map/chunks/chunk_10_01.blc and b/blight-map/src/main/map/chunks/chunk_10_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_10_02.blc b/blight-map/src/main/map/chunks/chunk_10_02.blc index ece8562..219c665 100644 Binary files a/blight-map/src/main/map/chunks/chunk_10_02.blc and b/blight-map/src/main/map/chunks/chunk_10_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_11_00.blc b/blight-map/src/main/map/chunks/chunk_11_00.blc index d95b83e..92c5676 100644 Binary files a/blight-map/src/main/map/chunks/chunk_11_00.blc and b/blight-map/src/main/map/chunks/chunk_11_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_11_01.blc b/blight-map/src/main/map/chunks/chunk_11_01.blc index c93d5a9..4a30761 100644 Binary files a/blight-map/src/main/map/chunks/chunk_11_01.blc and b/blight-map/src/main/map/chunks/chunk_11_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_11_02.blc b/blight-map/src/main/map/chunks/chunk_11_02.blc index ab26827..e3f9a10 100644 Binary files a/blight-map/src/main/map/chunks/chunk_11_02.blc and b/blight-map/src/main/map/chunks/chunk_11_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_12_00.blc b/blight-map/src/main/map/chunks/chunk_12_00.blc index 35d8c7d..6cea942 100644 Binary files a/blight-map/src/main/map/chunks/chunk_12_00.blc and b/blight-map/src/main/map/chunks/chunk_12_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_12_01.blc b/blight-map/src/main/map/chunks/chunk_12_01.blc index bd72549..3305ca3 100644 Binary files a/blight-map/src/main/map/chunks/chunk_12_01.blc and b/blight-map/src/main/map/chunks/chunk_12_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_12_02.blc b/blight-map/src/main/map/chunks/chunk_12_02.blc index 11f1c2e..69c2364 100644 Binary files a/blight-map/src/main/map/chunks/chunk_12_02.blc and b/blight-map/src/main/map/chunks/chunk_12_02.blc differ