2026-08-07 19:11:43 +02:00
|
|
|
import type { Mob } from "../Mob"
|
2026-08-08 14:15:32 +02:00
|
|
|
import { STRIDE, type Mesh } from "../../../engine/scene/Mesh"
|
2026-08-07 19:11:43 +02:00
|
|
|
|
|
|
|
|
// Shared building blocks for the per-kind mob definitions (Frog/Bee/Robin): the
|
|
|
|
|
// faceted geometry primitives and the deterministic wander helpers. Kept in its own
|
|
|
|
|
// module (no runtime import of `Mob`, only its type) so the per-kind files and the
|
|
|
|
|
// `Mob` registry don't form an import cycle.
|
|
|
|
|
|
|
|
|
|
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: Mob, 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: Mob): 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 (see renderScene), 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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|