aboutsummaryrefslogtreecommitdiff
path: root/lib/leaves/wind.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/leaves/wind.ts')
-rw-r--r--lib/leaves/wind.ts259
1 files changed, 0 insertions, 259 deletions
diff --git a/lib/leaves/wind.ts b/lib/leaves/wind.ts
deleted file mode 100644
index 5bcac7f..0000000
--- a/lib/leaves/wind.ts
+++ /dev/null
@@ -1,259 +0,0 @@
-import type { SeededRNG } from '@hollowdark/rng/seeded'
-import type {
- Gust,
- GustKind,
- SceneDimensions,
- WindParticle,
- WindSystem
-} from '@hollowdark/lib/leaves/types'
-
-const PREVAILING_VX_PX_PER_S = -28
-const PREVAILING_VY_PX_PER_S = 8
-
-const NEXT_GUST_MIN_MS = 9_000
-const NEXT_GUST_MAX_MS = 22_000
-
-const GUST_KIND_WEIGHTS: readonly (readonly [GustKind, number])[] = [
- ['breeze', 6],
- ['gust', 3],
- ['whirl', 1]
-]
-
-const BREEZE_DURATION_MS = [2_400, 4_200] as const
-const GUST_DURATION_MS = [3_200, 5_500] as const
-const WHIRL_DURATION_MS = [4_500, 7_500] as const
-
-const BREEZE_RADIUS = [70, 140] as const
-const GUST_RADIUS = [150, 260] as const
-const WHIRL_RADIUS = [90, 170] as const
-
-const BREEZE_STRENGTH = [25, 55] as const
-const GUST_STRENGTH = [70, 130] as const
-const WHIRL_STRENGTH = [55, 95] as const
-
-/** Construct the initial wind system. Schedules the first gust a few
- * seconds out so the scene opens on the gentle prevailing wind. */
-export function initialWindSystem(rng: SeededRNG): WindSystem {
- return {
- prevailingVx: PREVAILING_VX_PX_PER_S,
- prevailingVy: PREVAILING_VY_PX_PER_S,
- activeGust: null,
- nextGustInMs: 4_000 + rng.next() * 6_000
- }
-}
-
-/**
- * Force per unit mass at a given point. Returns the prevailing wind plus,
- * if inside a gust, the gust's contribution with linear radial falloff.
- */
-export function windForceAt(
- wind: WindSystem,
- x: number,
- y: number
-): { readonly fx: number; readonly fy: number } {
- let fx = wind.prevailingVx
- let fy = wind.prevailingVy
-
- const g = wind.activeGust
- if (g) {
- const dx = x - g.centerX
- const dy = y - g.centerY
- const dist = Math.sqrt(dx * dx + dy * dy)
- if (dist < g.radius) {
- const falloff = 1 - dist / g.radius
- if (g.kind === 'whirl') {
- const safe = Math.max(0.001, dist)
- const tx = -dy / safe
- const ty = dx / safe
- fx += tx * g.strength * falloff
- fy += ty * g.strength * falloff
- } else {
- fx += Math.cos(g.direction) * g.strength * falloff
- fy += Math.sin(g.direction) * g.strength * falloff
- }
- }
- }
- return { fx, fy }
-}
-
-/** Counter-holder so the caller can issue unique monotonic ids. */
-export interface IdSource {
- nextGustId(): number
- nextParticleId(): number
-}
-
-/**
- * Step the wind system by `dtMs`. Advances particles inside an active
- * gust, spawns new particles, retires the gust when its duration ends,
- * and schedules the next gust after an idle pause.
- */
-export function stepWind(
- wind: WindSystem,
- dtMs: number,
- scene: SceneDimensions,
- rng: SeededRNG,
- ids: IdSource
-): WindSystem {
- if (wind.activeGust) {
- const g = wind.activeGust
- const elapsed = g.elapsedMs + dtMs
-
- if (elapsed >= g.totalDurationMs) {
- return {
- ...wind,
- activeGust: null,
- nextGustInMs: NEXT_GUST_MIN_MS + rng.next() * (NEXT_GUST_MAX_MS - NEXT_GUST_MIN_MS)
- }
- }
-
- const advanced = advanceParticles(g, dtMs)
- let particles = advanced
- let nextSpawnInMs = g.nextParticleSpawnInMs - dtMs
-
- if (nextSpawnInMs <= 0) {
- particles = [...advanced, spawnParticle(g, ids.nextParticleId(), rng)]
- nextSpawnInMs = particleSpawnIntervalMs(g.kind)
- }
-
- return {
- ...wind,
- activeGust: {
- ...g,
- elapsedMs: elapsed,
- particles,
- nextParticleSpawnInMs: nextSpawnInMs
- }
- }
- }
-
- const remaining = wind.nextGustInMs - dtMs
- if (remaining <= 0) {
- return {
- ...wind,
- activeGust: createGust(ids.nextGustId(), scene, rng),
- nextGustInMs: 0
- }
- }
- return { ...wind, nextGustInMs: remaining }
-}
-
-function createGust(id: number, scene: SceneDimensions, rng: SeededRNG): Gust {
- const kind = rng.weightedPick(GUST_KIND_WEIGHTS)
- const centerX = rng.next() * scene.width
- const centerY = scene.height * (0.25 + rng.next() * 0.55)
-
- let radius: number
- let strength: number
- let duration: number
- let direction: number
-
- if (kind === 'breeze') {
- radius = BREEZE_RADIUS[0] + rng.next() * (BREEZE_RADIUS[1] - BREEZE_RADIUS[0])
- strength = BREEZE_STRENGTH[0] + rng.next() * (BREEZE_STRENGTH[1] - BREEZE_STRENGTH[0])
- duration = BREEZE_DURATION_MS[0] + rng.next() * (BREEZE_DURATION_MS[1] - BREEZE_DURATION_MS[0])
- direction = Math.PI + (rng.next() - 0.5) * 0.5
- } else if (kind === 'gust') {
- radius = GUST_RADIUS[0] + rng.next() * (GUST_RADIUS[1] - GUST_RADIUS[0])
- strength = GUST_STRENGTH[0] + rng.next() * (GUST_STRENGTH[1] - GUST_STRENGTH[0])
- duration = GUST_DURATION_MS[0] + rng.next() * (GUST_DURATION_MS[1] - GUST_DURATION_MS[0])
- direction = Math.PI + (rng.next() - 0.5) * 0.35
- } else {
- radius = WHIRL_RADIUS[0] + rng.next() * (WHIRL_RADIUS[1] - WHIRL_RADIUS[0])
- strength = WHIRL_STRENGTH[0] + rng.next() * (WHIRL_STRENGTH[1] - WHIRL_STRENGTH[0])
- duration = WHIRL_DURATION_MS[0] + rng.next() * (WHIRL_DURATION_MS[1] - WHIRL_DURATION_MS[0])
- direction = rng.nextBool(0.5) ? 1 : -1
- }
-
- return {
- id,
- kind,
- centerX,
- centerY,
- radius,
- strength,
- direction,
- elapsedMs: 0,
- totalDurationMs: duration,
- particles: [],
- nextParticleSpawnInMs: 0
- }
-}
-
-function particleSpawnIntervalMs(kind: GustKind): number {
- return kind === 'breeze' ? 160 : kind === 'gust' ? 110 : 140
-}
-
-function spawnParticle(gust: Gust, id: number, rng: SeededRNG): WindParticle {
- const angle = rng.next() * Math.PI * 2
- const r = rng.next() * gust.radius * 0.9
- const x = gust.centerX + Math.cos(angle) * r
- const y = gust.centerY + Math.sin(angle) * r
-
- let vx: number
- let vy: number
- if (gust.kind === 'whirl') {
- const safe = Math.max(0.001, r)
- vx = (-Math.sin(angle) * gust.direction * gust.strength) / Math.max(1, safe / 40)
- vy = (Math.cos(angle) * gust.direction * gust.strength) / Math.max(1, safe / 40)
- } else {
- vx = Math.cos(gust.direction) * gust.strength * 0.55
- vy = Math.sin(gust.direction) * gust.strength * 0.55
- }
-
- return {
- id,
- x,
- y,
- vx,
- vy,
- ageMs: 0,
- lifetimeMs: 800 + rng.next() * 1_200,
- size: 1.2 + rng.next() * 1.6
- }
-}
-
-function advanceParticles(gust: Gust, dtMs: number): readonly WindParticle[] {
- const dt = dtMs / 1000
- const out: WindParticle[] = []
- for (const p of gust.particles) {
- const age = p.ageMs + dtMs
- if (age >= p.lifetimeMs) continue
-
- let vx = p.vx
- let vy = p.vy
-
- if (gust.kind === 'whirl') {
- const dx = p.x - gust.centerX
- const dy = p.y - gust.centerY
- const dist = Math.sqrt(dx * dx + dy * dy)
- if (dist > 0) {
- const safe = Math.max(0.001, dist)
- vx += (-dy / safe) * gust.direction * 45 * dt
- vy += (dx / safe) * gust.direction * 45 * dt
- }
- }
-
- const drag = Math.pow(0.94, dt * 60)
- vx *= drag
- vy *= drag
-
- out.push({
- ...p,
- x: p.x + vx * dt,
- y: p.y + vy * dt,
- vx,
- vy,
- ageMs: age
- })
- }
- return out
-}
-
-/** Opacity envelope for a particle: fade in the first 20%, hold, fade out
- * the last 30%. */
-export function particleOpacity(p: WindParticle): number {
- const phase = p.ageMs / p.lifetimeMs
- if (phase < 0.2) return phase / 0.2
- if (phase > 0.7) return Math.max(0, (1 - phase) / 0.3)
- return 1
-}