feat(app): wire the voyage together

Controller drives world and curtain; caption with Back to sea; play,
previous and next in the timeline bar; keyboard; no-WebGL frieze stage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-30 15:11:43 +01:00
1 parent ac017787b5
commit d355b3a41f
5 files changed
+185 -18

No files matched your search

+32 -1
View File
@@ -29,11 +29,13 @@ function fakes() {
sailTo: (id) => later(`sail:${id}`),
setLanding: (id) => later(`land:${id}`),
setRibbons: () => {},
horizonFromFrieze: (y) => { log.push(`horizon:${y}`); },
};
const curtain: ControllerCurtain = {
drop: (saga) => later(`drop:${saga}`),
lift: () => later("lift"),
fade: () => later("fade"),
groundY: () => 500,
};
const flush = async () => {
for (let i = 0; i < 50; i++) {
@@ -57,7 +59,9 @@ test("rapid navigation ends at the last requested song with the curtain lifted",
const drops = f.log.filter((l) => l.startsWith("drop:") || l === "fade").length;
const lifts = f.log.filter((l) => l === "lift").length;
expect(lifts).toBeGreaterThanOrEqual(drops === 0 ? 0 : 1);
expect(f.log.at(-1)).toBe("land:arena");
// covered → sail → landing → lift: the stage is uncovered last, already inside the arena
expect(f.log.lastIndexOf("land:arena")).toBeLessThan(f.log.lastIndexOf("lift"));
expect(f.log.at(-1)).toBe("lift");
expect(ctl.busy()).toBe(false);
});
@@ -71,6 +75,19 @@ test("without a world the controller still follows the songs (frieze-only browsi
expect(f.log).toContain("drop:cyclops");
});
test("the horizon is matched to the frieze's ground line before the curtain lifts", async () => {
const f = fakes();
const store = createStore(DEFAULT_STATE, { history: false });
createController({ store, world: f.world, curtain: f.curtain, reducedMotion: false });
await f.flush();
store.set({ songId: "just-a-man" }); // same saga: no curtain
await f.flush();
expect(f.log.filter((l) => l.startsWith("horizon:"))).toHaveLength(0);
store.set({ songId: "polyphemus" }); // next saga: curtain
await f.flush();
expect(f.log.indexOf("horizon:500")).toBeLessThan(f.log.lastIndexOf("lift"));
});
test("reduced motion turns every scene change into a fade", async () => {
const f = fakes();
const store = createStore(DEFAULT_STATE, { history: false });
@@ -80,3 +97,17 @@ test("reduced motion turns every scene change into a fade", async () => {
expect(f.log).toContain("fade");
expect(f.log.some((l) => l.startsWith("drop:"))).toBe(false);
});
test("Back to sea leaves the landing without changing the song", async () => {
const f = fakes();
const store = createStore({ ...DEFAULT_STATE, songId: "survive" }, { history: false });
const ctl = createController({ store, world: f.world, curtain: f.curtain, reducedMotion: false });
await f.flush();
expect(ctl.landing()).toBe("cave");
const left = ctl.leaveLanding();
await f.flush();
await left;
expect(f.log.at(-1)).toBe("land:null");
expect(ctl.landing()).toBeNull();
expect(store.get().songId).toBe("survive");
});