Add browser-only editor export and 2.5D runtime presentation
This commit is contained in:
@@ -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);
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user