import { Terrain, type Terrain as Ground } from "../../../engine/world/Terrain" import { Mesh, type Mesh as Geometry } from "../../../engine/scene/Mesh" import type { Material } from "../../../engine/render/Material" import type { ActorDefinition } from "../../../engine/scene/Actor" import { Mob, type MobState } from "../Mob" import { ellipsoid, nextRand, wanderHeading } from "./mobkit" // Everything about the robin: round red-breasted bird that mostly hops like a frog // but now and then takes a short powered flight to a new perch. export type Robin = ActorDefinition export namespace Robin { export function create(material: Material): Robin { const mesh = Mesh.create() build(mesh) return { prototype: { groups: [{ mesh, material }], radius: 0.5, minY: -0.5, maxY: 1, }, update, transform: Mob.transform, collider: (state) => Mob.collider(state, 0.45, 0.55, state.grounded), } } } const LEASH = 6 const REST_MIN = 0.5 const REST_SPAN = 1.3 const HOP_SPEED = 1.4 const HOP_IMPULSE = 2.6 /** Fraction of a robin's moves that are a flight rather than a ground hop. */ const FLY_CHANCE = 0.35 const FLY_SPEED = 4.5 const FLY_IMPULSE = 3.5 const CRUISE = 0.8 const GRAVITY = 14 function build(mesh: Geometry): 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) } function update(mob: MobState, dt: number, terrain: Ground): 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, LEASH, 1) const fly = nextRand(mob) < FLY_CHANCE const speed = fly ? FLY_SPEED : HOP_SPEED mob.vx = Math.sin(mob.heading) * speed mob.vz = Math.cos(mob.heading) * speed mob.vy = fly ? FLY_IMPULSE : HOP_IMPULSE mob.phase = fly ? 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 the cruise ends. mob.phase -= dt mob.vy += (0 - mob.vy) * Math.min(1, dt * 6) } else { mob.vy -= 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 = REST_MIN + nextRand(mob) * REST_SPAN } }