refactor: move world concepts into engine
This commit is contained in:
parent
eeedcb8e48
commit
2d15c7ab8d
52 changed files with 3298 additions and 1558 deletions
|
|
@ -1,5 +1,7 @@
|
|||
import type { Vec3 } from "../../engine/math/Vec3"
|
||||
import { STRIDE, type Mesh } from "../../engine/scene/Mesh"
|
||||
import type { Material } from "../../engine/render/Material"
|
||||
import type { Prefab } from "../../engine/scene/Prefab"
|
||||
|
||||
const TAU = Math.PI * 2
|
||||
|
||||
|
|
@ -22,8 +24,35 @@ export type Boulder = {
|
|||
* field of boulders batches into a single draw call.
|
||||
*/
|
||||
export namespace Boulder {
|
||||
export function create(material: Material): Prefab<Boulder> {
|
||||
return {
|
||||
position: (boulder) => boulder.position,
|
||||
bakeNear: (boulder, batch) => build(boulder, batch.mesh(material)),
|
||||
bakeFar: (boulder, batch) =>
|
||||
build(boulder, batch.mesh(material), "impostor"),
|
||||
collider(boulder) {
|
||||
if (boulder.radius <= 0.7) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
shape: "box",
|
||||
minX: boulder.position.x - boulder.radius,
|
||||
maxX: boulder.position.x + boulder.radius,
|
||||
minZ: boulder.position.z - boulder.radius,
|
||||
maxZ: boulder.position.z + boulder.radius,
|
||||
top: boulder.position.y + boulder.radius * 0.7,
|
||||
standable: false,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** `lod` "impostor" bakes a coarser rock (fewer facets) for far chunks. */
|
||||
export function build(boulder: Boulder, mesh: Mesh, lod: "full" | "impostor" = "full"): void {
|
||||
export function build(
|
||||
boulder: Boulder,
|
||||
mesh: Mesh,
|
||||
lod: "full" | "impostor" = "full",
|
||||
): void {
|
||||
const rand = rng(boulder.seed)
|
||||
const seg = lod === "impostor" ? 4 : 5
|
||||
const rings = lod === "impostor" ? 2 : 4
|
||||
|
|
@ -67,7 +96,11 @@ export namespace Boulder {
|
|||
/** Per-vertex radial scale in ~0.72..1.14 for a chunky, angular surface. The
|
||||
* longitude seam (last column == first) and each pole row (one shared value)
|
||||
* match so the mesh stays closed. */
|
||||
function jitterGrid(seg: number, rings: number, rand: () => number): number[][] {
|
||||
function jitterGrid(
|
||||
seg: number,
|
||||
rings: number,
|
||||
rand: () => number,
|
||||
): number[][] {
|
||||
const grid: number[][] = []
|
||||
for (let ir = 0; ir <= rings; ir++) {
|
||||
const pole = ir === 0 || ir === rings
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import type { Vec3 } from "../../engine/math/Vec3"
|
||||
import { STRIDE, type Mesh } from "../../engine/scene/Mesh"
|
||||
import type { Material } from "../../engine/render/Material"
|
||||
import type { Prefab } from "../../engine/scene/Prefab"
|
||||
|
||||
const TAU = Math.PI * 2
|
||||
|
||||
|
|
@ -17,6 +19,13 @@ export type Bush = {
|
|||
* wound outward, so backface culling is safe. `build` appends into a shared
|
||||
* (leaf-textured) mesh. */
|
||||
export namespace Bush {
|
||||
export function create(material: Material): Prefab<Bush> {
|
||||
return {
|
||||
position: (bush) => bush.position,
|
||||
bakeNear: (bush, batch) => build(bush, batch.mesh(material)),
|
||||
}
|
||||
}
|
||||
|
||||
export function build(bush: Bush, mesh: Mesh): void {
|
||||
const rand = rng(bush.seed)
|
||||
// A handful of smaller overlapping lumps reads as a soft shrub; one big
|
||||
|
|
@ -34,7 +43,14 @@ export namespace Bush {
|
|||
}
|
||||
|
||||
/** A small lumpy low-poly sphere, wound outward (matches the oak canopy blob). */
|
||||
function blob(mesh: Mesh, cx: number, cy: number, cz: number, radius: number, rand: () => number): void {
|
||||
function blob(
|
||||
mesh: Mesh,
|
||||
cx: number,
|
||||
cy: number,
|
||||
cz: number,
|
||||
radius: number,
|
||||
rand: () => number,
|
||||
): void {
|
||||
const seg = 6
|
||||
const rings = 4
|
||||
const start = mesh.verts.length / STRIDE
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
import type { Vec3 } from "../../engine/math/Vec3"
|
||||
import { Mesh } from "../../engine/scene/Mesh"
|
||||
import type { Material } from "../../engine/render/Material"
|
||||
import type { Prefab } from "../../engine/scene/Prefab"
|
||||
|
||||
const TAU = Math.PI * 2
|
||||
|
||||
/** Flower bloom color, indexing a region of the `flower` texture atlas. */
|
||||
export type FlowerColor = "white" | "red" | "yellow"
|
||||
/** Concrete atlas style referenced directly by flower instances. */
|
||||
export type FlowerStyle = { bloomUv: [number, number] }
|
||||
|
||||
/** A single small flower: a thin crossed-quad stem plus a shallow fan of petals.
|
||||
* Tiny, so it is drawn double-sided (no backface cull) and carries no collider.
|
||||
* `size` is roughly its height; `seed` jitters the petals. */
|
||||
export type Flower = {
|
||||
position: Vec3
|
||||
color: FlowerColor
|
||||
style: FlowerStyle
|
||||
size: number
|
||||
seed: number
|
||||
}
|
||||
|
|
@ -23,16 +25,21 @@ export type Flower = {
|
|||
* or draw call. `build` appends into one shared flower mesh.
|
||||
*/
|
||||
export namespace Flower {
|
||||
/** uv center of each bloom color's atlas region (tile units). */
|
||||
const BLOOM_UV: Record<FlowerColor, [number, number]> = {
|
||||
white: [0.75, 0.25],
|
||||
red: [0.25, 0.75],
|
||||
yellow: [0.75, 0.75],
|
||||
}
|
||||
export const white: FlowerStyle = { bloomUv: [0.75, 0.25] }
|
||||
export const red: FlowerStyle = { bloomUv: [0.25, 0.75] }
|
||||
export const yellow: FlowerStyle = { bloomUv: [0.75, 0.75] }
|
||||
|
||||
/** uv center of the green stem region. */
|
||||
const STEM_U = 0.25
|
||||
const STEM_V = 0.25
|
||||
|
||||
export function create(material: Material): Prefab<Flower> {
|
||||
return {
|
||||
position: (flower) => flower.position,
|
||||
bakeNear: (flower, batch) => build(flower, batch.mesh(material)),
|
||||
}
|
||||
}
|
||||
|
||||
export function build(flower: Flower, mesh: Mesh): void {
|
||||
const rand = rng(flower.seed)
|
||||
const p = flower.position
|
||||
|
|
@ -43,14 +50,21 @@ export namespace Flower {
|
|||
stem(mesh, p.x, p.y, p.z, bloomY, w, 0)
|
||||
stem(mesh, p.x, p.y, p.z, bloomY, 0, w)
|
||||
// Bloom: a shallow fan of petals, center raised a touch so it domes.
|
||||
const [bu, bv] = BLOOM_UV[flower.color]
|
||||
const [bu, bv] = flower.style.bloomUv
|
||||
const rad = flower.size * 0.38
|
||||
const center = Mesh.push(mesh, p.x, bloomY + rad * 0.3, p.z, bu, bv)
|
||||
const ring = center + 1
|
||||
const petals = 5
|
||||
for (let i = 0; i <= petals; i++) {
|
||||
const angle = (i / petals) * TAU + rand() * 0.4
|
||||
Mesh.push(mesh, p.x + Math.cos(angle) * rad, bloomY, p.z + Math.sin(angle) * rad, bu, bv)
|
||||
Mesh.push(
|
||||
mesh,
|
||||
p.x + Math.cos(angle) * rad,
|
||||
bloomY,
|
||||
p.z + Math.sin(angle) * rad,
|
||||
bu,
|
||||
bv,
|
||||
)
|
||||
}
|
||||
for (let i = 0; i < petals; i++) {
|
||||
mesh.indices.push(center, ring + i, ring + i + 1)
|
||||
|
|
@ -58,7 +72,15 @@ export namespace Flower {
|
|||
}
|
||||
|
||||
/** A thin vertical quad from the ground to `y1`, width along (dx, dz). */
|
||||
function stem(mesh: Mesh, x: number, y0: number, z: number, y1: number, dx: number, dz: number): void {
|
||||
function stem(
|
||||
mesh: Mesh,
|
||||
x: number,
|
||||
y0: number,
|
||||
z: number,
|
||||
y1: number,
|
||||
dx: number,
|
||||
dz: number,
|
||||
): void {
|
||||
const a = Mesh.push(mesh, x - dx, y0, z - dz, STEM_U, STEM_V)
|
||||
const b = Mesh.push(mesh, x + dx, y0, z + dz, STEM_U, STEM_V)
|
||||
const c = Mesh.push(mesh, x + dx, y1, z + dz, STEM_U, STEM_V)
|
||||
|
|
|
|||
|
|
@ -1,27 +1,14 @@
|
|||
import type { Terrain } from "../Terrain"
|
||||
import type { Vec3 } from "../../engine/math/Vec3"
|
||||
import type { Mesh } from "../../engine/scene/Mesh"
|
||||
import type { Entity } from "../../engine/scene/Actor"
|
||||
import { frog } from "./mobs/Frog"
|
||||
import { bee } from "./mobs/Bee"
|
||||
import { robin } from "./mobs/Robin"
|
||||
import type { Actor } from "../../engine/scene/Actor"
|
||||
import type { RenderTransform } from "../../engine/render/RenderScene"
|
||||
import type { BoxCollider } from "../../engine/world/Collider"
|
||||
import type { Terrain } from "../../engine/world/Terrain"
|
||||
|
||||
/** A roaming creature drawn as a moving low-poly mesh (unlike the static baked
|
||||
* world). Each kind is an `Entity` definition (geometry + behavior + bounds) living
|
||||
* in its own module under `mobs/`; this file just assembles them into a registry
|
||||
* and exposes a thin per-kind dispatch. Adding a kind = add a `mobs/<Kind>.ts` +
|
||||
* one entry in `MOB_KINDS`/`DEFS`.
|
||||
*
|
||||
* A mob's geometry 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 on the instance 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" | "robin"
|
||||
/** Runtime actor using shared roaming-creature state. Concrete definitions are
|
||||
* direct object references supplied by Frog, Bee, Robin, or future content. */
|
||||
export type Mob = Actor<Terrain>
|
||||
|
||||
export type Mob = {
|
||||
kind: MobKind
|
||||
export type MobState = {
|
||||
/** Leash anchor (where it was scattered); wandering is pulled back toward it. */
|
||||
home: Vec3
|
||||
/** Live feet-center (frog/robin) / body-center (bee), advanced each frame. */
|
||||
|
|
@ -39,46 +26,39 @@ export type Mob = {
|
|||
vy: number
|
||||
/** Countdown to the next decision (frog/robin: next hop; bee: next heading change). */
|
||||
timer: number
|
||||
/** Per-kind scratch clock: the bee's hover-bob phase; the robin's remaining
|
||||
/** Behavior scratch clock: the bee's hover-bob phase; the robin's remaining
|
||||
* powered-flight cruise time (>0 while gliding between perches). */
|
||||
phase: number
|
||||
/** Frog/robin: resting on the ground vs airborne (a hop or a flight). */
|
||||
grounded: boolean
|
||||
}
|
||||
|
||||
/** Canonical kind order. **The index is the id packed into the mob SAB** (see
|
||||
* renderer/worker), so this order must be identical in every context and must not
|
||||
* change under existing kinds -- `mobs.test.ts` guards it. Append new kinds. */
|
||||
export const MOB_KINDS: MobKind[] = ["frog", "bee", "robin"]
|
||||
|
||||
/** The per-kind `Entity` definitions, one module each. Imported (not cloned) into
|
||||
* whatever context uses it, so it works the same on the main thread and in workers. */
|
||||
const DEFS: Record<MobKind, Entity<Mob, Terrain>> = { frog, bee, robin }
|
||||
|
||||
export namespace Mob {
|
||||
/** The definition for a kind (geometry, behavior, bounds). */
|
||||
export function def(kind: MobKind): Entity<Mob, Terrain> {
|
||||
return DEFS[kind]
|
||||
export function transform(state: MobState): RenderTransform {
|
||||
return {
|
||||
x: state.position.x,
|
||||
y: state.position.y,
|
||||
z: state.position.z,
|
||||
heading: state.heading,
|
||||
scale: state.scale,
|
||||
}
|
||||
}
|
||||
|
||||
/** Advance one mob by `dt` seconds, sampling `terrain` for ground height. */
|
||||
export function update(mob: Mob, dt: number, terrain: Terrain): void {
|
||||
DEFS[mob.kind].update(mob, dt, terrain)
|
||||
}
|
||||
|
||||
/** Append the canonical local-space mesh for `kind` into `mesh` (once per kind at
|
||||
* load; every instance shares it, differing only by transform). */
|
||||
export function build(kind: MobKind, mesh: Mesh): void {
|
||||
DEFS[kind].build(mesh)
|
||||
}
|
||||
|
||||
/** Local bounding radius (pre-scale), for building the per-frame cull AABB. */
|
||||
export function boundingRadius(kind: MobKind): number {
|
||||
return DEFS[kind].boundingRadius
|
||||
}
|
||||
|
||||
/** Local body height (pre-scale), for the top of the stand-on collider. */
|
||||
export function bodyHeight(kind: MobKind): number {
|
||||
return DEFS[kind].bodyHeight
|
||||
export function collider(
|
||||
state: MobState,
|
||||
radius: number,
|
||||
height: number,
|
||||
standable: boolean,
|
||||
): BoxCollider {
|
||||
const half = radius * state.scale * 0.7
|
||||
return {
|
||||
shape: "box",
|
||||
minX: state.position.x - half,
|
||||
maxX: state.position.x + half,
|
||||
minZ: state.position.z - half,
|
||||
maxZ: state.position.z + half,
|
||||
top: state.position.y + height * state.scale,
|
||||
standable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,54 +1,11 @@
|
|||
import type { Vec3 } from "../../engine/math/Vec3"
|
||||
import type { Mesh } from "../../engine/scene/Mesh"
|
||||
import { oak } from "./trees/Oak"
|
||||
import { spruce } from "./trees/Spruce"
|
||||
import { birch } from "./trees/Birch"
|
||||
|
||||
export type TreeKind = "oak" | "spruce" | "birch"
|
||||
|
||||
/** One procedural tree instance. `growth` 0..1 runs sapling -> full grown: it scales
|
||||
* height and girth and adds canopy blobs / tiers. `seed` drives the per-tree random
|
||||
* wobble so a forest doesn't look cloned. */
|
||||
export type Tree = {
|
||||
kind: TreeKind
|
||||
/** Trunk base, sitting on the ground. */
|
||||
position: Vec3
|
||||
growth: number
|
||||
seed: number
|
||||
}
|
||||
|
||||
/** Definition of a tree species: which chunk materials its trunk + foliage bake
|
||||
* into, plus how to append its geometry. Each lives in its own `trees/<Kind>.ts`
|
||||
* module (silhouette carries the species read); this file just assembles them.
|
||||
* `trunk`/`foliage` are chunk-material keys (see `level.ts` `ChunkMaterials`):
|
||||
* oak/spruce use the brown `bark`, birch the white `birch`; foliage is the oak
|
||||
* `leaf` or spruce `needle`. */
|
||||
export type TreeSpecies = {
|
||||
kind: TreeKind
|
||||
trunk: string
|
||||
foliage: string
|
||||
build: (tree: Tree, trunk: Mesh, foliage: Mesh, lod: "full" | "impostor") => void
|
||||
}
|
||||
|
||||
/** All tree species (also the placement roll's palette). Trees are baked at load,
|
||||
* not shipped per frame, so this order isn't an id contract like `MOB_KINDS` -- but
|
||||
* keeping it lets placement + tests stay registry-driven. */
|
||||
export const TREE_KINDS: TreeKind[] = ["oak", "spruce", "birch"]
|
||||
|
||||
/** The per-species definitions, one module each. Imported (not cloned) wherever
|
||||
* used, so it works the same on the main thread and in workers. */
|
||||
const SPECIES: Record<TreeKind, TreeSpecies> = { oak, spruce, birch }
|
||||
|
||||
export namespace Tree {
|
||||
/** The species definition for a kind (its trunk/foliage materials + geometry). */
|
||||
export function species(kind: TreeKind): TreeSpecies {
|
||||
return SPECIES[kind]
|
||||
}
|
||||
|
||||
/** Append one tree into the caller-provided `trunk` + `foliage` meshes (which the
|
||||
* caller selects from the species' `trunk`/`foliage` material keys). `lod`
|
||||
* "impostor" bakes a much cheaper stand-in for far chunks; "full" is up close. */
|
||||
export function build(tree: Tree, trunk: Mesh, foliage: Mesh, lod: "full" | "impostor" = "full"): void {
|
||||
SPECIES[tree.kind].build(tree, trunk, foliage, lod)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,28 @@
|
|||
import { Terrain } from "../../Terrain"
|
||||
import type { Mesh } from "../../../engine/scene/Mesh"
|
||||
import type { Mob } from "../Mob"
|
||||
import type { Entity } from "../../../engine/scene/Actor"
|
||||
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, ovoidZ, wanderHeading, wing } from "./mobkit"
|
||||
|
||||
export const bee: Entity<Mob, Terrain> = {
|
||||
name: "bee",
|
||||
build,
|
||||
update,
|
||||
boundingRadius: 0.5,
|
||||
bodyHeight: 0.5,
|
||||
export type Bee = ActorDefinition<MobState, Ground>
|
||||
|
||||
export namespace Bee {
|
||||
export function create(material: Material): Bee {
|
||||
const mesh = Mesh.create()
|
||||
build(mesh)
|
||||
return {
|
||||
prototype: {
|
||||
groups: [{ mesh, material }],
|
||||
radius: 0.6,
|
||||
minY: -0.5,
|
||||
maxY: 1,
|
||||
},
|
||||
update,
|
||||
transform: Mob.transform,
|
||||
collider: (state) => Mob.collider(state, 0.5, 0.5, false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const LEASH = 6
|
||||
|
|
@ -20,7 +33,7 @@ const HOVER = 1.1
|
|||
const BOB_AMP = 0.18
|
||||
const BOB_FREQ = 3
|
||||
|
||||
function build(mesh: Mesh): void {
|
||||
function build(mesh: Geometry): void {
|
||||
// Fore-aft ovoid body striped along its length, a dark head at the front, two
|
||||
// pale wings. UVs: bee texture is stripe bands (left), head-dark (mid), wing-pale
|
||||
// (right); the body maps v along z so the stripes band across it.
|
||||
|
|
@ -30,7 +43,7 @@ function build(mesh: Mesh): void {
|
|||
wing(mesh, -1, 0.83, 0.99, 0, 1)
|
||||
}
|
||||
|
||||
function update(mob: Mob, dt: number, terrain: Terrain): void {
|
||||
function update(mob: MobState, dt: number, terrain: Ground): void {
|
||||
mob.phase += dt
|
||||
mob.timer -= dt
|
||||
if (mob.timer <= 0) {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,30 @@
|
|||
import { Terrain } from "../../Terrain"
|
||||
import type { Mesh } from "../../../engine/scene/Mesh"
|
||||
import type { Mob } from "../Mob"
|
||||
import type { Entity } from "../../../engine/scene/Actor"
|
||||
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 frog: squat, ground-bound, sits then springs a ballistic hop.
|
||||
|
||||
export const frog: Entity<Mob, Terrain> = {
|
||||
name: "frog",
|
||||
build,
|
||||
update,
|
||||
boundingRadius: 0.7,
|
||||
bodyHeight: 0.6,
|
||||
export type Frog = ActorDefinition<MobState, Ground>
|
||||
|
||||
export namespace Frog {
|
||||
export function create(material: Material): Frog {
|
||||
const mesh = Mesh.create()
|
||||
build(mesh)
|
||||
return {
|
||||
prototype: {
|
||||
groups: [{ mesh, material }],
|
||||
radius: 0.7,
|
||||
minY: -0.7,
|
||||
maxY: 1.3,
|
||||
},
|
||||
update,
|
||||
transform: Mob.transform,
|
||||
collider: (state) => Mob.collider(state, 0.7, 0.6, state.grounded),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const LEASH = 5
|
||||
|
|
@ -21,7 +34,7 @@ const HOP_SPEED = 1.6
|
|||
const HOP_IMPULSE = 3.2
|
||||
const GRAVITY = 14
|
||||
|
||||
function build(mesh: Mesh): void {
|
||||
function build(mesh: Geometry): void {
|
||||
// Wide squat body, two eye bumps on the top-front, two hind haunches. UVs:
|
||||
// the frog texture is green skin on the left, a dark eye tone on the right.
|
||||
ellipsoid(mesh, 0, 0.26, 0, 0.5, 0.28, 0.52, 6, 4, 0, 0.68, 0, 1)
|
||||
|
|
@ -31,7 +44,7 @@ function build(mesh: Mesh): void {
|
|||
ellipsoid(mesh, -0.3, 0.2, -0.26, 0.2, 0.2, 0.26, 4, 3, 0, 0.68, 0, 1)
|
||||
}
|
||||
|
||||
function update(mob: Mob, dt: number, terrain: Terrain): void {
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,31 @@
|
|||
import { Terrain } from "../../Terrain"
|
||||
import type { Mesh } from "../../../engine/scene/Mesh"
|
||||
import type { Mob } from "../Mob"
|
||||
import type { Entity } from "../../../engine/scene/Actor"
|
||||
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 const robin: Entity<Mob, Terrain> = {
|
||||
name: "robin",
|
||||
build,
|
||||
update,
|
||||
boundingRadius: 0.45,
|
||||
bodyHeight: 0.55,
|
||||
export type Robin = ActorDefinition<MobState, Ground>
|
||||
|
||||
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
|
||||
|
|
@ -27,7 +40,7 @@ const FLY_IMPULSE = 3.5
|
|||
const CRUISE = 0.8
|
||||
const GRAVITY = 14
|
||||
|
||||
function build(mesh: Mesh): void {
|
||||
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).
|
||||
|
|
@ -40,7 +53,7 @@ function build(mesh: Mesh): void {
|
|||
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 {
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import type { Mob } from "../Mob"
|
||||
import type { MobState } from "../Mob"
|
||||
import { STRIDE, type Mesh } from "../../../engine/scene/Mesh"
|
||||
|
||||
// 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.
|
||||
// Shared building blocks for concrete mob definitions: faceted geometry primitives
|
||||
// and deterministic wander helpers.
|
||||
|
||||
export const TAU = Math.PI * 2
|
||||
|
||||
|
|
@ -13,7 +11,11 @@ export const TAU = Math.PI * 2
|
|||
/** 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 {
|
||||
export function wanderHeading(
|
||||
mob: MobState,
|
||||
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) {
|
||||
|
|
@ -24,7 +26,7 @@ export function wanderHeading(mob: Mob, leash: number, jitter: number): number {
|
|||
|
||||
/** 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 {
|
||||
export function nextRand(mob: MobState): number {
|
||||
const a = (mob.seed + 0x6D2B79F5) | 0
|
||||
mob.seed = a
|
||||
let t = Math.imul(a ^ (a >>> 15), 1 | a)
|
||||
|
|
@ -33,7 +35,7 @@ export function nextRand(mob: Mob): number {
|
|||
}
|
||||
|
||||
// --- Geometry primitives --------------------------------------------------
|
||||
// Mobs are drawn double-sided (see renderScene), so winding is not load-bearing --
|
||||
// Mobs are drawn double-sided, 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. */
|
||||
|
|
@ -61,7 +63,13 @@ export function ellipsoid(
|
|||
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)
|
||||
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)
|
||||
|
|
@ -97,13 +105,36 @@ export function ovoidZ(
|
|||
}
|
||||
|
||||
/** 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 {
|
||||
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,
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,51 @@
|
|||
import { Vec3 } from "../../../engine/math/Vec3"
|
||||
import type { Mesh } from "../../../engine/scene/Mesh"
|
||||
import type { Tree, TreeSpecies } from "../Tree"
|
||||
import type { Material } from "../../../engine/render/Material"
|
||||
import type { Prefab } from "../../../engine/scene/Prefab"
|
||||
import type { Tree } from "../Tree"
|
||||
import { blob, lerp, limb, TAU, rng } from "./treekit"
|
||||
|
||||
// Silver birch: tall, slender, near-straight trunk under an airy, high, slightly
|
||||
// drooping canopy -- a lean silhouette between the broad oak and conical spruce.
|
||||
// Trunk = white birch bark, foliage = oak leaf (the white trunk carries the read).
|
||||
|
||||
export const birch: TreeSpecies = { kind: "birch", trunk: "birch", foliage: "leaf", build }
|
||||
export type Birch = Prefab<Tree>
|
||||
|
||||
function build(tree: Tree, trunk: Mesh, leaves: Mesh, lod: "full" | "impostor"): void {
|
||||
export namespace Birch {
|
||||
export function create(trunk: Material, foliage: Material): Birch {
|
||||
return {
|
||||
position: (tree) => tree.position,
|
||||
bakeNear: (tree, batch) =>
|
||||
build(tree, batch.mesh(trunk), batch.mesh(foliage), "full"),
|
||||
bakeFar: (tree, batch) =>
|
||||
build(tree, batch.mesh(trunk), batch.mesh(foliage), "impostor"),
|
||||
collider: treeCollider,
|
||||
}
|
||||
}
|
||||
|
||||
function treeCollider(tree: Tree) {
|
||||
if (tree.growth <= 0.35) {
|
||||
return null
|
||||
}
|
||||
const radius = tree.growth * 0.2 + 0.15
|
||||
return {
|
||||
shape: "box" as const,
|
||||
minX: tree.position.x - radius,
|
||||
maxX: tree.position.x + radius,
|
||||
minZ: tree.position.z - radius,
|
||||
maxZ: tree.position.z + radius,
|
||||
top: tree.position.y + 3,
|
||||
standable: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function build(
|
||||
tree: Tree,
|
||||
trunk: Mesh,
|
||||
leaves: Mesh,
|
||||
lod: "full" | "impostor",
|
||||
): void {
|
||||
const base = tree.position
|
||||
const g = tree.growth
|
||||
const rand = rng(tree.seed)
|
||||
|
|
@ -18,11 +54,25 @@ function build(tree: Tree, trunk: Mesh, leaves: Mesh, lod: "full" | "impostor"):
|
|||
const canopyY = base.y + h * 0.75
|
||||
const blobR = h * 0.22
|
||||
if (lod === "impostor") {
|
||||
limb(trunk, base, { x: base.x, y: base.y + h * 0.9, z: base.z }, rTrunk, rTrunk * 0.5, 3)
|
||||
limb(
|
||||
trunk,
|
||||
base,
|
||||
{ x: base.x, y: base.y + h * 0.9, z: base.z },
|
||||
rTrunk,
|
||||
rTrunk * 0.5,
|
||||
3,
|
||||
)
|
||||
blob(leaves, { x: base.x, y: canopyY, z: base.z }, blobR * 1.1, rand, 4, 2)
|
||||
return
|
||||
}
|
||||
limb(trunk, base, { x: base.x, y: base.y + h * 0.88, z: base.z }, rTrunk, rTrunk * 0.35, 5)
|
||||
limb(
|
||||
trunk,
|
||||
base,
|
||||
{ x: base.x, y: base.y + h * 0.88, z: base.z },
|
||||
rTrunk,
|
||||
rTrunk * 0.35,
|
||||
5,
|
||||
)
|
||||
|
||||
const spread = h * 0.22
|
||||
// Sparse small blobs clustered high, biased downward so the crown droops.
|
||||
|
|
@ -42,7 +92,11 @@ function build(tree: Tree, trunk: Mesh, leaves: Mesh, lod: "full" | "impostor"):
|
|||
const branches = 2 + Math.round(rand())
|
||||
for (let i = 0; i < branches; i++) {
|
||||
const angle = rand() * TAU
|
||||
const dir = Vec3.normalize({ x: Math.cos(angle), y: 0.6, z: Math.sin(angle) })
|
||||
const dir = Vec3.normalize({
|
||||
x: Math.cos(angle),
|
||||
y: 0.6,
|
||||
z: Math.sin(angle),
|
||||
})
|
||||
const start = { x: base.x, y: base.y + h * 0.7, z: base.z }
|
||||
const end = Vec3.add(start, Vec3.scale(dir, h * 0.22))
|
||||
limb(trunk, start, end, rTrunk * 0.4, rTrunk * 0.15, 4)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,48 @@
|
|||
import { Vec3 } from "../../../engine/math/Vec3"
|
||||
import type { Mesh } from "../../../engine/scene/Mesh"
|
||||
import type { Tree, TreeSpecies } from "../Tree"
|
||||
import type { Material } from "../../../engine/render/Material"
|
||||
import type { Prefab } from "../../../engine/scene/Prefab"
|
||||
import type { Tree } from "../Tree"
|
||||
import { blob, lerp, limb, TAU, rng } from "./treekit"
|
||||
|
||||
// Oak: short tapered trunk, a couple of branches, a broad cluster of rounded canopy
|
||||
// blobs (bushy, wider than tall). Trunk = brown bark, foliage = oak leaf.
|
||||
|
||||
export const oak: TreeSpecies = { kind: "oak", trunk: "bark", foliage: "leaf", build }
|
||||
export type Oak = Prefab<Tree>
|
||||
|
||||
function build(tree: Tree, trunk: Mesh, leaves: Mesh, lod: "full" | "impostor"): void {
|
||||
export namespace Oak {
|
||||
export function create(trunk: Material, foliage: Material): Oak {
|
||||
return {
|
||||
position: (tree) => tree.position,
|
||||
bakeNear: (tree, batch) =>
|
||||
build(tree, batch.mesh(trunk), batch.mesh(foliage), "full"),
|
||||
bakeFar: (tree, batch) =>
|
||||
build(tree, batch.mesh(trunk), batch.mesh(foliage), "impostor"),
|
||||
collider(tree) {
|
||||
if (tree.growth <= 0.35) {
|
||||
return null
|
||||
}
|
||||
const radius = tree.growth * 0.3 + 0.15
|
||||
return {
|
||||
shape: "box",
|
||||
minX: tree.position.x - radius,
|
||||
maxX: tree.position.x + radius,
|
||||
minZ: tree.position.z - radius,
|
||||
maxZ: tree.position.z + radius,
|
||||
top: tree.position.y + 3,
|
||||
standable: false,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function build(
|
||||
tree: Tree,
|
||||
trunk: Mesh,
|
||||
leaves: Mesh,
|
||||
lod: "full" | "impostor",
|
||||
): void {
|
||||
const base = tree.position
|
||||
const g = tree.growth
|
||||
const rand = rng(tree.seed)
|
||||
|
|
@ -19,7 +53,14 @@ function build(tree: Tree, trunk: Mesh, leaves: Mesh, lod: "full" | "impostor"):
|
|||
const blobR = h * 0.3
|
||||
if (lod === "impostor") {
|
||||
// One low-poly blob on a stubby trunk -- reads as an oak at distance.
|
||||
limb(trunk, base, { x: base.x, y: forkY, z: base.z }, rTrunk, rTrunk * 0.6, 3)
|
||||
limb(
|
||||
trunk,
|
||||
base,
|
||||
{ x: base.x, y: forkY, z: base.z },
|
||||
rTrunk,
|
||||
rTrunk * 0.6,
|
||||
3,
|
||||
)
|
||||
blob(leaves, { x: base.x, y: canopyY, z: base.z }, blobR * 1.15, rand, 4, 2)
|
||||
return
|
||||
}
|
||||
|
|
@ -43,7 +84,11 @@ function build(tree: Tree, trunk: Mesh, leaves: Mesh, lod: "full" | "impostor"):
|
|||
const branches = 2 + Math.round(rand())
|
||||
for (let i = 0; i < branches; i++) {
|
||||
const angle = rand() * TAU
|
||||
const dir = Vec3.normalize({ x: Math.cos(angle), y: 1.2, z: Math.sin(angle) })
|
||||
const dir = Vec3.normalize({
|
||||
x: Math.cos(angle),
|
||||
y: 1.2,
|
||||
z: Math.sin(angle),
|
||||
})
|
||||
const start = { x: base.x, y: base.y + h * 0.42, z: base.z }
|
||||
const end = Vec3.add(start, Vec3.scale(dir, h * 0.3))
|
||||
limb(trunk, start, end, rTrunk * 0.4, rTrunk * 0.2, 4)
|
||||
|
|
|
|||
|
|
@ -1,20 +1,63 @@
|
|||
import type { Mesh } from "../../../engine/scene/Mesh"
|
||||
import type { Tree, TreeSpecies } from "../Tree"
|
||||
import type { Material } from "../../../engine/render/Material"
|
||||
import type { Prefab } from "../../../engine/scene/Prefab"
|
||||
import type { Tree } from "../Tree"
|
||||
import { cone, lerp, limb, rng } from "./treekit"
|
||||
|
||||
// Spruce: tall thin trunk under stacked cones that narrow to a point (tiered, taller
|
||||
// than wide). Trunk = brown bark, foliage = spruce needle.
|
||||
|
||||
export const spruce: TreeSpecies = { kind: "spruce", trunk: "bark", foliage: "needle", build }
|
||||
export type Spruce = Prefab<Tree>
|
||||
|
||||
function build(tree: Tree, trunk: Mesh, needles: Mesh, lod: "full" | "impostor"): void {
|
||||
export namespace Spruce {
|
||||
export function create(trunk: Material, foliage: Material): Spruce {
|
||||
return {
|
||||
position: (tree) => tree.position,
|
||||
bakeNear: (tree, batch) =>
|
||||
build(tree, batch.mesh(trunk), batch.mesh(foliage), "full"),
|
||||
bakeFar: (tree, batch) =>
|
||||
build(tree, batch.mesh(trunk), batch.mesh(foliage), "impostor"),
|
||||
collider: treeCollider,
|
||||
}
|
||||
}
|
||||
|
||||
function treeCollider(tree: Tree) {
|
||||
if (tree.growth <= 0.35) {
|
||||
return null
|
||||
}
|
||||
const radius = tree.growth * 0.2 + 0.15
|
||||
return {
|
||||
shape: "box" as const,
|
||||
minX: tree.position.x - radius,
|
||||
maxX: tree.position.x + radius,
|
||||
minZ: tree.position.z - radius,
|
||||
maxZ: tree.position.z + radius,
|
||||
top: tree.position.y + 3,
|
||||
standable: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function build(
|
||||
tree: Tree,
|
||||
trunk: Mesh,
|
||||
needles: Mesh,
|
||||
lod: "full" | "impostor",
|
||||
): void {
|
||||
const base = tree.position
|
||||
const g = tree.growth
|
||||
const rand = rng(tree.seed)
|
||||
const h = lerp(0.6, 9, g)
|
||||
const rTrunk = lerp(0.03, 0.2, g)
|
||||
const impostor = lod === "impostor"
|
||||
limb(trunk, base, { x: base.x, y: base.y + h, z: base.z }, rTrunk, rTrunk * 0.25, impostor ? 3 : 5)
|
||||
limb(
|
||||
trunk,
|
||||
base,
|
||||
{ x: base.x, y: base.y + h, z: base.z },
|
||||
rTrunk,
|
||||
rTrunk * 0.25,
|
||||
impostor ? 3 : 5,
|
||||
)
|
||||
|
||||
// Stacked cones: widest low, shrinking to a point up top -> conical tiers. The
|
||||
// impostor keeps the first two tiers at low sides (same seed => aligned).
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { Vec3 } from "../../../engine/math/Vec3"
|
||||
import { STRIDE, type Mesh } from "../../../engine/scene/Mesh"
|
||||
|
||||
// Shared faceted-geometry primitives + the per-tree RNG, used by the species
|
||||
// modules (Oak/Spruce/Birch). Kept separate so a species and the `Tree` registry
|
||||
// don't form an import cycle.
|
||||
// Shared faceted-geometry primitives + the per-tree RNG used by concrete tree
|
||||
// prefab modules.
|
||||
|
||||
export const TAU = Math.PI * 2
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue