Minimap und ingame Karte - umsetzung begonnen
This commit is contained in:
BIN
assets/imported/models/bjoernson.fbx
Normal file
BIN
assets/imported/models/bjoernson.fbx
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Characters/Models/bjoernson.j3o
Normal file
BIN
blight-assets/src/main/resources/Characters/Models/bjoernson.j3o
Normal file
Binary file not shown.
24
blight-assets/src/main/resources/MatDefs/FogOfWar.j3md
Normal file
24
blight-assets/src/main/resources/MatDefs/FogOfWar.j3md
Normal file
@@ -0,0 +1,24 @@
|
||||
MaterialDef FogOfWar {
|
||||
|
||||
MaterialParameters {
|
||||
Texture2D ExploreMap // Alpha=0 erkundet, Alpha=1 Nebel
|
||||
Float Time : 0.0 // Sekunden seit Spielstart (für Animation)
|
||||
Vector2 OverlayOrigin // Overlay-Position in Bildschirm-Pixeln (bottom-left)
|
||||
Vector2 OverlaySize // Overlay-Größe in Pixeln
|
||||
Float FadePx : 5.0 // Breite des Weich-Übergangs
|
||||
}
|
||||
|
||||
Technique {
|
||||
VertexShader GLSL150: Shaders/FogOfWar.vert
|
||||
FragmentShader GLSL150: Shaders/FogOfWar.frag
|
||||
|
||||
WorldParameters {
|
||||
WorldViewProjectionMatrix
|
||||
}
|
||||
|
||||
RenderState {
|
||||
Blend Alpha
|
||||
DepthWrite Off
|
||||
}
|
||||
}
|
||||
}
|
||||
23
blight-assets/src/main/resources/MatDefs/MapOverlay.j3md
Normal file
23
blight-assets/src/main/resources/MatDefs/MapOverlay.j3md
Normal file
@@ -0,0 +1,23 @@
|
||||
MaterialDef MapOverlay {
|
||||
|
||||
MaterialParameters {
|
||||
Texture2D ColorMap
|
||||
Vector2 OverlayOrigin // Overlay-Position in Bildschirm-Pixeln (bottom-left)
|
||||
Vector2 OverlaySize // Overlay-Größe in Pixeln
|
||||
Float FadePx : 5.0 // Breite des Weich-Übergangs in Pixeln
|
||||
}
|
||||
|
||||
Technique {
|
||||
VertexShader GLSL150: Shaders/FogOfWar.vert
|
||||
FragmentShader GLSL150: Shaders/MapOverlay.frag
|
||||
|
||||
WorldParameters {
|
||||
WorldViewProjectionMatrix
|
||||
}
|
||||
|
||||
RenderState {
|
||||
Blend Alpha
|
||||
DepthWrite Off
|
||||
}
|
||||
}
|
||||
}
|
||||
98
blight-assets/src/main/resources/Shaders/FogOfWar.frag
Normal file
98
blight-assets/src/main/resources/Shaders/FogOfWar.frag
Normal file
@@ -0,0 +1,98 @@
|
||||
uniform sampler2D m_ExploreMap;
|
||||
uniform float m_Time;
|
||||
uniform vec2 m_OverlayOrigin;
|
||||
uniform vec2 m_OverlaySize;
|
||||
uniform float m_FadePx;
|
||||
|
||||
in vec2 texCoord;
|
||||
out vec4 outFragColor;
|
||||
|
||||
// ── Procedural Value Noise ────────────────────────────────────────────────────
|
||||
|
||||
float hash(vec2 p) {
|
||||
vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));
|
||||
p3 += dot(p3, p3.yzx + 33.33);
|
||||
return fract((p3.x + p3.y) * p3.z);
|
||||
}
|
||||
|
||||
float vnoise(vec2 p) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
f = f * f * (3.0 - 2.0 * f);
|
||||
return mix(
|
||||
mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x),
|
||||
mix(hash(i + vec2(0.0,1.0)), hash(i + vec2(1.0, 1.0)), f.x),
|
||||
f.y);
|
||||
}
|
||||
|
||||
// FBM: 3 Oktaven
|
||||
float fbm(vec2 p) {
|
||||
float v = 0.0;
|
||||
v += vnoise(p) * 0.500;
|
||||
v += vnoise(p * 2.1 + vec2(1.7, 9.2)) * 0.250;
|
||||
v += vnoise(p * 4.3 + vec2(8.3, 2.8)) * 0.125;
|
||||
return v / 0.875;
|
||||
}
|
||||
|
||||
// ── Hauptprogramm ─────────────────────────────────────────────────────────────
|
||||
|
||||
void main() {
|
||||
|
||||
// Weichgezeichnete Erkundungsmaske (3×3-Kernel, 8-Texel-Radius = ~32 m)
|
||||
float ts = 8.0 / 512.0;
|
||||
float density =
|
||||
texture(m_ExploreMap, texCoord + vec2(-ts,-ts)).a +
|
||||
texture(m_ExploreMap, texCoord + vec2(0.0,-ts)).a +
|
||||
texture(m_ExploreMap, texCoord + vec2( ts,-ts)).a +
|
||||
texture(m_ExploreMap, texCoord + vec2(-ts, 0.0)).a +
|
||||
texture(m_ExploreMap, texCoord ).a +
|
||||
texture(m_ExploreMap, texCoord + vec2( ts, 0.0)).a +
|
||||
texture(m_ExploreMap, texCoord + vec2(-ts, ts)).a +
|
||||
texture(m_ExploreMap, texCoord + vec2(0.0, ts)).a +
|
||||
texture(m_ExploreMap, texCoord + vec2( ts, ts)).a;
|
||||
density /= 9.0;
|
||||
|
||||
// Vollständig erkundet → diesen Fragment überspringen
|
||||
if (density <= 0.0) { discard; }
|
||||
|
||||
// ── Animierter Nebel mit Domain-Warping ───────────────────────────────────
|
||||
|
||||
float t = m_Time * 0.18;
|
||||
vec2 uv = texCoord;
|
||||
|
||||
// Erste Warp-Schicht: langsam driftende Wirbel
|
||||
vec2 q = vec2(
|
||||
fbm(uv * 2.5 + vec2(t * 0.40, t * 0.15)),
|
||||
fbm(uv * 2.5 + vec2(t * 0.15, t * 0.30) + vec2(5.2, 1.3))
|
||||
);
|
||||
|
||||
// Zweite Warp-Schicht: gibt Tiefe und Komplexität
|
||||
vec2 r = vec2(
|
||||
fbm(uv * 2.0 + q * 0.9 + vec2(1.7, 9.2) + vec2(t * 0.10, 0.0)),
|
||||
fbm(uv * 2.0 + q * 0.9 + vec2(8.3, 2.8) + vec2(0.0, t * 0.08))
|
||||
);
|
||||
|
||||
// Finaler Noise-Wert [0..1]
|
||||
float f = fbm(uv * 3.0 + r * 1.2 + vec2(t * 0.05, t * 0.04));
|
||||
|
||||
// ── Farbe + Alpha ─────────────────────────────────────────────────────────
|
||||
|
||||
// Dunkles Blaugrau – etwas heller an den "Schwaden"
|
||||
vec3 fogColor = mix(
|
||||
vec3(0.04, 0.05, 0.09), // tiefer Schatten
|
||||
vec3(0.11, 0.13, 0.20), // heller Dunst
|
||||
f
|
||||
);
|
||||
|
||||
// Weich-Übergang am Overlay-Rand (Screen-Space, zoom-unabhängig)
|
||||
float distL = gl_FragCoord.x - m_OverlayOrigin.x;
|
||||
float distR = (m_OverlayOrigin.x + m_OverlaySize.x) - gl_FragCoord.x;
|
||||
float distB = gl_FragCoord.y - m_OverlayOrigin.y;
|
||||
float distT = (m_OverlayOrigin.y + m_OverlaySize.y) - gl_FragCoord.y;
|
||||
float edgeFade = clamp(min(min(distL, distR), min(distB, distT)) / m_FadePx, 0.0, 1.0);
|
||||
|
||||
// Alpha: voll opak im unentdeckten Innern, weicher Rand durch Kernel + Edge-Fade
|
||||
float alpha = density * (0.88 + f * 0.12) * edgeFade;
|
||||
|
||||
outFragColor = vec4(fogColor, alpha);
|
||||
}
|
||||
11
blight-assets/src/main/resources/Shaders/FogOfWar.vert
Normal file
11
blight-assets/src/main/resources/Shaders/FogOfWar.vert
Normal file
@@ -0,0 +1,11 @@
|
||||
uniform mat4 g_WorldViewProjectionMatrix;
|
||||
|
||||
in vec3 inPosition;
|
||||
in vec2 inTexCoord;
|
||||
|
||||
out vec2 texCoord;
|
||||
|
||||
void main() {
|
||||
texCoord = inTexCoord;
|
||||
gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0);
|
||||
}
|
||||
20
blight-assets/src/main/resources/Shaders/MapOverlay.frag
Normal file
20
blight-assets/src/main/resources/Shaders/MapOverlay.frag
Normal file
@@ -0,0 +1,20 @@
|
||||
uniform sampler2D m_ColorMap;
|
||||
uniform vec2 m_OverlayOrigin;
|
||||
uniform vec2 m_OverlaySize;
|
||||
uniform float m_FadePx;
|
||||
|
||||
in vec2 texCoord;
|
||||
out vec4 outFragColor;
|
||||
|
||||
void main() {
|
||||
vec4 col = texture(m_ColorMap, texCoord);
|
||||
|
||||
// Abstand des Fragments vom nächsten Overlay-Rand in Screen-Pixeln
|
||||
float distL = gl_FragCoord.x - m_OverlayOrigin.x;
|
||||
float distR = (m_OverlayOrigin.x + m_OverlaySize.x) - gl_FragCoord.x;
|
||||
float distB = gl_FragCoord.y - m_OverlayOrigin.y;
|
||||
float distT = (m_OverlayOrigin.y + m_OverlaySize.y) - gl_FragCoord.y;
|
||||
float edgeFade = clamp(min(min(distL, distR), min(distB, distT)) / m_FadePx, 0.0, 1.0);
|
||||
|
||||
outFragColor = vec4(col.rgb, col.a * edgeFade);
|
||||
}
|
||||
BIN
blight-assets/src/main/resources/Textures/hud/minimap_world.png
Normal file
BIN
blight-assets/src/main/resources/Textures/hud/minimap_world.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -0,0 +1,609 @@
|
||||
package de.blight.common.map;
|
||||
|
||||
import de.blight.common.*;
|
||||
import de.blight.common.model.Location;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Rendert eine 2D-Draufsicht der Spielwelt als {@link BufferedImage}.
|
||||
*
|
||||
* Koordinaten-Konvention:
|
||||
* pixelX=0 / pixelY=0 → worldX=-1024 / worldZ=-1024
|
||||
* pixelX=size-1 → worldX=+1024
|
||||
* pixelY=size-1 → worldZ=+1024
|
||||
*/
|
||||
public final class WorldMapRenderer {
|
||||
|
||||
public static final float WORLD_HALF = 1024f;
|
||||
public static final float WORLD_SIZE = 2048f;
|
||||
|
||||
public record RenderInput(
|
||||
MapData mapData,
|
||||
List<PlacedArea> areas,
|
||||
List<PlacedLocationZone> zones,
|
||||
List<Location> locations,
|
||||
List<PlacedWater> waters,
|
||||
List<PlacedModel> models,
|
||||
int[] slotColorsRGB // 12 Werte: [R0,G0,B0, R1,G1,B1, R2,G2,B2, R3,G3,B3], null = Defaults
|
||||
) {
|
||||
public RenderInput(MapData mapData, List<PlacedArea> areas, List<PlacedLocationZone> zones,
|
||||
List<Location> locations, List<PlacedWater> waters, List<PlacedModel> models) {
|
||||
this(mapData, areas, zones, locations, waters, models, null);
|
||||
}
|
||||
}
|
||||
|
||||
public record RenderOptions(
|
||||
boolean showTerrain,
|
||||
boolean showSplatColors,
|
||||
boolean showWater,
|
||||
boolean showAreas,
|
||||
boolean showZones,
|
||||
boolean showLocations,
|
||||
boolean showModels
|
||||
) {
|
||||
public static RenderOptions all() {
|
||||
return new RenderOptions(true, true, true, true, true, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Default-Slot-Farben für Slots 1-8 (Base-Layer 1-4 + Upper-Layer 5-8)
|
||||
private static final int[] DEF_SLOT_R = { 71, 115, 140, 204, 90, 130, 110, 180 };
|
||||
private static final int[] DEF_SLOT_G = { 148, 82, 115, 184, 80, 90, 60, 100 };
|
||||
private static final int[] DEF_SLOT_B = { 46, 64, 77, 128, 40, 60, 50, 70 };
|
||||
|
||||
private WorldMapRenderer() {}
|
||||
|
||||
public static BufferedImage render(RenderInput input, int targetSize, RenderOptions opts) {
|
||||
MapData m = input.mapData();
|
||||
int TV = MapData.TERRAIN_VERTS;
|
||||
int SS = MapData.SPLAT_SIZE;
|
||||
int[] slotR = slotChannel(input, 0);
|
||||
int[] slotG = slotChannel(input, 1);
|
||||
int[] slotB = slotChannel(input, 2);
|
||||
|
||||
// ── 1. Heightmap auf Zielauflösung samplen ────────────────────────────
|
||||
float[] heights = new float[targetSize * targetSize];
|
||||
float minH = Float.MAX_VALUE, maxH = -Float.MAX_VALUE;
|
||||
|
||||
for (int py = 0; py < targetSize; py++) {
|
||||
for (int px = 0; px < targetSize; px++) {
|
||||
int hx = Math.min((int)((float) px / (targetSize - 1) * (TV - 1)), TV - 1);
|
||||
int hz = Math.min((int)((float) py / (targetSize - 1) * (TV - 1)), TV - 1);
|
||||
float h = m.terrainHeight[hz * TV + hx];
|
||||
heights[py * targetSize + px] = h;
|
||||
if (h < minH) minH = h;
|
||||
if (h > maxH) maxH = h;
|
||||
}
|
||||
}
|
||||
|
||||
float heightRange = Math.max(0.01f, maxH - minH);
|
||||
|
||||
// ── 2. Pixel-Farben berechnen ─────────────────────────────────────────
|
||||
BufferedImage img = new BufferedImage(targetSize, targetSize, BufferedImage.TYPE_INT_RGB);
|
||||
|
||||
for (int py = 0; py < targetSize; py++) {
|
||||
for (int px = 0; px < targetSize; px++) {
|
||||
float h = heights[py * targetSize + px];
|
||||
float hn = (h - minH) / heightRange;
|
||||
|
||||
int r, g, b;
|
||||
|
||||
if (opts.showSplatColors()) {
|
||||
int sx = Math.min((int)((float) px / (targetSize - 1) * (SS - 1)), SS - 1);
|
||||
int sz = Math.min((int)((float)(targetSize - 1 - py) / (targetSize - 1) * (SS - 1)), SS - 1);
|
||||
int si = sz * SS + sx;
|
||||
// Base layer (Slots 1-4)
|
||||
float wg = (m.splatG[si] & 0xFF) / 255f;
|
||||
float wb = (m.splatB[si] & 0xFF) / 255f;
|
||||
float wa = (m.splatA[si] & 0xFF) / 255f;
|
||||
float fr = slotR[0], fg = slotG[0], fb = slotB[0];
|
||||
fr = fr*(1-wg) + slotR[1]*wg; fg = fg*(1-wg) + slotG[1]*wg; fb = fb*(1-wg) + slotB[1]*wg;
|
||||
fr = fr*(1-wb) + slotR[2]*wb; fg = fg*(1-wb) + slotG[2]*wb; fb = fb*(1-wb) + slotB[2]*wb;
|
||||
fr = fr*(1-wa) + slotR[3]*wa; fg = fg*(1-wa) + slotG[3]*wa; fb = fb*(1-wa) + slotB[3]*wa;
|
||||
// Upper layer (Slots 5-8)
|
||||
float u1 = (m.upperSplatR[si] & 0xFF) / 255f;
|
||||
float u2 = (m.upperSplatG[si] & 0xFF) / 255f;
|
||||
float u3 = (m.upperSplatB[si] & 0xFF) / 255f;
|
||||
float u4 = (m.upperSplatA[si] & 0xFF) / 255f;
|
||||
fr = fr*(1-u1) + slotR[4]*u1; fg = fg*(1-u1) + slotG[4]*u1; fb = fb*(1-u1) + slotB[4]*u1;
|
||||
fr = fr*(1-u2) + slotR[5]*u2; fg = fg*(1-u2) + slotG[5]*u2; fb = fb*(1-u2) + slotB[5]*u2;
|
||||
fr = fr*(1-u3) + slotR[6]*u3; fg = fg*(1-u3) + slotG[6]*u3; fb = fb*(1-u3) + slotB[6]*u3;
|
||||
fr = fr*(1-u4) + slotR[7]*u4; fg = fg*(1-u4) + slotG[7]*u4; fb = fb*(1-u4) + slotB[7]*u4;
|
||||
r = clamp((int) fr); g = clamp((int) fg); b = clamp((int) fb);
|
||||
} else {
|
||||
int lum = clamp(30 + (int)(hn * 200));
|
||||
r = g = b = lum;
|
||||
}
|
||||
|
||||
// Hillshading (NW-Licht)
|
||||
if (opts.showTerrain() && px > 0 && px < targetSize - 1 && py > 0 && py < targetSize - 1) {
|
||||
float dx = heights[py * targetSize + (px + 1)] - heights[py * targetSize + (px - 1)];
|
||||
float dz = heights[(py + 1) * targetSize + px] - heights[(py - 1) * targetSize + px];
|
||||
float nx = -dx * 0.25f, ny = 1f, nz = -dz * 0.25f;
|
||||
float len = (float) Math.sqrt(nx * nx + ny * ny + nz * nz);
|
||||
nx /= len; ny /= len; nz /= len;
|
||||
float shade = nx * (-0.577f) + ny * 0.577f + nz * (-0.577f);
|
||||
shade = 0.45f + shade * 0.85f;
|
||||
shade = Math.max(0.2f, Math.min(1.8f, shade));
|
||||
r = clamp((int)(r * shade));
|
||||
g = clamp((int)(g * shade));
|
||||
b = clamp((int)(b * shade));
|
||||
}
|
||||
|
||||
img.setRGB(px, py, (r << 16) | (g << 8) | b);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3. Vektor-Overlays ────────────────────────────────────────────────
|
||||
Graphics2D gfx = img.createGraphics();
|
||||
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
float lineW = Math.max(1.5f, targetSize / 400f);
|
||||
|
||||
// Wasser
|
||||
if (opts.showWater()) {
|
||||
final int WATER_RGB = (255 << 24) | (35 << 16) | (100 << 8) | 190;
|
||||
// Meer: Terrain unter Meeresspiegel
|
||||
for (int py = 0; py < targetSize; py++) {
|
||||
for (int px = 0; px < targetSize; px++) {
|
||||
if (heights[py * targetSize + px] < 0f) {
|
||||
img.setRGB(px, py, WATER_RGB);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Wasserflächen: nur dort sichtbar, wo Terrain unter Wasseroberfläche liegt
|
||||
for (PlacedWater w : input.waters()) {
|
||||
int[] xs = worldToPixels(w.pointsX(), targetSize);
|
||||
int[] ys = worldToPixels(w.pointsZ(), targetSize);
|
||||
Polygon poly = new Polygon(xs, ys, xs.length);
|
||||
Rectangle bb = poly.getBounds();
|
||||
int x0 = Math.max(0, bb.x);
|
||||
int y0 = Math.max(0, bb.y);
|
||||
int x1 = Math.min(targetSize - 1, bb.x + bb.width);
|
||||
int y1 = Math.min(targetSize - 1, bb.y + bb.height);
|
||||
float wh = w.waterHeight();
|
||||
for (int py = y0; py <= y1; py++) {
|
||||
for (int px = x0; px <= x1; px++) {
|
||||
if (poly.contains(px, py) && heights[py * targetSize + px] < wh) {
|
||||
img.setRGB(px, py, WATER_RGB);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Area-Polygone
|
||||
if (opts.showAreas()) {
|
||||
gfx.setStroke(new BasicStroke(lineW));
|
||||
for (PlacedArea a : input.areas()) {
|
||||
Color c = hashColor(a.areaId());
|
||||
int[] xs = worldToPixels(a.pointsX(), targetSize);
|
||||
int[] ys = worldToPixels(a.pointsZ(), targetSize);
|
||||
gfx.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), 55));
|
||||
gfx.fillPolygon(xs, ys, xs.length);
|
||||
gfx.setColor(c);
|
||||
gfx.drawPolygon(xs, ys, xs.length);
|
||||
}
|
||||
}
|
||||
|
||||
// Location-Zonen
|
||||
if (opts.showZones()) {
|
||||
gfx.setStroke(new BasicStroke(lineW));
|
||||
for (PlacedLocationZone z : input.zones()) {
|
||||
int[] xs = worldToPixels(z.pointsX(), targetSize);
|
||||
int[] ys = worldToPixels(z.pointsZ(), targetSize);
|
||||
gfx.setColor(new Color(240, 190, 40, 70));
|
||||
gfx.fillPolygon(xs, ys, xs.length);
|
||||
gfx.setColor(new Color(200, 150, 20));
|
||||
gfx.drawPolygon(xs, ys, xs.length);
|
||||
}
|
||||
}
|
||||
|
||||
// Modell-Punkte
|
||||
if (opts.showModels()) {
|
||||
int dotR = Math.max(1, targetSize / 600);
|
||||
gfx.setColor(new Color(160, 80, 20, 200));
|
||||
for (PlacedModel model : input.models()) {
|
||||
int mx = worldToPixel(model.x(), targetSize);
|
||||
int mz = worldToPixel(model.z(), targetSize);
|
||||
gfx.fillRect(mx - dotR, mz - dotR, dotR * 2 + 1, dotR * 2 + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Locations (Kreise + Labels)
|
||||
if (opts.showLocations()) {
|
||||
gfx.setStroke(new BasicStroke(lineW));
|
||||
int fontSize = Math.max(8, targetSize / 140);
|
||||
gfx.setFont(new Font("SansSerif", Font.BOLD, fontSize));
|
||||
for (Location loc : input.locations()) {
|
||||
int lx = worldToPixel(loc.getCenterX(), targetSize);
|
||||
int lz = worldToPixel(loc.getCenterZ(), targetSize);
|
||||
int lr = Math.max(4, (int)(loc.getRadius() / WORLD_SIZE * targetSize));
|
||||
|
||||
gfx.setColor(new Color(255, 255, 200, 50));
|
||||
gfx.fillOval(lx - lr, lz - lr, lr * 2, lr * 2);
|
||||
gfx.setColor(new Color(255, 220, 60));
|
||||
gfx.drawOval(lx - lr, lz - lr, lr * 2, lr * 2);
|
||||
|
||||
if (targetSize >= 512 && loc.getId() != null && !loc.getId().isEmpty()) {
|
||||
String label = friendlyLocationName(loc.getId());
|
||||
FontMetrics fm = gfx.getFontMetrics();
|
||||
int tw = fm.stringWidth(label);
|
||||
gfx.setColor(new Color(0, 0, 0, 160));
|
||||
gfx.drawString(label, lx - tw / 2 + 1, lz - lr - 3);
|
||||
gfx.setColor(Color.WHITE);
|
||||
gfx.drawString(label, lx - tw / 2, lz - lr - 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spawnpunkt
|
||||
int spx = worldToPixel(m.spawnX, targetSize);
|
||||
int spz = worldToPixel(m.spawnZ, targetSize);
|
||||
int cr = Math.max(5, targetSize / 180);
|
||||
gfx.setStroke(new BasicStroke(Math.max(2f, targetSize / 300f)));
|
||||
gfx.setColor(new Color(0, 0, 0, 120));
|
||||
gfx.drawLine(spx - cr + 1, spz + 1, spx + cr + 1, spz + 1);
|
||||
gfx.drawLine(spx + 1, spz - cr + 1, spx + 1, spz + cr + 1);
|
||||
gfx.setColor(new Color(60, 230, 90));
|
||||
gfx.drawLine(spx - cr, spz, spx + cr, spz);
|
||||
gfx.drawLine(spx, spz - cr, spx, spz + cr);
|
||||
|
||||
gfx.dispose();
|
||||
return img;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert einen rechteckigen Weltausschnitt als {@link BufferedImage}.
|
||||
* Koordinatenursprung und Skalierung passen sich dem Ausschnitt an,
|
||||
* sodass die Ausgabe immer {@code targetSize × targetSize} Pixel scharf gezeichnet wird.
|
||||
*
|
||||
* Wenn {@code opts.showTerrain() == false}, ist der Hintergrund transparent (ARGB).
|
||||
* Das ist nützlich für eine Vektor-Overlay-Textur über einem separaten Terrain-Layer.
|
||||
*
|
||||
* @param worldCenterX Weltkoordinate X des Mittelpunkts
|
||||
* @param worldCenterZ Weltkoordinate Z des Mittelpunkts
|
||||
* @param halfExtent Halbausdehnung in Weltmetern (sichtbarer Radius)
|
||||
*/
|
||||
public static BufferedImage renderRegion(RenderInput input, int targetSize, RenderOptions opts,
|
||||
float worldCenterX, float worldCenterZ, float halfExtent) {
|
||||
halfExtent = Math.max(1f, halfExtent);
|
||||
float wx0 = worldCenterX - halfExtent;
|
||||
float wz0 = worldCenterZ - halfExtent;
|
||||
float rSize = halfExtent * 2f;
|
||||
|
||||
int imageType = opts.showTerrain() ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
|
||||
BufferedImage img = new BufferedImage(targetSize, targetSize, imageType);
|
||||
|
||||
// ── Höhen samplen (für Terrain-Render und Wasser-Clipping) ──────────
|
||||
MapData m = input.mapData();
|
||||
int TV = MapData.TERRAIN_VERTS;
|
||||
float[] heights = null;
|
||||
float minH = 0f, maxH = 1f;
|
||||
|
||||
if (opts.showTerrain() || opts.showWater()) {
|
||||
heights = new float[targetSize * targetSize];
|
||||
minH = Float.MAX_VALUE;
|
||||
maxH = -Float.MAX_VALUE;
|
||||
for (int py = 0; py < targetSize; py++) {
|
||||
float wz = wz0 + (float) py / (targetSize - 1) * rSize;
|
||||
int hz = iclamp((int) ((wz + WORLD_HALF) / WORLD_SIZE * (TV - 1)), 0, TV - 1);
|
||||
for (int px = 0; px < targetSize; px++) {
|
||||
float wx = wx0 + (float) px / (targetSize - 1) * rSize;
|
||||
int hx = iclamp((int) ((wx + WORLD_HALF) / WORLD_SIZE * (TV - 1)), 0, TV - 1);
|
||||
float h = m.terrainHeight[hz * TV + hx];
|
||||
heights[py * targetSize + px] = h;
|
||||
if (h < minH) { minH = h; }
|
||||
if (h > maxH) { maxH = h; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Terrain (optional) ────────────────────────────────────────────────
|
||||
if (opts.showTerrain()) {
|
||||
float heightRange = Math.max(0.01f, maxH - minH);
|
||||
int[] sR = slotChannel(input, 0);
|
||||
int[] sG = slotChannel(input, 1);
|
||||
int[] sB = slotChannel(input, 2);
|
||||
|
||||
for (int py = 0; py < targetSize; py++) {
|
||||
for (int px = 0; px < targetSize; px++) {
|
||||
float h = heights[py * targetSize + px];
|
||||
float hn = (h - minH) / heightRange;
|
||||
int r, g, b;
|
||||
|
||||
if (opts.showSplatColors()) {
|
||||
float wx = wx0 + (float) px / (targetSize - 1) * rSize;
|
||||
float wz = wz0 + (float) py / (targetSize - 1) * rSize;
|
||||
int SS2 = MapData.SPLAT_SIZE;
|
||||
float sfx = Math.max(0f, Math.min(SS2 - 1, (wx + WORLD_HALF) / WORLD_SIZE * (SS2 - 1)));
|
||||
float sfz = Math.max(0f, Math.min(SS2 - 1, (WORLD_HALF - wz) / WORLD_SIZE * (SS2 - 1)));
|
||||
int sx0 = (int) sfx; int sx1 = Math.min(sx0 + 1, SS2 - 1);
|
||||
int sz0 = (int) sfz; int sz1 = Math.min(sz0 + 1, SS2 - 1);
|
||||
float tx = sfx - sx0, tz = sfz - sz0;
|
||||
int i00 = sz0 * SS2 + sx0, i10 = sz0 * SS2 + sx1;
|
||||
int i01 = sz1 * SS2 + sx0, i11 = sz1 * SS2 + sx1;
|
||||
// Base layer (Slots 1-4) – bilinear interpolated weights
|
||||
float wg = bilerp(m.splatG, i00, i10, i01, i11, tx, tz);
|
||||
float wb = bilerp(m.splatB, i00, i10, i01, i11, tx, tz);
|
||||
float wa = bilerp(m.splatA, i00, i10, i01, i11, tx, tz);
|
||||
float fr = sR[0], fg = sG[0], fb = sB[0];
|
||||
fr = fr*(1-wg) + sR[1]*wg; fg = fg*(1-wg) + sG[1]*wg; fb = fb*(1-wg) + sB[1]*wg;
|
||||
fr = fr*(1-wb) + sR[2]*wb; fg = fg*(1-wb) + sG[2]*wb; fb = fb*(1-wb) + sB[2]*wb;
|
||||
fr = fr*(1-wa) + sR[3]*wa; fg = fg*(1-wa) + sG[3]*wa; fb = fb*(1-wa) + sB[3]*wa;
|
||||
// Upper layer (Slots 5-8) – bilinear interpolated weights
|
||||
float u1 = bilerp(m.upperSplatR, i00, i10, i01, i11, tx, tz);
|
||||
float u2 = bilerp(m.upperSplatG, i00, i10, i01, i11, tx, tz);
|
||||
float u3 = bilerp(m.upperSplatB, i00, i10, i01, i11, tx, tz);
|
||||
float u4 = bilerp(m.upperSplatA, i00, i10, i01, i11, tx, tz);
|
||||
fr = fr*(1-u1) + sR[4]*u1; fg = fg*(1-u1) + sG[4]*u1; fb = fb*(1-u1) + sB[4]*u1;
|
||||
fr = fr*(1-u2) + sR[5]*u2; fg = fg*(1-u2) + sG[5]*u2; fb = fb*(1-u2) + sB[5]*u2;
|
||||
fr = fr*(1-u3) + sR[6]*u3; fg = fg*(1-u3) + sG[6]*u3; fb = fb*(1-u3) + sB[6]*u3;
|
||||
fr = fr*(1-u4) + sR[7]*u4; fg = fg*(1-u4) + sG[7]*u4; fb = fb*(1-u4) + sB[7]*u4;
|
||||
r = clamp((int) fr); g = clamp((int) fg); b = clamp((int) fb);
|
||||
} else {
|
||||
int lum = clamp(30 + (int) (hn * 200));
|
||||
r = g = b = lum;
|
||||
}
|
||||
|
||||
if (px > 0 && px < targetSize - 1 && py > 0 && py < targetSize - 1) {
|
||||
float dx = heights[py * targetSize + (px + 1)] - heights[py * targetSize + (px - 1)];
|
||||
float dz = heights[(py + 1) * targetSize + px] - heights[(py - 1) * targetSize + px];
|
||||
float nx = -dx * 0.25f, ny = 1f, nz = -dz * 0.25f;
|
||||
float len = (float) Math.sqrt(nx*nx + ny*ny + nz*nz);
|
||||
nx /= len; ny /= len; nz /= len;
|
||||
float shade = Math.max(0.2f, Math.min(1.8f,
|
||||
0.45f + (nx * (-0.577f) + ny * 0.577f + nz * (-0.577f)) * 0.85f));
|
||||
r = clamp((int) (r * shade));
|
||||
g = clamp((int) (g * shade));
|
||||
b = clamp((int) (b * shade));
|
||||
}
|
||||
|
||||
img.setRGB(px, py, (r << 16) | (g << 8) | b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Vektor-Overlays ───────────────────────────────────────────────────
|
||||
Graphics2D gfx = img.createGraphics();
|
||||
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
float lineW = Math.max(1.5f, targetSize / 400f);
|
||||
|
||||
if (opts.showWater()) {
|
||||
final int WATER_RGB = (255 << 24) | (35 << 16) | (100 << 8) | 190;
|
||||
// Meer: Terrain unter Meeresspiegel
|
||||
for (int py = 0; py < targetSize; py++) {
|
||||
for (int px = 0; px < targetSize; px++) {
|
||||
if (heights[py * targetSize + px] < 0f) {
|
||||
img.setRGB(px, py, WATER_RGB);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Wasserflächen: nur dort sichtbar, wo Terrain unter Wasseroberfläche liegt
|
||||
for (PlacedWater w : input.waters()) {
|
||||
int[] xs = wrp(w.pointsX(), wx0, rSize, targetSize);
|
||||
int[] ys = wrp(w.pointsZ(), wz0, rSize, targetSize);
|
||||
Polygon poly = new Polygon(xs, ys, xs.length);
|
||||
Rectangle bb = poly.getBounds();
|
||||
int x0 = Math.max(0, bb.x);
|
||||
int y0 = Math.max(0, bb.y);
|
||||
int x1 = Math.min(targetSize - 1, bb.x + bb.width);
|
||||
int y1 = Math.min(targetSize - 1, bb.y + bb.height);
|
||||
float wh = w.waterHeight();
|
||||
for (int py = y0; py <= y1; py++) {
|
||||
for (int px = x0; px <= x1; px++) {
|
||||
if (poly.contains(px, py) && heights[py * targetSize + px] < wh) {
|
||||
img.setRGB(px, py, WATER_RGB);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.showAreas()) {
|
||||
gfx.setStroke(new BasicStroke(lineW));
|
||||
for (PlacedArea a : input.areas()) {
|
||||
Color c = hashColor(a.areaId());
|
||||
int[] xs = wrp(a.pointsX(), wx0, rSize, targetSize);
|
||||
int[] ys = wrp(a.pointsZ(), wz0, rSize, targetSize);
|
||||
gfx.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), 55));
|
||||
gfx.fillPolygon(xs, ys, xs.length);
|
||||
gfx.setColor(c);
|
||||
gfx.drawPolygon(xs, ys, xs.length);
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.showZones()) {
|
||||
gfx.setStroke(new BasicStroke(lineW));
|
||||
for (PlacedLocationZone z : input.zones()) {
|
||||
int[] xs = wrp(z.pointsX(), wx0, rSize, targetSize);
|
||||
int[] ys = wrp(z.pointsZ(), wz0, rSize, targetSize);
|
||||
gfx.setColor(new Color(240, 190, 40, 70));
|
||||
gfx.fillPolygon(xs, ys, xs.length);
|
||||
gfx.setColor(new Color(200, 150, 20));
|
||||
gfx.drawPolygon(xs, ys, xs.length);
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.showModels()) {
|
||||
int dotR = Math.max(1, targetSize / 600);
|
||||
gfx.setColor(new Color(160, 80, 20, 200));
|
||||
for (PlacedModel model : input.models()) {
|
||||
int mx = wrp1(model.x(), wx0, rSize, targetSize);
|
||||
int mz = wrp1(model.z(), wz0, rSize, targetSize);
|
||||
gfx.fillRect(mx - dotR, mz - dotR, dotR * 2 + 1, dotR * 2 + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.showLocations()) {
|
||||
gfx.setStroke(new BasicStroke(lineW));
|
||||
int fontSize = Math.max(8, targetSize / 140);
|
||||
gfx.setFont(new Font("SansSerif", Font.BOLD, fontSize));
|
||||
for (Location loc : input.locations()) {
|
||||
int lx = wrp1(loc.getCenterX(), wx0, rSize, targetSize);
|
||||
int lz = wrp1(loc.getCenterZ(), wz0, rSize, targetSize);
|
||||
int lr = Math.max(4, (int) (loc.getRadius() / rSize * targetSize));
|
||||
gfx.setColor(new Color(255, 255, 200, 50));
|
||||
gfx.fillOval(lx - lr, lz - lr, lr * 2, lr * 2);
|
||||
gfx.setColor(new Color(255, 220, 60));
|
||||
gfx.drawOval(lx - lr, lz - lr, lr * 2, lr * 2);
|
||||
if (loc.getId() != null && !loc.getId().isEmpty()) {
|
||||
String label = friendlyLocationName(loc.getId());
|
||||
FontMetrics fm = gfx.getFontMetrics();
|
||||
int tw = fm.stringWidth(label);
|
||||
gfx.setColor(new Color(0, 0, 0, 160));
|
||||
gfx.drawString(label, lx - tw / 2 + 1, lz - lr - 3);
|
||||
gfx.setColor(Color.WHITE);
|
||||
gfx.drawString(label, lx - tw / 2, lz - lr - 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int spx = wrp1(m.spawnX, wx0, rSize, targetSize);
|
||||
int spz = wrp1(m.spawnZ, wz0, rSize, targetSize);
|
||||
int cr = Math.max(5, targetSize / 180);
|
||||
gfx.setStroke(new BasicStroke(Math.max(2f, targetSize / 300f)));
|
||||
gfx.setColor(new Color(0, 0, 0, 120));
|
||||
gfx.drawLine(spx - cr + 1, spz + 1, spx + cr + 1, spz + 1);
|
||||
gfx.drawLine(spx + 1, spz - cr + 1, spx + 1, spz + cr + 1);
|
||||
gfx.setColor(new Color(60, 230, 90));
|
||||
gfx.drawLine(spx - cr, spz, spx + cr, spz);
|
||||
gfx.drawLine(spx, spz - cr, spx, spz + cr);
|
||||
|
||||
gfx.dispose();
|
||||
return img;
|
||||
}
|
||||
|
||||
// ── Koordinaten-Hilfsmethoden ─────────────────────────────────────────────
|
||||
|
||||
public static int worldToPixel(float worldCoord, int targetSize) {
|
||||
int px = (int)((worldCoord + WORLD_HALF) / WORLD_SIZE * (targetSize - 1));
|
||||
return Math.max(0, Math.min(targetSize - 1, px));
|
||||
}
|
||||
|
||||
public static float pixelToWorld(double pixel, double canvasSize, double panOffset, double scale) {
|
||||
return (float)((pixel - panOffset) / scale / canvasSize * WORLD_SIZE - WORLD_HALF);
|
||||
}
|
||||
|
||||
private static int[] worldToPixels(float[] coords, int targetSize) {
|
||||
int[] px = new int[coords.length];
|
||||
for (int i = 0; i < coords.length; i++) {
|
||||
px[i] = worldToPixel(coords[i], targetSize);
|
||||
}
|
||||
return px;
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
private static int clamp(int v) {
|
||||
return Math.max(0, Math.min(255, v));
|
||||
}
|
||||
|
||||
// Gibt den R-, G- oder B-Kanal (channel=0/1/2) aller 8 Splatmap-Slots zurück.
|
||||
// slotColorsRGB: 24 Werte (8 Slots × 3), 12 Werte (4 Slots, Upper-Layer = Defaults) oder null.
|
||||
private static int[] slotChannel(RenderInput input, int channel) {
|
||||
int[] rgb = input.slotColorsRGB();
|
||||
int[] def = channel == 0 ? DEF_SLOT_R : (channel == 1 ? DEF_SLOT_G : DEF_SLOT_B);
|
||||
if (rgb == null || rgb.length < 12) { return def; }
|
||||
int[] out = new int[8];
|
||||
for (int s = 0; s < 8; s++) {
|
||||
out[s] = (rgb.length >= (s + 1) * 3) ? rgb[s * 3 + channel] : def[s];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static Color hashColor(String key) {
|
||||
int hash = key == null ? 0 : key.hashCode();
|
||||
int r = 100 + ((hash & 0xFF0000) >> 16) % 120;
|
||||
int g = 100 + ((hash & 0x00FF00) >> 8) % 120;
|
||||
int b = 100 + ((hash & 0x0000FF)) % 120;
|
||||
return new Color(r, g, b);
|
||||
}
|
||||
|
||||
private static String friendlyLocationName(String id) {
|
||||
String s = id.replace("location.", "").replace(".name", "");
|
||||
return s.isEmpty() ? id : s;
|
||||
}
|
||||
|
||||
// Konvertiert eine Weltkoordinate in einen Pixel-Index innerhalb einer Region.
|
||||
private static int wrp1(float worldCoord, float regionOrigin, float regionSize, int targetSize) {
|
||||
return (int) ((worldCoord - regionOrigin) / regionSize * (targetSize - 1));
|
||||
}
|
||||
|
||||
private static int[] wrp(float[] coords, float regionOrigin, float regionSize, int targetSize) {
|
||||
int[] px = new int[coords.length];
|
||||
for (int i = 0; i < coords.length; i++) {
|
||||
px[i] = wrp1(coords[i], regionOrigin, regionSize, targetSize);
|
||||
}
|
||||
return px;
|
||||
}
|
||||
|
||||
private static int iclamp(int v, int lo, int hi) {
|
||||
return Math.max(lo, Math.min(hi, v));
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die Durchschnittsfarben aller 8 Textur-Slots (Base 1-4 + Upper 5-8)
|
||||
* durch Pixel-Sampling der Texturdateien.
|
||||
*
|
||||
* @param mapData Kartendaten (terrainTextures / upperTextures)
|
||||
* @param assetRoot Wurzelverzeichnis der Assets (enthält Textures/…)
|
||||
* @param baseFallbacks Fallback-Pfade für leere Base-Slots (kann null sein)
|
||||
* @return int[24] mit [R0,G0,B0, R1,G1,B1, ..., R7,G7,B7] für Slots 1-8
|
||||
*/
|
||||
public static int[] computeSlotColors(MapData mapData, Path assetRoot, String[] baseFallbacks) {
|
||||
int[] result = {
|
||||
71,148, 46, 115, 82, 64, 140,115, 77, 204,184,128,
|
||||
90, 80, 40, 130, 90, 60, 110, 60, 50, 180,100, 70
|
||||
};
|
||||
sampleTexColors(mapData.terrainTextures, baseFallbacks, assetRoot, result, 0);
|
||||
sampleTexColors(mapData.upperTextures, null, assetRoot, result, 4);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void sampleTexColors(String[] textures, String[] fallbacks,
|
||||
Path assetRoot, int[] result, int slotOffset) {
|
||||
if (textures == null) { return; }
|
||||
for (int slot = 0; slot < 4 && slot < textures.length; slot++) {
|
||||
String rel = textures[slot];
|
||||
if ((rel == null || rel.isEmpty()) && fallbacks != null && slot < fallbacks.length) {
|
||||
rel = fallbacks[slot];
|
||||
}
|
||||
if (rel == null || rel.isEmpty()) { continue; }
|
||||
Path texFile = assetRoot.resolve(rel);
|
||||
if (!Files.exists(texFile)) { continue; }
|
||||
try {
|
||||
BufferedImage img = ImageIO.read(texFile.toFile());
|
||||
if (img == null) { continue; }
|
||||
long sumR = 0, sumG = 0, sumB = 0, count = 0;
|
||||
int stride = Math.max(1, img.getWidth() / 32);
|
||||
for (int y = 0; y < img.getHeight(); y += stride) {
|
||||
for (int x = 0; x < img.getWidth(); x += stride) {
|
||||
int rgb = img.getRGB(x, y);
|
||||
sumR += (rgb >> 16) & 0xFF;
|
||||
sumG += (rgb >> 8) & 0xFF;
|
||||
sumB += rgb & 0xFF;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count > 0) {
|
||||
int i = (slotOffset + slot) * 3;
|
||||
result[i] = (int)(sumR / count);
|
||||
result[i + 1] = (int)(sumG / count);
|
||||
result[i + 2] = (int)(sumB / count);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
private static float bilerp(byte[] arr, int i00, int i10, int i01, int i11, float tx, float tz) {
|
||||
float v00 = (arr[i00] & 0xFF) / 255f, v10 = (arr[i10] & 0xFF) / 255f;
|
||||
float v01 = (arr[i01] & 0xFF) / 255f, v11 = (arr[i11] & 0xFF) / 255f;
|
||||
return (v00*(1-tx) + v10*tx)*(1-tz) + (v01*(1-tx) + v11*tx)*tz;
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,7 @@ public class EditorApp extends Application {
|
||||
new java.util.concurrent.ConcurrentLinkedQueue<>();
|
||||
private VBox assetPanel;
|
||||
private MapObjectsView mapObjectsView;
|
||||
private de.blight.editor.ui.WorldMapView worldMapView;
|
||||
private de.blight.editor.ui.ThumbnailManagerView thumbnailManagerView;
|
||||
private StackPane worldViewport;
|
||||
private javafx.scene.canvas.Canvas compassCanvas;
|
||||
@@ -5637,7 +5638,14 @@ public class EditorApp extends Application {
|
||||
if (isSelected) mapObjectsView.refresh();
|
||||
});
|
||||
|
||||
TabPane tabPane = new TabPane(assetsTab, karteTab);
|
||||
worldMapView = new de.blight.editor.ui.WorldMapView(() -> primaryStage);
|
||||
Tab weltkartTab = new Tab("Weltkarte", worldMapView);
|
||||
weltkartTab.setClosable(false);
|
||||
weltkartTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
|
||||
if (isSelected && !worldMapView.isLoaded()) worldMapView.loadAndRender();
|
||||
});
|
||||
|
||||
TabPane tabPane = new TabPane(assetsTab, karteTab, weltkartTab);
|
||||
tabPane.setStyle("-fx-background-color: #e8e8e8;");
|
||||
VBox.setVgrow(tabPane, Priority.ALWAYS);
|
||||
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.*;
|
||||
import de.blight.common.model.Location;
|
||||
import de.blight.common.map.WorldMapRenderer;
|
||||
import de.blight.common.map.WorldMapRenderer.RenderInput;
|
||||
import de.blight.common.map.WorldMapRenderer.RenderOptions;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Interaktive 2D-Weltkarte im Editor-Tab.
|
||||
* Zoom via Mausrad, Pan via Linksklick-Drag.
|
||||
*/
|
||||
public class WorldMapView extends VBox {
|
||||
|
||||
private static final int RENDER_SIZE = 2048;
|
||||
|
||||
private final Canvas canvas = new Canvas(100, 100);
|
||||
private final Label statusLbl = new Label("Karte noch nicht geladen");
|
||||
private final ProgressBar progress = new ProgressBar(-1);
|
||||
private final StackPane canvasPane = new StackPane(canvas);
|
||||
|
||||
private final ToggleButton layerTerrain = layerBtn("Gelände");
|
||||
private final ToggleButton layerWater = layerBtn("Wasser");
|
||||
private final ToggleButton layerAreas = layerBtn("Areas");
|
||||
private final ToggleButton layerZones = layerBtn("Zonen");
|
||||
private final ToggleButton layerLocations = layerBtn("Orte");
|
||||
private final ToggleButton layerModels = layerBtn("Modelle");
|
||||
|
||||
private WritableImage mapFxImage;
|
||||
private BufferedImage mapBuffered;
|
||||
|
||||
private double panX = 0, panY = 0;
|
||||
private double scale = 1.0;
|
||||
private double dragStartX, dragStartY, dragStartPanX, dragStartPanY;
|
||||
|
||||
private final Supplier<Stage> stageSupplier;
|
||||
private final AtomicBoolean loading = new AtomicBoolean(false);
|
||||
|
||||
private static final String[] ASSET_BASES = {
|
||||
"blight-assets/src/main/resources",
|
||||
"../blight-assets/src/main/resources",
|
||||
"assets",
|
||||
".",
|
||||
};
|
||||
|
||||
private static final String[] TERRAIN_TEX_DEFAULTS = {
|
||||
"Textures/Terrain/splat/grass.jpg",
|
||||
"Textures/Terrain/Rock2/rock.jpg",
|
||||
"Textures/Terrain/splat/dirt.jpg",
|
||||
"",
|
||||
};
|
||||
|
||||
public WorldMapView(Supplier<Stage> stageSupplier) {
|
||||
this.stageSupplier = stageSupplier;
|
||||
buildUi();
|
||||
}
|
||||
|
||||
private static Path findAssetRoot() {
|
||||
for (String base : ASSET_BASES) {
|
||||
Path p = Paths.get(base);
|
||||
if (Files.isDirectory(p.resolve("Textures"))) return p;
|
||||
}
|
||||
return Paths.get(ASSET_BASES[0]);
|
||||
}
|
||||
|
||||
private static int[] computeSlotColors(MapData mapData) {
|
||||
return WorldMapRenderer.computeSlotColors(mapData, findAssetRoot(), TERRAIN_TEX_DEFAULTS);
|
||||
}
|
||||
|
||||
private void buildUi() {
|
||||
// ── Layer-Toolbar ──────────────────────────────────────────────────────
|
||||
Button refreshBtn = new Button("Aktualisieren");
|
||||
refreshBtn.setOnAction(e -> loadAndRender());
|
||||
|
||||
Button exportBtn = new Button("Als PNG exportieren…");
|
||||
exportBtn.setOnAction(e -> exportPng());
|
||||
|
||||
ToolBar toolbar = new ToolBar(
|
||||
new Label("Layer:"),
|
||||
layerTerrain, layerWater, layerAreas, layerZones, layerLocations, layerModels,
|
||||
new Separator(),
|
||||
refreshBtn,
|
||||
exportBtn
|
||||
);
|
||||
|
||||
// ── Canvas ────────────────────────────────────────────────────────────
|
||||
canvasPane.setStyle("-fx-background-color: #1a1a2a;");
|
||||
VBox.setVgrow(canvasPane, Priority.ALWAYS);
|
||||
|
||||
canvas.widthProperty().bind(canvasPane.widthProperty());
|
||||
canvas.heightProperty().bind(canvasPane.heightProperty());
|
||||
canvas.widthProperty().addListener(obs -> redraw());
|
||||
canvas.heightProperty().addListener(obs -> redraw());
|
||||
|
||||
// Layer-Toggle → neu rendern
|
||||
layerTerrain.setOnAction(e -> rerender());
|
||||
layerWater.setOnAction(e -> rerender());
|
||||
layerAreas.setOnAction(e -> rerender());
|
||||
layerZones.setOnAction(e -> rerender());
|
||||
layerLocations.setOnAction(e -> rerender());
|
||||
layerModels.setOnAction(e -> rerender());
|
||||
|
||||
// Pan
|
||||
canvas.setOnMousePressed(e -> {
|
||||
dragStartX = e.getX();
|
||||
dragStartY = e.getY();
|
||||
dragStartPanX = panX;
|
||||
dragStartPanY = panY;
|
||||
});
|
||||
canvas.setOnMouseDragged(e -> {
|
||||
panX = dragStartPanX + (e.getX() - dragStartX);
|
||||
panY = dragStartPanY + (e.getY() - dragStartY);
|
||||
redraw();
|
||||
});
|
||||
|
||||
// Zoom (Mausrad, Ziel = Mausposition)
|
||||
canvas.setOnScroll(e -> {
|
||||
if (e.getDeltaY() == 0) return;
|
||||
double factor = e.getDeltaY() > 0 ? 1.15 : 1.0 / 1.15;
|
||||
double oldScale = scale;
|
||||
scale = Math.max(0.1, Math.min(30.0, scale * factor));
|
||||
panX = e.getX() - (e.getX() - panX) * scale / oldScale;
|
||||
panY = e.getY() - (e.getY() - panY) * scale / oldScale;
|
||||
redraw();
|
||||
});
|
||||
|
||||
// Tooltip mit Weltkoordinaten
|
||||
canvas.setOnMouseMoved(e -> {
|
||||
if (mapFxImage == null) return;
|
||||
float wx = WorldMapRenderer.pixelToWorld(e.getX(), canvas.getWidth(), panX, scale);
|
||||
float wz = WorldMapRenderer.pixelToWorld(e.getY(), canvas.getHeight(), panY, scale);
|
||||
statusLbl.setText(String.format("X: %.1f Z: %.1f", wx, wz));
|
||||
});
|
||||
|
||||
// ── Statuszeile ───────────────────────────────────────────────────────
|
||||
progress.setPrefWidth(160);
|
||||
progress.setVisible(false);
|
||||
HBox statusBar = new HBox(8, statusLbl, progress);
|
||||
statusBar.setAlignment(Pos.CENTER_LEFT);
|
||||
statusBar.setPadding(new Insets(4, 8, 4, 8));
|
||||
statusBar.setStyle("-fx-background-color: #2a2a3a;");
|
||||
statusBar.getStyleClass().add("status-bar");
|
||||
|
||||
getChildren().addAll(toolbar, canvasPane, statusBar);
|
||||
setStyle("-fx-background-color: #1a1a2a;");
|
||||
}
|
||||
|
||||
// ── Öffentliche API ───────────────────────────────────────────────────────
|
||||
|
||||
/** True wenn die Karte bereits geladen wurde. */
|
||||
public boolean isLoaded() { return mapFxImage != null || loading.get(); }
|
||||
|
||||
/** Lädt Welt-Daten und rendert die Karte (im Hintergrund). */
|
||||
public void loadAndRender() {
|
||||
if (loading.getAndSet(true)) return;
|
||||
progress.setVisible(true);
|
||||
statusLbl.setText("Lade Welt-Daten…");
|
||||
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
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();
|
||||
|
||||
Platform.runLater(() -> statusLbl.setText("Rendere Karte…"));
|
||||
|
||||
int[] slotColors = computeSlotColors(mapData);
|
||||
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
||||
BufferedImage bi = WorldMapRenderer.render(input, RENDER_SIZE, buildOptions());
|
||||
|
||||
Platform.runLater(() -> {
|
||||
mapBuffered = bi;
|
||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||
fitToView();
|
||||
redraw();
|
||||
progress.setVisible(false);
|
||||
statusLbl.setText("Bereit – " + areas.size() + " Areas, " +
|
||||
locs.size() + " Orte, " + waters.size() + " Wasser");
|
||||
loading.set(false);
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
Platform.runLater(() -> {
|
||||
progress.setVisible(false);
|
||||
statusLbl.setText("Fehler: " + ex.getMessage());
|
||||
loading.set(false);
|
||||
});
|
||||
}
|
||||
}, "WorldMapRenderer");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
// ── Interne Methoden ──────────────────────────────────────────────────────
|
||||
|
||||
private void rerender() {
|
||||
if (mapBuffered == null) { loadAndRender(); return; }
|
||||
if (loading.getAndSet(true)) return;
|
||||
progress.setVisible(true);
|
||||
statusLbl.setText("Rendere…");
|
||||
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
// Bereits geladene Daten nochmal laden, damit Layer-Wechsel korrekt
|
||||
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[] slotColors2 = computeSlotColors(mapData);
|
||||
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors2);
|
||||
BufferedImage bi = WorldMapRenderer.render(input, RENDER_SIZE, buildOptions());
|
||||
Platform.runLater(() -> {
|
||||
mapBuffered = bi;
|
||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||
redraw();
|
||||
progress.setVisible(false);
|
||||
statusLbl.setText("Bereit");
|
||||
loading.set(false);
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
Platform.runLater(() -> {
|
||||
progress.setVisible(false);
|
||||
statusLbl.setText("Fehler: " + ex.getMessage());
|
||||
loading.set(false);
|
||||
});
|
||||
}
|
||||
}, "WorldMapRerender");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private void redraw() {
|
||||
GraphicsContext gc = canvas.getGraphicsContext2D();
|
||||
double w = canvas.getWidth(), h = canvas.getHeight();
|
||||
|
||||
gc.setFill(javafx.scene.paint.Color.rgb(20, 20, 35));
|
||||
gc.fillRect(0, 0, w, h);
|
||||
|
||||
if (mapFxImage == null) {
|
||||
gc.setFill(Color.GRAY);
|
||||
gc.fillText("Karte noch nicht geladen – 'Aktualisieren' klicken", 20, 40);
|
||||
return;
|
||||
}
|
||||
|
||||
double imgW = mapFxImage.getWidth() * scale;
|
||||
double imgH = mapFxImage.getHeight() * scale;
|
||||
gc.drawImage(mapFxImage, panX, panY, imgW, imgH);
|
||||
}
|
||||
|
||||
private void fitToView() {
|
||||
if (canvas.getWidth() <= 0 || mapFxImage == null) return;
|
||||
double sw = canvas.getWidth() / mapFxImage.getWidth();
|
||||
double sh = canvas.getHeight() / mapFxImage.getHeight();
|
||||
scale = Math.min(sw, sh);
|
||||
panX = (canvas.getWidth() - mapFxImage.getWidth() * scale) / 2;
|
||||
panY = (canvas.getHeight() - mapFxImage.getHeight() * scale) / 2;
|
||||
}
|
||||
|
||||
private void exportPng() {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle("Karte als PNG exportieren");
|
||||
fc.getExtensionFilters().add(new FileChooser.ExtensionFilter("PNG-Bild", "*.png"));
|
||||
fc.setInitialFileName("weltkarte.png");
|
||||
File file = fc.showSaveDialog(stageSupplier.get());
|
||||
if (file == null) return;
|
||||
|
||||
statusLbl.setText("Exportiere 4096×4096 PNG…");
|
||||
progress.setVisible(true);
|
||||
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
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[] slotColors3 = computeSlotColors(mapData);
|
||||
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors3);
|
||||
BufferedImage bi = WorldMapRenderer.render(input, 4096, buildOptions());
|
||||
ImageIO.write(bi, "PNG", file);
|
||||
Platform.runLater(() -> {
|
||||
progress.setVisible(false);
|
||||
statusLbl.setText("Exportiert: " + file.getName());
|
||||
});
|
||||
} catch (IOException ex) {
|
||||
Platform.runLater(() -> {
|
||||
progress.setVisible(false);
|
||||
statusLbl.setText("Export-Fehler: " + ex.getMessage());
|
||||
});
|
||||
}
|
||||
}, "MapPngExport");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private RenderOptions buildOptions() {
|
||||
return new RenderOptions(
|
||||
layerTerrain.isSelected(),
|
||||
layerTerrain.isSelected(),
|
||||
layerWater.isSelected(),
|
||||
layerAreas.isSelected(),
|
||||
layerZones.isSelected(),
|
||||
layerLocations.isSelected(),
|
||||
layerModels.isSelected()
|
||||
);
|
||||
}
|
||||
|
||||
private static ToggleButton layerBtn(String label) {
|
||||
ToggleButton btn = new ToggleButton(label);
|
||||
btn.setSelected(true);
|
||||
return btn;
|
||||
}
|
||||
}
|
||||
@@ -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