123 lines
3.7 KiB
TypeScript
123 lines
3.7 KiB
TypeScript
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<World> = {
|
|
terrain: Terrain
|
|
actorWorld: World
|
|
actors: readonly RuntimeActor<World>[]
|
|
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<World> = {
|
|
readonly terrain: Terrain
|
|
readonly actorWorld: World
|
|
readonly actors: readonly RuntimeActor<World>[]
|
|
readonly collision: Collision
|
|
readonly render: Scene
|
|
}
|
|
|
|
const prototypeIndexes = new WeakMap<object, ReadonlyMap<object, number>>()
|
|
|
|
export namespace Level {
|
|
export function create<World>(definition: LevelDefinition<World>): Level<World> {
|
|
const actors = Object.freeze([...definition.actors])
|
|
const prototypes: RenderPrototype[] = []
|
|
const prototypeIndex = new Map<object, number>()
|
|
for (const actor of actors) {
|
|
if (!prototypeIndex.has(actor.definition)) {
|
|
prototypeIndex.set(actor.definition, prototypes.length)
|
|
prototypes.push(actor.prototype)
|
|
}
|
|
}
|
|
const level: Level<World> = {
|
|
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<World>(level: Level<World>, dt: number): void {
|
|
for (const actor of level.actors) {
|
|
Actor.update(actor, dt, level.actorWorld)
|
|
}
|
|
}
|
|
|
|
export function visibleInstances<World>(
|
|
level: Level<World>,
|
|
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<World>(
|
|
level: Level<World>,
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|