diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e2cf6000..49c7817d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,7 +84,9 @@ jobs: - run: bun run games:clone env: GAMES_CLONE_TOKEN: ${{ secrets.GAMES_CLONE_TOKEN }} - - run: bun test packages apps/dev scripts + # Browser and physics suites cold-start Chromium/WASM; on a loaded runner the first test in a + # file can pass Bun's 5 s default and cascade through the file. Tests with their own timeout keep it. + - run: bun test --timeout 20000 packages apps/dev scripts web-build: runs-on: ubuntu-latest diff --git a/changes/doctor-silent-game.md b/changes/doctor-silent-game.md new file mode 100644 index 000000000..4d3d64e7c --- /dev/null +++ b/changes/doctor-silent-game.md @@ -0,0 +1,3 @@ +### Added + +- `jgengine doctor` flags a silent game: one that neither declares `defineGame({ audio })` cues nor calls `ctx.game.audio.play`/`loop`. Four reference games shipped without a single sound; the fix text points at procedural `SoundDef` synth patches, which need no asset files. diff --git a/packages/jgengine/src/create.test.ts b/packages/jgengine/src/create.test.ts index 28e793134..3ea318c44 100644 --- a/packages/jgengine/src/create.test.ts +++ b/packages/jgengine/src/create.test.ts @@ -56,7 +56,8 @@ describe("writeGame", () => { } const failures = diagnose(dir).filter( - (finding) => !finding.ok && !finding.label.includes("art-direction") && !finding.label.includes("skeleton"), + (finding) => + !finding.ok && !finding.label.includes("art-direction") && !finding.label.includes("skeleton") && !finding.label.includes("has sound"), ); expect(failures).toEqual([]); }); diff --git a/packages/jgengine/src/doctor.test.ts b/packages/jgengine/src/doctor.test.ts index cf2887c7b..902ba111f 100644 --- a/packages/jgengine/src/doctor.test.ts +++ b/packages/jgengine/src/doctor.test.ts @@ -85,6 +85,13 @@ describe("diagnose", () => { expect(failingLabels(dir)).toContain("Tailwind @source covers @jgengine/react and @jgengine/shell"); }); + test("flags a silent game until it declares or plays audio cues", () => { + const dir = scaffold(); + expect(failingLabels(dir)).toContain("game has sound (declares audio cues)"); + writeFileSync(join(dir, "src", "game", "sfx.ts"), 'export const cue = (ctx: { game: { audio: { play(id: string): void } } }) => ctx.game.audio.play("build");\n'); + expect(failingLabels(dir)).not.toContain("game has sound (declares audio cues)"); + }); + test("flags an aliased component folder that index.css never scans", () => { const dir = scaffold(); const tsconfigPath = join(dir, "tsconfig.json"); diff --git a/packages/jgengine/src/doctor.ts b/packages/jgengine/src/doctor.ts index 9988db60e..55238ed4e 100644 --- a/packages/jgengine/src/doctor.ts +++ b/packages/jgengine/src/doctor.ts @@ -254,6 +254,11 @@ export function diagnose(dir: string): Finding[] { if (existsSync(modelsPath)) related.push(readFileSync(modelsPath, "utf8")); const assetsPath = join(dir, "src", "game", "assets.ts"); if (existsSync(assetsPath)) related.push(readFileSync(assetsPath, "utf8")); + findings.push({ + ok: /\baudio\s*:/.test(configText) || /\baudio\.(play|loop)\(/.test(readSourceText(join(dir, "src"))), + label: "game has sound (declares audio cues)", + fix: 'a silent game reads unfinished — declare cues as procedural SoundDef synth patches (no asset files needed) in defineGame({ audio: { sounds } }) and call ctx.game.audio.play(id) on accepted actions: build, hit, purchase, objective, win/lose', + }); const prototype = assessPrototypeLook(related); findings.push({ ok: !prototype.isPrototype,