2026-08-08 14:15:32 +02:00
|
|
|
import { Terrain } from "../../Terrain"
|
|
|
|
|
import type { Mesh } from "../../../engine/scene/Mesh"
|
2026-08-07 19:11:43 +02:00
|
|
|
import type { Mob } from "../Mob"
|
2026-08-08 14:15:32 +02:00
|
|
|
import type { Entity } from "../../../engine/scene/Actor"
|
2026-08-07 19:11:43 +02:00
|
|
|
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.
|
|
|
|
|
|
2026-08-08 14:15:32 +02:00
|
|
|
export const robin: Entity<Mob, Terrain> = {
|
|
|
|
|
name: "robin",
|
|
|
|
|
build,
|
|
|
|
|
update,
|
|
|
|
|
boundingRadius: 0.45,
|
|
|
|
|
bodyHeight: 0.55,
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-07 19:11:43 +02:00
|
|
|
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: 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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function update(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, 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
|
|
|
|
|
}
|
|
|
|
|
}
|