Zwischenstand Minimap

This commit is contained in:
2026-08-24 20:39:22 +02:00
parent 9238378bc8
commit f8dbbb09c7
8 changed files with 785 additions and 149 deletions

View File

@@ -6,6 +6,7 @@ import de.blight.common.model.trigger.TriggerIO;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.Locale;
public final class AreaIO {
@@ -19,7 +20,7 @@ public final class AreaIO {
Path p = getPath();
Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("# polygon\tareaId\ttriggersJson");
w.write("# polygon\tareaId\ttriggersJson\tlabelX\tlabelZ");
w.newLine();
for (PlacedArea a : areas) {
w.write(SoundAreaIO.encodePolygon(a.pointsX(), a.pointsZ()));
@@ -27,6 +28,10 @@ public final class AreaIO {
w.write(a.areaId());
w.write('\t');
w.write(TriggerIO.serializeList(a.triggers()));
w.write('\t');
w.write(String.format(Locale.ROOT, "%.3f", a.labelX()));
w.write('\t');
w.write(String.format(Locale.ROOT, "%.3f", a.labelZ()));
w.newLine();
}
}
@@ -46,7 +51,9 @@ public final class AreaIO {
if (pts[0].length < 3) continue;
String areaId = f.length > 1 ? f[1].strip() : "";
List<Trigger> triggers = f.length > 2 ? TriggerIO.deserializeList(f[2]) : new ArrayList<>();
list.add(new PlacedArea(pts[0], pts[1], areaId, triggers));
float labelX = f.length > 3 ? Float.parseFloat(f[3].strip()) : Float.NaN;
float labelZ = f.length > 4 ? Float.parseFloat(f[4].strip()) : Float.NaN;
list.add(new PlacedArea(pts[0], pts[1], areaId, triggers, labelX, labelZ));
} catch (Exception ignored) {}
}
return list;

View File

@@ -26,7 +26,7 @@ public final class LocationIO {
Path p = getPath();
Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("# nameId\tcenterX\tcenterZ\tradius\ttriggersJson");
w.write("# nameId\tcenterX\tcenterZ\tradius\ttriggersJson\tshowOnMap\tlabelX\tlabelZ");
w.newLine();
for (Location loc : locations) {
w.write(loc.getId());
@@ -38,6 +38,12 @@ public final class LocationIO {
w.write(String.format(Locale.ROOT, "%.3f", loc.getRadius()));
w.write('\t');
w.write(TriggerIO.serializeList(loc.getTriggers()));
w.write('\t');
w.write(Boolean.toString(loc.isShowOnMap()));
w.write('\t');
w.write(String.format(Locale.ROOT, "%.3f", loc.getLabelX()));
w.write('\t');
w.write(String.format(Locale.ROOT, "%.3f", loc.getLabelZ()));
w.newLine();
}
}
@@ -62,6 +68,9 @@ public final class LocationIO {
List<Trigger> triggers = TriggerIO.deserializeList(f[4].strip());
loc.setTriggers(triggers);
}
loc.setShowOnMap(f.length > 5 ? Boolean.parseBoolean(f[5].strip()) : true);
loc.setLabelX(f.length > 6 ? Float.parseFloat(f[6].strip()) : Float.NaN);
loc.setLabelZ(f.length > 7 ? Float.parseFloat(f[7].strip()) : Float.NaN);
list.add(loc);
} catch (NumberFormatException ignored) {}
}

View File

@@ -8,9 +8,14 @@ public record PlacedArea(
float[] pointsX,
float[] pointsZ,
String areaId,
List<Trigger> triggers
List<Trigger> triggers,
float labelX,
float labelZ
) {
public PlacedArea(float[] pointsX, float[] pointsZ, String areaId) {
this(pointsX, pointsZ, areaId, List.of());
this(pointsX, pointsZ, areaId, List.of(), Float.NaN, Float.NaN);
}
public PlacedArea(float[] pointsX, float[] pointsZ, String areaId, List<Trigger> triggers) {
this(pointsX, pointsZ, areaId, triggers, Float.NaN, Float.NaN);
}
}

View File

@@ -5,6 +5,7 @@ import de.blight.common.model.Location;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.Path2D;
import java.awt.image.BufferedImage;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -42,14 +43,34 @@ public final class WorldMapRenderer {
boolean showTerrain,
boolean showSplatColors,
boolean showWater,
boolean showWaterWaves,
boolean showAreas,
boolean showZones,
boolean showLocations,
boolean showModels
) {
public static RenderOptions all() {
return new RenderOptions(true, true, true, true, true, true, true);
// Compat: alte 7-Parameter-Form → showWaterWaves = showWater
public RenderOptions(boolean showTerrain, boolean showSplatColors, boolean showWater,
boolean showAreas, boolean showZones, boolean showLocations, boolean showModels) {
this(showTerrain, showSplatColors, showWater, showWater,
showAreas, showZones, showLocations, showModels);
}
public static RenderOptions all() {
return new RenderOptions(true, true, true, true, true, true, true, true);
}
}
public static boolean[] buildSeaMask(MapData m, int size) {
int TV = MapData.TERRAIN_VERTS;
boolean[] mask = new boolean[size * size];
for (int py = 0; py < size; py++) {
for (int px = 0; px < size; px++) {
int hx = Math.min((int)((float) px / (size - 1) * (TV - 1)), TV - 1);
int hz = Math.min((int)((float) py / (size - 1) * (TV - 1)), TV - 1);
mask[py * size + px] = m.terrainHeight[hz * TV + hx] < 0f;
}
}
return mask;
}
// Default-Slot-Farben für Slots 1-8 (Base-Layer 1-4 + Upper-Layer 5-8)
@@ -147,47 +168,146 @@ public final class WorldMapRenderer {
// Wasser
if (opts.showWater()) {
final int WATER_RGB = (255 << 24) | (35 << 16) | (100 << 8) | 190;
// Meer: Terrain unter Meeresspiegel
// ── Weißfüllung ───────────────────────────────────────────────────────
for (int py = 0; py < targetSize; py++) {
for (int px = 0; px < targetSize; px++) {
if (heights[py * targetSize + px] < 0f) {
img.setRGB(px, py, WATER_RGB);
}
if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, 0xFFFFFFFF);
}
}
// 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);
Rectangle bb = poly.getBounds();
int x0 = Math.max(0, bb.x), 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 (poly.contains(px, py) && heights[py * targetSize + px] < wh)
img.setRGB(px, py, 0xFFFFFFFF);
}
}
}
// ── Wellenmarken ──────────────────────────────────────────────────────
if (opts.showWaterWaves()) {
float wmW = Math.max(20f, targetSize / 70f);
float wmA = wmW * 0.2f, spX = wmW * 1.8f, spY = wmW * 1.05f;
gfx.setColor(Color.BLACK);
gfx.setStroke(new BasicStroke(Math.max(0.7f, lineW * 0.4f),
BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
// Meer: versetzt + gejittert
int seaRow = 0;
for (float wy = spY * 0.5f; wy < targetSize; wy += spY, seaRow++) {
float rowOff = (seaRow & 1) == 1 ? spX * 0.5f : 0f;
int seaCol = 0;
for (float wx = rowOff; wx + wmW <= targetSize; wx += spX, seaCol++) {
float jx = (float)(Math.abs(Math.sin(seaRow * 73.1 + seaCol * 157.3)) * spX * 0.25);
float jy = (float)(Math.sin(seaRow * 211.7 + seaCol * 89.5) * spY * 0.2);
float ox = wx + jx, oy = wy + jy;
int cx = (int)(ox + wmW * 0.5f), cy = (int)oy;
if (cx >= 0 && cx < targetSize && cy >= 0 && cy < targetSize
&& ox >= 0 && ox + wmW < targetSize
&& heights[cy * targetSize + cx] < 0f) {
Path2D mark = new Path2D.Float();
mark.moveTo(ox, oy);
mark.curveTo(ox + wmW*0.3f, oy - wmA, ox + wmW*0.7f, oy + wmA, ox + wmW, oy);
gfx.draw(mark);
}
}
}
// Wasserflächen: nur wenn groß genug
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();
if (bb.width <= wmW * 1.5f || bb.height <= spY) continue;
gfx.setClip(poly);
int wRow = 0;
for (float wy = bb.y + spY * 0.5f; wy < bb.y + bb.height; wy += spY, wRow++) {
float rowOff = (wRow & 1) == 1 ? spX * 0.5f : 0f;
int wCol = 0;
for (float wx = bb.x + rowOff; wx + wmW <= bb.x + bb.width; wx += spX, wCol++) {
float jx = (float)(Math.abs(Math.sin(wRow * 73.1 + wCol * 157.3)) * spX * 0.25);
float jy = (float)(Math.sin(wRow * 211.7 + wCol * 89.5) * spY * 0.2);
float ox = wx + jx, oy = wy + jy;
if (ox < bb.x || ox + wmW > bb.x + bb.width) continue;
Path2D mark = new Path2D.Float();
mark.moveTo(ox, oy);
mark.curveTo(ox + wmW*0.3f, oy - wmA, ox + wmW*0.7f, oy + wmA, ox + wmW, oy);
gfx.draw(mark);
}
}
gfx.setClip(null);
}
}
// ── Uferlinie ─────────────────────────────────────────────────────────
if (opts.showWaterWaves()) {
gfx.setColor(Color.BLACK);
gfx.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
// Wasserflächen: Polygon-Umriss
for (PlacedWater w : input.waters()) {
int[] xs = worldToPixels(w.pointsX(), targetSize);
int[] ys = worldToPixels(w.pointsZ(), targetSize);
gfx.drawPolygon(xs, ys, xs.length);
}
// Meer: Pixel-Küstenlinie (morphologisch aufgeweitet)
int bw = 3;
boolean[] cur = new boolean[targetSize * targetSize];
boolean[] nxt = new boolean[targetSize * targetSize];
for (int py = 1; py < targetSize - 1; py++) {
for (int px = 1; px < targetSize - 1; px++) {
if (heights[py * targetSize + px] >= 0f) continue;
if (heights[py * targetSize + (px-1)] >= 0f || heights[py * targetSize + (px+1)] >= 0f ||
heights[(py-1) * targetSize + px] >= 0f || heights[(py+1) * targetSize + px] >= 0f)
cur[py * targetSize + px] = true;
}
}
for (int pass = 1; pass < bw; pass++) {
System.arraycopy(cur, 0, nxt, 0, cur.length);
for (int py = 1; py < targetSize - 1; py++) {
for (int px = 1; px < targetSize - 1; px++) {
if (!cur[py * targetSize + px]) continue;
if (heights[py * targetSize + (px-1)] < 0f) nxt[py * targetSize + (px-1)] = true;
if (heights[py * targetSize + (px+1)] < 0f) nxt[py * targetSize + (px+1)] = true;
if (heights[(py-1) * targetSize + px] < 0f) nxt[(py-1) * targetSize + px] = true;
if (heights[(py+1) * targetSize + px] < 0f) nxt[(py+1) * targetSize + px] = true;
}
}
boolean[] tmp = cur; cur = nxt; nxt = tmp;
}
for (int i = 0, n = cur.length; i < n; i++) {
if (cur[i]) img.setRGB(i % targetSize, i / targetSize, 0xFF000000);
}
}
}
// Area-Polygone
if (opts.showAreas()) {
gfx.setStroke(new BasicStroke(lineW));
float dash = lineW * 7f, gap = lineW * 4f;
BasicStroke dashed = new BasicStroke(lineW * 1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND,
10f, new float[]{dash, gap}, 0f);
int aFontSize = Math.max(8, targetSize / 160);
gfx.setFont(new Font("SansSerif", Font.BOLD, aFontSize));
for (PlacedArea a : input.areas()) {
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);
gfx.setStroke(dashed);
gfx.setColor(Color.BLACK);
gfx.draw(toPath(xs, ys));
String lbl = friendlyAreaName(a.areaId());
if (lbl != null) {
float[] cen = centroid(a.pointsX(), a.pointsZ());
float lx = Float.isNaN(a.labelX()) ? cen[0] : a.labelX();
float lz = Float.isNaN(a.labelZ()) ? cen[1] : a.labelZ();
int plx = worldToPixel(lx, targetSize);
int plz = worldToPixel(lz, targetSize);
drawLabel(gfx, lbl, plx, plz);
}
}
}
@@ -215,45 +335,21 @@ public final class WorldMapRenderer {
}
}
// Locations (Kreise + Labels)
// Locations (nur Label)
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);
}
if (!loc.isShowOnMap()) continue;
if (loc.getId() == null || loc.getId().isEmpty()) continue;
float wx = Float.isNaN(loc.getLabelX()) ? loc.getCenterX() : loc.getLabelX();
float wz = Float.isNaN(loc.getLabelZ()) ? loc.getCenterZ() : loc.getLabelZ();
int lx = worldToPixel(wx, targetSize);
int lz = worldToPixel(wz, targetSize);
drawLabel(gfx, friendlyLocationName(loc.getId()), lx, lz);
}
}
// 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;
}
@@ -375,46 +471,145 @@ public final class WorldMapRenderer {
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
// ── Weißfüllung ───────────────────────────────────────────────────────
for (int py = 0; py < targetSize; py++) {
for (int px = 0; px < targetSize; px++) {
if (heights[py * targetSize + px] < 0f) {
img.setRGB(px, py, WATER_RGB);
}
if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, 0xFFFFFFFF);
}
}
// 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);
Rectangle bb = poly.getBounds();
int x0 = Math.max(0, bb.x), 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 (poly.contains(px, py) && heights[py * targetSize + px] < wh)
img.setRGB(px, py, 0xFFFFFFFF);
}
}
}
// ── Wellenmarken ──────────────────────────────────────────────────────
if (opts.showWaterWaves()) {
float wmW = Math.max(20f, targetSize / 70f);
float wmA = wmW * 0.2f, spX = wmW * 1.8f, spY = wmW * 1.05f;
gfx.setColor(Color.BLACK);
gfx.setStroke(new BasicStroke(Math.max(0.7f, lineW * 0.4f),
BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
// Meer: versetzt + gejittert
int seaRow = 0;
for (float wy = spY * 0.5f; wy < targetSize; wy += spY, seaRow++) {
float rowOff = (seaRow & 1) == 1 ? spX * 0.5f : 0f;
int seaCol = 0;
for (float wx = rowOff; wx + wmW <= targetSize; wx += spX, seaCol++) {
float jx = (float)(Math.abs(Math.sin(seaRow * 73.1 + seaCol * 157.3)) * spX * 0.25);
float jy = (float)(Math.sin(seaRow * 211.7 + seaCol * 89.5) * spY * 0.2);
float ox = wx + jx, oy = wy + jy;
int cx = (int)(ox + wmW * 0.5f), cy = (int)oy;
if (cx >= 0 && cx < targetSize && cy >= 0 && cy < targetSize
&& ox >= 0 && ox + wmW < targetSize
&& heights[cy * targetSize + cx] < 0f) {
Path2D mark = new Path2D.Float();
mark.moveTo(ox, oy);
mark.curveTo(ox + wmW*0.3f, oy - wmA, ox + wmW*0.7f, oy + wmA, ox + wmW, oy);
gfx.draw(mark);
}
}
}
// Wasserflächen: nur wenn groß genug
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();
if (bb.width <= wmW * 1.5f || bb.height <= spY) continue;
gfx.setClip(poly);
int wRow = 0;
for (float wy = bb.y + spY * 0.5f; wy < bb.y + bb.height; wy += spY, wRow++) {
float rowOff = (wRow & 1) == 1 ? spX * 0.5f : 0f;
int wCol = 0;
for (float wx = bb.x + rowOff; wx + wmW <= bb.x + bb.width; wx += spX, wCol++) {
float jx = (float)(Math.abs(Math.sin(wRow * 73.1 + wCol * 157.3)) * spX * 0.25);
float jy = (float)(Math.sin(wRow * 211.7 + wCol * 89.5) * spY * 0.2);
float ox = wx + jx, oy = wy + jy;
if (ox < bb.x || ox + wmW > bb.x + bb.width) continue;
Path2D mark = new Path2D.Float();
mark.moveTo(ox, oy);
mark.curveTo(ox + wmW*0.3f, oy - wmA, ox + wmW*0.7f, oy + wmA, ox + wmW, oy);
gfx.draw(mark);
}
}
gfx.setClip(null);
}
}
// ── Uferlinie ─────────────────────────────────────────────────────────
if (opts.showWaterWaves()) {
gfx.setColor(Color.BLACK);
gfx.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
// Wasserflächen: Polygon-Umriss
for (PlacedWater w : input.waters()) {
int[] xs = wrp(w.pointsX(), wx0, rSize, targetSize);
int[] ys = wrp(w.pointsZ(), wz0, rSize, targetSize);
gfx.drawPolygon(xs, ys, xs.length);
}
// Meer: Pixel-Küstenlinie (morphologisch aufgeweitet)
int bw = 3;
boolean[] cur = new boolean[targetSize * targetSize];
boolean[] nxt = new boolean[targetSize * targetSize];
for (int py = 1; py < targetSize - 1; py++) {
for (int px = 1; px < targetSize - 1; px++) {
if (heights[py * targetSize + px] >= 0f) continue;
if (heights[py * targetSize + (px-1)] >= 0f || heights[py * targetSize + (px+1)] >= 0f ||
heights[(py-1) * targetSize + px] >= 0f || heights[(py+1) * targetSize + px] >= 0f)
cur[py * targetSize + px] = true;
}
}
for (int pass = 1; pass < bw; pass++) {
System.arraycopy(cur, 0, nxt, 0, cur.length);
for (int py = 1; py < targetSize - 1; py++) {
for (int px = 1; px < targetSize - 1; px++) {
if (!cur[py * targetSize + px]) continue;
if (heights[py * targetSize + (px-1)] < 0f) nxt[py * targetSize + (px-1)] = true;
if (heights[py * targetSize + (px+1)] < 0f) nxt[py * targetSize + (px+1)] = true;
if (heights[(py-1) * targetSize + px] < 0f) nxt[(py-1) * targetSize + px] = true;
if (heights[(py+1) * targetSize + px] < 0f) nxt[(py+1) * targetSize + px] = true;
}
}
boolean[] tmp = cur; cur = nxt; nxt = tmp;
}
for (int i = 0, n = cur.length; i < n; i++) {
if (cur[i]) img.setRGB(i % targetSize, i / targetSize, 0xFF000000);
}
}
}
if (opts.showAreas()) {
gfx.setStroke(new BasicStroke(lineW));
float dash = lineW * 7f, gap = lineW * 4f;
BasicStroke dashed = new BasicStroke(lineW * 1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND,
10f, new float[]{dash, gap}, 0f);
int aFontSize = Math.max(8, targetSize / 160);
gfx.setFont(new Font("SansSerif", Font.BOLD, aFontSize));
for (PlacedArea a : input.areas()) {
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);
gfx.setStroke(dashed);
gfx.setColor(Color.BLACK);
gfx.draw(toPath(xs, ys));
String lbl = friendlyAreaName(a.areaId());
if (lbl != null) {
float[] cen = centroid(a.pointsX(), a.pointsZ());
float lx = Float.isNaN(a.labelX()) ? cen[0] : a.labelX();
float lz = Float.isNaN(a.labelZ()) ? cen[1] : a.labelZ();
int plx = wrp1(lx, wx0, rSize, targetSize);
int plz = wrp1(lz, wz0, rSize, targetSize);
drawLabel(gfx, lbl, plx, plz);
}
}
}
@@ -441,40 +636,19 @@ public final class WorldMapRenderer {
}
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);
}
if (!loc.isShowOnMap()) continue;
if (loc.getId() == null || loc.getId().isEmpty()) continue;
float wx = Float.isNaN(loc.getLabelX()) ? loc.getCenterX() : loc.getLabelX();
float wz = Float.isNaN(loc.getLabelZ()) ? loc.getCenterZ() : loc.getLabelZ();
int lx = wrp1(wx, wx0, rSize, targetSize);
int lz = wrp1(wz, wz0, rSize, targetSize);
drawLabel(gfx, friendlyLocationName(loc.getId()), lx, lz);
}
}
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;
}
@@ -517,19 +691,41 @@ public final class WorldMapRenderer {
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;
}
private static String friendlyAreaName(String id) {
if (id == null || id.isBlank()) return null;
String s = id.replace("area.", "");
if (s.endsWith(".name")) s = s.substring(0, s.length() - 5);
return s.isBlank() ? null : s;
}
private static float[] centroid(float[] xs, float[] zs) {
float cx = 0f, cz = 0f;
for (int i = 0; i < xs.length; i++) { cx += xs[i]; cz += zs[i]; }
return new float[]{cx / xs.length, cz / xs.length};
}
private static Path2D toPath(int[] xs, int[] ys) {
Path2D p = new Path2D.Float();
p.moveTo(xs[0], ys[0]);
for (int i = 1; i < xs.length; i++) p.lineTo(xs[i], ys[i]);
p.closePath();
return p;
}
private static void drawLabel(Graphics2D g, String label, int x, int y) {
FontMetrics fm = g.getFontMetrics();
int tw = fm.stringWidth(label);
g.setColor(new Color(0, 0, 0, 180));
g.drawString(label, x - tw / 2 + 1, y + 1);
g.setColor(Color.WHITE);
g.drawString(label, x - tw / 2, y);
}
// 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));

View File

@@ -15,7 +15,10 @@ public class Location {
private float centerX;
private float centerZ;
private float radius;
private List<Trigger> triggers = new ArrayList<>();
private List<Trigger> triggers = new ArrayList<>();
private boolean showOnMap = true;
private float labelX = Float.NaN;
private float labelZ = Float.NaN;
/** Leitet die ID aus dem TextReference-Schlüssel ab eindeutiger Bezeichner der Location. */
public String getId() { return name != null ? name.id() : ""; }