Minimap und ingame Karte - umsetzung begonnen

This commit is contained in:
2026-08-23 22:13:32 +02:00
parent 46b504e050
commit 9238378bc8
16 changed files with 2119 additions and 8 deletions

View File

@@ -371,6 +371,7 @@ public class WorldScene extends BaseAppState {
app.getStateManager().attach(new de.blight.game.state.HudState(mc));
app.getStateManager().attach(new de.blight.game.state.HotbarState(mc));
app.getStateManager().attach(new de.blight.game.state.CompassHudState());
app.getStateManager().attach(new de.blight.game.state.MinimapState(character));
de.blight.game.state.CharacterState charState = new de.blight.game.state.CharacterState(mc);
charState.setEnabled(false);
app.getStateManager().attach(charState);

View File

@@ -18,6 +18,7 @@ import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Quad;
import com.jme3.texture.Texture;
import de.blight.game.animation.AnimationLibrary;
import de.blight.game.state.MinimapState;
import de.blight.game.config.MenuScreen;
import de.blight.game.config.OverlayState;
import org.slf4j.Logger;
@@ -43,8 +44,8 @@ public class CompassHudState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(CompassHudState.class);
private static final float SIZE = 100f;
private static final float MARGIN = 16f;
private static final float SIZE = 100f;
private static final float GAP = 8f; // Abstand zwischen Minimap und Kompass
private SimpleApplication app;
private Camera cam;
@@ -89,7 +90,7 @@ public class CompassHudState extends BaseAppState {
public void update(float tpf) {
if (compassNode == null) { return; }
boolean menuOpen = OverlayState.isAnyOpen() || MenuScreen.isAnyOpen();
boolean menuOpen = OverlayState.isAnyOpen() || MenuScreen.isAnyOpen() || MinimapState.isFullMapOpen();
if (menuOpen != lastMenuOpen) {
lastMenuOpen = menuOpen;
compassNode.setCullHint(menuOpen ? Spatial.CullHint.Always : Spatial.CullHint.Inherit);
@@ -98,7 +99,7 @@ public class CompassHudState extends BaseAppState {
Vector3f dir = cam.getDirection();
float yaw = FastMath.atan2(dir.x, -dir.z);
roseRot.fromAngleAxis(-yaw, Vector3f.UNIT_Z);
roseRot.fromAngleAxis(yaw, Vector3f.UNIT_Z);
roseNode.setLocalRotation(roseRot);
}
@@ -113,9 +114,9 @@ public class CompassHudState extends BaseAppState {
roseH = roseTex.getImage().getHeight();
} catch (Exception ignored) {}
// compassNode-Mittelpunkt so setzen, dass die Rose mit MARGIN Abstand zur Ecke endet
float cx = MARGIN + roseW / 2f;
float cy = MARGIN + roseH / 2f;
// 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;
compassNode = new Node("compass");
compassNode.setLocalTranslation(cx, cy, 0f);

View File

@@ -0,0 +1,180 @@
package de.blight.game.state;
import com.jme3.texture.Image;
import com.jme3.texture.Texture2D;
import com.jme3.texture.Texture.MagFilter;
import com.jme3.texture.Texture.MinFilter;
import com.jme3.texture.Texture.WrapMode;
import com.jme3.util.BufferUtils;
import de.blight.common.BlightHome;
import de.blight.common.map.WorldMapRenderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
/**
* Verfolgt welche Bereiche der Spielwelt der Spieler bereits erkundet hat.
*
* 512×512 Raster über 2048×2048 m = 4 m/Zelle.
* Erkundungsradius: 50 m = ~13 Zellen.
*
* Persistenz: ~/.blight/saves/explored.bin (flaches byte[]-Dump, 0=unbekannt 1=erkundet).
* Nebelmaske: RGBA8-ByteBuffer für JME3-Texture2D (Alpha=0 erkundet, Alpha=192 unbekannt).
*/
public final class ExploreTracker {
private static final Logger log = LoggerFactory.getLogger(ExploreTracker.class);
static final int GRID = 512;
static final float CELL = WorldMapRenderer.WORLD_SIZE / GRID; // 4 m pro Zelle
static final float EXPLORE_R = 50f; // Erkundungsradius in Metern
static final byte FOG_ALPHA = (byte) 0xFF; // 255 = vollständig opak
private static final Path SAVE_PATH = BlightHome.resolve("saves", "explored.bin");
private final byte[] cells = new byte[GRID * GRID]; // 0=unbekannt, 1=erkundet
private boolean dirty = false;
// JME3-Nebel-Textur
private final ByteBuffer fogBuffer = BufferUtils.createByteBuffer(GRID * GRID * 4);
private final Image fogImage = new Image(Image.Format.RGBA8, GRID, GRID, fogBuffer);
private final Texture2D fogTex;
public ExploreTracker() {
fogTex = new Texture2D(fogImage);
fogTex.setWrap(WrapMode.EdgeClamp);
fogTex.setMagFilter(MagFilter.Bilinear);
fogTex.setMinFilter(MinFilter.BilinearNoMipMaps);
// Gesamte Karte zunächst auf undurchsichtig setzen
fillFogOpaque();
}
// ── Erkundung ─────────────────────────────────────────────────────────────
/**
* Markiert alle Zellen im Radius {@value #EXPLORE_R} m um die Weltposition als erkundet.
* @return true wenn sich mindestens eine Zelle geändert hat
*/
public boolean markCircle(float worldX, float worldZ) {
int cx = worldToCell(worldX);
int cz = worldToCell(worldZ);
int cr = (int) Math.ceil(EXPLORE_R / CELL);
boolean changed = false;
for (int dz = -cr; dz <= cr; dz++) {
for (int dx = -cr; dx <= cr; dx++) {
if (dx * dx + dz * dz > cr * cr) { continue; }
int gx = cx + dx;
int gz = cz + dz;
if (gx < 0 || gx >= GRID || gz < 0 || gz >= GRID) { continue; }
int idx = gz * GRID + gx;
if (cells[idx] == 0) {
cells[idx] = 1;
changed = true;
writeFogCell(gx, gz, true);
}
}
}
if (changed) {
dirty = true;
fogImage.setUpdateNeeded();
}
return changed;
}
// ── Textur ────────────────────────────────────────────────────────────────
public Texture2D getFogTexture() { return fogTex; }
// ── Persistenz ────────────────────────────────────────────────────────────
public void load() {
if (!Files.exists(SAVE_PATH)) { return; }
try {
byte[] data = Files.readAllBytes(SAVE_PATH);
if (data.length == cells.length) {
System.arraycopy(data, 0, cells, 0, cells.length);
rebuildFogBuffer();
log.info("[Explore] Erkundungsdaten geladen: {}", SAVE_PATH);
} else {
log.warn("[Explore] Ungültige Erkundungsdatei ({}B, erwartet {}B) ignoriert",
data.length, cells.length);
}
} catch (IOException e) {
log.warn("[Explore] Laden fehlgeschlagen: {}", e.getMessage());
}
}
public void save() {
if (!dirty) { return; }
try {
Files.createDirectories(SAVE_PATH.getParent());
Path tmp = SAVE_PATH.resolveSibling("explored.tmp");
Files.write(tmp, cells);
Files.move(tmp, SAVE_PATH, StandardCopyOption.REPLACE_EXISTING);
dirty = false;
log.debug("[Explore] Erkundungsdaten gespeichert");
} catch (IOException e) {
log.warn("[Explore] Speichern fehlgeschlagen: {}", e.getMessage());
}
}
/** Wie viele Zellen bereits erkundet wurden (für Diagnostik). */
public int exploredCount() {
int n = 0;
for (byte c : cells) { if (c != 0) n++; }
return n;
}
// ── Interne Helfer ────────────────────────────────────────────────────────
private int worldToCell(float worldCoord) {
return Math.max(0, Math.min(GRID - 1,
(int) ((worldCoord + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * GRID)));
}
/**
* Füllt den fogBuffer vollständig anhand des cells[]-Arrays.
* Kostenintensiver Rebuild nur beim Laden nötig.
*/
private void rebuildFogBuffer() {
for (int gz = 0; gz < GRID; gz++) {
for (int gx = 0; gx < GRID; gx++) {
writeFogCell(gx, gz, cells[gz * GRID + gx] != 0);
}
}
fogImage.setUpdateNeeded();
}
/** Setzt den gesamten Buffer auf undurchsichtig (Startzustand). */
private void fillFogOpaque() {
for (int i = 0; i < GRID * GRID; i++) {
int off = i * 4;
fogBuffer.put(off, (byte) 0);
fogBuffer.put(off + 1, (byte) 0);
fogBuffer.put(off + 2, (byte) 0);
fogBuffer.put(off + 3, FOG_ALPHA);
}
fogImage.setUpdateNeeded();
}
/**
* Schreibt einen einzelnen Fog-Pixel in den Buffer.
* Y-Achse wird gespiegelt damit die Textur mit der Welt-Map-Textur übereinstimmt
* (JME3 AWTLoader spiegelt PNGs; direkter ByteBuffer wird nicht gespiegelt → manuell).
*/
private void writeFogCell(int gx, int gz, boolean explored) {
int bufRow = GRID - 1 - gz; // Spiegeln: gz=0 (Norden) → letzte Buffer-Zeile (V=1)
int off = (bufRow * GRID + gx) * 4;
fogBuffer.put(off, (byte) 0);
fogBuffer.put(off + 1, (byte) 0);
fogBuffer.put(off + 2, (byte) 0);
fogBuffer.put(off + 3, explored ? (byte) 0 : FOG_ALPHA);
}
}

View File

@@ -0,0 +1,28 @@
package de.blight.game.state;
import com.jme3.asset.AssetManager;
import com.jme3.material.Material;
import com.jme3.post.Filter;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
public final class GaussianBlurFilter extends Filter {
private final float strength;
public GaussianBlurFilter(float strength) {
super("GaussianBlur");
this.strength = strength;
}
@Override
protected Material getMaterial() {
return material;
}
@Override
protected void initFilter(AssetManager manager, RenderManager rm, ViewPort vp, int w, int h) {
material = new Material(manager, "MatDefs/GaussianBlur.j3md");
material.setFloat("BlurScale", strength);
}
}

View File

@@ -0,0 +1,766 @@
package de.blight.game.state;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.AnalogListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseAxisTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.texture.Image;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.shape.Quad;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.util.BufferUtils;
import de.blight.common.AreaIO;
import de.blight.common.LocationIO;
import de.blight.common.LocationZoneIO;
import de.blight.common.MapData;
import de.blight.common.MapIO;
import de.blight.common.PlacedArea;
import de.blight.common.PlacedLocationZone;
import de.blight.common.PlacedModel;
import de.blight.common.PlacedModelIO;
import de.blight.common.PlacedWater;
import de.blight.common.WaterBodyIO;
import de.blight.common.map.WorldMapRenderer;
import de.blight.common.map.WorldMapRenderer.RenderInput;
import de.blight.common.map.WorldMapRenderer.RenderOptions;
import de.blight.common.model.Location;
import de.blight.game.animation.AnimationLibrary;
import de.blight.game.config.MenuScreen;
import de.blight.game.config.OverlayState;
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;
import java.nio.file.Path;
import java.util.List;
/**
* Ingame-Minimap (permanent, unten rechts) und Weltkarte-Overlay (M-Taste).
*
* Nebelmaske: Nur bereits erkundete Gebiete (Radius 50 m um den Spieler) sind sichtbar.
* Der Erkundungsfortschritt wird in ~/.blight/saves/explored.bin gespeichert.
*/
public class MinimapState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(MinimapState.class);
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 MARGIN = 16f;
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
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 String ACT_MAP_TOGGLE = "_MinimapToggle_";
private static final String ACT_MAP_ESC = "_MinimapEsc_";
private static final String ACT_FM_DRAG = "_MinimapDrag_";
private static final String ANA_FM_ZOOM_IN = "_FMZoomIn_";
private static final String ANA_FM_ZOOM_OUT = "_FMZoomOut_";
private Node playerNode;
private SimpleApplication app;
private Camera camera;
private boolean textureReady = false;
// ── Erkundungs-Nebel ──────────────────────────────────────────────────────
private ExploreTracker exploreTracker;
private Material fogMat; // Minimap-Nebel
private Material fogFullMat; // Vollbild-Nebel (eigene Overlay-Bounds)
private float fogTime = 0f;
// ── Blur-Filter (aktiv wenn Vollbild-Karte offen) ─────────────────────────
private de.blight.game.post.GaussianBlurFilter blurFilter;
// ── Vektor-Overlay (scharf bei jedem Zoom-Level) ──────────────────────────
private WorldMapRenderer.RenderInput renderInput;
private java.nio.ByteBuffer vectorBuf;
private Image vectorImage;
private Texture2D vectorTex;
private Mesh vectorMesh;
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;
private float lastMarkX = Float.NaN;
private float lastMarkZ = Float.NaN;
private float autoSaveTimer = 0f;
// Dynamischer Sichtradius der Minimap ([ / ] zum Zoomen)
private float viewRadius = VIEW_RADIUS_DEF;
// ── Minimap (immer sichtbar) ──────────────────────────────────────────────
private Node minimapNode;
private Mesh minimapMesh;
private Geometry minimapGeo;
private Geometry fogMiniGeo;
private Geometry playerDotMini;
// ── Vollbild-Overlay (M-Taste) ────────────────────────────────────────────
private static volatile boolean fullMapOpenGlobal = false;
public static boolean isFullMapOpen() { return fullMapOpenGlobal; }
private boolean fullMapOpen = false;
private Node fullMapNode;
private Mesh fullMapMesh;
private Geometry fullMapGeo;
private Geometry fogFullGeo;
private Geometry playerDotFull;
private float fmMapSize;
private float fmMapOriginX;
private float fmMapOriginY;
private float fmViewRadius = FM_VIEW_DEF;
private float fmCenterU = 0.5f;
private float fmCenterV = 0.5f;
private boolean fmDragging = false;
private float fmDragPrevX, fmDragPrevY;
// ── Konstruktor ───────────────────────────────────────────────────────────
public MinimapState(Node playerNode) {
this.playerNode = playerNode;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application application) {
app = (SimpleApplication) application;
camera = app.getCamera();
registerInput();
startRenderThread();
}
@Override
protected void cleanup(Application application) {
removeInput();
if (exploreTracker != null) {
exploreTracker.save();
}
}
@Override
protected void onEnable() {
if (textureReady && minimapNode != null) {
app.getGuiNode().attachChild(minimapNode);
}
}
@Override
protected void onDisable() {
closeFullMap();
if (minimapNode != null) {
app.getGuiNode().detachChild(minimapNode);
}
}
// ── Update ────────────────────────────────────────────────────────────────
@Override
public void update(float tpf) {
if (!textureReady) { return; }
boolean anyOverlay = OverlayState.isAnyOpen() || MenuScreen.isAnyOpen();
if (minimapNode != null) {
minimapNode.setCullHint(anyOverlay || fullMapOpen
? Spatial.CullHint.Always : Spatial.CullHint.Inherit);
}
float px = playerNode.getWorldTranslation().x;
float pz = playerNode.getWorldTranslation().z;
// Erkundung markieren + Nebel animieren
tickExploration(px, pz, tpf);
fogTime += tpf;
if (fogMat != null) { fogMat.setFloat("Time", fogTime); }
if (fogFullMat != null) { fogFullMat.setFloat("Time", fogTime); }
if (!fullMapOpen) {
updateMinimapUV(px, pz);
} else {
if (fmDragging) {
Vector2f cursor = app.getInputManager().getCursorPosition();
float dx = cursor.x - fmDragPrevX;
float dy = cursor.y - fmDragPrevY;
fmDragPrevX = cursor.x;
fmDragPrevY = cursor.y;
if (dx != 0f || dy != 0f) {
float uvPerPixel = (2f * fmViewRadius / WORLD_SIZE) / fmMapSize;
fmCenterU -= dx * uvPerPixel;
fmCenterV -= dy * uvPerPixel;
updateFullMapUV();
scheduleVectorRedraw();
}
}
updateFullMapPlayerDot(px, pz);
}
}
// ── Erkundung ─────────────────────────────────────────────────────────────
private void tickExploration(float px, float pz, float tpf) {
if (exploreTracker == null) { return; }
// Nur markieren wenn der Spieler sich weit genug bewegt hat
boolean moved = Float.isNaN(lastMarkX)
|| distSq(px, pz, lastMarkX, lastMarkZ) >= MARK_DIST_SQ;
if (moved) {
exploreTracker.markCircle(px, pz);
lastMarkX = px;
lastMarkZ = pz;
}
// Auto-Save
autoSaveTimer += tpf;
if (autoSaveTimer >= AUTOSAVE_SEC) {
autoSaveTimer = 0f;
exploreTracker.save();
}
}
private static float distSq(float x1, float z1, float x2, float z2) {
float dx = x2 - x1, dz = z2 - z1;
return dx * dx + dz * dz;
}
// ── Minimap UV ────────────────────────────────────────────────────────────
private void updateMinimapUV(float playerX, float playerZ) {
if (minimapMesh == null) { return; }
float cu = (playerX + WORLD_HALF) / WORLD_SIZE;
float cv = 1f - (playerZ + WORLD_HALF) / WORLD_SIZE;
float hu = viewRadius / WORLD_SIZE;
cu = Math.max(hu, Math.min(1f - hu, cu));
cv = Math.max(hu, Math.min(1f - hu, cv));
// Kamera-Yaw → Karte dreht sich so dass Blickrichtung immer oben ist
float cosA = 1f, sinA = 0f;
if (camera != null) {
Vector3f dir = camera.getDirection();
float yaw = FastMath.atan2(dir.x, -dir.z);
cosA = FastMath.cos(yaw);
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 };
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);
}
minimapMesh.getBuffer(VertexBuffer.Type.TexCoord).setUpdateNeeded();
}
// ── Vollbild-Overlay ──────────────────────────────────────────────────────
private void openFullMap() {
if (fullMapNode != null || !textureReady) { return; }
fullMapOpen = true;
fullMapOpenGlobal = true;
float sw = app.getCamera().getWidth();
float sh = app.getCamera().getHeight();
fmMapSize = Math.min(sw, sh) * 0.85f;
fmMapOriginX = (sw - fmMapSize) / 2f;
fmMapOriginY = (sh - fmMapSize) / 2f;
fmViewRadius = FM_VIEW_DEF;
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);
if (ws != null && ws.getSharedFPP() != null) {
blurFilter = new de.blight.game.post.GaussianBlurFilter(5f);
ws.getSharedFPP().addFilter(blurFilter);
}
fullMapNode = new Node("worldmap_overlay");
fullMapMesh = buildQuadMesh(fmMapOriginX, fmMapOriginY, fmMapSize, fmMapSize, 20f);
updateFullMapUV();
// Karten-Textur mit screen-space Edge-Fade
fullMapGeo = new Geometry("wm_geo", fullMapMesh);
fullMapGeo.setMaterial(buildOverlayTexMat(fmMapOriginX, fmMapOriginY, fmMapSize));
fullMapGeo.setQueueBucket(RenderQueue.Bucket.Gui);
fullMapNode.attachChild(fullMapGeo);
// Fog-Overlay: eigene Material-Instanz mit Overlay-Bounds für Edge-Fade
if (exploreTracker != null) {
fogFullMat = buildFogMat(fmMapOriginX, fmMapOriginY, fmMapSize, fmMapSize);
fogFullGeo = new Geometry("wm_fog", fullMapMesh);
fogFullGeo.setMaterial(fogFullMat);
fogFullGeo.setQueueBucket(RenderQueue.Bucket.Gui);
fogFullGeo.setLocalTranslation(0f, 0f, 0.5f);
fullMapNode.attachChild(fogFullGeo);
}
playerDotFull = makeSolidQuad("wm_dot", DOT_SIZE, DOT_SIZE,
new ColorRGBA(1f, 0.85f, 0f, 1f));
playerDotFull.setLocalTranslation(0f, 0f, 21f);
fullMapNode.attachChild(playerDotFull);
// Vektor-Overlay: ARGB ByteBuffer-Textur, die bei Zoom/Pan neu gerastert wird
vectorBuf = BufferUtils.createByteBuffer(VEC_TEX_SIZE * VEC_TEX_SIZE * 4);
vectorImage = new Image(Image.Format.RGBA8, VEC_TEX_SIZE, VEC_TEX_SIZE, vectorBuf);
vectorTex = new Texture2D(vectorImage);
vectorTex.setWrap(Texture.WrapMode.EdgeClamp);
vectorTex.setMagFilter(Texture.MagFilter.Bilinear);
vectorTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
vectorMesh = buildQuadMesh(fmMapOriginX, fmMapOriginY, fmMapSize, fmMapSize, 20.3f);
vectorGeo = new Geometry("wm_vec", vectorMesh);
vectorGeo.setMaterial(buildVectorMat(fmMapOriginX, fmMapOriginY, fmMapSize));
vectorGeo.setQueueBucket(RenderQueue.Bucket.Gui);
fullMapNode.attachChild(vectorGeo);
app.getGuiNode().attachChild(fullMapNode);
app.getInputManager().setCursorVisible(true);
WorldScene ws2 = app.getStateManager().getState(WorldScene.class);
if (ws2 != null) { ws2.setPaused(true); }
updateFullMapPlayerDot(px, pz);
scheduleVectorRedraw();
}
private void closeFullMap() {
if (!fullMapOpen || fullMapNode == null) { return; }
fullMapOpen = false;
fullMapOpenGlobal = false;
app.getGuiNode().detachChild(fullMapNode);
fullMapNode = null;
fullMapGeo = null;
fogFullGeo = null;
fogFullMat = null;
fullMapMesh = null;
playerDotFull = null;
vectorGeo = null;
vectorMesh = null;
vectorTex = null;
vectorImage = null;
vectorBuf = null;
vecRenderPending = false;
vecLayerDirty = false;
if (blurFilter != null) {
WorldScene ws = app.getStateManager().getState(WorldScene.class);
if (ws != null && ws.getSharedFPP() != null) {
ws.getSharedFPP().removeFilter(blurFilter);
}
blurFilter = null;
}
app.getInputManager().setCursorVisible(false);
WorldScene ws = app.getStateManager().getState(WorldScene.class);
if (ws != null) { ws.setPaused(false); }
}
private void updateFullMapPlayerDot(float px, float pz) {
if (playerDotFull == null) { return; }
float hu = fmViewRadius / WORLD_SIZE;
float pu = (px + WORLD_HALF) / WORLD_SIZE;
float pv = 1f - (pz + WORLD_HALF) / WORLD_SIZE;
float relU = (pu - (fmCenterU - hu)) / (2f * hu);
float relV = (pv - (fmCenterV - hu)) / (2f * hu);
float screenX = fmMapOriginX + relU * fmMapSize - DOT_SIZE / 2f;
float screenY = fmMapOriginY + relV * fmMapSize - DOT_SIZE / 2f;
playerDotFull.setLocalTranslation(screenX, screenY, 21f);
}
private void updateFullMapUV() {
if (fullMapMesh == null) { return; }
float hu = fmViewRadius / WORLD_SIZE;
fmCenterU = Math.max(hu, Math.min(1f - hu, fmCenterU));
fmCenterV = Math.max(hu, Math.min(1f - hu, fmCenterV));
FloatBuffer uv = (FloatBuffer) fullMapMesh.getBuffer(VertexBuffer.Type.TexCoord).getData();
uv.put(0, fmCenterU - hu); uv.put(1, fmCenterV - hu);
uv.put(2, fmCenterU + hu); uv.put(3, fmCenterV - hu);
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();
}
// ── Input ─────────────────────────────────────────────────────────────────
private void registerInput() {
app.getInputManager().addMapping(ACT_MAP_TOGGLE, new KeyTrigger(KeyInput.KEY_M));
app.getInputManager().addMapping(ACT_MAP_ESC, new KeyTrigger(KeyInput.KEY_ESCAPE));
app.getInputManager().addMapping(ACT_FM_DRAG, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
app.getInputManager().addMapping(ANA_FM_ZOOM_IN, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
app.getInputManager().addMapping(ANA_FM_ZOOM_OUT, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
app.getInputManager().addListener(actionListener, ACT_MAP_TOGGLE, ACT_MAP_ESC, ACT_FM_DRAG);
app.getInputManager().addListener(analogListener, ANA_FM_ZOOM_IN, ANA_FM_ZOOM_OUT);
}
private void removeInput() {
app.getInputManager().removeListener(actionListener);
app.getInputManager().removeListener(analogListener);
for (String m : new String[]{
ACT_MAP_TOGGLE, ACT_MAP_ESC, ACT_FM_DRAG,
ANA_FM_ZOOM_IN, ANA_FM_ZOOM_OUT }) {
if (app.getInputManager().hasMapping(m)) { app.getInputManager().deleteMapping(m); }
}
}
private final ActionListener actionListener = (name, isPressed, tpf) -> {
if (!isEnabled()) { return; }
if (ACT_MAP_TOGGLE.equals(name) && isPressed) {
if (fullMapOpen) { closeFullMap(); } else { openFullMap(); }
} else if (ACT_MAP_ESC.equals(name) && isPressed && fullMapOpen) {
closeFullMap();
} else if (ACT_FM_DRAG.equals(name) && fullMapOpen) {
fmDragging = isPressed;
if (isPressed) {
Vector2f c = app.getInputManager().getCursorPosition();
fmDragPrevX = c.x;
fmDragPrevY = c.y;
} else {
scheduleVectorRedraw();
}
}
};
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;
}
updateFullMapUV();
scheduleVectorRedraw();
float px = playerNode.getWorldTranslation().x;
float pz = playerNode.getWorldTranslation().z;
updateFullMapPlayerDot(px, pz);
};
// ── Welt-Renderer (Hintergrund-Thread) ────────────────────────────────────
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");
MapData mapData = MapIO.load();
List<PlacedArea> areas = AreaIO.load();
List<PlacedLocationZone> zones = LocationZoneIO.load();
List<Location> locs = LocationIO.load();
List<PlacedWater> waters = WaterBodyIO.load();
List<PlacedModel> models = PlacedModelIO.load();
int[] slotColors = computeSlotColors(mapData, root);
renderInput = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
boolean needsRender = !Files.exists(png);
if (!needsRender) {
// Cache ungültig wenn Karte neuer als das PNG
try {
long pngMod = Files.getLastModifiedTime(png).toMillis();
long mapMod = Files.getLastModifiedTime(MapIO.getMapPath()).toMillis();
if (mapMod > pngMod) { needsRender = true; }
} catch (Exception ignored) { needsRender = true; }
}
if (needsRender) {
log.info("[Minimap] Rendere Weltkarte {}×{}…", TEXTURE_SIZE, TEXTURE_SIZE);
BufferedImage bi = WorldMapRenderer.render(renderInput, TEXTURE_SIZE, RenderOptions.all());
ImageIO.write(bi, "PNG", png.toFile());
log.info("[Minimap] Weltkarte gespeichert: {}", png);
} else {
log.info("[Minimap] Gecachte Weltkarte: {}", png);
}
app.enqueue(() -> { onTextureReady(); return null; });
} catch (Exception e) {
log.error("[Minimap] Render-Fehler: {}", e.getMessage(), e);
}
}, "MinimapRenderer");
t.setDaemon(true);
t.start();
}
// Default-Texturen aus TerrainEditorState (werden genutzt wenn mapData.terrainTextures leer ist)
private static final String[] TERRAIN_TEX_DEFAULTS = {
"Textures/Terrain/splat/grass.jpg",
"Textures/Terrain/Rock2/rock.jpg",
"Textures/Terrain/splat/dirt.jpg",
""
};
private static int[] computeSlotColors(MapData mapData, Path assetRoot) {
return WorldMapRenderer.computeSlotColors(mapData, assetRoot, TERRAIN_TEX_DEFAULTS);
}
private void onTextureReady() {
exploreTracker = new ExploreTracker();
exploreTracker.load();
float x = MARGIN;
float y = MARGIN;
fogMat = buildFogMat(x, y, MINIMAP_SIZE, MINIMAP_SIZE);
minimapMesh = buildMinimapMesh(MINIMAP_SIZE, MINIMAP_SIZE);
// Welt-Textur-Geo
minimapGeo = new Geometry("minimap_geo", minimapMesh);
minimapGeo.setMaterial(buildTexMat());
minimapGeo.setQueueBucket(RenderQueue.Bucket.Gui);
// 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);
minimapNode = new Node("minimap");
minimapNode.setLocalTranslation(x, y, 48f);
minimapNode.attachChild(border);
minimapNode.attachChild(minimapGeo);
minimapNode.attachChild(fogMiniGeo);
minimapNode.attachChild(playerDotMini);
textureReady = true;
if (isEnabled()) {
app.getGuiNode().attachChild(minimapNode);
}
}
// ── Vektor-Layer ──────────────────────────────────────────────────────────
private void scheduleVectorRedraw() {
if (renderInput == 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;
float vr = fmViewRadius;
final float capU = fmCenterU;
final float capV = fmCenterV;
final float capHU = vr / WORLD_SIZE;
Thread t = new Thread(() -> {
RenderOptions opts = new RenderOptions(false, false, true, true, true, true, true);
BufferedImage bi = WorldMapRenderer.renderRegion(renderInput, VEC_TEX_SIZE, opts, wx, wz, vr);
app.enqueue(() -> {
updateVectorTexture(bi);
vecRenderU = capU;
vecRenderV = capV;
vecRenderHU = capHU;
updateVectorMeshUV();
vecRenderPending = false;
if (vecLayerDirty) { scheduleVectorRedraw(); }
return null;
});
}, "VecLayerRenderer");
t.setDaemon(true);
t.start();
}
private void updateVectorTexture(BufferedImage bi) {
if (vectorBuf == null || vectorImage == null) { return; }
int sz = VEC_TEX_SIZE;
for (int py = 0; py < sz; py++) {
int bufRow = sz - 1 - py; // Y-flip: ByteBuffer-V=0 ist unten (Süden)
for (int px = 0; px < sz; px++) {
int argb = bi.getRGB(px, py);
int off = (bufRow * sz + px) * 4;
vectorBuf.put(off, (byte) ((argb >> 16) & 0xFF));
vectorBuf.put(off + 1, (byte) ((argb >> 8) & 0xFF));
vectorBuf.put(off + 2, (byte) ( argb & 0xFF));
vectorBuf.put(off + 3, (byte) ((argb >> 24) & 0xFF));
}
}
vectorImage.setUpdateNeeded();
}
// ── Material-Helfer ───────────────────────────────────────────────────────
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");
mat.setColor("Color", new ColorRGBA(0.05f, 0.1f, 0.2f, 1f));
}
return mat;
}
private Material buildVectorMat(float originX, float originY, float size) {
Material mat = new Material(app.getAssetManager(), "MatDefs/MapOverlay.j3md");
mat.setTexture("ColorMap", vectorTex);
mat.setVector2("OverlayOrigin", new Vector2f(originX, originY));
mat.setVector2("OverlaySize", new Vector2f(size, size));
return mat;
}
private Material buildFogMat(float originX, float originY, float sizeW, float sizeH) {
Material mat = new Material(app.getAssetManager(), "MatDefs/FogOfWar.j3md");
mat.setTexture("ExploreMap", exploreTracker.getFogTexture());
mat.setFloat("Time", 0f);
mat.setVector2("OverlayOrigin", new Vector2f(originX, originY));
mat.setVector2("OverlaySize", new Vector2f(sizeW, sizeH));
return mat;
}
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");
}
mat.setVector2("OverlayOrigin", new Vector2f(originX, originY));
mat.setVector2("OverlaySize", new Vector2f(size, size));
return mat;
}
// ── Mesh-Helfer ───────────────────────────────────────────────────────────
private static Mesh buildMinimapMesh(float w, float h) {
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.setMode(Mesh.Mode.Triangles);
m.updateBound();
return m;
}
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(
x, y, z, x + w, y, z, x + w, y + h, z, x, y + h, z));
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.setMode(Mesh.Mode.Triangles);
m.updateBound();
return m;
}
private Geometry makeSolidQuad(String name, float w, float h, ColorRGBA color) {
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", color);
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
Geometry g = new Geometry(name, new Quad(w, h));
g.setMaterial(mat);
g.setQueueBucket(RenderQueue.Bucket.Gui);
return g;
}
}