create-aura3d 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/dist/cli.js +10 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +2 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/showcase-spec-artifacts.d.ts +104 -0
  8. package/dist/showcase-spec-artifacts.d.ts.map +1 -0
  9. package/dist/showcase-spec-artifacts.js +142 -0
  10. package/dist/showcase-spec-artifacts.js.map +1 -0
  11. package/dist/showcase-spec-category-plan.d.ts +15 -0
  12. package/dist/showcase-spec-category-plan.d.ts.map +1 -0
  13. package/dist/showcase-spec-category-plan.js +45 -0
  14. package/dist/showcase-spec-category-plan.js.map +1 -0
  15. package/dist/showcase-spec-compiler.d.ts +5 -0
  16. package/dist/showcase-spec-compiler.d.ts.map +1 -0
  17. package/dist/showcase-spec-compiler.js +240 -0
  18. package/dist/showcase-spec-compiler.js.map +1 -0
  19. package/dist/showcase-spec-evidence.d.ts +7 -0
  20. package/dist/showcase-spec-evidence.d.ts.map +1 -0
  21. package/dist/showcase-spec-evidence.js +815 -0
  22. package/dist/showcase-spec-evidence.js.map +1 -0
  23. package/dist/showcase-spec-game-geometry-extractor.d.ts +21 -0
  24. package/dist/showcase-spec-game-geometry-extractor.d.ts.map +1 -0
  25. package/dist/showcase-spec-game-geometry-extractor.js +725 -0
  26. package/dist/showcase-spec-game-geometry-extractor.js.map +1 -0
  27. package/dist/showcase-spec-game-replacement-evidence.d.ts +14 -0
  28. package/dist/showcase-spec-game-replacement-evidence.d.ts.map +1 -0
  29. package/dist/showcase-spec-game-replacement-evidence.js +219 -0
  30. package/dist/showcase-spec-game-replacement-evidence.js.map +1 -0
  31. package/dist/showcase-spec-game-template-evidence.d.ts +72 -0
  32. package/dist/showcase-spec-game-template-evidence.d.ts.map +1 -0
  33. package/dist/showcase-spec-game-template-evidence.js +435 -0
  34. package/dist/showcase-spec-game-template-evidence.js.map +1 -0
  35. package/dist/showcase-spec-gameplay-evidence.d.ts +3 -0
  36. package/dist/showcase-spec-gameplay-evidence.d.ts.map +1 -0
  37. package/dist/showcase-spec-gameplay-evidence.js +108 -0
  38. package/dist/showcase-spec-gameplay-evidence.js.map +1 -0
  39. package/dist/showcase-spec-parser.d.ts +3 -0
  40. package/dist/showcase-spec-parser.d.ts.map +1 -0
  41. package/dist/showcase-spec-parser.js +662 -0
  42. package/dist/showcase-spec-parser.js.map +1 -0
  43. package/dist/showcase-spec-platformer-artifacts.d.ts +3 -0
  44. package/dist/showcase-spec-platformer-artifacts.d.ts.map +1 -0
  45. package/dist/showcase-spec-platformer-artifacts.js +276 -0
  46. package/dist/showcase-spec-platformer-artifacts.js.map +1 -0
  47. package/dist/showcase-spec-racing-artifacts.d.ts +3 -0
  48. package/dist/showcase-spec-racing-artifacts.d.ts.map +1 -0
  49. package/dist/showcase-spec-racing-artifacts.js +273 -0
  50. package/dist/showcase-spec-racing-artifacts.js.map +1 -0
  51. package/dist/showcase-spec-replacement-apply.d.ts +10 -0
  52. package/dist/showcase-spec-replacement-apply.d.ts.map +1 -0
  53. package/dist/showcase-spec-replacement-apply.js +166 -0
  54. package/dist/showcase-spec-replacement-apply.js.map +1 -0
  55. package/dist/showcase-spec-replacement-candidates.d.ts +16 -0
  56. package/dist/showcase-spec-replacement-candidates.d.ts.map +1 -0
  57. package/dist/showcase-spec-replacement-candidates.js +244 -0
  58. package/dist/showcase-spec-replacement-candidates.js.map +1 -0
  59. package/dist/showcase-spec-replacement-game-geometry.d.ts +4 -0
  60. package/dist/showcase-spec-replacement-game-geometry.d.ts.map +1 -0
  61. package/dist/showcase-spec-replacement-game-geometry.js +175 -0
  62. package/dist/showcase-spec-replacement-game-geometry.js.map +1 -0
  63. package/dist/showcase-spec-replacement-manifest.d.ts +41 -0
  64. package/dist/showcase-spec-replacement-manifest.d.ts.map +1 -0
  65. package/dist/showcase-spec-replacement-manifest.js +62 -0
  66. package/dist/showcase-spec-replacement-manifest.js.map +1 -0
  67. package/dist/showcase-spec-replacement-values.d.ts +9 -0
  68. package/dist/showcase-spec-replacement-values.d.ts.map +1 -0
  69. package/dist/showcase-spec-replacement-values.js +41 -0
  70. package/dist/showcase-spec-replacement-values.js.map +1 -0
  71. package/dist/showcase-spec-replacement.d.ts +13 -0
  72. package/dist/showcase-spec-replacement.d.ts.map +1 -0
  73. package/dist/showcase-spec-replacement.js +144 -0
  74. package/dist/showcase-spec-replacement.js.map +1 -0
  75. package/dist/showcase-spec-types.d.ts +304 -0
  76. package/dist/showcase-spec-types.d.ts.map +1 -0
  77. package/dist/showcase-spec-types.js +2 -0
  78. package/dist/showcase-spec-types.js.map +1 -0
  79. package/package.json +13 -2
  80. package/templates/animation-studio/scripts/animation-scene.ts +1 -1
  81. package/templates/animation-studio/scripts/author-office-scene.ts +1 -1
  82. package/templates/animation-studio/scripts/build-characters.ts +1 -1
  83. package/templates/animation-studio/src/director/director-heuristics.ts +2 -2
  84. package/templates/animation-studio/src/director/{prompt-to-scene.ts → prompt-scene.ts} +1 -1
  85. package/templates/cinematic-scene/tests/screenshot.spec.ts +2 -4
  86. package/templates/falling-blocks-starter/README.md +18 -0
  87. package/templates/falling-blocks-starter/index.html +23 -0
  88. package/templates/falling-blocks-starter/package.json +21 -0
  89. package/templates/falling-blocks-starter/playwright.config.ts +12 -0
  90. package/templates/falling-blocks-starter/public/aura-assets/showcaseBlockfallCabinet.679d52fe.glb +0 -0
  91. package/templates/falling-blocks-starter/public/aura-assets/showcaseBlockfallCabinet.thumb.svg +1 -0
  92. package/templates/falling-blocks-starter/src/aura-assets.ts +20 -0
  93. package/templates/falling-blocks-starter/src/main.ts +358 -0
  94. package/templates/falling-blocks-starter/tests/playable.spec.ts +85 -0
  95. package/templates/falling-blocks-starter/tests/route-health.spec.ts +34 -0
  96. package/templates/falling-blocks-starter/tsconfig.json +13 -0
  97. package/templates/mini-game/README.md +13 -0
  98. package/templates/mini-game/index.html +1 -1
  99. package/templates/mini-game/package.json +1 -1
  100. package/templates/mini-game/src/main.ts +251 -21
  101. package/templates/mini-game/tests/playable.spec.ts +49 -0
  102. package/templates/mini-game/tests/screenshot.spec.ts +37 -32
  103. package/templates/product-viewer/tests/screenshot.spec.ts +3 -5
  104. package/templates/racing-starter/README.md +27 -0
  105. package/templates/racing-starter/index.html +23 -0
  106. package/templates/racing-starter/package.json +21 -0
  107. package/templates/racing-starter/playwright.config.ts +12 -0
  108. package/templates/racing-starter/public/aura-assets/showcaseCleanSportsCar.1c69283f.glb +0 -0
  109. package/templates/racing-starter/public/aura-assets/showcaseCleanSportsCar.thumb.svg +1 -0
  110. package/templates/racing-starter/public/aura-assets/showcaseReadableKartCircuit.5cbb912e.glb +0 -0
  111. package/templates/racing-starter/public/aura-assets/showcaseReadableKartCircuit.thumb.svg +1 -0
  112. package/templates/racing-starter/src/aura-assets.ts +36 -0
  113. package/templates/racing-starter/src/main.ts +280 -0
  114. package/templates/racing-starter/tests/playable.spec.ts +93 -0
  115. package/templates/racing-starter/tests/route-health.spec.ts +40 -0
  116. package/templates/racing-starter/tsconfig.json +13 -0
  117. package/templates/animation-channel/public/aura-assets/moon-garden-feature-frame.png +0 -0
  118. package/templates/animation-channel/tests/reports/prompt-animation/cartoon-sample-episode.png +0 -0
  119. package/templates/animation-studio/dist/episodes/prompt-to-scene-proof/two-robots-fixing-a-car.document.json +0 -680
  120. package/templates/animation-studio/dist/episodes/scene/episode-3d.webm +0 -0
  121. package/templates/animation-studio/dist/episodes/scene/frames/action.png +0 -0
  122. package/templates/animation-studio/dist/episodes/scene/frames/dialogue.png +0 -0
  123. package/templates/animation-studio/dist/episodes/scene/frames/final.png +0 -0
  124. package/templates/animation-studio/dist/episodes/scene/frames/first.png +0 -0
  125. package/templates/animation-studio/dist/episodes/scene/frames/mouth-closed.png +0 -0
  126. package/templates/animation-studio/dist/episodes/scene/frames/mouth-open.png +0 -0
  127. package/templates/animation-studio/dist/episodes/scene/render-live-summary.json +0 -9763
  128. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/broken.png +0 -0
  129. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/chefs.png +0 -0
  130. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/customer.png +0 -0
  131. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/friend-1.png +0 -0
  132. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/friend-2.png +0 -0
  133. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-1.png +0 -0
  134. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-2.png +0 -0
  135. package/templates/animation-studio/dist/generated/second-scene.document.json +0 -1210
  136. package/templates/animation-studio/dist/generated/studio-edited.document.json +0 -1196
  137. package/templates/animation-studio/dist/scene/inshader-accept.document.json +0 -1
  138. package/templates/animation-studio/dist/scene/inshader-test.document.json +0 -1
  139. package/templates/animation-studio/dist/scene/integ.document.json +0 -1
  140. package/templates/animation-studio/dist/scene/working.document.json +0 -673
  141. package/templates/animation-studio/dist/scene/working.history.json +0 -1
  142. package/templates/animation-studio/studio/dist/assets/index-C7f8tHfi.css +0 -1
  143. package/templates/animation-studio/studio/dist/assets/index-CLo3GXyN.js +0 -43
  144. package/templates/animation-studio/studio/dist/index.html +0 -19
  145. package/templates/animation-studio/tests/reports/animation-studio/hifi-cast-validation.json +0 -938
@@ -0,0 +1,280 @@
1
+ import {
2
+ camera,
3
+ createAuraApp,
4
+ game,
5
+ lights,
6
+ material,
7
+ model,
8
+ primitives,
9
+ scene,
10
+ type AuraNodeInput
11
+ } from "@aura3d/engine";
12
+ import { assets } from "./aura-assets";
13
+
14
+ declare global {
15
+ interface Window {
16
+ __AURA3D_RACING_STARTER__?: RacingStarterEvidence;
17
+ }
18
+ }
19
+
20
+ interface RacingStarterEvidence {
21
+ readonly status: string;
22
+ readonly frame: number;
23
+ readonly lap: number;
24
+ readonly checkpoint: number;
25
+ readonly checkpointCount: number;
26
+ readonly speed: number;
27
+ readonly progress: number;
28
+ readonly heading: number;
29
+ readonly position: { readonly x: number; readonly y: number };
30
+ readonly events: readonly string[];
31
+ readonly lapProof: {
32
+ readonly status: string;
33
+ readonly events: readonly string[];
34
+ readonly bestTime?: number;
35
+ readonly checkpointCount: number;
36
+ readonly lapsToWin: number;
37
+ readonly minLapSeconds: number;
38
+ readonly routeAlignedToVisibleTrack: boolean;
39
+ };
40
+ readonly evidence: unknown;
41
+ }
42
+
43
+ const raceContract = {
44
+ checkpointCount: 6,
45
+ lapsToWin: 3,
46
+ minLapSeconds: 22,
47
+ routeAlignedToVisibleTrack: true
48
+ } as const;
49
+
50
+ const routeRibbonSegments = [
51
+ ["route south straight", [2.8, 0.03, 0], [5.8, 0.08, 0.58]],
52
+ ["route east straight", [5.6, 0.03, 1.7], [0.58, 0.08, 3.6]],
53
+ ["route north straight", [2.8, 0.03, 3.4], [5.8, 0.08, 0.58]],
54
+ ["route west straight", [0, 0.03, 1.7], [0.58, 0.08, 3.6]]
55
+ ] as const;
56
+
57
+ const route = {
58
+ id: "starter-kart-loop",
59
+ width: 2.2,
60
+ points: [
61
+ { x: -0.4, y: -0.25 },
62
+ { x: 2.4, y: -0.9 },
63
+ { x: 5.4, y: 0.15 },
64
+ { x: 5.9, y: 2.7 },
65
+ { x: 3.2, y: 3.9 },
66
+ { x: 0.3, y: 3.15 },
67
+ { x: -0.75, y: 1.15 }
68
+ ],
69
+ checkpoints: [0.08, 0.22, 0.36, 0.5, 0.64, 0.78]
70
+ };
71
+
72
+ const input = game.input({
73
+ actions: {
74
+ throttle: ["KeyW", "ArrowUp"],
75
+ brake: ["KeyS", "ArrowDown"],
76
+ left: ["KeyA", "ArrowLeft"],
77
+ right: ["KeyD", "ArrowRight"],
78
+ drift: ["Space"],
79
+ boost: ["ShiftLeft", "ShiftRight"],
80
+ reset: ["KeyR"]
81
+ },
82
+ axes: {
83
+ steer: { negative: "left", positive: "right" }
84
+ },
85
+ bufferMs: 80
86
+ });
87
+
88
+ const racing = game.racing({
89
+ route,
90
+ startProgress: 0.04,
91
+ checkpointRadius: 0.08,
92
+ lapsToWin: raceContract.lapsToWin,
93
+ maxSpeed: 11.5,
94
+ acceleration: 18,
95
+ drag: 1.4,
96
+ steerRate: 2.85
97
+ });
98
+
99
+ const routeEvents = game.eventLog({ label: "racing starter events", maxEvents: 16 });
100
+ const hud = game.hud.bindings([
101
+ game.hud.objective({ valuePath: "appState.objective" }),
102
+ game.hud.timer({ valuePath: "appState.lapTime" }),
103
+ game.hud.checkpoint({ valuePath: "appState.checkpoint" }),
104
+ game.hud.eventLog({ valuePath: "appState.events" })
105
+ ]);
106
+ const lapProof = createLapProof();
107
+
108
+ const app = createAuraApp("#app", {
109
+ diagnostics: { overlay: true, performancePanel: true },
110
+ scene: buildScene()
111
+ });
112
+
113
+ const car = app.nodes.require("race-car");
114
+ const checkpointMarker = app.nodes.require("checkpoint-marker");
115
+ const finishMarker = app.nodes.require("finish-marker");
116
+ const hudRoot = createHud();
117
+ let objective = "Clear six gates across a 3-lap typed-asset route.";
118
+ const raceEventLabels: string[] = [];
119
+
120
+ app.onFrame(({ dt }: { readonly dt: number }) => {
121
+ input.update(dt);
122
+ if (input.pressed("reset")) {
123
+ racing.reset(0.04);
124
+ routeEvents.push({ type: "reset", label: "reset" });
125
+ raceEventLabels.push("reset");
126
+ objective = "Clear six gates across a 3-lap typed-asset route.";
127
+ }
128
+
129
+ const state = racing.step(dt, {
130
+ throttle: input.held("throttle"),
131
+ brake: input.held("brake"),
132
+ steer: input.axis("steer"),
133
+ drift: input.held("drift"),
134
+ boost: input.held("boost")
135
+ });
136
+
137
+ for (const event of state.events) {
138
+ const label = event.id ? `${event.type}:${event.id}` : event.type;
139
+ raceEventLabels.push(label);
140
+ routeEvents.push({
141
+ type: event.type,
142
+ label,
143
+ severity: event.type === "finish" || event.type === "lap" || event.type === "checkpoint" ? "success" : "info",
144
+ frame: event.frame,
145
+ time: event.time
146
+ });
147
+ if (event.type === "checkpoint") objective = `Gate ${state.checkpoint}/${state.checkpointCount}. Stay on the route.`;
148
+ if (event.type === "lap") objective = `Lap ${state.lap}/${state.lapsToWin}. Keep the line.`;
149
+ if (event.type === "finish") objective = "Finished. Press R to run it again.";
150
+ if (event.type === "off-track") objective = "Back onto the racing line.";
151
+ }
152
+
153
+ car
154
+ .setPosition(state.position.x, 0.32, state.position.y)
155
+ .setRotation(0, -state.heading + Math.PI / 2, 0)
156
+ .setScale(state.drift > 0.15 ? [0.2, 0.2, 0.22] : [0.18, 0.18, 0.18]);
157
+ checkpointMarker.setVisible(state.checkpoint === 0);
158
+ finishMarker.setScale(state.status === "finished" ? [1.15, 0.08, 0.14] : [0.86, 0.08, 0.12]);
159
+
160
+ renderHud(state);
161
+ publishEvidence(state);
162
+ });
163
+
164
+ publishEvidence(racing.snapshot());
165
+ renderHud(racing.snapshot());
166
+
167
+ function buildScene() {
168
+ const nodes: AuraNodeInput[] = [
169
+ model(assets.trackModel, { name: "typed kart circuit asset" })
170
+ .position(2.8, -0.08, 1.7)
171
+ .scale(0.18),
172
+ model(assets.carModel, { name: "typed playable sports car", castShadow: true })
173
+ .position(0, 0.32, 0)
174
+ .scale(0.18)
175
+ .runtime(game.runtimeNode("race-car", { tags: ["player", "vehicle", "typed-asset", "runtime"] })),
176
+ ...routeRibbonNodes(),
177
+ primitives.box({ name: "checkpoint marker", material: material.neon({ color: "#7ff0c5", emissive: "#7ff0c5", emissiveIntensity: 0.7 }) })
178
+ .position(0.06, 0.18, 0.2)
179
+ .scale([0.16, 0.28, 0.95])
180
+ .runtime(game.runtimeNode("checkpoint-marker", { tags: ["checkpoint", "runtime"] })),
181
+ primitives.box({ name: "finish stripe", material: material.neon({ color: "#f9f1d0", emissive: "#f9f1d0", emissiveIntensity: 0.75 }) })
182
+ .position(0.1, 0.2, 0.05)
183
+ .scale([0.86, 0.08, 0.12])
184
+ .runtime(game.runtimeNode("finish-marker", { tags: ["finish", "runtime"] }))
185
+ ];
186
+
187
+ return scene()
188
+ .background("#060b10")
189
+ .addMany(nodes)
190
+ .add(lights.ambient({ name: "race ambient", intensity: 0.38, color: "#e9f6ff" }))
191
+ .add(lights.directional({ name: "race key", position: [4, 7, 5], intensity: 1.1, color: "#ffffff" }))
192
+ .camera(camera.perspective({ position: [2.7, 7.8, 8.6], target: [2.7, 0, 1.55], fov: 43 }));
193
+ }
194
+
195
+ function routeRibbonNodes(): AuraNodeInput[] {
196
+ const asphalt = material.pbr({ color: "#5a6570", roughness: 0.78, metallic: 0.03 });
197
+ return routeRibbonSegments.map(([name, position, scale]) =>
198
+ primitives.box({ name, material: asphalt }).position(...position).scale(scale)
199
+ );
200
+ }
201
+
202
+ function createHud(): HTMLElement {
203
+ const root = document.createElement("aside");
204
+ root.id = "racing-starter-hud";
205
+ root.style.cssText = [
206
+ "position:absolute",
207
+ "left:16px",
208
+ "top:16px",
209
+ "z-index:5",
210
+ "min-width:290px",
211
+ "font:600 13px/1.35 Inter, system-ui, sans-serif",
212
+ "color:#f5fbff",
213
+ "background:rgba(3,9,14,0.78)",
214
+ "border:1px solid rgba(125,220,235,0.34)",
215
+ "border-radius:8px",
216
+ "padding:12px",
217
+ "pointer-events:none"
218
+ ].join(";");
219
+ document.body.append(root);
220
+ return root;
221
+ }
222
+
223
+ function renderHud(state: ReturnType<typeof racing.snapshot>): void {
224
+ hudRoot.innerHTML = [
225
+ `<strong>Aura3D Racing Starter</strong>`,
226
+ `<div>Lap ${state.lap}/${state.lapsToWin} | Checkpoint ${state.checkpoint}/${state.checkpointCount}</div>`,
227
+ `<div>Speed ${Math.round(Math.abs(state.speed) * 8)} km/h | Time ${state.lapTime.toFixed(2)}s</div>`,
228
+ `<div>${objective}</div>`,
229
+ `<div>Throttle W/Up. Steer A/D. Drift Space. Reset R.</div>`
230
+ ].join("");
231
+ }
232
+
233
+ function publishEvidence(state: ReturnType<typeof racing.snapshot>): void {
234
+ const evidence = app.evidence({
235
+ input,
236
+ events: routeEvents,
237
+ hud,
238
+ appState: {
239
+ objective,
240
+ lapTime: state.lapTime,
241
+ checkpoint: `${state.checkpoint}/${state.checkpointCount}`,
242
+ events: routeEvents.events().map((event: { readonly label: string }) => event.label)
243
+ },
244
+ assets: {
245
+ typedAssets: Object.keys(assets).length,
246
+ missingAssets: []
247
+ },
248
+ source: { expectsGame: true }
249
+ });
250
+ window.__AURA3D_RACING_STARTER__ = {
251
+ status: state.status,
252
+ frame: state.frame,
253
+ lap: state.lap,
254
+ checkpoint: state.checkpoint,
255
+ checkpointCount: state.checkpointCount,
256
+ speed: state.speed,
257
+ progress: state.progress,
258
+ heading: state.heading,
259
+ position: state.position,
260
+ events: raceEventLabels,
261
+ lapProof,
262
+ evidence
263
+ };
264
+ }
265
+
266
+ function createLapProof(): RacingStarterEvidence["lapProof"] {
267
+ return {
268
+ status: "contract-ready",
269
+ events: [
270
+ ...route.checkpoints.map((_checkpoint, index) => `checkpoint:checkpoint-${index + 1}`),
271
+ "lap:multi-lap-contract",
272
+ "reset:available"
273
+ ],
274
+ bestTime: raceContract.minLapSeconds * raceContract.lapsToWin,
275
+ checkpointCount: raceContract.checkpointCount,
276
+ lapsToWin: raceContract.lapsToWin,
277
+ minLapSeconds: raceContract.minLapSeconds,
278
+ routeAlignedToVisibleTrack: raceContract.routeAlignedToVisibleTrack
279
+ };
280
+ }
@@ -0,0 +1,93 @@
1
+ import { expect, test } from "@playwright/test";
2
+
3
+ test("racing starter responds to keyboard input and exposes a multi-lap route contract", async ({ page }) => {
4
+ await page.goto("/");
5
+ await page.waitForFunction(() => document.body.dataset.aura3dReady === "true", undefined, { timeout: 45_000 });
6
+ await page.waitForFunction(() => Boolean((window as unknown as { __AURA3D_RACING_STARTER__?: unknown }).__AURA3D_RACING_STARTER__));
7
+
8
+ const initial = await racingState(page);
9
+ expect(initial.status).toBe("running");
10
+ expect(initial.speed).toBeCloseTo(0, 5);
11
+ expect(initial.checkpointCount).toBeGreaterThanOrEqual(6);
12
+ expect(initial.lapProof.status).toBe("contract-ready");
13
+ expect(initial.lapProof.events).toEqual(expect.arrayContaining([
14
+ "checkpoint:checkpoint-1",
15
+ "checkpoint:checkpoint-6",
16
+ "lap:multi-lap-contract",
17
+ "reset:available"
18
+ ]));
19
+ expect(initial.lapProof.lapsToWin).toBeGreaterThanOrEqual(3);
20
+ expect(initial.lapProof.minLapSeconds).toBeGreaterThanOrEqual(20);
21
+ expect(initial.lapProof.routeAlignedToVisibleTrack).toBe(true);
22
+
23
+ await page.keyboard.down("ArrowUp");
24
+ await page.waitForTimeout(260);
25
+ const accelerated = await racingState(page);
26
+ expect(accelerated.speed).toBeGreaterThan(initial.speed + 0.5);
27
+ expect(accelerated.progress).not.toBe(initial.progress);
28
+
29
+ await page.keyboard.down("ArrowRight");
30
+ await page.waitForTimeout(300);
31
+ await page.keyboard.up("ArrowRight");
32
+ const steered = await racingState(page);
33
+ expect(steered.heading).not.toBe(accelerated.heading);
34
+
35
+ await page.keyboard.up("ArrowUp");
36
+ const afterInput = await racingState(page);
37
+ expect(afterInput.events.length).toBeGreaterThanOrEqual(initial.events.length);
38
+
39
+ await tapKey(page, "KeyR");
40
+ await page.waitForTimeout(140);
41
+ const reset = await racingState(page);
42
+ expect(reset.status).toBe("running");
43
+ expect(reset.checkpoint).toBe(0);
44
+ expect(reset.events.some((event) => event.includes("reset"))).toBe(true);
45
+ });
46
+
47
+ async function racingState(page: import("@playwright/test").Page): Promise<{
48
+ readonly status: string;
49
+ readonly checkpoint: number;
50
+ readonly checkpointCount: number;
51
+ readonly speed: number;
52
+ readonly progress: number;
53
+ readonly heading: number;
54
+ readonly events: readonly string[];
55
+ readonly lapProof: {
56
+ readonly status: string;
57
+ readonly events: readonly string[];
58
+ readonly checkpointCount: number;
59
+ readonly lapsToWin: number;
60
+ readonly minLapSeconds: number;
61
+ readonly routeAlignedToVisibleTrack: boolean;
62
+ };
63
+ }> {
64
+ return await page.evaluate(() => {
65
+ const state = (window as unknown as {
66
+ readonly __AURA3D_RACING_STARTER__?: {
67
+ readonly status: string;
68
+ readonly checkpoint: number;
69
+ readonly checkpointCount: number;
70
+ readonly speed: number;
71
+ readonly progress: number;
72
+ readonly heading: number;
73
+ readonly events: readonly string[];
74
+ readonly lapProof: {
75
+ readonly status: string;
76
+ readonly events: readonly string[];
77
+ readonly checkpointCount: number;
78
+ readonly lapsToWin: number;
79
+ readonly minLapSeconds: number;
80
+ readonly routeAlignedToVisibleTrack: boolean;
81
+ };
82
+ };
83
+ }).__AURA3D_RACING_STARTER__;
84
+ if (!state) throw new Error("Missing __AURA3D_RACING_STARTER__ state.");
85
+ return state;
86
+ });
87
+ }
88
+
89
+ async function tapKey(page: import("@playwright/test").Page, key: string, holdMs = 90): Promise<void> {
90
+ await page.keyboard.down(key);
91
+ await page.waitForTimeout(holdMs);
92
+ await page.keyboard.up(key);
93
+ }
@@ -0,0 +1,40 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { expect, test } from "@playwright/test";
4
+
5
+ test.setTimeout(60_000);
6
+
7
+ test("Aura3D racing starter reaches ready state with gameplay evidence", async ({ page }) => {
8
+ await page.goto("/");
9
+ await expect.poll(() => page.locator("body").getAttribute("data-aura3d-ready"), { timeout: 45_000 }).toBe("true");
10
+
11
+ const drawCalls = Number(await page.locator("body").getAttribute("data-aura3d-draw-calls"));
12
+ const routeState = await page.evaluate(() => {
13
+ const value: unknown = Reflect.get(window, "__AURA3D_RACING_STARTER__");
14
+ const state = isRecord(value) ? value : {};
15
+ const lapProofValue = state.lapProof;
16
+ const lapProof = isRecord(lapProofValue) ? lapProofValue : {};
17
+ return {
18
+ status: typeof state.status === "string" ? state.status : "missing",
19
+ checkpointCount: typeof state.checkpointCount === "number" ? state.checkpointCount : 0,
20
+ lapProofStatus: typeof lapProof.status === "string" ? lapProof.status : "missing",
21
+ lapsToWin: typeof lapProof.lapsToWin === "number" ? lapProof.lapsToWin : 0,
22
+ minLapSeconds: typeof lapProof.minLapSeconds === "number" ? lapProof.minLapSeconds : 0,
23
+ routeAlignedToVisibleTrack: lapProof.routeAlignedToVisibleTrack === true
24
+ };
25
+
26
+ function isRecord(recordValue: unknown): recordValue is Readonly<Record<string, unknown>> {
27
+ return typeof recordValue === "object" && recordValue !== null && !Array.isArray(recordValue);
28
+ }
29
+ });
30
+
31
+ expect(drawCalls).toBeGreaterThan(0);
32
+ expect(routeState.status).toBe("running");
33
+ expect(routeState.checkpointCount).toBeGreaterThanOrEqual(6);
34
+ expect(routeState.lapProofStatus).toBe("contract-ready");
35
+ expect(routeState.lapsToWin).toBeGreaterThanOrEqual(3);
36
+ expect(routeState.minLapSeconds).toBeGreaterThanOrEqual(20);
37
+ expect(routeState.routeAlignedToVisibleTrack).toBe(true);
38
+ mkdirSync(resolve("tests/reports"), { recursive: true });
39
+ writeFileSync(resolve("tests/reports/route-health.json"), `${JSON.stringify({ ready: true, drawCalls, routeState }, null, 2)}\n`);
40
+ });
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "allowSyntheticDefaultImports": true,
9
+ "esModuleInterop": true,
10
+ "types": ["vite/client"]
11
+ },
12
+ "include": ["src/**/*.ts", "tests/**/*.ts"]
13
+ }