Add browser-only editor export and 2.5D runtime presentation

This commit is contained in:
emil28092005
2026-09-12 00:05:19 +03:00
parent a25ed40805
commit 1a0bf725fa
16 changed files with 883 additions and 53 deletions
+24
View File
@@ -0,0 +1,24 @@
import test from "node:test";
import assert from "node:assert/strict";
import { normalizeHud } from "../engine/presentation.ts";
test("HUD bounds script values and handles malformed meter entries", () => {
const h = normalizeHud({
objective: "x".repeat(300),
counters: [1, 2, 3, 4, 5],
meters: [
null,
{ id: "life", max: 5, value: 12, style: "hearts" },
{ max: 0, value: NaN },
{ max: 10, value: -3 },
],
});
assert.equal(h.objective!.length, 180);
assert.equal(h.counters!.length, 4);
assert.equal(h.meters![0].value, 0);
assert.equal(h.meters![1].value, 5);
assert.equal(h.meters![1].style, "hearts");
assert.equal(h.meters![2].max, 1);
assert.equal(h.meters![3].value, 0);
assert.deepEqual(normalizeHud(null), {});
assert.equal(normalizeHud({ overlay: null }).overlay, null);
});
+92
View File
@@ -220,3 +220,95 @@ test(
}
},
);
test(
"fixed orthographic camera, alpha events and legacy projection restore",
{ timeout: 10000 },
async () => {
const r = runtime(),
p = defaultProject(true);
p.settings.rendering = { toneMapping: false };
p.scripts = [
{
id: "fade",
name: "Fade",
fields: {},
source:
'({start(api){api.patch("visual",{components:{material:{alpha:.3,unlit:true}}})}})',
},
];
activeScene(p).entities = [
entity(
"Camera",
{
camera: {
mode: "fixed",
projection: "orthographic",
orthoWidth: 24,
aspect: 16 / 9,
lookAt: [0, 0, 0],
},
},
[10, 20, 30],
"camera",
),
entity(
"Visual",
{
mesh: { type: "box", size: [1, 1, 1] },
material: { color: "#ff8800", alpha: 1 },
script: { scriptId: "fade" },
},
[0, 0, 0],
"visual",
),
];
try {
await r.play(p);
for (
let i = 0;
i < 60 &&
r.state.find((n) => n.id === "visual")?.components.material.alpha !==
0.3;
i++
)
await wait(50);
assert.equal(r.gameCamera.mode, 1);
assert.deepEqual(r.gameCamera.position.asArray(), [10, 20, 30]);
assert.equal(r.gameCamera.orthoLeft, -12);
assert.equal(r.gameCamera.orthoTop, 6.75);
assert.equal(r.gameCamera.viewport.height, 0.75);
assert.equal(
r.scene.imageProcessingConfiguration.toneMappingEnabled,
false,
);
const mat: any = r.nodes.get("visual")!.getChildMeshes()[0].material;
assert.equal(mat.alpha, 0.3);
assert.equal(mat.unlit, true);
await r.stop(p);
assert.equal(
r.state.find((n) => n.id === "visual")!.components.material.alpha,
1,
);
const legacy = defaultProject(true);
activeScene(legacy).entities = [
entity(
"Camera",
{ camera: { offset: [0, 13, -10] } },
[0, 13, -10],
"camera",
),
];
await r.play(legacy);
await wait(50);
assert.equal(r.gameCamera.mode, 0);
assert.equal(r.gameCamera.viewport.height, 1);
assert.equal(
r.scene.imageProcessingConfiguration.toneMappingEnabled,
true,
);
} finally {
r.dispose();
}
},
);
+29
View File
@@ -0,0 +1,29 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { loadEditorProject } from '../editor/startup.ts';
import { defaultProject } from '../engine/templates.ts';
const neverRequest: typeof fetch = async () => { throw Error('Unexpected network request'); };
test('hosted editor loads its bundled project without contacting a local engine', async()=>{
let requests=0;
const seed=defaultProject(true);seed.name='Bundled courtyard';
const result=await loadEditorProject({browserOnly:true,initialProject:seed},{request:async()=>{requests++;return new Response();},draft:async()=>null});
assert.equal(requests,0);assert.equal(result.project.name,seed.name);assert.equal(result.status,null);assert.equal(result.restored,false);
result.project.name='edited';assert.equal(seed.name,'Bundled courtyard');
});
test('hosted editor retains user edits across reloads instead of replacing them with its seed',async()=>{
const seed=defaultProject(true),saved=defaultProject(true);saved.name='User edits';
const result=await loadEditorProject({browserOnly:true,initialProject:seed},{request:neverRequest,draft:async()=>saved});
assert.equal(result.project.name,'User edits');assert.equal(result.restored,true);
});
test('unavailable or corrupt browser storage still opens the bundled project',async()=>{
const seed=defaultProject(true);seed.name='Seed';
for(const draft of [async()=>{throw Error('Storage unavailable');},async()=>({invalid:true} as any)]){
const result=await loadEditorProject({browserOnly:true,initialProject:seed},{request:neverRequest,draft});assert.equal(result.project.name,'Seed');
}
});
test('local editor still gives the connected Forma project priority over browser drafts',async()=>{
const project=defaultProject(true);project.name='Local engine project';
const status={forma:true,canUndo:true};
const result=await loadEditorProject({}, {request:async(url)=>Response.json(url==='/api/status'?status:{project}),draft:async()=>{throw Error('Must not read draft');}});
assert.equal(result.project.name,project.name);assert.deepEqual(result.status,status);
});