feat: robins
This commit is contained in:
parent
a4490f7a20
commit
bd88d3cbd6
12 changed files with 159 additions and 54 deletions
|
|
@ -57,8 +57,8 @@ export type RenderConfig = {
|
|||
* color depth, dither, vertex snap, and filtering from crunchy PS1 to clean. */
|
||||
export namespace RenderConfig {
|
||||
export const standard: RenderConfig = {
|
||||
internalWidth: 384,
|
||||
internalHeight: 216,
|
||||
internalWidth: 640,
|
||||
internalHeight: 360,
|
||||
upscaleFilter: "nearest",
|
||||
colorDepth: 5,
|
||||
dither: 1,
|
||||
|
|
|
|||
|
|
@ -5,23 +5,25 @@ import { STRIDE, type Mesh } from "./Mesh"
|
|||
const TAU = Math.PI * 2
|
||||
|
||||
/** A roaming creature drawn as a moving low-poly mesh (unlike the static baked
|
||||
* world). Two kinds, told apart by silhouette + motion:
|
||||
* frog -- squat, ground-bound, sits then springs a ballistic hop.
|
||||
* bee -- small, hovers and darts through the air, wings out.
|
||||
* world). Three kinds, told apart by silhouette + motion:
|
||||
* frog -- squat, ground-bound, sits then springs a ballistic hop.
|
||||
* bee -- small, hovers and darts through the air, wings out.
|
||||
* robin -- round red-breasted bird; mostly hops like a frog, but now and then
|
||||
* takes off on a short powered flight to a new perch.
|
||||
*
|
||||
* Unlike `Tree`/`Boulder` (baked once into world-space chunks), a mob's geometry
|
||||
* is a **canonical local-space mesh** built once per kind (front = +Z, frog feet
|
||||
* / bee body at the origin); the live `position`/`heading`/`scale` are turned
|
||||
* is a **canonical local-space mesh** built once per kind (front = +Z, frog/robin
|
||||
* feet / bee body at the origin); the live `position`/`heading`/`scale` are turned
|
||||
* into a per-frame model matrix by the renderer. All wander state lives here so
|
||||
* `update` is a pure stepping function of the mob + dt (deterministic via the
|
||||
* evolving `seed`), which keeps the sim on the main thread and cloneable-free. */
|
||||
export type MobKind = "frog" | "bee"
|
||||
export type MobKind = "frog" | "bee" | "robin"
|
||||
|
||||
export type Mob = {
|
||||
kind: MobKind
|
||||
/** Leash anchor (where it was scattered); wandering is pulled back toward it. */
|
||||
home: Vec3
|
||||
/** Live feet-center (frog) / body-center (bee), advanced each frame. */
|
||||
/** Live feet-center (frog/robin) / body-center (bee), advanced each frame. */
|
||||
position: Vec3
|
||||
/** Facing yaw; the mesh's front is local +Z, so world dir = (sin h, 0, cos h). */
|
||||
heading: number
|
||||
|
|
@ -29,16 +31,17 @@ export type Mob = {
|
|||
scale: number
|
||||
/** Evolving RNG state (mutated by `update`) -- keeps the sim deterministic. */
|
||||
seed: number
|
||||
/** Horizontal velocity (frog: only mid-hop; bee: cruise). */
|
||||
/** Horizontal velocity (frog/robin: mid-hop or -flight; bee: cruise). */
|
||||
vx: number
|
||||
vz: number
|
||||
/** Vertical velocity (frog ballistic hop; bee stays 0, it uses a bob). */
|
||||
/** Vertical velocity (frog/robin ballistic hop/flight; bee stays 0, uses a bob). */
|
||||
vy: number
|
||||
/** Countdown to the next decision (frog: next hop; bee: next heading change). */
|
||||
/** Countdown to the next decision (frog/robin: next hop; bee: next heading change). */
|
||||
timer: number
|
||||
/** Accumulated time, for the bee's hover bob. */
|
||||
/** Per-kind scratch clock: the bee's hover-bob phase; the robin's remaining
|
||||
* powered-flight cruise time (>0 while gliding between perches). */
|
||||
phase: number
|
||||
/** Frog only: resting on the ground vs airborne in a hop. */
|
||||
/** Frog/robin: resting on the ground vs airborne (a hop or a flight). */
|
||||
grounded: boolean
|
||||
}
|
||||
|
||||
|
|
@ -56,14 +59,27 @@ const BEE_TURN_SPAN = 1
|
|||
const BEE_HOVER = 1.1
|
||||
const BEE_BOB_AMP = 0.18
|
||||
const BEE_BOB_FREQ = 3
|
||||
const ROBIN_LEASH = 6
|
||||
const ROBIN_REST_MIN = 0.5
|
||||
const ROBIN_REST_SPAN = 1.3
|
||||
const ROBIN_HOP_SPEED = 1.4
|
||||
const ROBIN_HOP_IMPULSE = 2.6
|
||||
/** Fraction of a robin's moves that are a flight rather than a ground hop. */
|
||||
const ROBIN_FLY_CHANCE = 0.35
|
||||
const ROBIN_FLY_SPEED = 4.5
|
||||
const ROBIN_FLY_IMPULSE = 3.5
|
||||
const ROBIN_CRUISE = 0.8
|
||||
const ROBIN_GRAVITY = 14
|
||||
|
||||
export namespace Mob {
|
||||
/** Advance one mob by `dt` seconds, sampling `terrain` for ground height. */
|
||||
export function update(mob: Mob, dt: number, terrain: Terrain): void {
|
||||
if (mob.kind === "frog") {
|
||||
frog(mob, dt, terrain)
|
||||
} else {
|
||||
} else if (mob.kind === "bee") {
|
||||
bee(mob, dt, terrain)
|
||||
} else {
|
||||
robin(mob, dt, terrain)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,19 +88,21 @@ export namespace Mob {
|
|||
export function build(kind: MobKind, mesh: Mesh): void {
|
||||
if (kind === "frog") {
|
||||
buildFrog(mesh)
|
||||
} else {
|
||||
} else if (kind === "bee") {
|
||||
buildBee(mesh)
|
||||
} else {
|
||||
buildRobin(mesh)
|
||||
}
|
||||
}
|
||||
|
||||
/** Local bounding radius (pre-scale), for building the per-frame cull AABB. */
|
||||
export function boundingRadius(kind: MobKind): number {
|
||||
return kind === "frog" ? 0.7 : 0.5
|
||||
return kind === "frog" ? 0.7 : kind === "robin" ? 0.45 : 0.5
|
||||
}
|
||||
|
||||
/** Local body height (pre-scale), for the top of the stand-on collider. */
|
||||
export function bodyHeight(kind: MobKind): number {
|
||||
return kind === "frog" ? 0.6 : 0.5
|
||||
return kind === "frog" ? 0.6 : kind === "robin" ? 0.55 : 0.5
|
||||
}
|
||||
|
||||
// --- Simulation ---------------------------------------------------------
|
||||
|
|
@ -133,6 +151,49 @@ export namespace Mob {
|
|||
mob.position.y = ground + BEE_HOVER + Math.sin(mob.phase * BEE_BOB_FREQ) * BEE_BOB_AMP
|
||||
}
|
||||
|
||||
function robin(mob: Mob, dt: number, terrain: Terrain): void {
|
||||
if (mob.grounded) {
|
||||
mob.timer -= dt
|
||||
mob.position.y = Terrain.height(terrain, mob.position.x, mob.position.z)
|
||||
if (mob.timer > 0) {
|
||||
return
|
||||
}
|
||||
// Decide the next move: usually a short ground hop, sometimes a longer
|
||||
// powered flight -- higher + faster off the mark, then a flat glide (see
|
||||
// the cruise branch below) before settling onto a new perch.
|
||||
mob.heading = wanderHeading(mob, ROBIN_LEASH, 1)
|
||||
const fly = nextRand(mob) < ROBIN_FLY_CHANCE
|
||||
const speed = fly ? ROBIN_FLY_SPEED : ROBIN_HOP_SPEED
|
||||
mob.vx = Math.sin(mob.heading) * speed
|
||||
mob.vz = Math.cos(mob.heading) * speed
|
||||
mob.vy = fly ? ROBIN_FLY_IMPULSE : ROBIN_HOP_IMPULSE
|
||||
mob.phase = fly ? ROBIN_CRUISE : 0
|
||||
mob.grounded = false
|
||||
return
|
||||
}
|
||||
if (mob.phase > 0) {
|
||||
// In flight: bleed vertical speed toward level so it glides roughly flat
|
||||
// (a bird crossing the clearing), not a lob; gravity resumes once cruise ends.
|
||||
mob.phase -= dt
|
||||
mob.vy += (0 - mob.vy) * Math.min(1, dt * 6)
|
||||
} else {
|
||||
mob.vy -= ROBIN_GRAVITY * dt
|
||||
}
|
||||
mob.position.x += mob.vx * dt
|
||||
mob.position.y += mob.vy * dt
|
||||
mob.position.z += mob.vz * dt
|
||||
const ground = Terrain.height(terrain, mob.position.x, mob.position.z)
|
||||
if (mob.position.y <= ground && mob.vy < 0) {
|
||||
mob.position.y = ground
|
||||
mob.vx = 0
|
||||
mob.vy = 0
|
||||
mob.vz = 0
|
||||
mob.phase = 0
|
||||
mob.grounded = true
|
||||
mob.timer = ROBIN_REST_MIN + nextRand(mob) * ROBIN_REST_SPAN
|
||||
}
|
||||
}
|
||||
|
||||
/** 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). */
|
||||
|
|
@ -179,6 +240,19 @@ export namespace Mob {
|
|||
wing(mesh, -1, 0.83, 0.99, 0, 1)
|
||||
}
|
||||
|
||||
function buildRobin(mesh: Mesh): void {
|
||||
// Round European robin: plump brown body, an orange-red breast bulging on the
|
||||
// front, a round brown head with two dark eyes + a small dark beak, short tail.
|
||||
// UVs: robin texture is brown (left), orange breast (mid), dark eye/beak (right).
|
||||
ellipsoid(mesh, 0, 0.26, 0, 0.26, 0.26, 0.3, 6, 4, 0, 0.38, 0, 1) // body (brown)
|
||||
ellipsoid(mesh, 0, 0.18, 0.17, 0.22, 0.22, 0.16, 5, 4, 0.42, 0.68, 0, 1) // breast (orange)
|
||||
ellipsoid(mesh, 0, 0.48, 0.14, 0.18, 0.18, 0.18, 5, 4, 0, 0.38, 0, 1) // head (brown)
|
||||
ellipsoid(mesh, 0.09, 0.52, 0.26, 0.03, 0.03, 0.03, 3, 2, 0.85, 0.99, 0, 1) // eye
|
||||
ellipsoid(mesh, -0.09, 0.52, 0.26, 0.03, 0.03, 0.03, 3, 2, 0.85, 0.99, 0, 1) // eye
|
||||
ellipsoid(mesh, 0, 0.47, 0.35, 0.03, 0.025, 0.09, 3, 2, 0.85, 0.99, 0, 1) // beak (dark)
|
||||
ellipsoid(mesh, 0, 0.26, -0.32, 0.09, 0.05, 0.16, 4, 2, 0, 0.38, 0, 1) // tail (brown)
|
||||
}
|
||||
|
||||
/** A UV-rected ellipsoid (pole on Y), faceted like the boulders. */
|
||||
function ellipsoid(
|
||||
mesh: Mesh,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue