import { Actor, type Actor as RuntimeActor, } from "../scene/Actor" import { Collider, type Collider as CollisionShape } from "./Collider" import { CollisionWorld, type CollisionWorld as Collision, } from "./CollisionWorld" import type { Terrain } from "./Terrain" import { RenderScene, type Billboard, type RenderInstance, type RenderPrototype, type RenderScene as Scene, } from "../render/RenderScene" import type { Chunk } from "../render/Chunk" import type { DrawGroup } from "../render/Material" import type { SkyConfig } from "../render/Sky" import type { Mat4 } from "../math/Mat4" import type { Vec3 } from "../math/Vec3" export type LevelDefinition = { terrain: Terrain actorWorld: World actors: readonly RuntimeActor[] staticColliders: CollisionShape[] staticGroups: DrawGroup[] chunks: Chunk[] billboards: Billboard[] sky: SkyConfig } /** Live engine world. Behavior-bearing actors stay here on the main thread; only * `render` is clone-safe and sent to workers. */ export type Level = { readonly terrain: Terrain readonly actorWorld: World readonly actors: readonly RuntimeActor[] readonly collision: Collision readonly render: Scene } const prototypeIndexes = new WeakMap>() export namespace Level { export function create(definition: LevelDefinition): Level { const actors = Object.freeze([...definition.actors]) const prototypes: RenderPrototype[] = [] const prototypeIndex = new Map() for (const actor of actors) { if (!prototypeIndex.has(actor.definition)) { prototypeIndex.set(actor.definition, prototypes.length) prototypes.push(actor.prototype) } } const level: Level = { terrain: definition.terrain, actorWorld: definition.actorWorld, actors, collision: CollisionWorld.create( definition.terrain, definition.staticColliders, ), render: Object.freeze({ chunks: Object.freeze([...definition.chunks]), staticGroups: Object.freeze([...definition.staticGroups]), billboards: Object.freeze([...definition.billboards]), prototypes: Object.freeze(prototypes), maxInstances: actors.length, sky: definition.sky, }), } prototypeIndexes.set(level, prototypeIndex) return level } export function update(level: Level, dt: number): void { for (const actor of level.actors) { Actor.update(actor, dt, level.actorWorld) } } export function visibleInstances( level: Level, viewProjection: Mat4, ): RenderInstance[] { const prototypeIndex = prototypeIndexes.get(level) if (prototypeIndex === undefined) { throw new Error("level was not created by Level.create") } const instances: RenderInstance[] = [] for (const actor of level.actors) { const prototype = prototypeIndex.get(actor.definition) if (prototype !== undefined) { instances.push({ prototype, ...Actor.transform(actor) }) } } return RenderScene.visibleInstances(level.render, instances, viewProjection) } export function refreshActorColliders( level: Level, focus: Vec3, range: number, ): void { const dynamic = level.collision.dynamicColliders dynamic.length = 0 const rangeSquared = range * range for (const actor of level.actors) { const collider = Actor.collider(actor, level.actorWorld) if (collider === null) { continue } const dx = Collider.centerX(collider) - focus.x const dz = Collider.centerZ(collider) - focus.z if (dx * dx + dz * dz <= rangeSquared) { dynamic.push(collider) } } } }