import type { MobState } from "../Mob" import { STRIDE, type Mesh } from "engine/scene/Mesh" // Shared building blocks for concrete mob definitions: faceted geometry primitives // and deterministic wander helpers. export const TAU = Math.PI * 2 // --- Wander helpers ------------------------------------------------------- /** A new heading: free wander when inside the leash, else biased back toward home * so the mob never drifts off into the peaks (`jitter` = the random cone half-width * in radians layered on top of the homeward bearing). */ export function wanderHeading( mob: MobState, leash: number, jitter: number, ): number { const dx = mob.home.x - mob.position.x const dz = mob.home.z - mob.position.z if (dx * dx + dz * dz > leash * leash) { return Math.atan2(dx, dz) + (nextRand(mob) - 0.5) * jitter } return nextRand(mob) * TAU } /** mulberry32 step over the mob's own `seed` (mutated), so a mob's motion is * deterministic and needs no external RNG object to clone. */ export function nextRand(mob: MobState): number { const a = (mob.seed + 0x6D2B79F5) | 0 mob.seed = a let t = Math.imul(a ^ (a >>> 15), 1 | a) t ^= t + Math.imul(t ^ (t >>> 7), 61 | t) return ((t ^ (t >>> 14)) >>> 0) / 4294967296 } // --- Geometry primitives -------------------------------------------------- // Mobs are drawn double-sided, so winding is not load-bearing -- // these only need to place faceted, flat-shaded surfaces. /** A UV-rected ellipsoid (pole on Y), faceted like the boulders. */ export function ellipsoid( mesh: Mesh, cx: number, cy: number, cz: number, rx: number, ry: number, rz: number, seg: number, rings: number, u0: number, u1: number, v0: number, v1: number, ): void { const start = mesh.verts.length / STRIDE for (let ir = 0; ir <= rings; ir++) { const phi = (ir / rings) * Math.PI const cyv = Math.cos(phi) const crv = Math.sin(phi) const v = v0 + (v1 - v0) * (ir / rings) for (let is = 0; is <= seg; is++) { const theta = (is / seg) * TAU const u = u0 + (u1 - u0) * (is / seg) mesh.verts.push( cx + crv * Math.cos(theta) * rx, cy + cyv * ry, cz + crv * Math.sin(theta) * rz, u, v, ) } } quadGrid(mesh, start, seg, rings) } /** An ovoid whose pole axis is Z (rings step along z, tapering at both ends), so * the mapped `v` runs down the body's length -- used for the bee's stripes. */ export function ovoidZ( mesh: Mesh, z0: number, z1: number, r: number, seg: number, rings: number, u0: number, u1: number, v0: number, v1: number, ): void { const start = mesh.verts.length / STRIDE for (let ir = 0; ir <= rings; ir++) { const t = ir / rings const z = z0 + (z1 - z0) * t const rr = r * (0.15 + 0.85 * Math.sin(t * Math.PI)) const v = v0 + (v1 - v0) * t for (let is = 0; is <= seg; is++) { const theta = (is / seg) * TAU const u = u0 + (u1 - u0) * (is / seg) mesh.verts.push(Math.cos(theta) * rr, Math.sin(theta) * rr, z, u, v) } } quadGrid(mesh, start, seg, rings) } /** One flat wing quad on `side` (+1 right / -1 left), swept up and out. */ export function wing( mesh: Mesh, side: number, u0: number, u1: number, v0: number, v1: number, ): void { const base = mesh.verts.length / STRIDE mesh.verts.push( side * 0.06, 0.12, 0.14, u0, v0, side * 0.42, 0.24, 0.1, u1, v0, side * 0.42, 0.24, -0.12, u1, v1, side * 0.06, 0.12, -0.1, u0, v1, ) mesh.indices.push(base, base + 1, base + 2, base, base + 2, base + 3) } /** Index a (seg x rings) vertex grid (row = seg+1) into two tris per cell. */ function quadGrid(mesh: Mesh, start: number, seg: number, rings: number): void { const row = seg + 1 for (let ir = 0; ir < rings; ir++) { for (let is = 0; is < seg; is++) { const p = start + ir * row + is mesh.indices.push(p, p + 1, p + row + 1, p, p + row + 1, p + row) } } }