Files
nage/engine/Scene.ts
T
2026-03-01 11:55:27 +01:00

232 lines
5.8 KiB
TypeScript

import type { Box } from "#/engine/Box.ts";
import { type Drawable, draw, isDrawable } from "#/engine/Drawable.ts";
import {
entityMap,
entityProcessMap,
type Process as ProcessEntity,
} from "#/engine/Entity.ts";
import { type Graph, getNeighbours } from "#/engine/Graph.ts";
import { useWithAsyncCache } from "#/engine/lib/cache.ts";
import { animateSprite, type SpriteAnimation } from "#/engine/Sprite.ts";
export type EntityData = {
name: string;
init: object;
};
export type SceneData = {
name: string;
width: number;
height: number;
entities: Array<EntityData>;
};
export type EntityMap = Map<
string,
{
process: ProcessEntity<{ id: number }>;
instances: Array<{ id: number }>;
}
>;
export type AnimationMap = Map<number, Box<SpriteAnimation>>;
export type Scene = {
data: SceneData;
entityMap: EntityMap;
process: Process;
};
export type Process = (ctx: CanvasRenderingContext2D, delta: number) => void;
export type PreProcess = (entityMap: EntityMap) => void;
export type PostProcess = (entityMap: EntityMap) => void;
export type SceneChangeHandler = (to: SceneData, from: SceneData) => void;
export const sceneProcessMap = new Map<string, Process>();
export const scenePreProcessMap = new Map<string, PreProcess>();
export const scenePostProcessMap = new Map<string, PostProcess>();
export const sceneChangedHandlers = new Set<SceneChangeHandler>();
export const sceneDataMap = new Map<string, SceneData>();
export const sceneEntiyMap = new Map<string, EntityMap>();
export const sceneGraph: Graph<string> = new Map();
const sceneInstancesDrawMap = new Map<
string,
Array<Drawable & { id: number }>
>();
const sceneInstancesAnimationMap = new Map<string, AnimationMap>();
export const [loadScene, sceneCache] = useWithAsyncCache(
async (name: string) => {
await import(`#/game/scenes/${name}/scene.ts`);
const data = sceneDataMap.get(name)!;
const entityInstanceMap = sceneEntiyMap.get(name)!;
entityInstanceMap.clear();
await Promise.all(
data.entities.map(async (entityData: EntityData, index: number) => {
await import(`#/game/entities/${entityData.name}/entity.ts`);
if (!entityInstanceMap.has(entityData.name)) {
entityInstanceMap.set(entityData.name, {
process: entityProcessMap.get(entityData.name) ?? (() => {}),
instances: [],
});
}
try {
entityInstanceMap.get(entityData.name)!.instances.push(
structuredClone({
...entityMap.get(entityData.name)!,
...entityData,
id: index,
}),
);
} catch (error) {
console.error(entityData.name, error);
}
}),
);
},
);
export const currentScene: Scene = {
data: {
name: "",
width: 0,
height: 0,
entities: [],
},
process: () => {},
entityMap: new Map(),
};
function processEntities(delta: number): void {
for (const entry of currentScene.entityMap.values()) {
for (const instance of entry.instances) {
entry.process(instance, delta);
}
}
}
function processSystems(ctx: CanvasRenderingContext2D, delta: number): void {
const drawables = sceneInstancesDrawMap.get(currentScene.data.name)!;
const animationsMap = sceneInstancesAnimationMap.get(currentScene.data.name)!;
for (const drawable of drawables.sort(
(a, b) => a.position.y - b.position.y,
)) {
// this dosn't belong here
if (animationsMap.has(drawable.id)) {
animateSprite(animationsMap.get(drawable.id)!.value, delta);
}
draw(ctx, drawable);
}
}
function linkScenes(a: string, b: string): void {
if (sceneGraph.has(a)) {
sceneGraph.get(a)!.push(b);
} else {
sceneGraph.set(a, [b]);
}
}
export function defineScene(data: SceneData): {
linkScenes: <T extends readonly string[]>(
names: T,
) => (name: T[number]) => Promise<void>;
process: (fn: Process) => void;
preProcess: (fn: PreProcess) => void;
postProcess: (fn: PostProcess) => void;
} {
sceneDataMap.set(data.name, data);
sceneEntiyMap.set(data.name, new Map());
return {
linkScenes(ids) {
for (const id of ids) {
linkScenes(data.name, id);
}
return async (id) => {
return setScene(id);
};
},
process(fn) {
sceneProcessMap.set(data.name, (ctx, delta) => {
fn(ctx, delta);
processEntities(delta);
processSystems(ctx, delta);
});
},
preProcess(fn) {
scenePreProcessMap.set(data.name, fn);
},
postProcess(fn) {
scenePostProcessMap.set(data.name, fn);
},
};
}
export async function setScene(name: string): Promise<void> {
await loadScene(name);
for (const neighbour of getNeighbours(sceneGraph, name)) {
if (!sceneCache.has(neighbour)) {
sceneCache.set(neighbour, loadScene(neighbour));
}
}
const data = sceneDataMap.get(name)!;
const process = sceneProcessMap.get(name)!;
const entityInstanceMap = sceneEntiyMap.get(name)!;
const drawableInstances: Array<Drawable & { id: number }> = [];
sceneInstancesDrawMap.set(data.name, drawableInstances);
const animateInstances: AnimationMap = new Map();
sceneInstancesAnimationMap.set(data.name, animateInstances);
for (const entity of entityInstanceMap.values()) {
for (const instance of entity.instances) {
if (isDrawable(instance)) {
drawableInstances.push(instance);
}
if (
(instance as { id: number; animation: object }).animation !== undefined
) {
animateInstances.set(
instance.id,
(instance as { id: number; animation: Box<SpriteAnimation> })
.animation, // TODO
);
}
}
}
const preProcess = scenePreProcessMap.get(name);
if (preProcess) {
preProcess(sceneEntiyMap.get(name)!);
}
const postProcess = scenePostProcessMap.get(currentScene.data.name);
if (postProcess) {
postProcess(sceneEntiyMap.get(name)!);
}
currentScene.data = data;
self.dispatchEvent(new Event("resize"));
currentScene.process = process;
currentScene.entityMap = entityInstanceMap;
for (const handler of sceneChangedHandlers.values()) {
handler(data, currentScene.data);
}
}