155 lines
4.8 KiB
TypeScript
155 lines
4.8 KiB
TypeScript
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.35–0.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.55–0.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)
|
||
}
|
||
|