Files
Battle-for-Tismo/client/js/map_view/utils.js
T
adrien 6f73e2cc51 Reworked the world around a fixed sea share and settled coasts
World generation now aims at a fixed share of the ice-free tiles (60%) instead of a minimum continent count, bisecting the water level for it rather than sweeping every step, and still joins the ocean into one body. Ice and tundra are the outermost rows only, scaled down on small maps so the fixtures keep usable land.

Cities score their site, coastal plains best and landlocked worst, avoiding tundra and desert. Every nation that holds a coast gets a port city on each island it owns, and any landmass left empty is settled by the nearest nation, so no land starts unowned. A coastal city opens with a port already built.

Bundles the in-progress work in the tree: the commodity monthly price series and inflation the central bank's chart reads, and the rolling city-to-city migration graph the population map draws as flow arrows.
2026-09-22 17:00:38 +02:00

168 lines
5.4 KiB
JavaScript

// Pure helpers for the map view: geometry, path timing and colour blending.
// Nothing here touches the DOM or the MapView instance, so it is all trivially
// testable.
import { HEX_W, HEX_H } from "../../../shared/hex.js";
// Order-independent FNV-style hash of an [x, y, ...] entry array, used to skip
// rebuilding a layer when the snapshot did not actually change it.
export function hashEntries(entries) {
let hash = 2166136261;
for (const entry of entries) {
hash = Math.imul(hash ^ (entry[0] + 4096), 16777619);
hash = Math.imul(hash ^ (entry[1] + 4096), 16777619);
hash = Math.imul(hash ^ (entry.length > 2 ? entry[2] + 8 : 0), 16777619);
}
return `${entries.length}:${(hash >>> 0).toString(36)}`;
}
// How far along its path a unit is, in segment-hours, from a snapshot.
export function hoursAlong(data, segmentHours) {
const index = Math.min(data.pathIndex || 0, segmentHours.length);
let total = 0;
for (let i = 0; i < index; i++) total += segmentHours[i];
return total + (data.progressHours || 0);
}
export function samePath(a, b) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i].x !== b[i].x || a[i].y !== b[i].y) return false;
}
return true;
}
// The segment index a motion is currently on and how far into it it is.
export function pathProgress(motion) {
let remaining = motion.pathHours;
let index = 0;
while (index < motion.segmentHours.length) {
const step = motion.segmentHours[index];
if (step <= 0) {
index += 1;
continue;
}
if (remaining < step) break;
remaining -= step;
index += 1;
}
return { index, remaining };
}
export function nearestCorners(center, target) {
const half = { x: HEX_W / 2, y: HEX_H / 2 };
const corners = [
{ x: half.x, y: 0 },
{ x: half.x * 0.5, y: -half.y },
{ x: -half.x * 0.5, y: -half.y },
{ x: -half.x, y: 0 },
{ x: -half.x * 0.5, y: half.y },
{ x: half.x * 0.5, y: half.y },
];
let first = 0;
let second = 1;
let firstDistance = Infinity;
let secondDistance = Infinity;
for (let i = 0; i < corners.length; i++) {
const point = { x: center.x + corners[i].x, y: center.y + corners[i].y };
const distance = (point.x - target.x) ** 2 + (point.y - target.y) ** 2;
if (distance < firstDistance) {
secondDistance = firstDistance;
second = first;
firstDistance = distance;
first = i;
} else if (distance < secondDistance) {
secondDistance = distance;
second = i;
}
}
return [
{ x: center.x + corners[first].x, y: center.y + corners[first].y },
{ x: center.x + corners[second].x, y: center.y + corners[second].y },
];
}
// Signed distance to nudge an edge segment along its normal so it moves towards
// `center` (the owning tile). Used to keep neighbours' borders side by side
// instead of overlapping on the shared edge.
export function inwardOffset(a, b, center, distance) {
const dx = b.x - a.x;
const dy = b.y - a.y;
const length = Math.hypot(dx, dy) || 1;
const nx = -dy / length;
const ny = dx / length;
const midX = (a.x + b.x) / 2;
const midY = (a.y + b.y) / 2;
const dot = (center.x - midX) * nx + (center.y - midY) * ny;
return dot >= 0 ? distance : -distance;
}
export function makeSegment(a, b, colour, width, offset = 0) {
const div = document.createElement("div");
div.className = "border-seg";
const dx = b.x - a.x;
const dy = b.y - a.y;
const length = Math.hypot(dx, dy);
const angle = Math.atan2(dy, dx);
div.style.left = `${a.x}px`;
div.style.top = `${a.y - width / 2}px`;
div.style.width = `${length}px`;
div.style.height = `${width}px`;
div.style.background = colour;
div.style.transform = offset === 0
? `rotate(${angle}rad)`
: `rotate(${angle}rad) translateY(${offset}px)`;
div.style.borderRadius = `${width / 2}px`;
return div;
}
// A shaft plus a triangular head, from `a` to `b`, with the tip landing on
// `b`. `width` sets the shaft's thickness; the head grows with it so a thick
// arrow still reads as an arrow.
export function makeArrow(a, b, colour, width) {
const dx = b.x - a.x;
const dy = b.y - a.y;
const length = Math.hypot(dx, dy);
const angle = Math.atan2(dy, dx);
const head = Math.max(8, width * 2.4);
const group = document.createElement("div");
group.className = "migration-arrow";
if (length > head) {
const shaftEnd = {
x: b.x - (dx / length) * head,
y: b.y - (dy / length) * head,
};
group.appendChild(makeSegment(a, shaftEnd, colour, width));
}
const tip = document.createElement("div");
tip.className = "migration-arrow-head";
tip.style.left = `${b.x - head}px`;
tip.style.top = `${b.y - head / 2}px`;
tip.style.width = `${head}px`;
tip.style.height = `${head}px`;
tip.style.background = colour;
tip.style.transform = `rotate(${angle}rad)`;
group.appendChild(tip);
return group;
}
export function parseColour(value) {
if (value.charAt(0) === "#") {
const hex = value.slice(1);
return [
parseInt(hex.slice(0, 2), 16),
parseInt(hex.slice(2, 4), 16),
parseInt(hex.slice(4, 6), 16),
];
}
// Tolerates an "rgb(r, g, b)" string as well as a #rrggbb one.
const match = value.match(/(\d+)\D+(\d+)\D+(\d+)/);
if (match) return [Number(match[1]), Number(match[2]), Number(match[3])];
return [0, 0, 0];
}
export function clamp(value, min, max) {
if (min > max) return (min + max) / 2;
return Math.max(min, Math.min(max, value));
}