refactor: move world concepts into engine

This commit is contained in:
Toad 2026-08-24 15:17:53 +02:00
parent eeedcb8e48
commit 2d15c7ab8d
52 changed files with 3298 additions and 1558 deletions

View file

@ -1,26 +1,48 @@
import type { Mesh } from "./Mesh"
import type { RenderPrototype, RenderTransform } from "../render/RenderScene"
import type { Collider } from "../world/Collider"
/** Definition of an **Entity** actor kind: something that lives in the world with
* its own behavior and a live transform (mobs, and later the npc / powerups) -- as
* opposed to a baked, static `Prop`. `State` is the per-instance runtime record the
* behavior mutates; `World` is whatever that behavior reads (e.g. `Terrain`).
*
* Every field is plain data or a module function, so a definition is **imported
* into each context** (main thread + each render worker) rather than structured-
* cloned across the wire -- the per-kind polymorphism is code, not serialized
* state. That's what lets a registry of these stay compatible with the worker
* renderer (only plain instance data ever crosses; behavior is loaded per side). */
export type Entity<State, World> = {
/** Stable tag for the kind (also the texture key today). The registry's order,
* not this string, is what becomes the id packed into the mob SAB. */
name: string
/** Build the canonical local-space mesh once; every instance shares it, differing
* only by its per-frame model matrix. */
build: (mesh: Mesh) => void
/** Advance one instance by `dt` seconds. */
/** Open actor behavior and representation. Concrete definitions are ordinary game
* objects referenced directly by instances. */
export type ActorDefinition<State, World> = {
prototype: RenderPrototype
update: (state: State, dt: number, world: World) => void
/** Local bounding radius (pre-scale) for the per-frame cull AABB. */
boundingRadius: number
/** Local body height (pre-scale) for the top of the stand-on collider. */
bodyHeight: number
transform: (state: State) => RenderTransform
collider?: (state: State, world: World) => Collider | null
}
/** Type-erased live actor. `create` captures concrete state safely, allowing one
* level to hold unrelated actor state types without a content union. */
export type Actor<World> = {
readonly definition: object
readonly prototype: RenderPrototype
readonly updateState: (dt: number, world: World) => void
readonly readTransform: () => RenderTransform
readonly readCollider: (world: World) => Collider | null
}
export namespace Actor {
export function create<State, World>(
definition: ActorDefinition<State, World>,
state: State,
): Actor<World> {
return {
definition,
prototype: definition.prototype,
updateState: (dt, world) => definition.update(state, dt, world),
readTransform: () => definition.transform(state),
readCollider: (world) => definition.collider?.(state, world) ?? null,
}
}
export function update<World>(actor: Actor<World>, dt: number, world: World): void {
actor.updateState(dt, world)
}
export function transform<World>(actor: Actor<World>): RenderTransform {
return actor.readTransform()
}
export function collider<World>(actor: Actor<World>, world: World): Collider | null {
return actor.readCollider(world)
}
}

View file

@ -0,0 +1,80 @@
import { STRIDE, type Mesh } from "./Mesh"
type Corner = [number, number, number]
export namespace MeshBuilder {
export function quad(
mesh: Mesh,
a: Corner,
b: Corner,
c: Corner,
d: Corner,
uScale: number,
vScale: number,
): void {
const base = mesh.verts.length / STRIDE
mesh.verts.push(
a[0],
a[1],
a[2],
0,
0,
b[0],
b[1],
b[2],
uScale,
0,
c[0],
c[1],
c[2],
uScale,
vScale,
d[0],
d[1],
d[2],
0,
vScale,
)
mesh.indices.push(base, base + 1, base + 2, base, base + 2, base + 3)
}
export function slab(
mesh: Mesh,
x0: number,
x1: number,
z0: number,
z1: number,
y0: number,
y1: number,
tilesPerUnit: number,
): void {
const dx = (x1 - x0) * tilesPerUnit
const dz = (z1 - z0) * tilesPerUnit
const dy = (y1 - y0) * tilesPerUnit
quad(mesh, [x0, y1, z0], [x1, y1, z0], [x1, y1, z1], [x0, y1, z1], dx, dz)
quad(mesh, [x0, y0, z0], [x1, y0, z0], [x1, y1, z0], [x0, y1, z0], dx, dy)
quad(mesh, [x1, y0, z1], [x0, y0, z1], [x0, y1, z1], [x1, y1, z1], dx, dy)
quad(mesh, [x0, y0, z1], [x0, y0, z0], [x0, y1, z0], [x0, y1, z1], dz, dy)
quad(mesh, [x1, y0, z0], [x1, y0, z1], [x1, y1, z1], [x1, y1, z0], dz, dy)
}
export function box(
mesh: Mesh,
centerX: number,
centerZ: number,
half: number,
base: number,
height: number,
): void {
const x0 = centerX - half
const x1 = centerX + half
const z0 = centerZ - half
const z1 = centerZ + half
const y1 = base + height
quad(mesh, [x0, y1, z0], [x1, y1, z0], [x1, y1, z1], [x0, y1, z1], 1, 1)
quad(mesh, [x0, base, z0], [x1, base, z0], [x1, y1, z0], [x0, y1, z0], 1, 1)
quad(mesh, [x1, base, z1], [x0, base, z1], [x0, y1, z1], [x1, y1, z1], 1, 1)
quad(mesh, [x1, base, z0], [x1, base, z1], [x1, y1, z1], [x1, y1, z0], 1, 1)
quad(mesh, [x0, base, z1], [x0, base, z0], [x0, y1, z0], [x0, y1, z1], 1, 1)
}
}

38
engine/scene/Prefab.ts Normal file
View file

@ -0,0 +1,38 @@
import type { Vec3 } from "../math/Vec3"
import type { MeshBatch } from "../render/ChunkBuilder"
import type { Collider } from "../world/Collider"
/** Static content recipe. Concrete game prefabs implement this engine contract and
* are referenced as objects, never through a closed content-kind registry. */
export type Prefab<State> = {
position: (state: State) => Vec3
bakeNear: (state: State, batch: MeshBatch) => void
bakeFar?: (state: State, batch: MeshBatch) => void
collider?: (state: State) => Collider | null
}
/** Type-erased placed prefab consumed during level compilation only. */
export type PlacedPrefab = {
position: Vec3
bakeNear: (batch: MeshBatch) => void
bakeFar?: (batch: MeshBatch) => void
collider: Collider | null
}
export namespace Prefab {
export function place<State>(
definition: Prefab<State>,
state: State,
): PlacedPrefab {
const bakeFar = definition.bakeFar
return {
position: definition.position(state),
bakeNear: (batch) => definition.bakeNear(state, batch),
bakeFar:
bakeFar === undefined
? undefined
: (batch) => bakeFar(state, batch),
collider: definition.collider?.(state) ?? null,
}
}
}