Ingame Karte weitergebaut

This commit is contained in:
2026-09-07 22:22:27 +02:00
parent 9fa1643264
commit 52801b5ddf
9 changed files with 1244 additions and 342 deletions

View File

@@ -114,12 +114,12 @@ public class CompassHudState extends BaseAppState {
roseH = roseTex.getImage().getHeight();
} catch (Exception ignored) {}
// Kompass zentriert über der Minimap (links unten)
float cx = MinimapState.MARGIN + MinimapState.MINIMAP_SIZE / 2f;
float cy = MinimapState.MARGIN + MinimapState.MINIMAP_SIZE + GAP + roseH / 2f;
// Kompass-Mittelpunkt: 220px vom linken und unteren Bildschirmrand
float cx = 240f;
float cy = 240f;
compassNode = new Node("compass");
compassNode.setLocalTranslation(cx, cy, 0f);
compassNode.setLocalTranslation(cx, cy, 60f); // über Minimap (max z=51)
roseNode = new Node("rose");
compassNode.attachChild(roseNode);

View File

@@ -54,7 +54,6 @@ import de.blight.game.scene.WorldScene;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.nio.FloatBuffer;
import java.nio.file.Files;
@@ -73,17 +72,33 @@ public class MinimapState extends BaseAppState {
private static final float WORLD_HALF = WorldMapRenderer.WORLD_HALF;
private static final float WORLD_SIZE = WorldMapRenderer.WORLD_SIZE;
public static final float MINIMAP_SIZE = 200f;
public static final float MINIMAP_SIZE = 170f; // Karte: 170px Durchmesser im Rahmen
public static final float MINIMAP_RADIUS = MINIMAP_SIZE / 2f; // = 85px
public static final float MARGIN = 16f;
public static final float BORDER_DISPLAY = 256f; // Rahmen in natürlicher Größe
// Versatz vom Rahmen-Ursprung zur linken/unteren Kante des Kreises
private static final float MINIMAP_OFFSET = (BORDER_DISPLAY - MINIMAP_SIZE) / 2f; // = 43px
private static final float VIEW_RADIUS_DEF = 100f; // Minimap: 100 m Sichtradius (fest)
private static final float FM_VIEW_DEF = 200f; // Vollbild: Öffnungs-Radius 200 m
private static final float FM_VIEW_MIN = 50f; // Vollbild: engster Ausschnitt
private static final float FM_VIEW_MAX = WORLD_HALF; // Vollbild: ganze Welt
// 5 diskrete Zoom-Stufen (identisch zum Editor: 1×, 2×, 4×, 8×, 16× fit)
private static final float[] FM_ZOOM_RADII = { WORLD_HALF, 512f, 200f, 100f, 50f };
private static final int FM_ZOOM_DEFAULT = 2; // Index 2 = 200 m
private static final int TEXTURE_SIZE = 2048;
private static final int VEC_TEX_SIZE = 1024; // Vektor-Overlay-Textur (neu gerastert bei Zoom/Pan)
private static final float DOT_SIZE = 8f;
private static final float MARK_DIST_SQ = 4f * 4f;
private static final float AUTOSAVE_SEC = 60f;
private static final int MINIMAP_SEGS = 64;
// Vorberechnete cos/sin für die Kreissegmente (einmal beim Laden)
private static final float[] SEG_COS = new float[MINIMAP_SEGS];
private static final float[] SEG_SIN = new float[MINIMAP_SEGS];
static {
for (int i = 0; i < MINIMAP_SEGS; i++) {
double a = 2 * Math.PI * i / MINIMAP_SEGS;
SEG_COS[i] = (float) Math.cos(a);
SEG_SIN[i] = (float) Math.sin(a);
}
}
private static final String ACT_MAP_TOGGLE = "_MinimapToggle_";
private static final String ACT_MAP_ESC = "_MinimapEsc_";
@@ -97,13 +112,22 @@ public class MinimapState extends BaseAppState {
private Camera camera;
private boolean textureReady = false;
// Frisch gerenderte Karten-Images (vom Render-Thread, ausgewertet in onTextureReady im GL-Thread)
private volatile BufferedImage pendingWorldBi;
private volatile BufferedImage pendingCaveBi;
// Direkt aus BufferedImage erzeugte JME3-Texturen (kein AssetManager-Cache, immer aktuell)
private Texture2D worldMapTex;
private Texture2D caveMapTex;
// Vorberechnete Overlay-Daten (Pfade, Dichte-Maps) – einmalig beim Laden gecacht
private volatile de.blight.common.map.WorldMapRenderer.MapOverlayData overlayData;
private volatile de.blight.common.map.WorldMapRenderModel renderModel;
// ── Erkundungs-Nebel ──────────────────────────────────────────────────────
private ExploreTracker exploreTracker;
private Material fogMat; // Minimap-Nebel
private Material fogFullMat; // Vollbild-Nebel (eigene Overlay-Bounds)
private float fogTime = 0f;
private float posUpdateTimer = 0f;
// ── Blur-Filter (aktiv wenn Vollbild-Karte offen) ─────────────────────────
@@ -119,10 +143,10 @@ public class MinimapState extends BaseAppState {
private Geometry vectorGeo;
private volatile boolean vecRenderPending = false;
private volatile boolean vecLayerDirty = false;
// UV-Position des letzten abgeschlossenen Vektor-Renders (für Live-Verschiebung ohne Ghost)
private float vecRenderU = 0.5f;
private float vecRenderV = 0.5f;
private float vecRenderHU = 0.5f;
// UV-Mittelpunkt und Halbbreite des zuletzt abgeschlossenen Renders (für Live-Pan-Tracking)
private float vecRenderCenterU = 0.5f;
private float vecRenderCenterV = 0.5f;
private float vecRenderHU = 0f;
private float lastMarkX = Float.NaN;
private float lastMarkZ = Float.NaN;
private float autoSaveTimer = 0f;
@@ -154,13 +178,14 @@ public class MinimapState extends BaseAppState {
private Geometry caveGeoMini = null; // Höhlenkarte über Minimap
private Geometry caveGeoFull = null; // Höhlenkarte über Vollkarte
private float caveBlend = 0f; // 0 = Außen, 1 = Höhle
/** upperHole-Maske der geladenen Karte für Laufzeit-Erkennung; null = nicht verfügbar. */
private byte[] upperHoleMask = null;
/** Cave-Fußabdruck-Maske für Laufzeit-Erkennung (via WorldMapRenderer.isCavePoint). */
private boolean[] upperHoleMask = null;
private float fmMapSize;
private float fmMapOriginX;
private float fmMapOriginY;
private float fmViewRadius = FM_VIEW_DEF;
private int fmZoomLevelIdx = FM_ZOOM_DEFAULT;
private float fmViewRadius = FM_ZOOM_RADII[FM_ZOOM_DEFAULT];
private float fmCenterU = 0.5f;
private float fmCenterV = 0.5f;
private boolean fmDragging = false;
@@ -231,12 +256,8 @@ public class MinimapState extends BaseAppState {
if (fogMat != null) { fogMat.setFloat("Time", fogTime); }
if (fogFullMat != null) { fogFullMat.setFloat("Time", fogTime); }
posUpdateTimer += tpf;
boolean doPosUpdate = posUpdateTimer >= 1f;
if (doPosUpdate) posUpdateTimer = 0f;
if (!fullMapOpen) {
if (doPosUpdate) updateMinimapUV(px, pz);
updateMinimapUV(px, pz);
} else {
if (fmDragging) {
Vector2f cursor = app.getInputManager().getCursorPosition();
@@ -248,11 +269,11 @@ public class MinimapState extends BaseAppState {
float uvPerPixel = (2f * fmViewRadius / WORLD_SIZE) / fmMapSize;
fmCenterU -= dx * uvPerPixel;
fmCenterV -= dy * uvPerPixel;
// Nur UV des Hintergrunds verschieben – kein Re-Render während des Ziehens
updateFullMapUV();
scheduleVectorRedraw();
}
}
if (doPosUpdate) updateFullMapPlayerDot(px, pz);
updateFullMapPlayerDot(px, pz);
}
}
@@ -280,17 +301,17 @@ public class MinimapState extends BaseAppState {
private void tickCaveBlend(float px, float py, float pz, float tpf) {
if (upperHoleMask == null || caveGeoMini == null) return;
int C = MapData.UPPER_CELLS;
int cx = Math.max(0, Math.min(C-1, (int)((px + WORLD_HALF) / WORLD_SIZE * C)));
int cz = Math.max(0, Math.min(C-1, (int)((pz + WORLD_HALF) / WORLD_SIZE * C)));
boolean inHole = upperHoleMask[cz * C + cx] != 0;
boolean inHole = WorldMapRenderer.isCavePoint(upperHoleMask, px, pz);
float target = inHole ? 1f : 0f;
if (caveBlend != target) {
float speed = 1.5f; // blend completes in ~0.67s
float speed = 5f; // ~1 Sekunde Übergang (exponentiell, 99% nach 1s bei 60fps)
caveBlend += (target - caveBlend) * Math.min(tpf * speed, 1f);
if (Math.abs(caveBlend - target) < 0.005f) caveBlend = target;
ColorRGBA col = new ColorRGBA(1f, 1f, 1f, caveBlend);
caveGeoMini.getMaterial().setColor("Color", col);
if (caveGeoFull != null) {
caveGeoFull.getMaterial().setColor("Color", col);
}
}
}
@@ -319,14 +340,15 @@ public class MinimapState extends BaseAppState {
sinA = FastMath.sin(yaw);
}
// minimapMesh wird von minimapGeo UND fogMiniGeo geteilt
// Vertex-Reihenfolge: BL(-1,-1), BR(+1,-1), TR(+1,+1), TL(-1,+1)
float[] nx = { -1f, 1f, 1f, -1f };
float[] ny = { -1f, -1f, 1f, 1f };
// Kreis-Mesh: Vertex 0 = Zentrum, Vertices 1..MINIMAP_SEGS = Rand
FloatBuffer uv = (FloatBuffer) minimapMesh.getBuffer(VertexBuffer.Type.TexCoord).getData();
for (int i = 0; i < 4; i++) {
uv.put(i * 2, cu + (nx[i] * cosA + ny[i] * sinA) * hu);
uv.put(i * 2 + 1, cv + (-nx[i] * sinA + ny[i] * cosA) * hu);
uv.put(0, cu);
uv.put(1, cv);
for (int i = 0; i < MINIMAP_SEGS; i++) {
float nx = SEG_COS[i];
float ny = SEG_SIN[i];
uv.put((i + 1) * 2, cu + (nx * cosA + ny * sinA) * hu);
uv.put((i + 1) * 2 + 1, cv + (-nx * sinA + ny * cosA) * hu);
}
minimapMesh.getBuffer(VertexBuffer.Type.TexCoord).setUpdateNeeded();
}
@@ -344,14 +366,11 @@ public class MinimapState extends BaseAppState {
fmMapSize = Math.min(sw, sh) * 0.85f;
fmMapOriginX = (sw - fmMapSize) / 2f;
fmMapOriginY = (sh - fmMapSize) / 2f;
fmViewRadius = FM_VIEW_DEF;
fmViewRadius = FM_ZOOM_RADII[fmZoomLevelIdx];
float px = playerNode.getWorldTranslation().x;
float pz = playerNode.getWorldTranslation().z;
fmCenterU = (px + WORLD_HALF) / WORLD_SIZE;
fmCenterV = 1f - (pz + WORLD_HALF) / WORLD_SIZE;
vecRenderU = fmCenterU;
vecRenderV = fmCenterV;
vecRenderHU = FM_VIEW_DEF / WORLD_SIZE;
// Blur auf dem geteilten FPP – genau wie das ESC-Menü
WorldScene ws = app.getStateManager().getState(WorldScene.class);
@@ -399,6 +418,19 @@ public class MinimapState extends BaseAppState {
vectorGeo.setQueueBucket(RenderQueue.Bucket.Gui);
fullMapNode.attachChild(vectorGeo);
// Höhlenkarte-Overlay (teilt UV mit fullMapMesh, da gleicher Weltausschnitt)
if (caveMapTex != null) {
caveGeoFull = new Geometry("wm_cave", fullMapMesh);
Material caveFMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
caveFMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
caveFMat.setTexture("ColorMap", caveMapTex);
caveFMat.setColor("Color", new ColorRGBA(1f, 1f, 1f, caveBlend));
caveGeoFull.setMaterial(caveFMat);
caveGeoFull.setQueueBucket(RenderQueue.Bucket.Gui);
caveGeoFull.setLocalTranslation(0f, 0f, 0.4f); // über vectorGeo (z=20.3) und fullMapGeo (z=20)
fullMapNode.attachChild(caveGeoFull);
}
app.getGuiNode().attachChild(fullMapNode);
app.getInputManager().setCursorVisible(true);
@@ -466,23 +498,7 @@ public class MinimapState extends BaseAppState {
uv.put(4, fmCenterU + hu); uv.put(5, fmCenterV + hu);
uv.put(6, fmCenterU - hu); uv.put(7, fmCenterV + hu);
fullMapMesh.getBuffer(VertexBuffer.Type.TexCoord).setUpdateNeeded();
updateVectorMeshUV();
}
private void updateVectorMeshUV() {
if (vectorMesh == null || vecRenderHU <= 0f) { return; }
float hu = fmViewRadius / WORLD_SIZE;
float rhu = vecRenderHU;
float uMin = (fmCenterU - hu - (vecRenderU - rhu)) / (2f * rhu);
float uMax = (fmCenterU + hu - (vecRenderU - rhu)) / (2f * rhu);
float vMin = (fmCenterV - hu - (vecRenderV - rhu)) / (2f * rhu);
float vMax = (fmCenterV + hu - (vecRenderV - rhu)) / (2f * rhu);
FloatBuffer uv = (FloatBuffer) vectorMesh.getBuffer(VertexBuffer.Type.TexCoord).getData();
uv.put(0, uMin); uv.put(1, vMin);
uv.put(2, uMax); uv.put(3, vMin);
uv.put(4, uMax); uv.put(5, vMax);
uv.put(6, uMin); uv.put(7, vMax);
vectorMesh.getBuffer(VertexBuffer.Type.TexCoord).setUpdateNeeded();
updateVectorMeshUV(); // vectorGeo trackt Pan/Zoom live
}
// ── Input ─────────────────────────────────────────────────────────────────
@@ -527,29 +543,12 @@ public class MinimapState extends BaseAppState {
private final AnalogListener analogListener = (name, value, tpf) -> {
if (!isEnabled() || !fullMapOpen) { return; }
float factor = ANA_FM_ZOOM_IN.equals(name) ? 1f / 1.15f : 1.15f;
float oldHu = fmViewRadius / WORLD_SIZE;
fmViewRadius = Math.max(FM_VIEW_MIN, Math.min(FM_VIEW_MAX, fmViewRadius * factor));
float newHu = fmViewRadius / WORLD_SIZE;
// Cursor-Anker: UV unter dem Mauszeiger bleibt an gleicher Bildschirmposition
Vector2f c = app.getInputManager().getCursorPosition();
float rx = (c.x - fmMapOriginX) / fmMapSize;
float ry = (c.y - fmMapOriginY) / fmMapSize;
if (rx >= 0f && rx <= 1f && ry >= 0f && ry <= 1f) {
fmCenterU += (rx - 0.5f) * 2f * (oldHu - newHu);
fmCenterV += (ry - 0.5f) * 2f * (oldHu - newHu);
} else {
// Zoom auf Spielerposition wenn Cursor außerhalb der Karte
float wpx = playerNode.getWorldTranslation().x;
float wpz = playerNode.getWorldTranslation().z;
float pu = (wpx + WORLD_HALF) / WORLD_SIZE;
float pv = 1f - (wpz + WORLD_HALF) / WORLD_SIZE;
float ratio = newHu / oldHu;
fmCenterU = pu + (fmCenterU - pu) * ratio;
fmCenterV = pv + (fmCenterV - pv) * ratio;
}
// Diskrete Zoom-Stufen wie im Editor (Mausrad springt eine Stufe pro Klick)
int newIdx = fmZoomLevelIdx + (ANA_FM_ZOOM_IN.equals(name) ? 1 : -1);
newIdx = Math.max(0, Math.min(FM_ZOOM_RADII.length - 1, newIdx));
if (newIdx == fmZoomLevelIdx) { return; }
fmZoomLevelIdx = newIdx;
fmViewRadius = FM_ZOOM_RADII[fmZoomLevelIdx];
updateFullMapUV();
scheduleVectorRedraw();
float px = playerNode.getWorldTranslation().x;
@@ -562,12 +561,9 @@ public class MinimapState extends BaseAppState {
private void startRenderThread() {
Thread t = new Thread(() -> {
try {
Path root = AnimationLibrary.findAssetRoot();
Path dir = root.resolve("Textures").resolve("hud");
Files.createDirectories(dir);
Path png = dir.resolve("minimap_world.png");
Path cavePng = java.nio.file.Paths.get(System.getProperty("user.home"), ".blight", "map_chunks", "minimap_cave.png");
log.debug("[Minimap] Lade Weltdaten für Render-Modell…");
log.debug("[Minimap] Lade Weltdaten…");
long t0 = System.currentTimeMillis();
MapData mapData = MapIO.load();
List<PlacedArea> areas = AreaIO.load();
@@ -575,30 +571,44 @@ public class MinimapState extends BaseAppState {
List<Location> locs = LocationIO.load();
List<PlacedWater> waters = WaterBodyIO.load();
List<PlacedModel> models = PlacedModelIO.load();
log.debug("[Minimap] Weltdaten geladen ({} ms)", System.currentTimeMillis() - t0);
// Render-Modell aufbauen (wird für Weltkarte + Vektor-Layer verwendet)
List<PlacedWaterfall> waterfalls = WaterfallIO.load();
int[] slotColors = computeSlotColors(mapData, root);
int[][] tilePixels = WorldMapRenderer.loadMapTiles(root);
int[] slotColors = computeSlotColors(mapData, AnimationLibrary.findAssetRoot());
int[][] tilePixels = WorldMapRenderer.loadMapTiles(AnimationLibrary.findAssetRoot());
float[] voxelSurface = WorldMapRenderer.bakeVoxelSurface(mapData);
renderInput = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
de.blight.common.map.WorldMapRenderModel renderModel = new de.blight.common.map.WorldMapRenderModel(
de.blight.common.map.WorldMapRenderModel rm = new de.blight.common.map.WorldMapRenderModel(
mapData, areas, zones, locs, waters, models, waterfalls,
slotColors, null, voxelSurface, null, null, null, tilePixels, null);
log.debug("[Minimap] Render-Modell bereit – {} Areas, {} Orte, {} Wasser ({} ms)",
areas.size(), locs.size(), waters.size(), System.currentTimeMillis() - t0);
log.info("[Minimap] Rendere Weltkarte {}×{}…", TEXTURE_SIZE, TEXTURE_SIZE);
renderInput = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
// Overlay-Daten einmalig vorberechnen (Pfade, Dichte-Maps – für alle Zoom-Level)
log.info("[Minimap] Berechne Overlay-Daten…");
long t1 = System.currentTimeMillis();
BufferedImage bi = WorldMapRenderer.render(renderModel, TEXTURE_SIZE, RenderOptions.all());
ImageIO.write(bi, "PNG", png.toFile());
log.debug("[Minimap] Weltkarte gerendert und gespeichert ({} ms): {}",
System.currentTimeMillis() - t1, png);
overlayData = WorldMapRenderer.computeOverlayData(rm);
renderModel = rm;
log.info("[Minimap] Overlay-Daten fertig ({} ms)", System.currentTimeMillis() - t1);
BufferedImage caveBi = WorldMapRenderer.buildCaveMap(mapData, TEXTURE_SIZE);
ImageIO.write(caveBi, "PNG", dir.resolve("minimap_cave.png").toFile());
// Verwende Voxel-basierten Tunnel-Maske für Laufzeit-Erkennung (UPPER_CELLS Auflösung)
boolean[] tunnelMask256 = WorldMapRenderer.buildCaveTunnelMask(mapData, MapData.UPPER_CELLS);
upperHoleMask = new byte[tunnelMask256.length];
for (int i = 0; i < tunnelMask256.length; i++) upperHoleMask[i] = tunnelMask256[i] ? (byte)1 : 0;
// Weltkarte: Echtzeit-Rendering mit allen Layern (identisch zum Editor)
log.info("[Minimap] Rendere Weltkarte mit allen Layern {}×{}…", TEXTURE_SIZE, TEXTURE_SIZE);
long t2 = System.currentTimeMillis();
pendingWorldBi = WorldMapRenderer.renderAllLayers(rm, overlayData, TEXTURE_SIZE);
log.info("[Minimap] Weltkarte fertig ({} ms)", System.currentTimeMillis() - t2);
// Höhlenkarte
if (Files.exists(cavePng)) {
log.info("[Minimap] Lade Höhlenkarte aus Editor-Cache: {}", cavePng);
pendingCaveBi = javax.imageio.ImageIO.read(cavePng.toFile());
} else {
log.info("[Minimap] Kein Editor-Cache – rendere Höhlenkarte {}×{}…", TEXTURE_SIZE, TEXTURE_SIZE);
pendingCaveBi = WorldMapRenderer.buildCaveMap(mapData, TEXTURE_SIZE);
}
// Cave-Fußabdruck-Maske für Laufzeit-Erkennung (gleiche Logik wie beim Pfad-Splitting)
upperHoleMask = WorldMapRenderer.buildCaveFootprintMask(
mapData, WorldMapRenderer.VOXEL_SURFACE_RES);
app.enqueue(() -> { onTextureReady(); return null; });
} catch (Exception e) {
@@ -622,52 +632,60 @@ public class MinimapState extends BaseAppState {
}
private void onTextureReady() {
// Texturen direkt aus dem gerenderten BufferedImage erzeugen — kein AssetManager,
// kein Classpath-Cache, immer der aktuelle Render-Stand.
worldMapTex = biToTexture(pendingWorldBi);
caveMapTex = biToTexture(pendingCaveBi);
pendingWorldBi = null;
pendingCaveBi = null;
exploreTracker = new ExploreTracker();
exploreTracker.load();
float x = MARGIN;
float y = MARGIN;
// Node an der Rahmen-Ecke (MARGIN, MARGIN) – Rahmen füllt 256×256 ab dort
float x = MARGIN;
float y = MARGIN;
fogMat = buildFogMat(x, y, MINIMAP_SIZE, MINIMAP_SIZE);
// Fog-Material: Ursprung und Größe des Kreisbereichs in Bildschirm-Koordinaten
fogMat = buildFogMat(x + MINIMAP_OFFSET, y + MINIMAP_OFFSET, MINIMAP_SIZE, MINIMAP_SIZE);
minimapMesh = buildMinimapMesh(MINIMAP_SIZE, MINIMAP_SIZE);
// Kreisförmiges Mesh: Radius 85px, Mittelpunkt bei (85,85) in lokalem Raum
minimapMesh = buildCircleMesh(MINIMAP_RADIUS, MINIMAP_SEGS);
// Welt-Textur-Geo
// Alle Map-Layer teilen dasselbe Mesh; Kreis wird um MINIMAP_OFFSET verschoben
minimapGeo = new Geometry("minimap_geo", minimapMesh);
minimapGeo.setMaterial(buildTexMat());
minimapGeo.setQueueBucket(RenderQueue.Bucket.Gui);
minimapGeo.setLocalTranslation(MINIMAP_OFFSET, MINIMAP_OFFSET, 0f);
// Fog-Geo teilt dasselbe Mesh – UV-Updates gelten für beide
fogMiniGeo = new Geometry("minimap_fog", minimapMesh);
fogMiniGeo.setMaterial(fogMat);
fogMiniGeo.setQueueBucket(RenderQueue.Bucket.Gui);
fogMiniGeo.setLocalTranslation(0f, 0f, 1f); // eine Schicht vor der Karte
// Rand-Schatten
Geometry border = makeSolidQuad("minimap_border",
MINIMAP_SIZE + 4f, MINIMAP_SIZE + 4f,
new ColorRGBA(0f, 0f, 0f, 0.6f));
border.setLocalTranslation(-2f, -2f, -1f);
// Spieler-Punkt fest in der Mitte
playerDotMini = makeSolidQuad("minimap_dot", DOT_SIZE, DOT_SIZE,
new ColorRGBA(1f, 0.85f, 0f, 1f));
playerDotMini.setLocalTranslation(
MINIMAP_SIZE / 2f - DOT_SIZE / 2f,
MINIMAP_SIZE / 2f - DOT_SIZE / 2f, 2f);
fogMiniGeo.setLocalTranslation(MINIMAP_OFFSET, MINIMAP_OFFSET, 1f);
caveGeoMini = new Geometry("minimap_cave", minimapMesh);
caveGeoMini.setMaterial(buildCaveTexMat());
caveGeoMini.setQueueBucket(RenderQueue.Bucket.Gui);
caveGeoMini.setLocalTranslation(0f, 0f, 0.5f); // between world map and fog
caveGeoMini.setLocalTranslation(MINIMAP_OFFSET, MINIMAP_OFFSET, 0.5f);
// Spieler-Punkt fest in der Mitte des Kreises
playerDotMini = makeSolidQuad("minimap_dot", DOT_SIZE, DOT_SIZE,
new ColorRGBA(1f, 0.85f, 0f, 1f));
playerDotMini.setLocalTranslation(
MINIMAP_OFFSET + MINIMAP_RADIUS - DOT_SIZE / 2f,
MINIMAP_OFFSET + MINIMAP_RADIUS - DOT_SIZE / 2f, 2f);
// PNG-Rahmen: 256×256 px, liegt über allen anderen Layern
Geometry borderGeo = makeBorderGeo(BORDER_DISPLAY);
borderGeo.setLocalTranslation(0f, 0f, 3f);
minimapNode = new Node("minimap");
minimapNode.setLocalTranslation(x, y, 48f);
minimapNode.attachChild(border);
minimapNode.attachChild(minimapGeo);
minimapNode.attachChild(caveGeoMini);
minimapNode.attachChild(fogMiniGeo);
minimapNode.attachChild(playerDotMini);
minimapNode.attachChild(borderGeo);
textureReady = true;
if (isEnabled()) {
@@ -679,12 +697,8 @@ public class MinimapState extends BaseAppState {
private Material buildCaveTexMat() {
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
try {
Texture tex = app.getAssetManager().loadTexture("Textures/hud/minimap_cave.png");
tex.setWrap(Texture.WrapMode.EdgeClamp);
mat.setTexture("ColorMap", tex);
} catch (Exception e) {
log.warn("[Minimap] Höhlenkarten-Textur nicht ladbar");
if (caveMapTex != null) {
mat.setTexture("ColorMap", caveMapTex);
}
mat.setColor("Color", new ColorRGBA(1f, 1f, 1f, 0f)); // zunächst unsichtbar
return mat;
@@ -693,31 +707,31 @@ public class MinimapState extends BaseAppState {
// ── Vektor-Layer ──────────────────────────────────────────────────────────
private void scheduleVectorRedraw() {
if (renderInput == null || !fullMapOpen) { return; }
if (overlayData == null || !fullMapOpen) { return; }
if (vecRenderPending) { vecLayerDirty = true; return; }
vecRenderPending = true;
vecLayerDirty = false;
float wx = fmCenterU * WORLD_SIZE - WORLD_HALF;
float wz = (1f - fmCenterV) * WORLD_SIZE - WORLD_HALF;
// Render-Zielposition jetzt einfrieren; vectorGeo bleibt sichtbar und trackt Pan via UV
float capturedCU = fmCenterU;
float capturedCV = fmCenterV;
float capturedHU = fmViewRadius / WORLD_SIZE;
float wx = capturedCU * WORLD_SIZE - WORLD_HALF;
float wz = (1f - capturedCV) * WORLD_SIZE - WORLD_HALF;
float vr = fmViewRadius;
final float capU = fmCenterU;
final float capV = fmCenterV;
final float capHU = vr / WORLD_SIZE;
Thread t = new Thread(() -> {
log.debug("[Minimap] Vektor-Layer neu rendern – Zentrum ({}/{}), Radius {}",
log.debug("[Minimap] Region 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);
BufferedImage bi = WorldMapRenderer.renderFullRegion(overlayData, VEC_TEX_SIZE, wx, wz, vr);
log.debug("[Minimap] Region fertig ({} ms)", System.currentTimeMillis() - t0);
app.enqueue(() -> {
updateVectorTexture(bi);
vecRenderU = capU;
vecRenderV = capV;
vecRenderHU = capHU;
updateVectorMeshUV();
vecRenderCenterU = capturedCU;
vecRenderCenterV = capturedCV;
vecRenderHU = capturedHU;
updateVectorMeshUV(); // UV auf frischen Render ausrichten
vecRenderPending = false;
if (vecLayerDirty) { scheduleVectorRedraw(); }
return null;
@@ -727,6 +741,23 @@ public class MinimapState extends BaseAppState {
t.start();
}
/** Passt die UVs des vectorMesh an, damit das alte Render bei Pan/Zoom korrekt sitzt. */
private void updateVectorMeshUV() {
if (vectorMesh == null || vecRenderHU <= 0f) { return; }
float hu = fmViewRadius / WORLD_SIZE;
float rhu = vecRenderHU;
float uMin = (fmCenterU - hu - vecRenderCenterU + rhu) / (2f * rhu);
float uMax = (fmCenterU + hu - vecRenderCenterU + rhu) / (2f * rhu);
float vMin = (fmCenterV - hu - vecRenderCenterV + rhu) / (2f * rhu);
float vMax = (fmCenterV + hu - vecRenderCenterV + rhu) / (2f * rhu);
FloatBuffer uv = (FloatBuffer) vectorMesh.getBuffer(VertexBuffer.Type.TexCoord).getData();
uv.put(0, uMin); uv.put(1, vMin);
uv.put(2, uMax); uv.put(3, vMin);
uv.put(4, uMax); uv.put(5, vMax);
uv.put(6, uMin); uv.put(7, vMax);
vectorMesh.getBuffer(VertexBuffer.Type.TexCoord).setUpdateNeeded();
}
private void updateVectorTexture(BufferedImage bi) {
if (vectorBuf == null || vectorImage == null) { return; }
int sz = VEC_TEX_SIZE;
@@ -749,12 +780,9 @@ public class MinimapState extends BaseAppState {
private Material buildTexMat() {
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Off);
try {
Texture tex = app.getAssetManager().loadTexture("Textures/hud/minimap_world.png");
tex.setWrap(Texture.WrapMode.EdgeClamp);
mat.setTexture("ColorMap", tex);
} catch (Exception e) {
log.warn("[Minimap] Weltkarten-Textur nicht ladbar");
if (worldMapTex != null) {
mat.setTexture("ColorMap", worldMapTex);
} else {
mat.setColor("Color", new ColorRGBA(0.05f, 0.1f, 0.2f, 1f));
}
return mat;
@@ -779,12 +807,8 @@ public class MinimapState extends BaseAppState {
private Material buildOverlayTexMat(float originX, float originY, float size) {
Material mat = new Material(app.getAssetManager(), "MatDefs/MapOverlay.j3md");
try {
Texture tex = app.getAssetManager().loadTexture("Textures/hud/minimap_world.png");
tex.setWrap(Texture.WrapMode.EdgeClamp);
mat.setTexture("ColorMap", tex);
} catch (Exception e) {
log.warn("[Minimap] Weltkarten-Textur nicht ladbar");
if (worldMapTex != null) {
mat.setTexture("ColorMap", worldMapTex);
}
mat.setVector2("OverlayOrigin", new Vector2f(originX, originY));
mat.setVector2("OverlaySize", new Vector2f(size, size));
@@ -793,19 +817,85 @@ public class MinimapState extends BaseAppState {
// ── Mesh-Helfer ───────────────────────────────────────────────────────────
private static Mesh buildMinimapMesh(float w, float h) {
/**
* Konvertiert ein BufferedImage direkt in eine JME3-Textur (Y-flip für OpenGL-Konvention).
* Umgeht den AssetManager-Cache vollständig — immer aktueller Render-Stand.
*/
private static Texture2D biToTexture(BufferedImage bi) {
int w = bi.getWidth(), h = bi.getHeight();
int[] pixels = bi.getRGB(0, 0, w, h, null, 0, w); // Bulk-Read aller Pixel auf einmal
java.nio.ByteBuffer buf = BufferUtils.createByteBuffer(w * h * 4);
for (int row = 0; row < h; row++) {
int bufRow = h - 1 - row; // Y-flip: OpenGL V=0 ist unten, Bild-Row 0 ist oben
for (int col = 0; col < w; col++) {
int argb = pixels[row * w + col];
int off = (bufRow * w + col) * 4;
buf.put(off, (byte) ((argb >> 16) & 0xFF));
buf.put(off + 1, (byte) ((argb >> 8) & 0xFF));
buf.put(off + 2, (byte) ( argb & 0xFF));
buf.put(off + 3, (byte) ((argb >> 24) & 0xFF));
}
}
Image jmeImg = new Image(Image.Format.RGBA8, w, h, buf);
Texture2D tex = new Texture2D(jmeImg);
tex.setWrap(Texture.WrapMode.EdgeClamp);
tex.setMagFilter(Texture.MagFilter.Bilinear);
tex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
return tex;
}
/**
* Kreis-Mesh als Triangle-Fan: Vertex 0 = Mittelpunkt, Vertices 1..segs = Rand.
* Mittelpunkt liegt bei (radius, radius) in lokalem Koordinatenraum.
*/
private static Mesh buildCircleMesh(float radius, int segs) {
float cx = radius, cy = radius;
float[] pos = new float[(segs + 1) * 3];
float[] uvs = new float[(segs + 1) * 2];
// Mittelpunkt
pos[0] = cx; pos[1] = cy; pos[2] = 0f;
uvs[0] = 0.5f; uvs[1] = 0.5f;
// Randpunkte
for (int i = 0; i < segs; i++) {
double a = 2 * Math.PI * i / segs;
float ca = (float) Math.cos(a), sa = (float) Math.sin(a);
pos[(i + 1) * 3] = cx + radius * ca;
pos[(i + 1) * 3 + 1] = cy + radius * sa;
pos[(i + 1) * 3 + 2] = 0f;
uvs[(i + 1) * 2] = 0.5f + 0.5f * ca;
uvs[(i + 1) * 2 + 1] = 0.5f + 0.5f * sa;
}
// Triangle-Fan-Indizes
short[] idx = new short[segs * 3];
for (int i = 0; i < segs; i++) {
idx[i * 3] = 0;
idx[i * 3 + 1] = (short) (i + 1);
idx[i * 3 + 2] = (short) (i + 1 < segs ? i + 2 : 1);
}
Mesh m = new Mesh();
m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(
0f, 0f, 0f, w, 0f, 0f, w, h, 0f, 0f, h, 0f));
m.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(
0f, 0f, 1f, 0f, 1f, 1f, 0f, 1f));
m.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createShortBuffer(
(short) 0, (short) 1, (short) 2, (short) 0, (short) 2, (short) 3));
m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(pos));
m.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(uvs));
m.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createShortBuffer(idx));
m.setMode(Mesh.Mode.Triangles);
m.updateBound();
return m;
}
/** Lädt map_border.png und erzeugt ein quadratisches Geo mit Alpha-Blending. */
private Geometry makeBorderGeo(float size) {
Texture2D tex = (Texture2D) app.getAssetManager().loadTexture("Textures/hud/map_border.png");
tex.setWrap(Texture.WrapMode.EdgeClamp);
tex.setMagFilter(Texture.MagFilter.Bilinear);
tex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setTexture("ColorMap", tex);
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
Geometry g = new Geometry("minimap_border_img", new Quad(size, size));
g.setMaterial(mat);
g.setQueueBucket(RenderQueue.Bucket.Gui);
return g;
}
private static Mesh buildQuadMesh(float x, float y, float w, float h, float z) {
Mesh m = new Mesh();
m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(