Minimap und ingame Karte - umsetzung begonnen
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
766
blight-game/src/main/java/de/blight/game/state/MinimapState.java
Normal file
766
blight-game/src/main/java/de/blight/game/state/MinimapState.java
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user