|
|
|
|
@ -8,7 +8,6 @@ interface MinimapProps {
|
|
|
|
|
heroX: number;
|
|
|
|
|
heroY: number;
|
|
|
|
|
towns: Town[];
|
|
|
|
|
routeWaypoints: Array<{ x: number; y: number }> | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 0 = свернуто, 1 = маленькая, 2 = большая */
|
|
|
|
|
@ -40,20 +39,6 @@ const TERRAIN_BG: Record<string, string> = {
|
|
|
|
|
|
|
|
|
|
const DEFAULT_BG = '#1e2420';
|
|
|
|
|
|
|
|
|
|
function hexToRgb(hex: string): [number, number, number] {
|
|
|
|
|
const s = hex.replace('#', '');
|
|
|
|
|
return [
|
|
|
|
|
parseInt(s.slice(0, 2), 16),
|
|
|
|
|
parseInt(s.slice(2, 4), 16),
|
|
|
|
|
parseInt(s.slice(4, 6), 16),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const TERRAIN_RGB = new Map<string, [number, number, number]>(
|
|
|
|
|
Object.entries(TERRAIN_BG).map(([k, v]) => [k, hexToRgb(v)]),
|
|
|
|
|
);
|
|
|
|
|
const DEFAULT_RGB = hexToRgb(DEFAULT_BG);
|
|
|
|
|
|
|
|
|
|
// ---- Styles ----
|
|
|
|
|
|
|
|
|
|
const containerStyle: CSSProperties = {
|
|
|
|
|
@ -104,13 +89,11 @@ function modeLabel(mode: MapMode): string {
|
|
|
|
|
|
|
|
|
|
// ---- Component ----
|
|
|
|
|
|
|
|
|
|
export function Minimap({ heroX, heroY, towns, routeWaypoints }: MinimapProps) {
|
|
|
|
|
export function Minimap({ heroX, heroY, towns }: MinimapProps) {
|
|
|
|
|
const [mode, setMode] = useState<MapMode>(1);
|
|
|
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
|
|
|
const lastDrawPos = useRef<{ x: number; y: number }>({ x: NaN, y: NaN });
|
|
|
|
|
const lastHeroTile = useRef<{ tx: number; ty: number } | null>(null);
|
|
|
|
|
const lastRouteKey = useRef<string>('');
|
|
|
|
|
const lastTownsKey = useRef<string>('');
|
|
|
|
|
|
|
|
|
|
const size = mode === 2 ? SIZE_LARGE : SIZE_SMALL;
|
|
|
|
|
const collapsed = mode === 0;
|
|
|
|
|
@ -119,26 +102,12 @@ export function Minimap({ heroX, heroY, towns, routeWaypoints }: MinimapProps) {
|
|
|
|
|
if (!collapsed) {
|
|
|
|
|
lastDrawPos.current = { x: NaN, y: NaN };
|
|
|
|
|
lastHeroTile.current = null;
|
|
|
|
|
lastRouteKey.current = '';
|
|
|
|
|
lastTownsKey.current = '';
|
|
|
|
|
}
|
|
|
|
|
}, [collapsed, size]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (collapsed) return;
|
|
|
|
|
|
|
|
|
|
const routeKey =
|
|
|
|
|
routeWaypoints && routeWaypoints.length >= 2
|
|
|
|
|
? routeWaypoints.map((p) => `${p.x},${p.y}`).join(';')
|
|
|
|
|
: '';
|
|
|
|
|
const routeChanged = routeKey !== lastRouteKey.current;
|
|
|
|
|
|
|
|
|
|
const townsKey =
|
|
|
|
|
towns.length === 0
|
|
|
|
|
? ''
|
|
|
|
|
: towns.map((t) => `${t.id}:${t.worldX}:${t.worldY}`).join(';');
|
|
|
|
|
const townsChanged = townsKey !== lastTownsKey.current;
|
|
|
|
|
|
|
|
|
|
const tileX = Math.floor(heroX);
|
|
|
|
|
const tileY = Math.floor(heroY);
|
|
|
|
|
const last = lastDrawPos.current;
|
|
|
|
|
@ -146,17 +115,7 @@ export function Minimap({ heroX, heroY, towns, routeWaypoints }: MinimapProps) {
|
|
|
|
|
const dy = Math.abs(heroY - last.y);
|
|
|
|
|
const lt = lastHeroTile.current;
|
|
|
|
|
const tileChanged = !lt || lt.tx !== tileX || lt.ty !== tileY;
|
|
|
|
|
if (
|
|
|
|
|
!routeChanged &&
|
|
|
|
|
!townsChanged &&
|
|
|
|
|
!tileChanged &&
|
|
|
|
|
dx < REDRAW_THRESHOLD &&
|
|
|
|
|
dy < REDRAW_THRESHOLD
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
lastRouteKey.current = routeKey;
|
|
|
|
|
lastTownsKey.current = townsKey;
|
|
|
|
|
if (!tileChanged && dx < REDRAW_THRESHOLD && dy < REDRAW_THRESHOLD) return;
|
|
|
|
|
|
|
|
|
|
const canvas = canvasRef.current;
|
|
|
|
|
if (!canvas) return;
|
|
|
|
|
@ -178,36 +137,21 @@ export function Minimap({ heroX, heroY, towns, routeWaypoints }: MinimapProps) {
|
|
|
|
|
const heroR = Math.max(3, Math.round(minDim * 0.035));
|
|
|
|
|
const glowR = Math.max(8, Math.round(minDim * 0.09));
|
|
|
|
|
|
|
|
|
|
const activeRoute =
|
|
|
|
|
routeWaypoints && routeWaypoints.length >= 2 ? routeWaypoints : null;
|
|
|
|
|
const miniCtx =
|
|
|
|
|
towns.length === 0
|
|
|
|
|
? null
|
|
|
|
|
: buildWorldTerrainContext(townsApiToInfluences(towns), activeRoute);
|
|
|
|
|
|
|
|
|
|
if (!miniCtx || miniCtx.towns.length === 0) {
|
|
|
|
|
const terrain = proceduralTerrain(tileX, tileY, null);
|
|
|
|
|
: buildWorldTerrainContext(townsApiToInfluences(towns), null);
|
|
|
|
|
const terrain = proceduralTerrain(tileX, tileY, miniCtx);
|
|
|
|
|
ctx.fillStyle = TERRAIN_BG[terrain] ?? DEFAULT_BG;
|
|
|
|
|
ctx.fillRect(0, 0, w, h);
|
|
|
|
|
} else {
|
|
|
|
|
const img = ctx.createImageData(w, h);
|
|
|
|
|
const data = img.data;
|
|
|
|
|
let q = 0;
|
|
|
|
|
const scale = WORLD_UNITS_PER_PX;
|
|
|
|
|
for (let py = 0; py < h; py++) {
|
|
|
|
|
const wy = Math.floor(heroY + (py - cy) * scale);
|
|
|
|
|
for (let px = 0; px < w; px++) {
|
|
|
|
|
const wx = Math.floor(heroX + (px - cx) * scale);
|
|
|
|
|
const terrain = proceduralTerrain(wx, wy, miniCtx);
|
|
|
|
|
const rgb = TERRAIN_RGB.get(terrain) ?? DEFAULT_RGB;
|
|
|
|
|
data[q++] = rgb[0];
|
|
|
|
|
data[q++] = rgb[1];
|
|
|
|
|
data[q++] = rgb[2];
|
|
|
|
|
data[q++] = 255;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ctx.putImageData(img, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.strokeStyle = 'rgba(180, 170, 140, 0.4)';
|
|
|
|
|
ctx.lineWidth = Math.max(1, minDim / 70);
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
const diagOff = minDim * 0.06;
|
|
|
|
|
ctx.moveTo(0, cy + diagOff);
|
|
|
|
|
ctx.lineTo(w, cy - diagOff);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
|
|
|
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.06)';
|
|
|
|
|
ctx.lineWidth = 0.5;
|
|
|
|
|
@ -265,7 +209,7 @@ export function Minimap({ heroX, heroY, towns, routeWaypoints }: MinimapProps) {
|
|
|
|
|
ctx.strokeStyle = '#ffffff';
|
|
|
|
|
ctx.lineWidth = 1.5;
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
}, [heroX, heroY, towns, routeWaypoints, collapsed, size]);
|
|
|
|
|
}, [heroX, heroY, towns, collapsed, size]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div style={containerStyle}>
|
|
|
|
|
|