magicciv/packages/engine-ts/src/HexGrid.ts
2026-03-26 00:06:47 -07:00

155 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { GRID_WIDTH, GRID_HEIGHT } from './configs'
export { GRID_WIDTH, GRID_HEIGHT }
// Odd-q offset neighbor deltas: [even_col_offsets, odd_col_offsets]
// Each entry is [dcol, drow] for directions E, NE, NW, W, SW, SE
const ODD_Q_NEIGHBORS: readonly [readonly [number, number][], readonly [number, number][]] = [
// even col
[[1, 0], [1, -1], [0, -1], [-1, 0], [-1, 1], [0, 1]],
// odd col
[[1, 0], [1, 1], [0, 1], [-1, 0], [-1, -1], [0, -1]],
] as const
// Axial direction vectors: E(0), NE(1), NW(2), W(3), SW(4), SE(5)
// These match the GDScript HexUtils.AXIAL_DIRECTIONS order
export const AXIAL_DIRECTIONS: readonly [number, number][] = [
[1, 0], [1, -1], [0, -1], [-1, 0], [-1, 1], [0, 1],
] as const
// ---------------------------------------------------------------------------
// Core grid helpers
// ---------------------------------------------------------------------------
export function idx(col: number, row: number, w: number): number {
return row * w + col
}
export function neighbors(
col: number,
row: number,
w: number,
h: number,
): Array<{ col: number; row: number }> {
const parity = col & 1
const deltas = ODD_Q_NEIGHBORS[parity]
const result: Array<{ col: number; row: number }> = []
for (const [dc, dr] of deltas) {
const nc = col + dc
const nr = row + dr
if (nc >= 0 && nc < w && nr >= 0 && nr < h) {
result.push({ col: nc, row: nr })
}
}
return result
}
export function axialToOffset(q: number, r: number): { col: number; row: number } {
const col = q
const row = r + (q - (q & 1)) / 2
return { col, row }
}
/** The tile that wind arrives FROM (opposite direction, clamped to grid). */
export function upwindPos(
col: number,
row: number,
windDir: number,
w: number,
h: number,
): { col: number; row: number } | null {
const oppositeDir = (windDir + 3) % 6
const parity = col & 1
const [dc, dr] = ODD_Q_NEIGHBORS[parity][oppositeDir]
const nc = col + dc
const nr = row + dr
if (nc < 0 || nc >= w || nr < 0 || nr >= h) return null
return { col: nc, row: nr }
}
/** Step one hex in direction dir (0-5), returning null if out of bounds. */
export function neighborInDir(
col: number,
row: number,
dir: number,
w: number,
h: number,
): { col: number; row: number } | null {
const parity = col & 1
const [dc, dr] = ODD_Q_NEIGHBORS[parity][dir % 6]
const nc = col + dc
const nr = row + dr
if (nc < 0 || nc >= w || nr < 0 || nr >= h) return null
return { col: nc, row: nr }
}
/** Solar insolation at a row, mapped to habitable range [solarMin, solarMax]. */
export function solarByRow(row: number, height: number, solarMin = 0.15, solarMax = 0.70): number {
const centerRow = height / 2.0
const raw = 1.0 - Math.abs((row - centerRow) / centerRow)
return solarMin + (solarMax - solarMin) * raw
}
// ---------------------------------------------------------------------------
// Terrain classification
// ---------------------------------------------------------------------------
export function classifyTerrain(
temp: number,
moisture: number,
elevation: number,
): string {
// Only the highest peaks are locked terrain — elevation interacts with
// climate for hills (cold hills→tundra, wet hills→forest).
if (elevation > 0.82) return 'mountains'
if (elevation > 0.70) {
if (temp < 0.25) return 'tundra'
if (moisture > 0.65 && temp > 0.55) return 'forest'
return 'hills'
}
// Polar / cold zones
if (temp < 0.10) return 'snow'
if (temp < 0.20) return moisture >= 0.30 ? 'boreal_forest' : 'tundra'
if (temp < 0.35) {
if (moisture >= 0.50) return 'boreal_forest'
if (moisture >= 0.30) return 'grassland'
return 'tundra'
}
// Temperate zone (0.350.55) — this is where most diversity should live
if (temp < 0.55) {
if (moisture < 0.25) return 'desert'
if (moisture < 0.40) return 'plains'
if (moisture < 0.55) return 'grassland'
if (moisture < 0.70) return 'forest'
return 'swamp'
}
// Warm zone (0.550.80)
if (temp < 0.80) {
if (moisture < 0.25) return 'desert'
if (moisture < 0.40) return 'plains'
if (moisture < 0.55) return 'grassland'
if (moisture < 0.68) return 'forest'
return 'jungle'
}
// Hot zone (>0.80) — true tropics, equatorial only
if (moisture < 0.30) return 'desert'
if (moisture < 0.45) return 'plains'
if (moisture < 0.60) return 'grassland'
if (moisture < 0.68) return 'forest'
return 'jungle'
}
// ---------------------------------------------------------------------------
// Seeded noise
// ---------------------------------------------------------------------------
export function hashNoise(x: number, y: number, seed: number): number {
// Returns a deterministic pseudo-random float in [0, 1)
const v = Math.sin(x * 127.1 + y * 311.7 + seed * 74.3) * 43758.5453
return v - Math.floor(v)
}