create-playcanvas 0.8.1 → 0.10.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/package.json +2 -2
  2. package/skills/add-effects/SKILL.md +21 -83
  3. package/skills/apply-conventions/SKILL.md +33 -59
  4. package/skills/assemble-scene/SKILL.md +18 -43
  5. package/skills/assemble-scene/references/direct-engine.md +2 -2
  6. package/skills/assemble-scene/references/react.md +3 -3
  7. package/skills/assemble-scene/references/web-components.md +2 -2
  8. package/skills/bake-lighting/SKILL.md +25 -56
  9. package/skills/build-app/SKILL.md +38 -37
  10. package/skills/build-app/references/web-components.md +3 -3
  11. package/skills/calibrate-model/SKILL.md +18 -48
  12. package/skills/calibrate-model/references/web-components.md +3 -3
  13. package/skills/configure-animation/SKILL.md +21 -37
  14. package/skills/configure-animation/references/direct-engine.md +2 -2
  15. package/skills/configure-animation/references/react.md +3 -3
  16. package/skills/find-examples/SKILL.md +24 -60
  17. package/skills/inspect-glb/SKILL.md +21 -62
  18. package/skills/inspect-runtime/SKILL.md +41 -0
  19. package/skills/inspect-runtime/scripts/devtools-hook.js +23 -0
  20. package/skills/light-scene/SKILL.md +29 -97
  21. package/skills/load-assets/SKILL.md +52 -0
  22. package/skills/override-shader-chunks/SKILL.md +21 -61
  23. package/skills/reduce-draw-calls/SKILL.md +24 -54
  24. package/skills/reuse-scripts/SKILL.md +23 -46
  25. package/skills/reuse-scripts/references/direct-engine.md +3 -3
  26. package/skills/verify-pixels/SKILL.md +24 -58
  27. package/templates/engine/base/README.md +4 -0
  28. package/templates/engine/base/package.json +3 -2
  29. package/templates/engine/base/src/inspector.css +33 -0
  30. package/templates/engine/base/src/inspector.ts +38 -0
  31. package/templates/engine/base/tsconfig.json +1 -1
  32. package/templates/engine/base/vite.config.ts +16 -0
  33. package/templates/engine/interactive-sphere/src/App.ts +2 -2
  34. package/templates/engine/model-viewer/src/main.ts +1 -2
  35. package/templates/engine/product-configurator/src/main.ts +6 -7
  36. package/templates/react/base/README.md +4 -0
  37. package/templates/react/base/package.json +3 -2
  38. package/templates/react/base/src/inspector.css +33 -0
  39. package/templates/react/base/src/inspector.ts +38 -0
  40. package/templates/react/base/vite.config.ts +12 -1
  41. package/templates/react/product-configurator/src/App.tsx +6 -6
  42. package/templates/web-components/base/README.md +4 -0
  43. package/templates/web-components/base/package.json +4 -3
  44. package/templates/web-components/base/src/inspector.css +33 -0
  45. package/templates/web-components/base/src/inspector.ts +38 -0
  46. package/templates/web-components/base/tsconfig.json +1 -1
  47. package/templates/web-components/base/vite.config.ts +16 -0
  48. package/templates/web-components/interactive-sphere/src/main.ts +1 -2
  49. package/templates/web-components/product-configurator/src/main.ts +5 -6
  50. package/skills/build-hud/SKILL.md +0 -38
  51. package/skills/manage-game-state/SKILL.md +0 -48
@@ -0,0 +1,33 @@
1
+ /* Inspector key hint, added on the dev server only by src/inspector.ts. Styled like the starter panels */
2
+ .inspector-hint {
3
+ position: fixed;
4
+ bottom: 20px;
5
+ left: 20px;
6
+ z-index: 10;
7
+ margin: 0;
8
+ padding: 8px 12px;
9
+ border: 1px solid #ffffff24;
10
+ border-radius: 10px;
11
+ background: #0b1019d9;
12
+ box-shadow: 0 12px 40px #0008;
13
+ color: #aeb8c8;
14
+ font:
15
+ 12px/1.6 Inter,
16
+ ui-sans-serif,
17
+ system-ui,
18
+ sans-serif;
19
+ backdrop-filter: blur(12px);
20
+ pointer-events: none;
21
+ }
22
+
23
+ .inspector-hint kbd {
24
+ padding: 1px 5px;
25
+ border-radius: 4px;
26
+ background: #263247;
27
+ color: #f8fafc;
28
+ font:
29
+ 11px ui-monospace,
30
+ SFMono-Regular,
31
+ Menlo,
32
+ monospace;
33
+ }
@@ -0,0 +1,38 @@
1
+ // Loaded by the dev server only (see vite.config.ts), so production builds never include it
2
+ import { Inspector } from '@playcanvas/inspector';
3
+ import type { AppBase } from 'playcanvas';
4
+
5
+ import './inspector.css';
6
+
7
+ type Hook = {
8
+ register?(app: AppBase, info: object): void;
9
+ unregister?(app: AppBase): void;
10
+ };
11
+
12
+ // Every app announces itself to this hook as it starts, whichever format created it
13
+ const KEY = Symbol.for('playcanvas.inspector');
14
+ const scope = globalThis as unknown as Record<symbol, Hook | undefined>;
15
+
16
+ // Keep a hook installed earlier, such as a devtools extension's
17
+ const prev = scope[KEY];
18
+
19
+ scope[KEY] = {
20
+ register(app, info) {
21
+ prev?.register?.(app, info);
22
+
23
+ // Skip headless apps, such as the null apps @playcanvas/react creates for validation. Each
24
+ // panel destroys itself with its app
25
+ if (!app.graphicsDevice.isNull) new Inspector(app, { visible: false });
26
+ },
27
+ unregister(app) {
28
+ prev?.unregister?.(app);
29
+ }
30
+ };
31
+
32
+ console.info('PlayCanvas Inspector: press ` to toggle');
33
+
34
+ // Key hint, a separate corner chip so the starter's own UI stays untouched
35
+ const hint = document.createElement('p');
36
+ hint.className = 'inspector-hint';
37
+ hint.innerHTML = '<kbd>`</kbd> inspector · <kbd>F9</kbd> pause · <kbd>F10</kbd> step';
38
+ document.body.append(hint);
@@ -21,5 +21,5 @@
21
21
  "noFallthroughCasesInSwitch": true,
22
22
  "noUncheckedSideEffectImports": true
23
23
  },
24
- "include": ["src"]
24
+ "include": ["src", "vite.config.ts"]
25
25
  }
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'vite';
2
+
3
+ // https://vite.dev/config/
4
+ export default defineConfig({
5
+ plugins: [
6
+ {
7
+ // Dev only: install the PlayCanvas Inspector before the app starts (press ` to toggle)
8
+ name: 'playcanvas-inspector',
9
+ apply: 'serve',
10
+ config: () => ({ optimizeDeps: { include: ['@playcanvas/inspector'] } }),
11
+ transformIndexHtml: () => [
12
+ { tag: 'script', attrs: { type: 'module', src: '/src/inspector.ts' }, injectTo: 'head-prepend' }
13
+ ]
14
+ }
15
+ ]
16
+ });
@@ -1,4 +1,4 @@
1
- import type { Layer, Texture } from 'playcanvas';
1
+ import type { Layer } from 'playcanvas';
2
2
  import {
3
3
  createGraphicsDevice,
4
4
  AppBase,
@@ -89,7 +89,7 @@ const setupApp = async (canvas: HTMLCanvasElement, onClick: () => void) => {
89
89
  app.start();
90
90
 
91
91
  // Set up environment lighting (no skybox, just IBL)
92
- app.scene.envAtlas = assets.envAtlas.resource as Texture;
92
+ app.scene.envAtlas = assets.envAtlas.resource!;
93
93
  const skyboxLayer = app.scene.layers.getLayerByName('Skybox');
94
94
  if (skyboxLayer) {
95
95
  skyboxLayer.enabled = false;
@@ -1,4 +1,3 @@
1
- import type { ContainerResource } from 'playcanvas';
2
1
  import {
3
2
  AppBase,
4
3
  AppOptions,
@@ -57,7 +56,7 @@ await new Promise<void>((resolve) => {
57
56
  app.start();
58
57
 
59
58
  // A container asset holds the whole glTF scene, so instantiate it to get an entity hierarchy
60
- app.root.addChild((model.resource as ContainerResource).instantiateRenderEntity());
59
+ app.root.addChild(model.resource!.instantiateRenderEntity());
61
60
 
62
61
  // The procedural sky is both the background and the image-based lighting, so no light or
63
62
  // environment map asset is needed. Its default luminance is scene-scale bright, so turn it down to
@@ -1,4 +1,3 @@
1
- import type { ContainerResource, RenderComponent } from 'playcanvas';
2
1
  import {
3
2
  AppBase,
4
3
  AppOptions,
@@ -62,12 +61,12 @@ await new Promise<void>((resolve) => new AssetListLoader([product], app.assets).
62
61
 
63
62
  app.start();
64
63
 
65
- const car = (product.resource as ContainerResource).instantiateRenderEntity();
64
+ const car = product.resource!.instantiateRenderEntity();
66
65
  car.setLocalEulerAngles(-90, -25, 0);
67
66
  app.root.addChild(car);
68
67
  app.root.syncHierarchy();
69
68
 
70
- const meshes = (car.findComponents('render') as RenderComponent[]).flatMap((render) => render.meshInstances);
69
+ const meshes = car.findComponents('render').flatMap((render) => render.meshInstances);
71
70
  const bounds = meshes[0].aabb.clone();
72
71
  meshes.slice(1).forEach((mesh) => bounds.add(mesh.aabb));
73
72
  const scale = 5 / Math.max(bounds.halfExtents.x * 2, bounds.halfExtents.z * 2);
@@ -77,10 +76,10 @@ bounds.copy(meshes[0].aabb);
77
76
  meshes.slice(1).forEach((mesh) => bounds.add(mesh.aabb));
78
77
  car.setPosition(-bounds.center.x, 0.03 - bounds.getMin().y, -bounds.center.z);
79
78
 
80
- const body = meshes.filter((mesh) => mesh.material.name === 'material');
81
- const panels = meshes.filter((mesh) => mesh.material.name === 'material_1');
82
- const paint = body[0].material.clone() as StandardMaterial;
83
- const shade = panels[0].material.clone() as StandardMaterial;
79
+ const body = meshes.filter((mesh) => mesh.material?.name === 'material');
80
+ const panels = meshes.filter((mesh) => mesh.material?.name === 'material_1');
81
+ const paint = body[0].material!.clone() as StandardMaterial;
82
+ const shade = panels[0].material!.clone() as StandardMaterial;
84
83
  body.forEach((mesh) => (mesh.material = paint));
85
84
  panels.forEach((mesh) => (mesh.material = shade));
86
85
  paint.diffuse.fromString(PAINTS[0].color);
@@ -17,6 +17,10 @@ npm run dev
17
17
 
18
18
  Open <http://localhost:5173>. Edit the files under `src/` and save to see the scene update.
19
19
 
20
+ ## Inspector
21
+
22
+ `npm run dev` loads the [PlayCanvas Inspector](https://github.com/playcanvas/inspector), a debug panel for the hierarchy, assets, materials, textures, frame graph and physics. Press the backquote key (<kbd>&#96;</kbd>) to show or hide it, <kbd>F9</kbd> to pause and <kbd>F10</kbd> to step a frame. It is wired up in `vite.config.ts` and `src/inspector.ts` and is left out of production builds.
23
+
20
24
  ## Scripts
21
25
 
22
26
  | Command | Description |
@@ -16,19 +16,20 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@playcanvas/react": "^0.11.7",
19
- "playcanvas": "^2.22.6",
19
+ "playcanvas": "^2.23.0",
20
20
  "react": "^19.3.0",
21
21
  "react-dom": "^19.3.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@playcanvas/eslint-config": "3.0.0-beta.8",
25
+ "@playcanvas/inspector": "^0.2.0",
25
26
  "@types/react": "^19.3.0",
26
27
  "@types/react-dom": "^19.3.0",
27
28
  "@vitejs/plugin-react": "^6.1.1",
28
29
  "eslint": "^9.39.5",
29
30
  "prettier": "^3.9.9",
30
31
  "typescript": "~6.0.3",
31
- "vite": "^8.3.1"
32
+ "vite": "^8.3.2"
32
33
  },
33
34
  "engines": {
34
35
  "node": ">=22.23.2"
@@ -0,0 +1,33 @@
1
+ /* Inspector key hint, added on the dev server only by src/inspector.ts. Styled like the starter panels */
2
+ .inspector-hint {
3
+ position: fixed;
4
+ bottom: 20px;
5
+ left: 20px;
6
+ z-index: 10;
7
+ margin: 0;
8
+ padding: 8px 12px;
9
+ border: 1px solid #ffffff24;
10
+ border-radius: 10px;
11
+ background: #0b1019d9;
12
+ box-shadow: 0 12px 40px #0008;
13
+ color: #aeb8c8;
14
+ font:
15
+ 12px/1.6 Inter,
16
+ ui-sans-serif,
17
+ system-ui,
18
+ sans-serif;
19
+ backdrop-filter: blur(12px);
20
+ pointer-events: none;
21
+ }
22
+
23
+ .inspector-hint kbd {
24
+ padding: 1px 5px;
25
+ border-radius: 4px;
26
+ background: #263247;
27
+ color: #f8fafc;
28
+ font:
29
+ 11px ui-monospace,
30
+ SFMono-Regular,
31
+ Menlo,
32
+ monospace;
33
+ }
@@ -0,0 +1,38 @@
1
+ // Loaded by the dev server only (see vite.config.ts), so production builds never include it
2
+ import { Inspector } from '@playcanvas/inspector';
3
+ import type { AppBase } from 'playcanvas';
4
+
5
+ import './inspector.css';
6
+
7
+ type Hook = {
8
+ register?(app: AppBase, info: object): void;
9
+ unregister?(app: AppBase): void;
10
+ };
11
+
12
+ // Every app announces itself to this hook as it starts, whichever format created it
13
+ const KEY = Symbol.for('playcanvas.inspector');
14
+ const scope = globalThis as unknown as Record<symbol, Hook | undefined>;
15
+
16
+ // Keep a hook installed earlier, such as a devtools extension's
17
+ const prev = scope[KEY];
18
+
19
+ scope[KEY] = {
20
+ register(app, info) {
21
+ prev?.register?.(app, info);
22
+
23
+ // Skip headless apps, such as the null apps @playcanvas/react creates for validation. Each
24
+ // panel destroys itself with its app
25
+ if (!app.graphicsDevice.isNull) new Inspector(app, { visible: false });
26
+ },
27
+ unregister(app) {
28
+ prev?.unregister?.(app);
29
+ }
30
+ };
31
+
32
+ console.info('PlayCanvas Inspector: press ` to toggle');
33
+
34
+ // Key hint, a separate corner chip so the starter's own UI stays untouched
35
+ const hint = document.createElement('p');
36
+ hint.className = 'inspector-hint';
37
+ hint.innerHTML = '<kbd>`</kbd> inspector · <kbd>F9</kbd> pause · <kbd>F10</kbd> step';
38
+ document.body.append(hint);
@@ -3,5 +3,16 @@ import { defineConfig } from 'vite';
3
3
 
4
4
  // https://vite.dev/config/
5
5
  export default defineConfig({
6
- plugins: [react()]
6
+ plugins: [
7
+ react(),
8
+ {
9
+ // Dev only: install the PlayCanvas Inspector before the app starts (press ` to toggle)
10
+ name: 'playcanvas-inspector',
11
+ apply: 'serve',
12
+ config: () => ({ optimizeDeps: { include: ['@playcanvas/inspector'] } }),
13
+ transformIndexHtml: () => [
14
+ { tag: 'script', attrs: { type: 'module', src: '/src/inspector.ts' }, injectTo: 'head-prepend' }
15
+ ]
16
+ }
17
+ ]
7
18
  });
@@ -1,7 +1,7 @@
1
1
  import { Application, Entity } from '@playcanvas/react';
2
2
  import { Camera, Light, Script } from '@playcanvas/react/components';
3
3
  import { useApp, useModel } from '@playcanvas/react/hooks';
4
- import type { ContainerResource, Entity as PcEntity, RenderComponent } from 'playcanvas';
4
+ import type { ContainerResource, Entity as PcEntity } from 'playcanvas';
5
5
  import { Color, CylinderGeometry, Mesh, MeshInstance, StandardMaterial, TONEMAP_ACES2, Vec2, Vec3 } from 'playcanvas';
6
6
  import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
7
7
  import { ProceduralSky } from 'playcanvas/scripts/esm/sky/procedural-sky.mjs';
@@ -32,7 +32,7 @@ function Product({ color, shade }: { color: string; shade: string }) {
32
32
  entity.setLocalEulerAngles(-90, -25, 0);
33
33
  root.current.addChild(entity);
34
34
 
35
- const meshes = (entity.findComponents('render') as RenderComponent[]).flatMap((render) => render.meshInstances);
35
+ const meshes = entity.findComponents('render').flatMap((render) => render.meshInstances);
36
36
  const bounds = meshes[0].aabb.clone();
37
37
  meshes.slice(1).forEach((mesh) => bounds.add(mesh.aabb));
38
38
  const scale = 5 / Math.max(bounds.halfExtents.x * 2, bounds.halfExtents.z * 2);
@@ -42,10 +42,10 @@ function Product({ color, shade }: { color: string; shade: string }) {
42
42
  meshes.slice(1).forEach((mesh) => bounds.add(mesh.aabb));
43
43
  entity.setPosition(-bounds.center.x, 0.03 - bounds.getMin().y, -bounds.center.z);
44
44
 
45
- const body = meshes.filter((mesh) => mesh.material.name === 'material');
46
- const panels = meshes.filter((mesh) => mesh.material.name === 'material_1');
47
- paint.current = body[0].material.clone() as StandardMaterial;
48
- tone.current = panels[0].material.clone() as StandardMaterial;
45
+ const body = meshes.filter((mesh) => mesh.material?.name === 'material');
46
+ const panels = meshes.filter((mesh) => mesh.material?.name === 'material_1');
47
+ paint.current = body[0].material!.clone() as StandardMaterial;
48
+ tone.current = panels[0].material!.clone() as StandardMaterial;
49
49
  body.forEach((mesh) => (mesh.material = paint.current!));
50
50
  panels.forEach((mesh) => (mesh.material = tone.current!));
51
51
  return () => {
@@ -18,6 +18,10 @@ npm run dev
18
18
 
19
19
  Open <http://localhost:5173>. Edit `index.html` to change the scene.
20
20
 
21
+ ## Inspector
22
+
23
+ `npm run dev` loads the [PlayCanvas Inspector](https://github.com/playcanvas/inspector), a debug panel for the hierarchy, assets, materials, textures, frame graph and physics. Press the backquote key (<kbd>&#96;</kbd>) to show or hide it, <kbd>F9</kbd> to pause and <kbd>F10</kbd> to step a frame. It is wired up in `vite.config.ts` and `src/inspector.ts` and is left out of production builds.
24
+
21
25
  ## Scripts
22
26
 
23
27
  | Command | Description |
@@ -15,15 +15,16 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "@playcanvas/web-components": "^0.26.0",
19
- "playcanvas": "^2.22.6"
18
+ "@playcanvas/web-components": "^0.28.0",
19
+ "playcanvas": "^2.23.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@playcanvas/eslint-config": "3.0.0-beta.8",
23
+ "@playcanvas/inspector": "^0.2.0",
23
24
  "eslint": "^9.39.5",
24
25
  "prettier": "^3.9.9",
25
26
  "typescript": "~6.0.3",
26
- "vite": "^8.3.1"
27
+ "vite": "^8.3.2"
27
28
  },
28
29
  "engines": {
29
30
  "node": ">=22.23.2"
@@ -0,0 +1,33 @@
1
+ /* Inspector key hint, added on the dev server only by src/inspector.ts. Styled like the starter panels */
2
+ .inspector-hint {
3
+ position: fixed;
4
+ bottom: 20px;
5
+ left: 20px;
6
+ z-index: 10;
7
+ margin: 0;
8
+ padding: 8px 12px;
9
+ border: 1px solid #ffffff24;
10
+ border-radius: 10px;
11
+ background: #0b1019d9;
12
+ box-shadow: 0 12px 40px #0008;
13
+ color: #aeb8c8;
14
+ font:
15
+ 12px/1.6 Inter,
16
+ ui-sans-serif,
17
+ system-ui,
18
+ sans-serif;
19
+ backdrop-filter: blur(12px);
20
+ pointer-events: none;
21
+ }
22
+
23
+ .inspector-hint kbd {
24
+ padding: 1px 5px;
25
+ border-radius: 4px;
26
+ background: #263247;
27
+ color: #f8fafc;
28
+ font:
29
+ 11px ui-monospace,
30
+ SFMono-Regular,
31
+ Menlo,
32
+ monospace;
33
+ }
@@ -0,0 +1,38 @@
1
+ // Loaded by the dev server only (see vite.config.ts), so production builds never include it
2
+ import { Inspector } from '@playcanvas/inspector';
3
+ import type { AppBase } from 'playcanvas';
4
+
5
+ import './inspector.css';
6
+
7
+ type Hook = {
8
+ register?(app: AppBase, info: object): void;
9
+ unregister?(app: AppBase): void;
10
+ };
11
+
12
+ // Every app announces itself to this hook as it starts, whichever format created it
13
+ const KEY = Symbol.for('playcanvas.inspector');
14
+ const scope = globalThis as unknown as Record<symbol, Hook | undefined>;
15
+
16
+ // Keep a hook installed earlier, such as a devtools extension's
17
+ const prev = scope[KEY];
18
+
19
+ scope[KEY] = {
20
+ register(app, info) {
21
+ prev?.register?.(app, info);
22
+
23
+ // Skip headless apps, such as the null apps @playcanvas/react creates for validation. Each
24
+ // panel destroys itself with its app
25
+ if (!app.graphicsDevice.isNull) new Inspector(app, { visible: false });
26
+ },
27
+ unregister(app) {
28
+ prev?.unregister?.(app);
29
+ }
30
+ };
31
+
32
+ console.info('PlayCanvas Inspector: press ` to toggle');
33
+
34
+ // Key hint, a separate corner chip so the starter's own UI stays untouched
35
+ const hint = document.createElement('p');
36
+ hint.className = 'inspector-hint';
37
+ hint.innerHTML = '<kbd>`</kbd> inspector · <kbd>F9</kbd> pause · <kbd>F10</kbd> step';
38
+ document.body.append(hint);
@@ -21,5 +21,5 @@
21
21
  "noFallthroughCasesInSwitch": true,
22
22
  "noUncheckedSideEffectImports": true
23
23
  },
24
- "include": ["src"]
24
+ "include": ["src", "vite.config.ts"]
25
25
  }
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'vite';
2
+
3
+ // https://vite.dev/config/
4
+ export default defineConfig({
5
+ plugins: [
6
+ {
7
+ // Dev only: install the PlayCanvas Inspector before the app starts (press ` to toggle)
8
+ name: 'playcanvas-inspector',
9
+ apply: 'serve',
10
+ config: () => ({ optimizeDeps: { include: ['@playcanvas/inspector'] } }),
11
+ transformIndexHtml: () => [
12
+ { tag: 'script', attrs: { type: 'module', src: '/src/inspector.ts' }, injectTo: 'head-prepend' }
13
+ ]
14
+ }
15
+ ]
16
+ });
@@ -1,6 +1,5 @@
1
1
  import type { EntityElement } from '@playcanvas/web-components';
2
2
  import { whenReady } from '@playcanvas/web-components';
3
- import type { Texture } from 'playcanvas';
4
3
  import { Asset, TEXTURETYPE_RGBP } from 'playcanvas';
5
4
  import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
6
5
  import { Grid } from 'playcanvas/scripts/esm/grid.mjs';
@@ -40,7 +39,7 @@ await new Promise<void>((resolve) => {
40
39
  });
41
40
 
42
41
  // Light the scene from the environment, with the skybox itself left hidden
43
- app.scene.envAtlas = envAtlas.resource as Texture;
42
+ app.scene.envAtlas = envAtlas.resource!;
44
43
  const skyboxLayer = app.scene.layers.getLayerByName('Skybox');
45
44
  if (skyboxLayer) {
46
45
  skyboxLayer.enabled = false;
@@ -1,6 +1,5 @@
1
1
  import type { AssetElement, EntityElement, ModelElement } from '@playcanvas/web-components';
2
2
  import { whenReady } from '@playcanvas/web-components';
3
- import type { RenderComponent } from 'playcanvas';
4
3
  import { Color, CylinderGeometry, Mesh, MeshInstance, StandardMaterial, Vec2, Vec3 } from 'playcanvas';
5
4
  import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
6
5
  import { ProceduralSky } from 'playcanvas/scripts/esm/sky/procedural-sky.mjs';
@@ -63,7 +62,7 @@ ground.addComponent('render', {
63
62
  const entity = model.entity!;
64
63
  entity.setLocalEulerAngles(0, -25, 0);
65
64
  entity.root.syncHierarchy();
66
- const meshes = (entity.findComponents('render') as RenderComponent[]).flatMap((render) => render.meshInstances);
65
+ const meshes = entity.findComponents('render').flatMap((render) => render.meshInstances);
67
66
  const bounds = meshes[0].aabb.clone();
68
67
  meshes.slice(1).forEach((mesh) => bounds.add(mesh.aabb));
69
68
  const scale = 5 / Math.max(bounds.halfExtents.x * 2, bounds.halfExtents.z * 2);
@@ -73,10 +72,10 @@ bounds.copy(meshes[0].aabb);
73
72
  meshes.slice(1).forEach((mesh) => bounds.add(mesh.aabb));
74
73
  entity.setPosition(-bounds.center.x, 0.03 - bounds.getMin().y, -bounds.center.z);
75
74
 
76
- const body = meshes.filter((mesh) => mesh.material.name === 'material');
77
- const panels = meshes.filter((mesh) => mesh.material.name === 'material_1');
78
- const paint = body[0].material.clone() as StandardMaterial;
79
- const shade = panels[0].material.clone() as StandardMaterial;
75
+ const body = meshes.filter((mesh) => mesh.material?.name === 'material');
76
+ const panels = meshes.filter((mesh) => mesh.material?.name === 'material_1');
77
+ const paint = body[0].material!.clone() as StandardMaterial;
78
+ const shade = panels[0].material!.clone() as StandardMaterial;
80
79
  body.forEach((mesh) => (mesh.material = paint));
81
80
  panels.forEach((mesh) => (mesh.material = shade));
82
81
  paint.diffuse.fromString('#d6293e');
@@ -1,38 +0,0 @@
1
- ---
2
- name: build-hud
3
- description: Use when building a PlayCanvas game's on-screen interface such as ready, pause, and victory overlays, menus, and live indicators like gauges, timers, or reload meters, so the interface is driven by game state, readable over the scene, and separate from the 3D hierarchy.
4
- ---
5
-
6
- # Game interface
7
-
8
- A HUD is a view of game state, not a source of it. Render it from the current state each frame or on
9
- each transition; never store gameplay truth in the DOM or a UI element.
10
-
11
- ## Choose the UI layer
12
-
13
- - Build the interface in the natural UI layer for the surface — a DOM or React overlay, or the
14
- Engine `screen` and `element` UI — decided with the `build-app` skill. Keep it outside gameplay
15
- model hierarchies, as `assemble-scene` requires.
16
- - For a screen-space Engine UI, adapt a user-interface example with `find-examples` rather than
17
- guessing element anchoring, scaling, and font handling.
18
-
19
- ## Drive from state
20
-
21
- - Map each overlay to a game state and each indicator to a snapshot field. A transition shows or
22
- hides overlays; it does not duplicate the state the game already owns (see `manage-game-state`).
23
- - An indicator that is idle most of the time toggles `enabled` with its visibility; an element left
24
- drawing at opacity 0 still costs a draw call every frame (see `reduce-draw-calls`).
25
- - Anchor and scale elements to the viewport so the layout holds at the target resolution and on
26
- resize. Verify at the acceptance resolution, not the default window size.
27
- - Keep copy minimal and legible over the scene: back text with a plate or shadow where the frame is
28
- bright, and unify interface colour with the scene's grade from `light-scene`.
29
-
30
- ## Prove it reflects state
31
-
32
- Drive real input through every transition and confirm with screenshots that the correct overlay
33
- shows and each indicator tracks the snapshot — a reload meter empties and refills, a timer advances,
34
- a menu appears only in its state. A saved filepath or a visibility flag is not visual evidence;
35
- inspect the returned pixels as the local agent guide requires.
36
-
37
- Choose the authoring surface with the `build-app` skill and build the interface through its own UI
38
- primitives before reaching into the Engine.
@@ -1,48 +0,0 @@
1
- ---
2
- name: manage-game-state
3
- description: Use when structuring a PlayCanvas game's control flow — a small state machine such as ready, playing, paused, and over, pointer-lock capture, pausing on focus or pointer-lock loss, a full reset, and a stable clock and timestep — so the loop stays deterministic and recoverable.
4
- ---
5
-
6
- # Game state and loop
7
-
8
- Model the game as a small explicit state machine with one owner of the clock. Every transition names
9
- its states; behaviour, input, and the interface read the current state rather than a scatter of
10
- booleans.
11
-
12
- ## States and transitions
13
-
14
- - Enumerate the states — for example ready, playing, paused, over — and make transitions explicit
15
- and total. Do not infer state from side effects.
16
- - Gate the simulation on the active state: advance gameplay, timers, and physics only while playing.
17
-
18
- ## Capture input and pause
19
-
20
- - Request pointer lock from a user gesture such as a click, never on load. Treat loss of pointer lock
21
- and tab blur as a pause: freeze the simulation and the clock, and resume on the next gesture.
22
- - Make `pointerlockchange` idempotent: when `document.pointerLockElement` is the canvas, transition
23
- ready or paused to playing and leave playing unchanged; only transition playing to paused when the
24
- element is no longer the canvas. Never implement it as a toggle. Browsers and automation can both
25
- report an already-acquired lock, and a duplicate enter event must not pause the game.
26
- - Clamp the per-frame delta before integrating anything, as `build-app` requires, so a backgrounded
27
- tab or a slow frame cannot inject one large step into movement, cooldowns, or the clock.
28
-
29
- ## Reset
30
-
31
- - A reset restores every owned system to its ready values — entities, camera, clock, timers,
32
- cooldowns, effects, and overlays — not only the player. Route reset through the same setup the
33
- initial state uses, so ready and reset cannot drift apart.
34
- - Transition to `ready` explicitly before releasing pointer lock. Never derive the reset state from
35
- `document.pointerLockElement`; pointer lock is an input side effect, not the state machine.
36
- - Define timer semantics once and keep them literal in state and snapshots. A field named `reload`
37
- is remaining cooldown time: `0` means ready, firing assigns a positive duration, and the active
38
- loop counts it down to exactly `0`. Do not expose a normalized readiness value under that name.
39
-
40
- ## Prove the loop
41
-
42
- Drive the full cycle with real input: enter play, lose pointer lock and confirm the clock and
43
- simulation freeze, resume, reset and confirm every field returns to its ready value, then reach the
44
- end state and restart. Read these from the app's own state snapshot, never from console mutation —
45
- manufactured state is not evidence.
46
-
47
- Choose the authoring surface with the `build-app` skill and own the loop and lifecycle in the surface
48
- that creates the application.