create-aura3d 2.0.4 → 3.0.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 (51) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +1 -1
  3. package/package.json +2 -2
  4. package/templates/animation-channel/package.json +2 -1
  5. package/templates/animation-studio/package.json +3 -2
  6. package/templates/animation-studio/src/scene-player.ts +28 -1
  7. package/templates/character-controller/README.md +1 -1
  8. package/templates/character-controller/aura.assets.json +31 -1
  9. package/templates/character-controller/package.json +5 -4
  10. package/templates/character-controller/public/aura-assets/showcaseWalkAnimatedGirl.93872fc2.glb +0 -0
  11. package/templates/character-controller/public/aura-assets/showcaseWalkAnimatedGirl.thumb.svg +1 -0
  12. package/templates/character-controller/src/aura-assets.ts +15 -1
  13. package/templates/character-controller/src/main.ts +32 -8
  14. package/templates/character-controller/tests/certified-rig.spec.ts +52 -0
  15. package/templates/cinematic-scene/package.json +2 -1
  16. package/templates/episode-builder/package.json +2 -1
  17. package/templates/falling-blocks-starter/package.json +2 -1
  18. package/templates/fighting-game/README.md +8 -7
  19. package/templates/fighting-game/aura.assets.json +62 -1
  20. package/templates/fighting-game/package.json +3 -2
  21. package/templates/fighting-game/public/aura-assets/showcaseRunnerRobot.252b3a16.glb +0 -0
  22. package/templates/fighting-game/public/aura-assets/showcaseRunnerRobot.thumb.svg +1 -0
  23. package/templates/fighting-game/public/aura-assets/showcaseWalkAnimatedGirl.93872fc2.glb +0 -0
  24. package/templates/fighting-game/public/aura-assets/showcaseWalkAnimatedGirl.thumb.svg +1 -0
  25. package/templates/fighting-game/src/aura-assets.ts +36 -5
  26. package/templates/fighting-game/src/game/fighters.ts +23 -8
  27. package/templates/fighting-game/src/main.ts +21 -4
  28. package/templates/fighting-game/tests/certified-rig.spec.ts +53 -0
  29. package/templates/fighting-game/tests/gameplay-smoke.spec.ts +6 -1
  30. package/templates/mini-game/aura.assets.json +38 -14
  31. package/templates/mini-game/package.json +2 -2
  32. package/templates/mini-game/public/aura-assets/showcaseKenneyOobiPlatformerHero.3f821141.glb +0 -0
  33. package/templates/mini-game/public/aura-assets/showcaseKenneyOobiPlatformerHero.thumb.svg +1 -0
  34. package/templates/mini-game/src/aura-assets.ts +33 -7
  35. package/templates/mini-game/src/main.ts +40 -5
  36. package/templates/mini-game/tests/certified-rig.spec.ts +40 -0
  37. package/templates/product-viewer/package.json +1 -1
  38. package/templates/product-viewer/src/main.ts +9 -2
  39. package/templates/prompt-animation-channel/package.json +2 -1
  40. package/templates/racing-starter/package.json +2 -1
  41. package/templates/racing-starter/src/main.ts +31 -0
  42. package/templates/three-compat-architecture-interior/package.json +2 -1
  43. package/templates/three-compat-asset-inspector/package.json +2 -1
  44. package/templates/three-compat-character-viewer/package.json +2 -1
  45. package/templates/three-compat-custom-threejs-migration/package.json +2 -1
  46. package/templates/three-compat-large-scene/package.json +2 -1
  47. package/templates/three-compat-material-authoring/package.json +2 -1
  48. package/templates/three-compat-postprocess-scene/package.json +2 -1
  49. package/templates/three-compat-premium-product-viewer/package.json +2 -1
  50. package/templates/mini-game/public/aura-assets/player-fixture.glb +0 -0
  51. package/templates/mini-game/public/aura-assets/player.thumb.svg +0 -8
@@ -9,7 +9,20 @@ import {
9
9
  } from "@aura3d/engine";
10
10
 
11
11
  export type FighterId = "player" | "rival";
12
- export type FighterAssetKey = "playerFighter" | "rivalFighter";
12
+ /**
13
+ * Certified E1 hero rigs mounted as the two fighters. The asset keys ARE the
14
+ * certification roster ids (docs/rendering/skinning-and-morphs.md), so the
15
+ * mounted asset id asserts directly against the roster:
16
+ * - player: showcaseWalkAnimatedGirl (humanoid-a) / "Take 001"
17
+ * - rival: showcaseRunnerRobot (creature) / "WALK"
18
+ */
19
+ export const PLAYER_FIGHTER_ASSET = "showcaseWalkAnimatedGirl" as const;
20
+ export const RIVAL_FIGHTER_ASSET = "showcaseRunnerRobot" as const;
21
+ export type FighterAssetKey = typeof PLAYER_FIGHTER_ASSET | typeof RIVAL_FIGHTER_ASSET;
22
+ export const FIGHTER_CERTIFIED_CLIP: Record<FighterId, string> = {
23
+ player: "Take 001",
24
+ rival: "WALK"
25
+ };
13
26
  export type FighterClip = "idle" | "walk" | "jump" | "dash" | "guard" | "light" | "heavy" | "special" | "hitstun";
14
27
 
15
28
  export type TypedFighterAssets = Partial<Record<FighterAssetKey, AuraAssetRef<"model">>>;
@@ -20,7 +33,7 @@ export interface ResolvedFighterAssets {
20
33
  readonly typedFighterAssetCount: number;
21
34
  }
22
35
 
23
- export const REQUIRED_FIGHTER_ASSETS: readonly FighterAssetKey[] = ["playerFighter", "rivalFighter"];
36
+ export const REQUIRED_FIGHTER_ASSETS: readonly FighterAssetKey[] = [PLAYER_FIGHTER_ASSET, RIVAL_FIGHTER_ASSET];
24
37
  export const REQUIRED_FIGHTER_CLIPS: readonly FighterClip[] = [
25
38
  "idle",
26
39
  "walk",
@@ -34,9 +47,10 @@ export const REQUIRED_FIGHTER_CLIPS: readonly FighterClip[] = [
34
47
  ];
35
48
 
36
49
  export const publicAssetInstructions = [
50
+ "certified default heroes: showcaseWalkAnimatedGirl (player) + showcaseRunnerRobot (rival)",
37
51
  "npx @aura3d/cli@latest assets search \"animated humanoid fighting character\" --profile fighting-character --json",
38
- "npx @aura3d/cli@latest assets resolve \"animated humanoid fighting character\" --name playerFighter --profile fighting-character",
39
- "npx @aura3d/cli@latest assets resolve \"animated humanoid fighting character\" --name rivalFighter --profile fighting-character",
52
+ "npx @aura3d/cli@latest assets resolve \"animated humanoid fighting character\" --name showcaseWalkAnimatedGirl --profile fighting-character",
53
+ "npx @aura3d/cli@latest assets resolve \"animated creature fighting character\" --name showcaseRunnerRobot --profile fighting-character",
40
54
  "npx @aura3d/cli@latest assets validate-game --profile fighting-character --json"
41
55
  ] as const;
42
56
 
@@ -66,8 +80,8 @@ export const fighterAnimationClips: readonly AuraNamedAnimationClipDefinition<Fi
66
80
 
67
81
  export function resolveTypedFighterAssets(assetManifest: TypedFighterAssets): ResolvedFighterAssets {
68
82
  const typedFighterAssets: TypedFighterAssets = {
69
- playerFighter: assetManifest.playerFighter,
70
- rivalFighter: assetManifest.rivalFighter
83
+ [PLAYER_FIGHTER_ASSET]: assetManifest[PLAYER_FIGHTER_ASSET],
84
+ [RIVAL_FIGHTER_ASSET]: assetManifest[RIVAL_FIGHTER_ASSET]
71
85
  };
72
86
  const missingFighterAssets = REQUIRED_FIGHTER_ASSETS.filter((key) => !typedFighterAssets[key]);
73
87
  return {
@@ -112,7 +126,8 @@ export function createFighterNode(
112
126
  position: readonly [number, number, number],
113
127
  facing: 1 | -1,
114
128
  color: string,
115
- typedFighterAssets: TypedFighterAssets
129
+ typedFighterAssets: TypedFighterAssets,
130
+ visualScale = 1
116
131
  ) {
117
132
  const asset = typedFighterAssets[assetKey];
118
133
  const runtime = game.runtimeNode(id, {
@@ -123,7 +138,7 @@ export function createFighterNode(
123
138
  return model(asset, { name: label })
124
139
  .position(position[0], position[1], position[2])
125
140
  .rotate(0, facing < 0 ? Math.PI : 0, 0)
126
- .scale(0.72)
141
+ .scale(visualScale)
127
142
  .runtime(runtime);
128
143
  }
129
144
 
@@ -4,10 +4,13 @@ import {
4
4
  animationLayer,
5
5
  createFighterAnimationController,
6
6
  createFighterNode,
7
+ FIGHTER_CERTIFIED_CLIP,
7
8
  isLoopingFighterClip,
9
+ PLAYER_FIGHTER_ASSET,
8
10
  publicAssetInstructions,
9
11
  REQUIRED_FIGHTER_ASSETS,
10
12
  resolveTypedFighterAssets,
13
+ RIVAL_FIGHTER_ASSET,
11
14
  type FighterClip
12
15
  } from "./game/fighters";
13
16
  import { heavyMove, lightMove, specialMove } from "./game/moves";
@@ -129,8 +132,10 @@ const inputOptions = {
129
132
  const arena = scene()
130
133
  .background("#10071c")
131
134
  .addMany(fightingStage.nodes)
132
- .add(createFighterNode("player", "playerFighter", "Player fighter", playerStart, 1, "#45f5bb", typedFighterAssets))
133
- .add(createFighterNode("rival", "rivalFighter", "Rival fighter", rivalStart, -1, "#ffca5f", typedFighterAssets))
135
+ // The creature rival renders roughly twice the player height at scale 1, so
136
+ // it mounts slightly under scale to share the frame with the humanoid player.
137
+ .add(createFighterNode("player", PLAYER_FIGHTER_ASSET, "Player fighter", playerStart, 1, "#45f5bb", typedFighterAssets, 1))
138
+ .add(createFighterNode("rival", RIVAL_FIGHTER_ASSET, "Rival fighter", rivalStart, -1, "#ffca5f", typedFighterAssets, 0.75))
134
139
  .addMany([
135
140
  effects.bloom({ intensity: 0.32 }),
136
141
  lights.studio({ intensity: 1.15 }),
@@ -183,8 +188,8 @@ const director = game.cameraDirector({
183
188
  });
184
189
  const playerNode = app.nodes.require("player");
185
190
  const rivalNode = app.nodes.require("rival");
186
- const playerAnimation = createFighterAnimationController("player", typedFighterAssets.playerFighter);
187
- const rivalAnimation = createFighterAnimationController("rival", typedFighterAssets.rivalFighter);
191
+ const playerAnimation = createFighterAnimationController("player", typedFighterAssets[PLAYER_FIGHTER_ASSET]);
192
+ const rivalAnimation = createFighterAnimationController("rival", typedFighterAssets[RIVAL_FIGHTER_ASSET]);
188
193
 
189
194
  playerAnimation.bindRuntimeNode(playerNode, { defaultClipId: "idle", fallbackClipId: "idle" });
190
195
  rivalAnimation.bindRuntimeNode(rivalNode, { defaultClipId: "idle", fallbackClipId: "idle" });
@@ -207,6 +212,18 @@ gameWindow.__AURA3D_GAME_SOURCE__ = {
207
212
  typedAssetPattern: "src/aura-assets.ts",
208
213
  typedAssetKeys: REQUIRED_FIGHTER_ASSETS,
209
214
  missingAssets: missingFighterAssets,
215
+ heroAssets: {
216
+ player: {
217
+ assetId: assets[PLAYER_FIGHTER_ASSET]?.id ?? PLAYER_FIGHTER_ASSET,
218
+ url: assets[PLAYER_FIGHTER_ASSET]?.url,
219
+ certifiedClip: FIGHTER_CERTIFIED_CLIP.player
220
+ },
221
+ rival: {
222
+ assetId: assets[RIVAL_FIGHTER_ASSET]?.id ?? RIVAL_FIGHTER_ASSET,
223
+ url: assets[RIVAL_FIGHTER_ASSET]?.url,
224
+ certifiedClip: FIGHTER_CERTIFIED_CLIP.rival
225
+ }
226
+ },
210
227
  addAssets: publicAssetInstructions,
211
228
  touchControls: touchLayout,
212
229
  readiness: routeReadiness
@@ -0,0 +1,53 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { expect, test } from "@playwright/test";
5
+
6
+ test.setTimeout(90_000);
7
+
8
+ /**
9
+ * E1 certified-rig mount proof for the fighting-game template.
10
+ *
11
+ * Both fighters must be E1-certified hero rigs
12
+ * (docs/rendering/skinning-and-morphs.md "Certified hero rigs"):
13
+ * - player: showcaseWalkAnimatedGirl (humanoid-a) / "Take 001"
14
+ * - rival: showcaseRunnerRobot (creature) / "WALK"
15
+ * The mounted asset ids AND urls assert against that roster; a placeholder or
16
+ * fixture swap regresses this spec instead of silently shipping uncertified
17
+ * heroes.
18
+ */
19
+ /*
20
+ * No raw GLB URL literal lives in this file (public-example-boundary): expected
21
+ * urls are read from the template's own generated typed assets. A placeholder
22
+ * or fixture swap (roster id gone from aura-assets.ts) throws here instead of
23
+ * silently shipping uncertified heroes.
24
+ */
25
+ const CERTIFIED_ROSTER = {
26
+ player: { assetId: "showcaseWalkAnimatedGirl", certifiedClip: "Take 001" },
27
+ rival: { assetId: "showcaseRunnerRobot", certifiedClip: "WALK" }
28
+ } as const;
29
+ function heroUrlFromTypedAssets(assetId: string): string {
30
+ const source = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "src", "aura-assets.ts"), "utf8");
31
+ const pattern = new RegExp(`${assetId}:([\\s\\S]*?)\\n \\},?\\n`);
32
+ const block = pattern.exec(source)?.[1] ?? "";
33
+ const url = /url:\s*"([^"]+)"/.exec(block)?.[1];
34
+ if (!url) throw new Error(`certified-roster hero ${assetId} missing from the template typed assets`);
35
+ return url;
36
+ }
37
+ const CERTIFIED_HEROES = {
38
+ player: { ...CERTIFIED_ROSTER.player, url: heroUrlFromTypedAssets(CERTIFIED_ROSTER.player.assetId) },
39
+ rival: { ...CERTIFIED_ROSTER.rival, url: heroUrlFromTypedAssets(CERTIFIED_ROSTER.rival.assetId) }
40
+ } as const;
41
+
42
+ test("fighting-game fighters mount certified hero rigs", async ({ page }) => {
43
+ await page.goto("/");
44
+ await page.waitForFunction(() => Boolean((window as any).__AURA3D_GAME_SOURCE__?.readiness), undefined, {
45
+ polling: 100,
46
+ timeout: 60_000
47
+ });
48
+ const source = await page.evaluate(() => (window as any).__AURA3D_GAME_SOURCE__);
49
+ expect(source?.heroAssets?.player).toMatchObject(CERTIFIED_HEROES.player);
50
+ expect(source?.heroAssets?.rival).toMatchObject(CERTIFIED_HEROES.rival);
51
+ expect(source?.missingAssets ?? []).toEqual([]);
52
+ expect(source?.readiness?.proofMode).toBe("typed-assets");
53
+ });
@@ -33,6 +33,11 @@ test("input replay produces runtime evidence and a hit declaration", async ({ pa
33
33
  expect(evidence.systems.cameraPlan).toBeTruthy();
34
34
  expect(evidence.systems.stagePlan).toBeTruthy();
35
35
  expect(replay.hitCount).toBeGreaterThan(0);
36
- expect(source.readiness.sourceOnly).toBe(true);
36
+ // Certified heroes ship as typed fighter assets (humanoid-a + creature), so
37
+ // the route runs in typed-assets proof mode instead of source placeholders.
38
+ expect(source.readiness.sourceOnly).toBe(false);
39
+ expect(source.readiness.placeholderMode).toBe(false);
40
+ expect(source.readiness.proofMode).toBe("typed-assets");
41
+ expect(source.readiness.missingTypedAssets ?? []).toEqual([]);
37
42
  expect(source.readiness.publicEngineApis).toContain("games.fighting.stagePreset");
38
43
  });
@@ -5,27 +5,51 @@
5
5
  "typegen": "src/aura-assets.ts",
6
6
  "assets": [
7
7
  {
8
- "id": "playerModel",
8
+ "id": "showcaseKenneyOobiPlatformerHero",
9
9
  "type": "model",
10
10
  "format": "glb",
11
- "source": "public/aura-assets/player-fixture.glb",
12
- "outputPath": "public/aura-assets/player-fixture.glb",
13
- "url": "/aura-assets/player-fixture.glb",
14
- "hash": "sha256-047f5e5fb3bb6d378bd1df16ca6137f2a596c99b3a1b5690b4020c05aaf6f319",
15
- "sizeBytes": 463988,
11
+ "source": "public/aura-assets/showcaseKenneyOobiPlatformerHero.3f821141.glb",
12
+ "outputPath": "public/aura-assets/showcaseKenneyOobiPlatformerHero.3f821141.glb",
13
+ "url": "/aura-assets/showcaseKenneyOobiPlatformerHero.3f821141.glb",
14
+ "hash": "sha256-3f82114135cdf4b627d463901308eb0dcf4bbbb10f1958f044eaa42160ad5df5",
15
+ "sizeBytes": 208028,
16
16
  "bounds": [
17
- 1.2,
18
- 1.9,
19
- 0.8
17
+ 0.868,
18
+ 0.907,
19
+ 0.603
20
20
  ],
21
21
  "materials": [
22
- "painted robot body",
23
- "black joints",
24
- "white armor"
22
+ "colormap"
23
+ ],
24
+ "animations": [
25
+ "attack-kick-left",
26
+ "attack-kick-right",
27
+ "attack-melee-left",
28
+ "attack-melee-right",
29
+ "crouch",
30
+ "die",
31
+ "drive",
32
+ "emote-no",
33
+ "emote-yes",
34
+ "fall",
35
+ "holding-both",
36
+ "holding-both-shoot",
37
+ "holding-left",
38
+ "holding-left-shoot",
39
+ "holding-right",
40
+ "holding-right-shoot",
41
+ "idle",
42
+ "interact-left",
43
+ "interact-right",
44
+ "jump",
45
+ "pick-up",
46
+ "sit",
47
+ "sprint",
48
+ "static",
49
+ "walk"
25
50
  ],
26
- "animations": [],
27
51
  "textures": [],
28
- "thumbnailUrl": "/aura-assets/player.thumb.svg"
52
+ "thumbnailUrl": "/aura-assets/showcaseKenneyOobiPlatformerHero.thumb.svg"
29
53
  }
30
54
  ]
31
55
  }
@@ -8,10 +8,10 @@
8
8
  "typecheck": "tsc --noEmit",
9
9
  "build": "npm run typecheck && vite build",
10
10
  "preview": "vite preview --host 127.0.0.1",
11
- "test": "playwright test tests/route-health.spec.ts tests/playable.spec.ts tests/screenshot.spec.ts --workers=1"
11
+ "test": "playwright test tests/route-health.spec.ts tests/playable.spec.ts tests/screenshot.spec.ts tests/certified-rig.spec.ts --workers=1"
12
12
  },
13
13
  "dependencies": {
14
- "@aura3d/lean": "2.0.4"
14
+ "@aura3d/lean": "3.0.0"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@playwright/test": "^1.52.0",
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180"><rect width="320" height="180" fill="#101720"/><path d="M80 126 160 46l80 80-80 28z" fill="#77a7ff"/><text x="160" y="160" text-anchor="middle" font-family="Arial" font-size="18" fill="#f4f7fb">showcaseKenneyOobiPlatformerHero 0.868x0.907x0.603</text></svg>
@@ -1,17 +1,43 @@
1
1
  import { defineAuraAssets } from "@aura3d/lean/game";
2
2
 
3
3
  export const assets = defineAuraAssets({
4
- playerModel: {
4
+ showcaseKenneyOobiPlatformerHero: {
5
5
  type: "model",
6
6
  format: "glb",
7
- url: "/aura-assets/player-fixture.glb",
8
- bounds: [1.2, 1.9, 0.8],
9
- hash: "sha256-047f5e5fb3bb6d378bd1df16ca6137f2a596c99b3a1b5690b4020c05aaf6f319",
7
+ url: "/aura-assets/showcaseKenneyOobiPlatformerHero.3f821141.glb",
8
+ bounds: [0.868, 0.907, 0.603],
9
+ hash: "sha256-3f82114135cdf4b627d463901308eb0dcf4bbbb10f1958f044eaa42160ad5df5",
10
10
  metadata: {
11
- materials: ["painted robot body", "black joints", "white armor"],
12
- animations: [],
11
+ materials: ["colormap"],
12
+ animations: [
13
+ "attack-kick-left",
14
+ "attack-kick-right",
15
+ "attack-melee-left",
16
+ "attack-melee-right",
17
+ "crouch",
18
+ "die",
19
+ "drive",
20
+ "emote-no",
21
+ "emote-yes",
22
+ "fall",
23
+ "holding-both",
24
+ "holding-both-shoot",
25
+ "holding-left",
26
+ "holding-left-shoot",
27
+ "holding-right",
28
+ "holding-right-shoot",
29
+ "idle",
30
+ "interact-left",
31
+ "interact-right",
32
+ "jump",
33
+ "pick-up",
34
+ "sit",
35
+ "sprint",
36
+ "static",
37
+ "walk"
38
+ ],
13
39
  textures: [],
14
- thumbnailUrl: "/aura-assets/player.thumb.svg"
40
+ thumbnailUrl: "/aura-assets/showcaseKenneyOobiPlatformerHero.thumb.svg"
15
41
  }
16
42
  }
17
43
  } as const);
@@ -22,8 +22,13 @@ interface MiniGameEvidence {
22
22
  readonly deaths: number;
23
23
  readonly checkpointId: string;
24
24
  readonly collected: readonly string[];
25
+ readonly hero: { readonly assetId: string; readonly url: string };
25
26
  readonly player: { readonly x: number; readonly y: number; readonly grounded: boolean };
26
27
  readonly events: readonly string[];
28
+ readonly cameraRig: { readonly kind: string; readonly position: readonly [number, number, number] };
29
+ readonly gameFeel: { readonly trauma: number; readonly frozen: boolean };
30
+ readonly debugDraw: boolean;
31
+ readonly governor: { readonly resolutionScale: number; readonly particleScale: number };
27
32
  readonly evidence: { readonly entry: string; readonly physics: string; readonly typedAssets: number };
28
33
  }
29
34
 
@@ -58,12 +63,20 @@ const input = game.input({
58
63
  jump: ["Space", "KeyW", "ArrowUp"],
59
64
  dash: ["ShiftLeft", "ShiftRight"],
60
65
  down: ["KeyS", "ArrowDown"],
61
- reset: ["KeyR"]
66
+ reset: ["KeyR"],
67
+ debug: ["KeyT"]
62
68
  },
63
69
  axes: { moveX: { negative: "left", positive: "right" } },
64
70
  bufferMs: 140
65
71
  });
66
72
  const platformer = game.platformer(level);
73
+ // J3 scaffold surface: camera rig frames the opening shot, game feel adds
74
+ // trauma/shake on impacts, the debug toggle gates overlays, and the lean perf
75
+ // governor degrades resolution/particle scale when frames run long.
76
+ const cameraRig = game.cameraRig({ kind: "side-view-follow", offset: [0, 2.98, 10.4] });
77
+ const gameFeel = game.gameFeel();
78
+ const debugDraw = game.debugDraw();
79
+ const governor = game.performanceGovernor("conservative");
67
80
  const routeEvents: string[] = [];
68
81
  const app = createAuraApp("#app", { scene: buildScene() });
69
82
  const player = app.nodes.require("mini-player");
@@ -76,6 +89,9 @@ let objective = "Collect coins, avoid spikes, reach the goal";
76
89
 
77
90
  app.onFrame((dt) => {
78
91
  input.update(dt);
92
+ governor.step(Math.max(0, dt) * 1000);
93
+ gameFeel.update(Math.max(0, dt));
94
+ if (input.pressed("debug")) debugDraw.toggle();
79
95
  if (input.pressed("reset")) {
80
96
  platformer.reset();
81
97
  routeEvents.push("reset:Route reset");
@@ -96,12 +112,22 @@ app.onFrame((dt) => {
96
112
  dashPressed: firstSubstep && dashPressed,
97
113
  fastFall: input.held("down")
98
114
  });
99
- for (const event of state.events) updateObjective(event);
115
+ for (const event of state.events) {
116
+ updateObjective(event);
117
+ if (event.type === "collect") gameFeel.addTrauma(0.25);
118
+ if (event.type === "hazard" || event.type === "fall") {
119
+ gameFeel.addTrauma(0.7);
120
+ gameFeel.hitStop();
121
+ }
122
+ if (event.type === "land") gameFeel.addTrauma(0.12);
123
+ }
100
124
  remaining -= substep;
101
125
  firstSubstep = false;
102
126
  } while (remaining > 0.000_001);
103
127
 
104
- player.setPosition(state.player.x + 0.38, state.player.y + 0.5, 0);
128
+ // The certified Oobi hero is feet-origin (GLB min y = 0), so the visual
129
+ // rides at the physics point instead of the old fixture's +0.5 lift.
130
+ player.setPosition(state.player.x + 0.38, state.player.y + 0.01, 0);
105
131
  lift.setPosition(level.movingPlatforms[0].x + level.movingPlatforms[0].width / 2, movingLiftY(state.time), 0);
106
132
  for (const [id, node] of coins) node.setVisible(!state.collected.includes(id));
107
133
  checkpoint.setVisible(!state.activatedCheckpoints.includes("mid"));
@@ -139,10 +165,14 @@ function buildScene() {
139
165
  primitives.box({ name: "finish portal", material: material.pbr({ color: "#ffad57", metallic: 0.2, roughness: 0.18 }) })
140
166
  .position(level.finish.x, level.finish.y + 0.7, 0.08).scale([0.22, 0.92, 0.16]).runtime("goal")
141
167
  ];
168
+ // Opening shot is framed by the side-view camera rig. The offset reproduces
169
+ // the certified framing exactly (position [6.1, 3.8, 10.4], target
170
+ // [6.1, 0.82, 0]) so screenshot baselines do not shift.
171
+ const opening = cameraRig.snap([6.1, 0.82, 0]);
142
172
  return scene().background("#071015")
143
- .add(model(assets.playerModel, { name: "typed mini-game player" }).position(level.start.x + 0.38, level.start.y + 0.5, 0).scale(0.28).runtime("mini-player"))
173
+ .add(model(assets.showcaseKenneyOobiPlatformerHero, { name: "certified hero vehicle-driver" }).position(level.start.x + 0.38, level.start.y + 0.01, 0).scale(1).runtime("mini-player"))
144
174
  .addMany(nodes)
145
- .camera(camera.perspective({ position: [6.1, 3.8, 10.4], target: [6.1, 0.82, 0], fov: 46 }));
175
+ .camera(camera.perspective({ position: [opening[0], opening[1], opening[2]], target: [6.1, 0.82, 0], fov: 46 }));
146
176
  }
147
177
 
148
178
  function updateObjective(event: LeanPlatformerEvent): void {
@@ -178,8 +208,13 @@ function publishEvidence(state: ReturnType<typeof platformer.snapshot>): void {
178
208
  deaths: state.deaths,
179
209
  checkpointId: state.checkpointId,
180
210
  collected: state.collected,
211
+ hero: { assetId: assets.showcaseKenneyOobiPlatformerHero.id, url: assets.showcaseKenneyOobiPlatformerHero.url },
181
212
  player: { x: state.player.x, y: state.player.y, grounded: state.player.grounded },
182
213
  events: [...routeEvents],
214
+ cameraRig: { kind: cameraRig.kind, position: cameraRig.follow([state.player.x, state.player.y, 0], 1 / 60) },
215
+ gameFeel: gameFeel.snapshot(),
216
+ debugDraw: debugDraw.enabled,
217
+ governor: { ...governor.settings },
183
218
  evidence: { entry: "@aura3d/lean/game", physics: "solver-free deterministic arcade", typedAssets: Object.keys(assets).length }
184
219
  };
185
220
  }
@@ -0,0 +1,40 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { expect, test } from "@playwright/test";
5
+
6
+ test.setTimeout(60_000);
7
+
8
+ /**
9
+ * E1 certified-rig mount proof for the mini-game template.
10
+ *
11
+ * The template hero must be one of the E1-certified hero rigs
12
+ * (docs/rendering/skinning-and-morphs.md "Certified hero rigs") — currently
13
+ * the vehicle-driver slot: showcaseKenneyOobiPlatformerHero / "walk".
14
+ * The mounted asset id AND url assert against that roster; a fixture swap
15
+ * regresses this spec instead of silently shipping an uncertified hero.
16
+ *
17
+ * No raw GLB URL literal lives in this file (public-example-boundary): the
18
+ * expected url is read from the template's own generated typed assets.
19
+ */
20
+ const CERTIFIED_ROSTER = ["showcaseKenneyOobiPlatformerHero"] as const;
21
+ function certifiedHeroFromTypedAssets(): { assetId: string; url: string } {
22
+ const source = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "src", "aura-assets.ts"), "utf8");
23
+ for (const assetId of CERTIFIED_ROSTER) {
24
+ const pattern = new RegExp(`${assetId}:[\\s\\S]*?url:\\s*"([^"]+)"`);
25
+ const match = pattern.exec(source);
26
+ if (match?.[1]) return { assetId, url: match[1] };
27
+ }
28
+ throw new Error(`no certified-roster hero declared in the template typed assets (roster: ${CERTIFIED_ROSTER.join(", ")})`);
29
+ }
30
+ const CERTIFIED_HERO = certifiedHeroFromTypedAssets();
31
+
32
+ test("mini-game hero mounts the certified vehicle-driver rig", async ({ page }) => {
33
+ await page.goto("/");
34
+ await expect.poll(() => page.locator("body").getAttribute("data-aura3d-ready"), { timeout: 45_000 }).toBe("true");
35
+ const hero = await page.evaluate(
36
+ () => (window as unknown as { readonly __AURA3D_MINI_GAME__?: { readonly hero?: { readonly assetId?: string; readonly url?: string } } }).__AURA3D_MINI_GAME__?.hero
37
+ );
38
+ expect(hero?.assetId).toBe(CERTIFIED_HERO.assetId);
39
+ expect(hero?.url).toBe(CERTIFIED_HERO.url);
40
+ });
@@ -11,7 +11,7 @@
11
11
  "test": "playwright test tests/route-health.spec.ts tests/screenshot.spec.ts --workers=1"
12
12
  },
13
13
  "dependencies": {
14
- "@aura3d/lean": "2.0.4"
14
+ "@aura3d/lean": "3.0.0"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@playwright/test": "^1.52.0",
@@ -10,11 +10,18 @@ import {
10
10
  } from "@aura3d/lean/product";
11
11
  import { assets } from "./aura-assets";
12
12
 
13
+ // PART C2 game-ready adoption: the plinth wears the @aura3d/materials
14
+ // "carPaint" preset shell (clearcoat 1 + flake normal scale) in a pearl studio
15
+ // base, and the floor carries the "glassThin" preset color/roughness values as
16
+ // a glass display deck (transmission itself lives in the full engine API).
17
+ const carPaintPlinth = material.clearcoatPaint({ color: "#d7dce2", roughness: 0.3, metallic: 0.55, clearcoat: 1 });
18
+ const glassThinDeck = material.pbr({ color: "#9fc6d4", roughness: 0.08, metallic: 0 });
19
+
13
20
  const productScene = scene()
14
21
  .background("#071018")
15
- .add(primitives.plane({ name: "studio floor", material: material.pbr({ color: "#22313b", roughness: 0.32, metallic: 0.08 }) })
22
+ .add(primitives.plane({ name: "glass deck floor", material: glassThinDeck })
16
23
  .position(0, -0.05, -0.62).scale([6.2, 1, 5.2]))
17
- .add(primitives.box({ name: "product plinth", material: material.clearcoatPaint({ color: "#dce7ed", roughness: 0.2, metallic: 0.18 }) })
24
+ .add(primitives.box({ name: "carPaint product plinth", material: carPaintPlinth })
18
25
  .position(0, 0.06, -0.62).scale([1.82, 0.18, 1.4]))
19
26
  .add(model(assets.product, { name: "typed studio product" }).position(0, 1.08, -0.62).scale(0.66))
20
27
  .add(environments.studio())
@@ -11,7 +11,8 @@
11
11
  "test": "playwright test tests/route-health.spec.ts tests/storyboard-playback.spec.ts --workers=1"
12
12
  },
13
13
  "dependencies": {
14
- "@aura3d/engine": "2.0.4"
14
+ "@aura3d/engine": "3.0.0",
15
+ "@aura3d/navigation-recast": "3.0.0"
15
16
  },
16
17
  "devDependencies": {
17
18
  "@playwright/test": "^1.52.0",
@@ -11,7 +11,8 @@
11
11
  "test": "playwright test tests/playable.spec.ts --workers=1"
12
12
  },
13
13
  "dependencies": {
14
- "@aura3d/engine": "2.0.4"
14
+ "@aura3d/engine": "3.0.0",
15
+ "@aura3d/navigation-recast": "3.0.0"
15
16
  },
16
17
  "devDependencies": {
17
18
  "@types/node": "^22.15.30",
@@ -174,6 +174,7 @@ function buildScene() {
174
174
  .scale(0.18)
175
175
  .runtime(game.runtimeNode("race-car", { tags: ["player", "vehicle", "typed-asset", "runtime"] })),
176
176
  ...routeRibbonNodes(),
177
+ ...paintAndGlassGantryNodes(),
177
178
  primitives.box({ name: "checkpoint marker", material: material.neon({ color: "#7ff0c5", emissive: "#7ff0c5", emissiveIntensity: 0.7 }) })
178
179
  .position(0.06, 0.18, 0.2)
179
180
  .scale([0.16, 0.28, 0.95])
@@ -199,6 +200,36 @@ function routeRibbonNodes(): AuraNodeInput[] {
199
200
  );
200
201
  }
201
202
 
203
+ // PART C2 game-ready adoption: a static paint-and-glass gantry south of the
204
+ // start straight wearing the @aura3d/materials "carPaint" preset (clearcoat
205
+ // shell + flake normal) on the showcase shell and the "glassThin" preset
206
+ // (thin-walled transmission) on the canopy panel.
207
+ function paintAndGlassGantryNodes(): AuraNodeInput[] {
208
+ const carPaintShell = material.clearcoatPaint({
209
+ color: "#c1121f",
210
+ roughness: 0.32,
211
+ metallic: 0.65,
212
+ clearcoat: 1,
213
+ clearcoatRoughness: 0.06,
214
+ normalScale: 0.35,
215
+ envMapIntensity: 1.4
216
+ });
217
+ const glassThinCanopy = material.glass({
218
+ color: "#bfe6f2",
219
+ opacity: 0.35,
220
+ transmission: 0.9,
221
+ thickness: 0.02,
222
+ roughness: 0.05
223
+ });
224
+ const post = material.pbr({ color: "#1c2530", roughness: 0.6, metallic: 0.4 });
225
+ return [
226
+ primitives.box({ name: "carPaint showcase shell", material: carPaintShell }).position(2.7, 0.3, -1.7).scale([1.5, 0.55, 0.75]),
227
+ primitives.box({ name: "glassThin canopy", material: glassThinCanopy }).position(2.7, 1.5, -1.7).scale([1.7, 0.06, 0.95]),
228
+ primitives.box({ name: "gantry post west", material: post }).position(1.95, 0.75, -1.7).scale([0.08, 1.5, 0.08]),
229
+ primitives.box({ name: "gantry post east", material: post }).position(3.45, 0.75, -1.7).scale([0.08, 1.5, 0.08])
230
+ ];
231
+ }
232
+
202
233
  function createHud(): HTMLElement {
203
234
  const root = document.createElement("aside");
204
235
  root.id = "racing-starter-hud";
@@ -11,7 +11,8 @@
11
11
  "test": "playwright test tests/route-health.spec.ts tests/screenshot.spec.ts --workers=1"
12
12
  },
13
13
  "dependencies": {
14
- "@aura3d/engine": "2.0.4"
14
+ "@aura3d/engine": "3.0.0",
15
+ "@aura3d/navigation-recast": "3.0.0"
15
16
  },
16
17
  "devDependencies": {
17
18
  "@playwright/test": "^1.52.0",
@@ -11,7 +11,8 @@
11
11
  "test": "playwright test tests/route-health.spec.ts tests/screenshot.spec.ts --workers=1"
12
12
  },
13
13
  "dependencies": {
14
- "@aura3d/engine": "2.0.4"
14
+ "@aura3d/engine": "3.0.0",
15
+ "@aura3d/navigation-recast": "3.0.0"
15
16
  },
16
17
  "devDependencies": {
17
18
  "@playwright/test": "^1.52.0",
@@ -11,7 +11,8 @@
11
11
  "test": "playwright test tests/route-health.spec.ts tests/screenshot.spec.ts --workers=1"
12
12
  },
13
13
  "dependencies": {
14
- "@aura3d/engine": "2.0.4"
14
+ "@aura3d/engine": "3.0.0",
15
+ "@aura3d/navigation-recast": "3.0.0"
15
16
  },
16
17
  "devDependencies": {
17
18
  "@playwright/test": "^1.52.0",
@@ -11,7 +11,8 @@
11
11
  "test": "playwright test tests/route-health.spec.ts tests/screenshot.spec.ts --workers=1"
12
12
  },
13
13
  "dependencies": {
14
- "@aura3d/engine": "2.0.4"
14
+ "@aura3d/engine": "3.0.0",
15
+ "@aura3d/navigation-recast": "3.0.0"
15
16
  },
16
17
  "devDependencies": {
17
18
  "@playwright/test": "^1.52.0",
@@ -11,7 +11,8 @@
11
11
  "test": "playwright test tests/route-health.spec.ts tests/screenshot.spec.ts --workers=1"
12
12
  },
13
13
  "dependencies": {
14
- "@aura3d/engine": "2.0.4"
14
+ "@aura3d/engine": "3.0.0",
15
+ "@aura3d/navigation-recast": "3.0.0"
15
16
  },
16
17
  "devDependencies": {
17
18
  "@playwright/test": "^1.52.0",
@@ -11,7 +11,8 @@
11
11
  "test": "playwright test tests/route-health.spec.ts tests/screenshot.spec.ts --workers=1"
12
12
  },
13
13
  "dependencies": {
14
- "@aura3d/engine": "2.0.4"
14
+ "@aura3d/engine": "3.0.0",
15
+ "@aura3d/navigation-recast": "3.0.0"
15
16
  },
16
17
  "devDependencies": {
17
18
  "@playwright/test": "^1.52.0",