feat: bees and frogs

This commit is contained in:
Dan Finch 2026-08-07 12:49:13 +02:00
parent 46e7070b6b
commit 16f205babe
13 changed files with 540 additions and 31 deletions

View file

@ -4,6 +4,7 @@ import { STRIDE, type Mesh } from "../engine/scene/Mesh"
import { Boulder } from "../engine/scene/Boulder"
import { Bush } from "../engine/scene/Bush"
import { Flower, type FlowerColor } from "../engine/scene/Flower"
import type { Mob, MobKind } from "../engine/scene/Mob"
import { Terrain } from "../engine/scene/Terrain"
import { Tree } from "../engine/scene/Tree"
@ -58,6 +59,9 @@ export type Level = {
chunks: Chunk[]
colliders: Aabb[]
npcPosition: { x: number; y: number; z: number }
/** Roaming mobs -- simulated on the main thread each frame (see main.ts), not
* baked into the static culled chunks. */
mobs: Mob[]
terrain: Terrain
sky: SkyConfig
}
@ -112,6 +116,14 @@ const FLOWER_SEED = 0xF10E
const FLOWER_REACH = 0.3
const FLOWER_COLORS: FlowerColor[] = ["white", "red", "yellow"]
/** Roaming mobs: how many frogs/bees to scatter, their seed, and how far out they
* reach (fraction of the world). Kept modest -- roaming meshes are drawn every
* frame (frustum-culled), not baked into the static chunks. */
const FROG_COUNT = 40
const BEE_COUNT = 30
const MOB_SEED = 0x30B
const MOB_REACH = 0.5
/** Spatial partition of the world for frustum culling: `CHUNK_GRID` x
* `CHUNK_GRID` square cells over [-outer, outer]. Smaller cells cull tighter
* (less drawn off-screen) but cost more per-cell tests + bounds; this is the
@ -199,8 +211,9 @@ export function buildLevel(): Level {
const bushes = placeBushes()
const flowers = placeFlowers()
const chunks = buildChunks(trees, boulders, bushes, flowers)
const mobs = placeMobs()
return { floor, walls, crate, chunks, colliders, npcPosition, terrain: TERRAIN, sky }
return { floor, walls, crate, chunks, colliders, npcPosition, mobs, terrain: TERRAIN, sky }
}
/** Bake the terrain + props into a `CHUNK_GRID` x `CHUNK_GRID` set of spatial
@ -383,6 +396,43 @@ function placeFlowers(): Flower[] {
return flowers
}
/** Scatter frogs + bees across the grass (like the boulders), each at its home
* anchor with a random heading and size. No colliders here -- mobs move, so their
* block/stand-on AABBs are rebuilt per frame in `main`. */
function placeMobs(): Mob[] {
const rand = mulberry(MOB_SEED)
const maxDist = TERRAIN.outer * MOB_REACH
const mobs: Mob[] = []
const total = FROG_COUNT + BEE_COUNT
for (let guard = 0; mobs.length < total && guard < total * 20; guard++) {
const angle = rand() * Math.PI * 2
const dist = ARENA + 3 + rand() * (maxDist - ARENA - 3)
const x = Math.cos(angle) * dist
const z = Math.sin(angle) * dist
if (Math.max(Math.abs(x), Math.abs(z)) < TERRAIN.inner + 2) {
continue
}
const kind: MobKind = mobs.length < FROG_COUNT ? "frog" : "bee"
const y = Terrain.height(TERRAIN, x, z)
const scale = kind === "frog" ? 0.5 + rand() * 0.35 : 0.5 + rand() * 0.3
mobs.push({
kind,
home: { x, y, z },
position: { x, y, z },
heading: rand() * Math.PI * 2,
scale,
seed: (rand() * 0xFFFFFFFF) | 0,
vx: 0,
vz: 0,
vy: 0,
timer: rand() * 1.5,
phase: rand() * 10,
grounded: true,
})
}
return mobs
}
/** Deterministic 0..1 generator (mulberry32) for tree placement. */
function mulberry(seed: number): () => number {
let a = seed >>> 0