|
|
|
|
@@ -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));
|
|
|
|
|
|