create-aura3d 1.0.5
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.
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +27 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +70 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
- package/templates/cartoon-channel/README.md +42 -0
- package/templates/cartoon-channel/index.html +13 -0
- package/templates/cartoon-channel/package.json +21 -0
- package/templates/cartoon-channel/playwright.config.ts +13 -0
- package/templates/cartoon-channel/src/aura-assets.ts +14 -0
- package/templates/cartoon-channel/src/characters.ts +28 -0
- package/templates/cartoon-channel/src/contract.ts +1 -0
- package/templates/cartoon-channel/src/episode.ts +300 -0
- package/templates/cartoon-channel/src/main.ts +322 -0
- package/templates/cartoon-channel/src/render-plan.ts +405 -0
- package/templates/cartoon-channel/src/sets.ts +14 -0
- package/templates/cartoon-channel/tests/route-health.spec.ts +7 -0
- package/templates/cartoon-channel/tests/storyboard-playback.spec.ts +74 -0
- package/templates/cartoon-channel/tsconfig.json +12 -0
- package/templates/cinematic-scene/README.md +11 -0
- package/templates/cinematic-scene/index.html +13 -0
- package/templates/cinematic-scene/package.json +21 -0
- package/templates/cinematic-scene/playwright.config.ts +14 -0
- package/templates/cinematic-scene/public/aura-assets/hero-fixture.glb +0 -0
- package/templates/cinematic-scene/public/aura-assets/hero.thumb.svg +1 -0
- package/templates/cinematic-scene/src/aura-assets.ts +17 -0
- package/templates/cinematic-scene/src/main.ts +27 -0
- package/templates/cinematic-scene/tests/route-health.spec.ts +16 -0
- package/templates/cinematic-scene/tests/screenshot.spec.ts +56 -0
- package/templates/cinematic-scene/tsconfig.json +15 -0
- package/templates/fighting-game/README.md +64 -0
- package/templates/fighting-game/index.html +13 -0
- package/templates/fighting-game/package.json +21 -0
- package/templates/fighting-game/playwright.config.ts +12 -0
- package/templates/fighting-game/src/aura-assets.ts +11 -0
- package/templates/fighting-game/src/game/fighters.ts +152 -0
- package/templates/fighting-game/src/game/moves.ts +70 -0
- package/templates/fighting-game/src/game/stage.ts +98 -0
- package/templates/fighting-game/src/main.ts +417 -0
- package/templates/fighting-game/src/styles.css +124 -0
- package/templates/fighting-game/tests/gameplay-smoke.spec.ts +23 -0
- package/templates/fighting-game/tests/route-health.spec.ts +11 -0
- package/templates/fighting-game/tsconfig.json +19 -0
- package/templates/mini-game/README.md +10 -0
- package/templates/mini-game/index.html +13 -0
- package/templates/mini-game/package.json +21 -0
- package/templates/mini-game/playwright.config.ts +14 -0
- package/templates/mini-game/public/aura-assets/player-fixture.glb +0 -0
- package/templates/mini-game/public/aura-assets/player.thumb.svg +8 -0
- package/templates/mini-game/src/aura-assets.ts +17 -0
- package/templates/mini-game/src/main.ts +27 -0
- package/templates/mini-game/tests/route-health.spec.ts +16 -0
- package/templates/mini-game/tests/screenshot.spec.ts +63 -0
- package/templates/mini-game/tsconfig.json +15 -0
- package/templates/product-viewer/README.md +16 -0
- package/templates/product-viewer/index.html +16 -0
- package/templates/product-viewer/package.json +21 -0
- package/templates/product-viewer/playwright.config.ts +14 -0
- package/templates/product-viewer/public/aura-assets/product-fixture.glb +0 -0
- package/templates/product-viewer/public/aura-assets/product.thumb.svg +1 -0
- package/templates/product-viewer/src/aura-assets.ts +17 -0
- package/templates/product-viewer/src/main.ts +27 -0
- package/templates/product-viewer/tests/route-health.spec.ts +16 -0
- package/templates/product-viewer/tests/screenshot.spec.ts +60 -0
- package/templates/product-viewer/tsconfig.json +15 -0
- package/templates/prompt-cartoon-channel/README.md +42 -0
- package/templates/prompt-cartoon-channel/index.html +13 -0
- package/templates/prompt-cartoon-channel/package.json +21 -0
- package/templates/prompt-cartoon-channel/playwright.config.ts +13 -0
- package/templates/prompt-cartoon-channel/src/aura-assets.ts +14 -0
- package/templates/prompt-cartoon-channel/src/characters.ts +28 -0
- package/templates/prompt-cartoon-channel/src/contract.ts +1 -0
- package/templates/prompt-cartoon-channel/src/episode.ts +300 -0
- package/templates/prompt-cartoon-channel/src/main.ts +314 -0
- package/templates/prompt-cartoon-channel/src/render-plan.ts +405 -0
- package/templates/prompt-cartoon-channel/src/sets.ts +14 -0
- package/templates/prompt-cartoon-channel/tests/route-health.spec.ts +7 -0
- package/templates/prompt-cartoon-channel/tests/storyboard-playback.spec.ts +74 -0
- package/templates/prompt-cartoon-channel/tsconfig.json +12 -0
|
@@ -0,0 +1,56 @@
|
|
|
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 cinematic scene screenshot shows the rainy neon hero prompt", async ({ page }) => {
|
|
8
|
+
await page.goto("/");
|
|
9
|
+
await expect.poll(() => page.locator("body").getAttribute("data-aura3d-ready"), { timeout: 45_000 }).toBe("true");
|
|
10
|
+
const canvas = page.locator("canvas");
|
|
11
|
+
const profile = await canvas.evaluate((element) => {
|
|
12
|
+
const target = element as HTMLCanvasElement;
|
|
13
|
+
const gl = target.getContext("webgl2", { preserveDrawingBuffer: true });
|
|
14
|
+
if (!gl) return { error: "missing-webgl2", cyanPixels: 0, amberPixels: 0, rainPixels: 0, wetReflectionPixels: 0, centerHeroPixels: 0, darkAlleyPixels: 0, uniqueBuckets: 0 };
|
|
15
|
+
const pixels = new Uint8Array(target.width * target.height * 4);
|
|
16
|
+
gl.readPixels(0, 0, target.width, target.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
17
|
+
const buckets = new Set<string>();
|
|
18
|
+
let cyanPixels = 0;
|
|
19
|
+
let amberPixels = 0;
|
|
20
|
+
let rainPixels = 0;
|
|
21
|
+
let wetReflectionPixels = 0;
|
|
22
|
+
let centerHeroPixels = 0;
|
|
23
|
+
let darkAlleyPixels = 0;
|
|
24
|
+
for (let y = 0; y < target.height; y += 4) {
|
|
25
|
+
for (let x = 0; x < target.width; x += 4) {
|
|
26
|
+
if (x > target.width * 0.76 && y > target.height * 0.74) continue;
|
|
27
|
+
const offset = (y * target.width + x) * 4;
|
|
28
|
+
const r = pixels[offset] ?? 0;
|
|
29
|
+
const g = pixels[offset + 1] ?? 0;
|
|
30
|
+
const b = pixels[offset + 2] ?? 0;
|
|
31
|
+
const luminance = r * 0.2126 + g * 0.7152 + b * 0.0722;
|
|
32
|
+
if (luminance > 30) buckets.add(`${r >> 5}-${g >> 5}-${b >> 5}`);
|
|
33
|
+
if (b > 110 && g > 90 && b > r * 1.22) cyanPixels += 1;
|
|
34
|
+
if (r > 160 && g > 105 && b < 125) amberPixels += 1;
|
|
35
|
+
if (r > 165 && g > 185 && b > 205) rainPixels += 1;
|
|
36
|
+
if (y < target.height * 0.36 && ((b > 95 && g > 65 && b > r * 1.12) || (r > 105 && g > 62 && b < 95))) wetReflectionPixels += 1;
|
|
37
|
+
if (x > target.width * 0.32 && x < target.width * 0.68 && y > target.height * 0.26 && y < target.height * 0.78 && luminance > 75) centerHeroPixels += 1;
|
|
38
|
+
if (luminance > 8 && luminance < 36 && b >= r && x > target.width * 0.08 && x < target.width * 0.92) darkAlleyPixels += 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { cyanPixels, amberPixels, rainPixels, wetReflectionPixels, centerHeroPixels, darkAlleyPixels, uniqueBuckets: buckets.size };
|
|
42
|
+
});
|
|
43
|
+
const screenshot = await page.screenshot({ fullPage: false });
|
|
44
|
+
mkdirSync(resolve("tests/reports"), { recursive: true });
|
|
45
|
+
writeFileSync(resolve("tests/reports/screenshot.png"), screenshot);
|
|
46
|
+
writeFileSync(resolve("tests/reports/screenshot.json"), `${JSON.stringify({ bytes: screenshot.byteLength, profile }, null, 2)}\n`);
|
|
47
|
+
expect(profile.error).toBeUndefined();
|
|
48
|
+
expect(profile.cyanPixels).toBeGreaterThan(320);
|
|
49
|
+
expect(profile.amberPixels).toBeGreaterThan(20);
|
|
50
|
+
expect(profile.rainPixels).toBeGreaterThan(90);
|
|
51
|
+
expect(profile.wetReflectionPixels).toBeGreaterThan(60);
|
|
52
|
+
expect(profile.centerHeroPixels).toBeGreaterThan(600);
|
|
53
|
+
expect(profile.darkAlleyPixels).toBeGreaterThan(180);
|
|
54
|
+
expect(profile.uniqueBuckets).toBeGreaterThan(22);
|
|
55
|
+
expect(screenshot.byteLength).toBeGreaterThan(1000);
|
|
56
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "Bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"strict": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src", "tests", "playwright.config.ts"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Aura3D fighting-game template
|
|
2
|
+
|
|
3
|
+
This template demonstrates Aura3D's public game runtime APIs:
|
|
4
|
+
|
|
5
|
+
- `createAuraApp(...)` for one mounted Aura app.
|
|
6
|
+
- `.runtime(game.runtimeNode("id"))` for mutable fighter and camera nodes.
|
|
7
|
+
- `app.input(...)` for app-owned keyboard/gamepad input.
|
|
8
|
+
- `game.inputReplay(...)` and `game.inputReplayDriver(...)` for deterministic replay smoke paths.
|
|
9
|
+
- `game.kinematicBody(...)` and `game.jumpAssist(...)` for 2.5D movement, gravity, coyote time, jump buffering, dash, and knockback.
|
|
10
|
+
- `game.combatWorld(...)` for hitboxes, hurtboxes, guard, hit-stop, stun, recovery, and typed combat events.
|
|
11
|
+
- `AnimationController` bound to runtime nodes for public animation clip switching and pose-baked fallback declarations.
|
|
12
|
+
- `game.effects(...)`, `game.cameraDirector(...)`, and `game.debug.overlay(...)` for hit sparks, impact shake, zoom, colliders, and runtime evidence.
|
|
13
|
+
- `games.fighting.stagePreset(...)` and `games.fighting.validateStage(...)` for fighting-stage source, safe zones, combat bounds, and readiness warnings.
|
|
14
|
+
- `game.hud.*`, `game.accessibility.*`, and `ui.*` helpers for HUD, controls, and a11y evidence.
|
|
15
|
+
- `game.evidence(app, ...)` for proving the route is using runtime systems instead of a static scene.
|
|
16
|
+
|
|
17
|
+
## Source layout
|
|
18
|
+
|
|
19
|
+
- `src/main.ts` mounts the route, owns frame updates, and publishes `window.__AURA3D_GAME_*` evidence objects.
|
|
20
|
+
- `src/game/fighters.ts` owns typed fighter asset resolution, runtime fighter nodes, and animation controller setup.
|
|
21
|
+
- `src/game/moves.ts` owns combat move declarations and hitbox source.
|
|
22
|
+
- `src/game/stage.ts` owns `games.fighting` stage helpers, touch controls, debug colliders, and route/build readiness declarations.
|
|
23
|
+
- `src/aura-assets.ts` is the typed asset manifest generated by the Aura3D CLI.
|
|
24
|
+
|
|
25
|
+
The scaffold does not ship invented fighter GLBs or raw model URLs. It starts
|
|
26
|
+
with Aura3D runtime-node source placeholders so the route is playable and the
|
|
27
|
+
combat systems are inspectable. Add real fighter assets through the Aura3D CLI
|
|
28
|
+
to switch the placeholders to typed `model(assets.playerFighter)` and
|
|
29
|
+
`model(assets.rivalFighter)` nodes automatically.
|
|
30
|
+
|
|
31
|
+
Controls:
|
|
32
|
+
|
|
33
|
+
- Move: `A/D` or arrow keys.
|
|
34
|
+
- Jump: `W`, `ArrowUp`, or `Space`.
|
|
35
|
+
- Guard: `Q`.
|
|
36
|
+
- Dash: `Shift`.
|
|
37
|
+
- Light: `J`.
|
|
38
|
+
- Heavy: `K`.
|
|
39
|
+
- Special: `L`.
|
|
40
|
+
- Pause: `Escape` or the HUD pause button.
|
|
41
|
+
- Replay: HUD replay button.
|
|
42
|
+
|
|
43
|
+
Add fighter assets:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx @aura3d/cli@latest assets add ./assets/player-fighter.glb --name playerFighter
|
|
47
|
+
npx @aura3d/cli@latest assets add ./assets/rival-fighter.glb --name rivalFighter
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The CLI writes typed definitions to `src/aura-assets.ts`. The starter imports
|
|
51
|
+
`assets` from `./aura-assets` and only calls `model(...)` with those typed asset
|
|
52
|
+
refs. Do not use string asset ids, three.js loaders, `GLTFLoader`, raw GLB URLs,
|
|
53
|
+
or `unsafeModelUrl(...)`.
|
|
54
|
+
|
|
55
|
+
Before claiming the route is asset-ready, run:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx @aura3d/cli@latest assets validate-game
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The source-level readiness declaration in `src/game/stage.ts` is not launch
|
|
62
|
+
evidence. Before claiming build, package, visual, accessibility, or launch
|
|
63
|
+
readiness, archive the matching command output, runtime evidence JSON,
|
|
64
|
+
browser screenshot, and review result.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Aura3D Fighting Game Runtime</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app" aria-label="Aura3D fighting game viewport"></div>
|
|
10
|
+
<div id="hud" class="hud" role="region" aria-label="Fighting game status and controls" aria-live="polite"></div>
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aura3d-fighting-game",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite --host 127.0.0.1",
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"build": "npm run typecheck && vite build",
|
|
10
|
+
"preview": "vite preview --host 127.0.0.1",
|
|
11
|
+
"test": "playwright test tests/route-health.spec.ts tests/gameplay-smoke.spec.ts --workers=1"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@aura3d/engine": "1.0.5"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@playwright/test": "^1.52.0",
|
|
18
|
+
"typescript": "^5.8.3",
|
|
19
|
+
"vite": "^7.3.2"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineAuraAssets } from "@aura3d/engine";
|
|
2
|
+
|
|
3
|
+
export const assets = defineAuraAssets({
|
|
4
|
+
/*
|
|
5
|
+
* Add your own fighter GLBs with the Aura3D CLI, then keep these generated
|
|
6
|
+
* keys so src/main.ts can switch from source placeholders to typed models:
|
|
7
|
+
*
|
|
8
|
+
* npx @aura3d/cli@latest assets add ./assets/player-fighter.glb --name playerFighter
|
|
9
|
+
* npx @aura3d/cli@latest assets add ./assets/rival-fighter.glb --name rivalFighter
|
|
10
|
+
*/
|
|
11
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnimationController,
|
|
3
|
+
type AuraAssetRef,
|
|
4
|
+
type AuraNamedAnimationClipDefinition,
|
|
5
|
+
game,
|
|
6
|
+
material,
|
|
7
|
+
model,
|
|
8
|
+
primitives
|
|
9
|
+
} from "@aura3d/engine";
|
|
10
|
+
|
|
11
|
+
export type FighterId = "player" | "rival";
|
|
12
|
+
export type FighterAssetKey = "playerFighter" | "rivalFighter";
|
|
13
|
+
export type FighterClip = "idle" | "walk" | "jump" | "dash" | "guard" | "light" | "heavy" | "special" | "hitstun";
|
|
14
|
+
|
|
15
|
+
export type TypedFighterAssets = Partial<Record<FighterAssetKey, AuraAssetRef<"model">>>;
|
|
16
|
+
|
|
17
|
+
export interface ResolvedFighterAssets {
|
|
18
|
+
readonly typedFighterAssets: TypedFighterAssets;
|
|
19
|
+
readonly missingFighterAssets: readonly FighterAssetKey[];
|
|
20
|
+
readonly typedFighterAssetCount: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const REQUIRED_FIGHTER_ASSETS: readonly FighterAssetKey[] = ["playerFighter", "rivalFighter"];
|
|
24
|
+
export const REQUIRED_FIGHTER_CLIPS: readonly FighterClip[] = [
|
|
25
|
+
"idle",
|
|
26
|
+
"walk",
|
|
27
|
+
"jump",
|
|
28
|
+
"dash",
|
|
29
|
+
"guard",
|
|
30
|
+
"light",
|
|
31
|
+
"heavy",
|
|
32
|
+
"special",
|
|
33
|
+
"hitstun"
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export const publicAssetInstructions = [
|
|
37
|
+
"npx @aura3d/cli@latest assets add ./assets/player-fighter.glb --name playerFighter",
|
|
38
|
+
"npx @aura3d/cli@latest assets add ./assets/rival-fighter.glb --name rivalFighter",
|
|
39
|
+
"npx @aura3d/cli@latest assets validate-game"
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
export const fighterAnimationClips: readonly AuraNamedAnimationClipDefinition<FighterClip>[] = REQUIRED_FIGHTER_CLIPS.map((id) => ({
|
|
43
|
+
id,
|
|
44
|
+
duration: animationDuration(id),
|
|
45
|
+
frameRate: 60,
|
|
46
|
+
loop: id === "idle" || id === "walk" || id === "guard" || id === "jump",
|
|
47
|
+
layer: animationLayer(id),
|
|
48
|
+
layerMetadata: {
|
|
49
|
+
id: animationLayer(id),
|
|
50
|
+
role: id === "light" || id === "heavy" || id === "special" ? "attack" : "base",
|
|
51
|
+
bodyMask: id === "light" || id === "heavy" || id === "special" ? "upper-body" : "full-body",
|
|
52
|
+
restartFromFrameZero: id === "light" || id === "heavy" || id === "special" || id === "dash"
|
|
53
|
+
},
|
|
54
|
+
restartFromFrameZero: id === "light" || id === "heavy" || id === "special" || id === "dash",
|
|
55
|
+
suppressRootMotion: true,
|
|
56
|
+
attack: id === "light" || id === "heavy" || id === "special",
|
|
57
|
+
tracks: [],
|
|
58
|
+
metadata: {
|
|
59
|
+
source: "fighting-game-template-pose-fallback",
|
|
60
|
+
poseBakedFallback: true,
|
|
61
|
+
suppressRootMotion: true,
|
|
62
|
+
templateReadiness: "source-only"
|
|
63
|
+
}
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
export function resolveTypedFighterAssets(assetManifest: TypedFighterAssets): ResolvedFighterAssets {
|
|
67
|
+
const typedFighterAssets: TypedFighterAssets = {
|
|
68
|
+
playerFighter: assetManifest.playerFighter,
|
|
69
|
+
rivalFighter: assetManifest.rivalFighter
|
|
70
|
+
};
|
|
71
|
+
const missingFighterAssets = REQUIRED_FIGHTER_ASSETS.filter((key) => !typedFighterAssets[key]);
|
|
72
|
+
return {
|
|
73
|
+
typedFighterAssets,
|
|
74
|
+
missingFighterAssets,
|
|
75
|
+
typedFighterAssetCount: REQUIRED_FIGHTER_ASSETS.length - missingFighterAssets.length
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createFighterAnimationController(
|
|
80
|
+
id: FighterId,
|
|
81
|
+
asset?: AuraAssetRef<"model">
|
|
82
|
+
): AnimationController<FighterClip> {
|
|
83
|
+
return new AnimationController<FighterClip>({
|
|
84
|
+
id: `${id}-animation-controller`,
|
|
85
|
+
...(asset ? { clipRegistry: asset } : {}),
|
|
86
|
+
clips: fighterAnimationClips,
|
|
87
|
+
requiredClips: REQUIRED_FIGHTER_CLIPS,
|
|
88
|
+
suppressRootMotion: true,
|
|
89
|
+
layers: [
|
|
90
|
+
{ id: "base", role: "base", bodyMask: "full-body" },
|
|
91
|
+
{ id: "upper-body", role: "attack", bodyMask: "upper-body", restartFromFrameZero: true }
|
|
92
|
+
]
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createFighterNode(
|
|
97
|
+
id: FighterId,
|
|
98
|
+
assetKey: FighterAssetKey,
|
|
99
|
+
label: string,
|
|
100
|
+
position: readonly [number, number, number],
|
|
101
|
+
facing: 1 | -1,
|
|
102
|
+
color: string,
|
|
103
|
+
typedFighterAssets: TypedFighterAssets
|
|
104
|
+
) {
|
|
105
|
+
const asset = typedFighterAssets[assetKey];
|
|
106
|
+
const runtime = game.runtimeNode(id, {
|
|
107
|
+
tags: ["fighter", id, asset ? "typed-asset" : "source-placeholder"]
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (asset) {
|
|
111
|
+
return model(asset, { name: label })
|
|
112
|
+
.position(position[0], position[1], position[2])
|
|
113
|
+
.rotate(0, facing < 0 ? Math.PI : 0, 0)
|
|
114
|
+
.scale(0.72)
|
|
115
|
+
.runtime(runtime);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return primitives
|
|
119
|
+
.capsule({
|
|
120
|
+
name: `${label} source placeholder - add ${assetKey} with the Aura3D CLI`,
|
|
121
|
+
material: material.pbr({ color, roughness: 0.62 })
|
|
122
|
+
})
|
|
123
|
+
.position(position[0], position[1] + 0.82, position[2])
|
|
124
|
+
.rotate(0, facing < 0 ? Math.PI : 0, 0)
|
|
125
|
+
.scale([0.34, 0.74, 0.34])
|
|
126
|
+
.runtime(runtime);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function isLoopingFighterClip(clip: FighterClip): boolean {
|
|
130
|
+
return clip === "idle" || clip === "walk" || clip === "guard" || clip === "jump";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function animationLayer(clip: FighterClip): "base" | "upper-body" {
|
|
134
|
+
return clip === "light" || clip === "heavy" || clip === "special" ? "upper-body" : "base";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function animationDuration(clip: FighterClip): number {
|
|
138
|
+
switch (clip) {
|
|
139
|
+
case "light":
|
|
140
|
+
return 0.28;
|
|
141
|
+
case "heavy":
|
|
142
|
+
return 0.46;
|
|
143
|
+
case "special":
|
|
144
|
+
return 0.72;
|
|
145
|
+
case "dash":
|
|
146
|
+
return 0.22;
|
|
147
|
+
case "hitstun":
|
|
148
|
+
return 0.34;
|
|
149
|
+
default:
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type FighterFacing = 1 | -1;
|
|
2
|
+
|
|
3
|
+
export function lightMove(id: string, facing: FighterFacing) {
|
|
4
|
+
return {
|
|
5
|
+
id,
|
|
6
|
+
name: "Template Light Strike",
|
|
7
|
+
damage: 7,
|
|
8
|
+
guardDamage: 3,
|
|
9
|
+
hitStop: 0.08,
|
|
10
|
+
hitStun: 0.22,
|
|
11
|
+
blockStun: 0.12,
|
|
12
|
+
recovery: 0.18,
|
|
13
|
+
activeFrames: [2, 8] as const,
|
|
14
|
+
durationFrames: 18,
|
|
15
|
+
knockback: [facing * 1.8, 0.5, 0] as const,
|
|
16
|
+
hitboxes: [
|
|
17
|
+
{
|
|
18
|
+
id: "jab",
|
|
19
|
+
offset: [facing * 0.68, 0.82, 0] as const,
|
|
20
|
+
size: [0.86, 0.45, 0.45] as const
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function heavyMove(id: string, facing: FighterFacing) {
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
name: "Template Heavy Breaker",
|
|
30
|
+
damage: 13,
|
|
31
|
+
guardDamage: 8,
|
|
32
|
+
hitStop: 0.12,
|
|
33
|
+
hitStun: 0.34,
|
|
34
|
+
blockStun: 0.18,
|
|
35
|
+
recovery: 0.32,
|
|
36
|
+
activeFrames: [4, 12] as const,
|
|
37
|
+
durationFrames: 28,
|
|
38
|
+
knockback: [facing * 2.5, 0.8, 0] as const,
|
|
39
|
+
hitboxes: [
|
|
40
|
+
{
|
|
41
|
+
id: "heavy",
|
|
42
|
+
offset: [facing * 0.78, 0.92, 0] as const,
|
|
43
|
+
size: [1.08, 0.6, 0.52] as const
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function specialMove(id: string, facing: FighterFacing) {
|
|
50
|
+
return {
|
|
51
|
+
id,
|
|
52
|
+
name: "Template Aura Burst",
|
|
53
|
+
damage: 18,
|
|
54
|
+
guardDamage: 12,
|
|
55
|
+
hitStop: 0.16,
|
|
56
|
+
hitStun: 0.42,
|
|
57
|
+
blockStun: 0.24,
|
|
58
|
+
recovery: 0.46,
|
|
59
|
+
activeFrames: [3, 18] as const,
|
|
60
|
+
durationFrames: 42,
|
|
61
|
+
knockback: [facing * 3.2, 1.1, 0] as const,
|
|
62
|
+
hitboxes: [
|
|
63
|
+
{
|
|
64
|
+
id: "aura-burst",
|
|
65
|
+
offset: [facing * 0.86, 0.95, 0] as const,
|
|
66
|
+
size: [1.8, 0.85, 0.65] as const
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { game, games } from "@aura3d/engine";
|
|
2
|
+
import { publicAssetInstructions, REQUIRED_FIGHTER_ASSETS, REQUIRED_FIGHTER_CLIPS } from "./fighters";
|
|
3
|
+
|
|
4
|
+
type GameVec3 = readonly [number, number, number];
|
|
5
|
+
|
|
6
|
+
export const fightingControls = games.fighting.controls();
|
|
7
|
+
export const fightingStage = games.fighting.stagePreset("neon-dojo", {
|
|
8
|
+
id: "fighting-template-arena",
|
|
9
|
+
width: 7.6,
|
|
10
|
+
depth: 2.5,
|
|
11
|
+
safeInset: 0.48
|
|
12
|
+
});
|
|
13
|
+
export const fightingStageIssues = games.fighting.validateStage(fightingStage);
|
|
14
|
+
|
|
15
|
+
export const fightingStageBounds = {
|
|
16
|
+
minX: fightingStage.combatBounds.minX,
|
|
17
|
+
maxX: fightingStage.combatBounds.maxX,
|
|
18
|
+
minY: fightingStage.combatBounds.minY,
|
|
19
|
+
maxY: fightingStage.combatBounds.maxY,
|
|
20
|
+
minZ: fightingStage.combatBounds.minZ,
|
|
21
|
+
maxZ: fightingStage.combatBounds.maxZ
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const fightingRouteReadiness = {
|
|
25
|
+
kind: "aura3d-fighting-game-route-readiness",
|
|
26
|
+
sourceOnly: true,
|
|
27
|
+
template: "fighting-game",
|
|
28
|
+
route: "/",
|
|
29
|
+
packageName: "aura3d-fighting-game",
|
|
30
|
+
entry: "src/main.ts",
|
|
31
|
+
sourceFiles: ["src/main.ts", "src/aura-assets.ts", "src/game/fighters.ts", "src/game/moves.ts", "src/game/stage.ts"],
|
|
32
|
+
publicEngineApis: [
|
|
33
|
+
"createAuraApp",
|
|
34
|
+
"scene",
|
|
35
|
+
"model(typed AuraAssetRef)",
|
|
36
|
+
"game.runtimeNode",
|
|
37
|
+
"app.onFrame",
|
|
38
|
+
"app.input",
|
|
39
|
+
"game.evidence",
|
|
40
|
+
"game.kinematicBody",
|
|
41
|
+
"game.jumpAssist",
|
|
42
|
+
"game.combatWorld",
|
|
43
|
+
"AnimationController",
|
|
44
|
+
"game.effects",
|
|
45
|
+
"game.cameraDirector",
|
|
46
|
+
"games.fighting.stagePreset",
|
|
47
|
+
"games.fighting.validateStage",
|
|
48
|
+
"game.hud",
|
|
49
|
+
"game.accessibility",
|
|
50
|
+
"game.debug.overlay"
|
|
51
|
+
],
|
|
52
|
+
requiredTypedAssets: REQUIRED_FIGHTER_ASSETS,
|
|
53
|
+
requiredAnimationClips: REQUIRED_FIGHTER_CLIPS,
|
|
54
|
+
assetCommands: publicAssetInstructions,
|
|
55
|
+
buildDeclarations: {
|
|
56
|
+
routeHealthSpec: "tests/route-health.spec.ts",
|
|
57
|
+
gameplaySmokeSpec: "tests/gameplay-smoke.spec.ts",
|
|
58
|
+
localCommandsNotRunByTemplate: ["npm run typecheck", "npm run build", "npm run test"],
|
|
59
|
+
evidenceRequiredBeforeLaunchReady: [
|
|
60
|
+
"npx @aura3d/cli@latest assets validate-game output",
|
|
61
|
+
"npm run test scaffold output proving route load and replay hit",
|
|
62
|
+
"browser screenshot showing a typed GLB fighter moved by runtime node mutation",
|
|
63
|
+
"archived collectGameRuntimeEvidence/app.evidence JSON under tests/reports/game-runtime/",
|
|
64
|
+
"human or automated visual/accessibility approval"
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
stage: {
|
|
68
|
+
id: fightingStage.id,
|
|
69
|
+
safeZone: fightingStage.safeZone,
|
|
70
|
+
combatBounds: fightingStage.combatBounds,
|
|
71
|
+
validationIssues: fightingStageIssues.map((issue) => ({
|
|
72
|
+
severity: issue.severity,
|
|
73
|
+
code: issue.code,
|
|
74
|
+
message: issue.message
|
|
75
|
+
}))
|
|
76
|
+
}
|
|
77
|
+
} as const;
|
|
78
|
+
|
|
79
|
+
export function createFighterColliders(playerPosition: GameVec3, rivalPosition: GameVec3) {
|
|
80
|
+
return [
|
|
81
|
+
game.collider.capsule({ id: "player-body", center: playerPosition, radius: 0.34, height: 1.7, tags: ["fighter", "player"] }),
|
|
82
|
+
game.collider.capsule({ id: "rival-body", center: rivalPosition, radius: 0.34, height: 1.7, tags: ["fighter", "rival"] })
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createTouchLayout(width: number, height: number) {
|
|
87
|
+
return game.touchControls({
|
|
88
|
+
width,
|
|
89
|
+
height,
|
|
90
|
+
buttons: [
|
|
91
|
+
{ action: "jump", label: "Jump", binding: "TouchJump" },
|
|
92
|
+
{ action: "guard", label: "Guard", binding: "TouchGuard" },
|
|
93
|
+
{ action: "light", label: "Light", binding: "TouchLight" },
|
|
94
|
+
{ action: "heavy", label: "Heavy", binding: "TouchHeavy" },
|
|
95
|
+
{ action: "special", label: "Special", binding: "TouchSpecial" }
|
|
96
|
+
]
|
|
97
|
+
});
|
|
98
|
+
}
|