nova64 0.2.4 → 0.2.6

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 (140) hide show
  1. package/README.md +25 -8
  2. package/bin/nova64.js +165 -0
  3. package/dist/assets/console-CY_kygm3.js +14 -0
  4. package/dist/assets/console-CY_kygm3.js.map +1 -0
  5. package/dist/assets/main-l0sNRNKZ.js.map +1 -0
  6. package/dist/assets/sky/studio/nx.png +0 -0
  7. package/dist/assets/sky/studio/ny.png +0 -0
  8. package/dist/assets/sky/studio/nz.png +0 -0
  9. package/dist/assets/sky/studio/px.png +0 -0
  10. package/dist/assets/sky/studio/py.png +0 -0
  11. package/dist/assets/sky/studio/pz.png +0 -0
  12. package/dist/assets/vanilla-Dcuy32gi.js +2 -0
  13. package/dist/assets/vanilla-Dcuy32gi.js.map +1 -0
  14. package/dist/console.html +899 -0
  15. package/dist/docs/BENCHMARK.md +77 -0
  16. package/dist/docs/CHEATSHEET.md +255 -0
  17. package/dist/docs/EFFECTS_API_GUIDE.md +577 -0
  18. package/dist/docs/EFFECTS_QUICK_REFERENCE.md +331 -0
  19. package/dist/docs/FONT_CHARACTER_REFERENCE.md +219 -0
  20. package/dist/docs/FREE_GLB_ASSETS.md +330 -0
  21. package/dist/docs/FULLSCREEN_BUTTON_FEATURE.md +296 -0
  22. package/dist/docs/GAMEPAD_SUPPORT.md +348 -0
  23. package/dist/docs/GAME_IMPROVEMENTS.md +278 -0
  24. package/dist/docs/GAME_QUALITY_STATUS.md +300 -0
  25. package/dist/docs/MIGRATION_GUIDE.md +553 -0
  26. package/dist/docs/NOVA64_3D_API.md +356 -0
  27. package/dist/docs/NOVA64_API_REFERENCE.md +1406 -0
  28. package/dist/docs/NOVA64_UI_API.md +503 -0
  29. package/dist/docs/UI_SYSTEM_SUMMARY.md +445 -0
  30. package/dist/docs/VOXEL_ENGINE_GUIDE.md +662 -0
  31. package/dist/docs/VOXEL_QUICK_REFERENCE.md +386 -0
  32. package/dist/docs/api-3d.html +750 -0
  33. package/dist/docs/api-effects.html +385 -0
  34. package/dist/docs/api-improvements.md +121 -0
  35. package/dist/docs/api-skybox.html +407 -0
  36. package/dist/docs/api-sprites.html +321 -0
  37. package/dist/docs/api-voxel.html +337 -0
  38. package/dist/docs/api.html +543 -0
  39. package/dist/docs/assets.html +306 -0
  40. package/dist/docs/audio.html +340 -0
  41. package/dist/docs/blogs.html +286 -0
  42. package/dist/docs/collision.html +316 -0
  43. package/dist/docs/console.html +247 -0
  44. package/dist/docs/editor.html +297 -0
  45. package/dist/docs/font.html +247 -0
  46. package/dist/docs/framebuffer.html +247 -0
  47. package/dist/docs/fullscreen-button.html +297 -0
  48. package/dist/docs/gpu-systems.html +247 -0
  49. package/dist/docs/index.html +580 -0
  50. package/dist/docs/input.html +491 -0
  51. package/dist/docs/physics.html +311 -0
  52. package/dist/docs/screens.html +311 -0
  53. package/dist/docs/storage.html +311 -0
  54. package/dist/docs/textinput.html +332 -0
  55. package/dist/docs/ui.html +488 -0
  56. package/dist/examples/3d-advanced/code.js +695 -0
  57. package/dist/examples/adventure-comic-3d/code.js +342 -0
  58. package/dist/examples/audio-lab/code.js +150 -0
  59. package/dist/examples/boids-flocking/code.js +270 -0
  60. package/dist/examples/crystal-cathedral-3d/code.js +706 -0
  61. package/dist/examples/cyberpunk-city-3d/code.js +1383 -0
  62. package/dist/examples/demoscene/README.md +192 -0
  63. package/dist/examples/demoscene/code.js +1081 -0
  64. package/dist/examples/demoscene/meta.json +21 -0
  65. package/dist/examples/dungeon-crawler-3d/code.js +1117 -0
  66. package/dist/examples/f-zero-nova-3d/code.js +865 -0
  67. package/dist/examples/f-zero-nova-3d/code_old.js +1555 -0
  68. package/dist/examples/fps-demo-3d/code.js +744 -0
  69. package/dist/examples/game-of-life-3d/code.js +338 -0
  70. package/dist/examples/generative-art/code.js +632 -0
  71. package/dist/examples/hello-3d/code.js +325 -0
  72. package/dist/examples/hello-skybox/code.js +183 -0
  73. package/dist/examples/hello-world/code.js +19 -0
  74. package/dist/examples/input-showcase/code.js +109 -0
  75. package/dist/examples/instancing-demo/code.js +315 -0
  76. package/dist/examples/minecraft-demo/code.js +387 -0
  77. package/dist/examples/model-viewer-3d/code.js +114 -0
  78. package/dist/examples/mystical-realm-3d/code.js +1203 -0
  79. package/dist/examples/nature-explorer-3d/code.js +1318 -0
  80. package/dist/examples/particles-demo/code.js +522 -0
  81. package/dist/examples/pbr-showcase/code.js +140 -0
  82. package/dist/examples/physics-demo-3d/code.js +948 -0
  83. package/dist/examples/screen-demo/code.js +267 -0
  84. package/dist/examples/shooter-demo-3d/code.js +1286 -0
  85. package/dist/examples/space-combat-3d/IMPLEMENTATION_SUMMARY.md +109 -0
  86. package/dist/examples/space-combat-3d/README.md +135 -0
  87. package/dist/examples/space-combat-3d/code.js +1332 -0
  88. package/dist/examples/space-harrier-3d/code.js +923 -0
  89. package/dist/examples/star-fox-nova-3d/code.js +1116 -0
  90. package/dist/examples/star-fox-nova-3d/code_backup.js +410 -0
  91. package/dist/examples/star-fox-nova-3d/code_broken.js +1821 -0
  92. package/dist/examples/storage-quest/code.js +209 -0
  93. package/dist/examples/strider-demo-3d/IMPROVEMENT_OPTIONS.md +285 -0
  94. package/dist/examples/strider-demo-3d/cache-test.html +132 -0
  95. package/dist/examples/strider-demo-3d/code-fixed.js +582 -0
  96. package/dist/examples/strider-demo-3d/code-old.js +1537 -0
  97. package/dist/examples/strider-demo-3d/code.js +1462 -0
  98. package/dist/examples/strider-demo-3d/code.js.bak2 +1169 -0
  99. package/dist/examples/strider-demo-3d/fix-game.sh +53 -0
  100. package/dist/examples/super-plumber-64/README.md +128 -0
  101. package/dist/examples/super-plumber-64/code.js +1185 -0
  102. package/dist/examples/super-plumber-64/index.html +88 -0
  103. package/dist/examples/test-2d-overlay/code.js +32 -0
  104. package/dist/examples/test-font/code.js +51 -0
  105. package/dist/examples/test-minimal/code.js +21 -0
  106. package/dist/examples/ui-demo/code.js +306 -0
  107. package/dist/examples/wing-commander-space/README.md +180 -0
  108. package/dist/examples/wing-commander-space/code.js +1285 -0
  109. package/dist/examples/wizardry-3d/CHANGELOG.md +366 -0
  110. package/dist/examples/wizardry-3d/code.js +3928 -0
  111. package/dist/index.html +666 -0
  112. package/dist/os9-shell/assets/index-DIHfrTaW.css +1 -0
  113. package/dist/os9-shell/assets/index-KchE_ngx.js +483 -0
  114. package/dist/os9-shell/assets/index-KchE_ngx.js.map +1 -0
  115. package/dist/os9-shell/index.html +23 -0
  116. package/dist/os9-shell/nova-icon.svg +12 -0
  117. package/index.html +6 -1
  118. package/package.json +37 -32
  119. package/public/assets/sky/studio/nx.png +0 -0
  120. package/public/assets/sky/studio/ny.png +0 -0
  121. package/public/assets/sky/studio/nz.png +0 -0
  122. package/public/assets/sky/studio/px.png +0 -0
  123. package/public/assets/sky/studio/py.png +0 -0
  124. package/public/assets/sky/studio/pz.png +0 -0
  125. package/public/os9-shell/assets/index-KchE_ngx.js +483 -0
  126. package/public/os9-shell/assets/index-KchE_ngx.js.map +1 -0
  127. package/public/os9-shell/index.html +10 -1
  128. package/runtime/api-2d.js +301 -21
  129. package/runtime/api-3d/pbr.js +45 -1
  130. package/runtime/api-3d.js +1 -0
  131. package/runtime/api-effects.js +90 -3
  132. package/runtime/api-gameutils.js +476 -0
  133. package/runtime/api-generative.js +610 -0
  134. package/runtime/api-skybox.js +54 -0
  135. package/runtime/api-voxel.js +139 -28
  136. package/runtime/gpu-threejs.js +13 -9
  137. package/runtime/ui.js +2 -2
  138. package/src/main.js +24 -1
  139. package/public/os9-shell/assets/index-B1Uvacma.js +0 -32825
  140. package/public/os9-shell/assets/index-B1Uvacma.js.map +0 -1
@@ -0,0 +1,342 @@
1
+ // THE VERDICT — A 3D Noir Comic Adventure
2
+ // Interrogate a suspect using evidence found at the crime scene.
3
+ // WASD to move | SPACE to interact / advance dialogue
4
+
5
+ // ── State ────────────────────────────────────────────────────────────────────
6
+ let state = 'title'; // 'title' | 'explore' | 'dialogue'
7
+ let sceneTime = 0; // seconds since last state enter
8
+ let textScroll = 0;
9
+ let lastCharCount = 0; // for typewriter sfx
10
+ let tickTimer = 0;
11
+
12
+ let currentText = '';
13
+ let speaker = '';
14
+ let dialogStage = 0;
15
+ let currentScript = [];
16
+ let hasEvidence = false;
17
+ let gameFinished = false;
18
+
19
+ // ── 3D scene handles ─────────────────────────────────────────────────────────
20
+ let detective = { body: null, head: null };
21
+ let suspect = { body: null, head: null };
22
+ let evidence = null;
23
+ let deskLamp = null;
24
+ let evidenceAngle = 0;
25
+
26
+ // ── Player state ─────────────────────────────────────────────────────────────
27
+ let playerPos = { x: 0, z: 6 };
28
+ const SPEED = 5.0; // units per second
29
+
30
+ // ── Overlay dimensions ───────────────────────────────────────────────────────
31
+ const W = 320;
32
+ const H = 240;
33
+
34
+ // ── Scripts ──────────────────────────────────────────────────────────────────
35
+ const SCRIPT_SUSPECT = [
36
+ { s: 'Detective', t: 'Where were you on the night of the 14th?' },
37
+ { s: 'Suspect', t: 'I told you, I was at the movies.' },
38
+ { s: 'Detective', t: 'Alone?' },
39
+ { s: 'Suspect', t: 'Yeah. Why would I lie about that?' },
40
+ { s: 'Detective', t: "We'll see..." },
41
+ ];
42
+
43
+ const SCRIPT_EVIDENCE = [
44
+ { s: 'Detective', t: 'The safe was forced open...' },
45
+ { s: 'Detective', t: "But the lock mechanism isn't broken." },
46
+ { s: 'Detective', t: "There's a strange glowing cube inside." },
47
+ { s: 'Detective', t: 'I should ask the suspect about this.' },
48
+ ];
49
+
50
+ const SCRIPT_CONFRONT = [
51
+ { s: 'Detective', t: 'I found this glowing cube in the safe.' },
52
+ { s: 'Suspect', t: "I've never seen that before in my life!" },
53
+ { s: 'Detective', t: 'Then how did your fingerprints get on it?' },
54
+ { s: 'Narrator', t: '... THE END ...' },
55
+ ];
56
+
57
+ // ── Init ─────────────────────────────────────────────────────────────────────
58
+ export function init() {
59
+ // Atmosphere
60
+ setFog(0x0a0a14, 8, 28);
61
+ enableBloom(1.0, 0.5, 0.4);
62
+ enableFXAA();
63
+ enableVignette(1.0, 0.85);
64
+
65
+ // Lighting — visible but dramatic
66
+ setAmbientLight(0x2a3040, 1.2); // dark blue-gray, still readable
67
+ setLightDirection(-0.4, -1.0, -0.3);
68
+ setLightColor(0xaabbcc); // cool off-white overhead
69
+ deskLamp = createPointLight(0xff9933, 6.0, 14, 0, 2.8, -0.2); // warm amber desk lamp
70
+
71
+ // ── Floor ──────────────────────────────────────────────────────────────────
72
+ const floor = createPlane(20, 20, 0x2a1800, [0, 0, 0], { material: 'standard', roughness: 0.9 });
73
+ setRotation(floor, -Math.PI / 2, 0, 0);
74
+
75
+ // ── Walls ──────────────────────────────────────────────────────────────────
76
+ // Back wall
77
+ const wallBack = createPlane(20, 6, 0x222222, [0, 3, -8]);
78
+
79
+ // Left wall
80
+ const wallLeft = createPlane(20, 6, 0x1e1e1e, [-8, 3, 0]);
81
+ setRotation(wallLeft, 0, Math.PI / 2, 0);
82
+
83
+ // Right wall (has moonlit window)
84
+ const wallRight = createPlane(20, 6, 0x1e1e1e, [8, 3, 0]);
85
+ setRotation(wallRight, 0, -Math.PI / 2, 0);
86
+
87
+ // Moonlit window (emissive blue-white glow inset on right wall)
88
+ const window1 = createPlane(3, 2.5, 0x8ab4d4, [7.9, 3, 0], {
89
+ material: 'emissive',
90
+ emissive: 0x8ab4d4,
91
+ });
92
+ setRotation(window1, 0, -Math.PI / 2, 0);
93
+
94
+ // ── Desk ──────────────────────────────────────────────────────────────────
95
+ const desk = createCube(1, 0x2a1600, [0, 0.4, 0], { material: 'standard', roughness: 0.5 });
96
+ setScale(desk, 5, 0.8, 2.5);
97
+
98
+ // Desk lamp post
99
+ const lampPost = createCube(0.12, 0x222222, [1.5, 1.5, -0.3], {
100
+ material: 'metallic',
101
+ metalness: 0.8,
102
+ });
103
+ setScale(lampPost, 1, 4, 1);
104
+
105
+ // Desk lamp shade (emissive)
106
+ const lampShade = createCube(0.5, 0xffdd88, [1.5, 2.8, -0.3], {
107
+ material: 'emissive',
108
+ emissive: 0xffdd88,
109
+ });
110
+ setScale(lampShade, 3, 0.7, 3);
111
+
112
+ // ── Filing cabinet (back-left corner) ────────────────────────────────────
113
+ const cab1 = createCube(1, 0x333333, [-6, 0.75, -6.5], { material: 'metallic', metalness: 0.6 });
114
+ setScale(cab1, 2, 1.5, 1.5);
115
+ const cab2 = createCube(1, 0x2a2a2a, [-6, 2.25, -6.5], { material: 'metallic', metalness: 0.6 });
116
+ setScale(cab2, 2, 1.5, 1.5);
117
+ // Cabinet handle details
118
+ const handle = createCube(0.08, 0x888888, [-6.9, 0.85, -6.5], {
119
+ material: 'metallic',
120
+ metalness: 0.9,
121
+ });
122
+ setScale(handle, 1, 1, 5);
123
+
124
+ // ── Evidence (glowing cube on desk) ──────────────────────────────────────
125
+ evidence = createCube(0.4, 0x00ff88, [0, 1.1, 0], { material: 'emissive', emissive: 0x00ff88 });
126
+
127
+ // ── Detective (multi-part) ────────────────────────────────────────────────
128
+ detective.body = createCube(1, 0x334477, [playerPos.x, 0.9, playerPos.z], {
129
+ material: 'metallic',
130
+ metalness: 0.4,
131
+ roughness: 0.6,
132
+ });
133
+ setScale(detective.body, 0.9, 1.8, 0.7);
134
+ detective.head = createCube(0.7, 0x556688, [playerPos.x, 2.2, playerPos.z], {
135
+ material: 'standard',
136
+ roughness: 0.5,
137
+ });
138
+
139
+ // ── Suspect (multi-part) ──────────────────────────────────────────────────
140
+ suspect.body = createCube(1, 0x883322, [3, 0.9, -2], { material: 'standard', roughness: 0.6 });
141
+ setScale(suspect.body, 0.9, 1.8, 0.7);
142
+ suspect.head = createCube(0.7, 0xaa4433, [3, 2.2, -2], { material: 'standard', roughness: 0.5 });
143
+
144
+ // Camera
145
+ setCameraPosition(0, 3, 8);
146
+ setCameraTarget(0, 1, 0);
147
+ setCameraFOV(60);
148
+ }
149
+
150
+ // ── Helpers ───────────────────────────────────────────────────────────────────
151
+ function setDetectivePos(x, z) {
152
+ setPosition(detective.body, x, 0.9, z);
153
+ setPosition(detective.head, x, 2.2, z);
154
+ }
155
+
156
+ function processDialog() {
157
+ if (dialogStage >= currentScript.length) {
158
+ state = 'explore';
159
+ setCameraPosition(playerPos.x, 9, playerPos.z + 6);
160
+ setCameraTarget(playerPos.x, 0, playerPos.z);
161
+ sceneTime = 0;
162
+ if (gameFinished) state = 'title';
163
+ return;
164
+ }
165
+
166
+ speaker = currentScript[dialogStage].s;
167
+ currentText = currentScript[dialogStage].t;
168
+ textScroll = 0;
169
+
170
+ // Cinematic camera angles per speaker
171
+ if (speaker === 'Detective') {
172
+ setCameraPosition(playerPos.x - 1.5, 2.5, playerPos.z + 2);
173
+ setCameraTarget(playerPos.x, 2, playerPos.z - 1);
174
+ } else if (speaker === 'Suspect') {
175
+ setCameraPosition(4.5, 2.2, 0);
176
+ setCameraTarget(3, 2, -2);
177
+ // Face the detective (fixed facing, not sceneTime bug)
178
+ setRotation(suspect.body, 0, Math.PI * 0.2, 0);
179
+ setRotation(suspect.head, 0, Math.PI * 0.2, 0);
180
+ } else if (speaker === 'Narrator') {
181
+ setCameraPosition(0, 9, 0.1);
182
+ setCameraTarget(0, 0, 0);
183
+ }
184
+ }
185
+
186
+ // ── Update ────────────────────────────────────────────────────────────────────
187
+ export function update(dt) {
188
+ sceneTime += dt;
189
+
190
+ // Spinning evidence
191
+ evidenceAngle += dt * 3.0;
192
+ setRotation(evidence, 0, evidenceAngle, evidenceAngle * 0.4);
193
+
194
+ if (state === 'title') {
195
+ // Slow cinematic orbit
196
+ const orb = sceneTime * 0.3;
197
+ setCameraPosition(Math.sin(orb) * 6, 4, Math.cos(orb) * 6 + 2);
198
+ setCameraTarget(0, 1, 0);
199
+
200
+ if (key('Space') || btn('A')) {
201
+ state = 'explore';
202
+ sfx('confirm');
203
+ hasEvidence = false;
204
+ gameFinished = false;
205
+ playerPos = { x: 0, z: 6 };
206
+ sceneTime = 0;
207
+ // Reset suspect pose
208
+ setRotation(suspect.body, 0, 0, 0);
209
+ setRotation(suspect.head, 0, 0, 0);
210
+ setCameraPosition(playerPos.x, 9, playerPos.z + 6);
211
+ setCameraTarget(playerPos.x, 0, playerPos.z);
212
+ }
213
+ } else if (state === 'explore') {
214
+ // Top-down camera follows player
215
+ setCameraPosition(playerPos.x, 9, playerPos.z + 6);
216
+ setCameraTarget(playerPos.x, 0, playerPos.z);
217
+
218
+ if (key('KeyW') || key('ArrowUp')) playerPos.z -= SPEED * dt;
219
+ if (key('KeyS') || key('ArrowDown')) playerPos.z += SPEED * dt;
220
+ if (key('KeyA') || key('ArrowLeft')) playerPos.x -= SPEED * dt;
221
+ if (key('KeyD') || key('ArrowRight')) playerPos.x += SPEED * dt;
222
+
223
+ setDetectivePos(playerPos.x, playerPos.z);
224
+
225
+ const distToDesk = Math.hypot(playerPos.x, playerPos.z);
226
+ const distToSuspect = Math.hypot(playerPos.x - 3, playerPos.z + 2);
227
+
228
+ if (sceneTime > 0.25 && (key('Space') || btn('A'))) {
229
+ if (distToDesk < 2.5 && !hasEvidence) {
230
+ hasEvidence = true;
231
+ state = 'dialogue';
232
+ currentScript = SCRIPT_EVIDENCE;
233
+ dialogStage = 0;
234
+ processDialog();
235
+ sceneTime = 0;
236
+ sfx('coin');
237
+ } else if (distToSuspect < 2.5) {
238
+ state = 'dialogue';
239
+ currentScript = hasEvidence ? SCRIPT_CONFRONT : SCRIPT_SUSPECT;
240
+ gameFinished = hasEvidence;
241
+ dialogStage = 0;
242
+ processDialog();
243
+ sceneTime = 0;
244
+ sfx('select');
245
+ }
246
+ }
247
+ } else if (state === 'dialogue') {
248
+ textScroll += 30 * dt; // ~30 chars/sec typewriter
249
+
250
+ // Typewriter tick sound
251
+ const charCount = Math.floor(textScroll);
252
+ if (charCount > lastCharCount && charCount <= currentText.length) {
253
+ tickTimer -= dt;
254
+ if (tickTimer <= 0) {
255
+ sfx('blip');
256
+ tickTimer = 0.06;
257
+ }
258
+ }
259
+ lastCharCount = charCount;
260
+
261
+ if (sceneTime > 0.25 && (key('Space') || btn('A'))) {
262
+ if (textScroll < currentText.length) {
263
+ textScroll = currentText.length; // skip to end
264
+ sceneTime = 0;
265
+ } else {
266
+ dialogStage++;
267
+ processDialog();
268
+ sceneTime = 0;
269
+ sfx('select');
270
+ }
271
+ }
272
+ }
273
+ }
274
+
275
+ // ── Draw helpers ──────────────────────────────────────────────────────────────
276
+ function drawComicPanel(x, y, w, h) {
277
+ rectfill(x, y, w, h, rgba8(10, 10, 15, 255));
278
+ // Double border (comic book style)
279
+ rect(x, y, w, h, rgba8(255, 255, 255, 255));
280
+ rect(x + 2, y + 2, w - 4, h - 4, rgba8(200, 200, 200, 255));
281
+ }
282
+
283
+ // ── Draw ──────────────────────────────────────────────────────────────────────
284
+ export function draw() {
285
+ if (state === 'title') {
286
+ // Title card
287
+ drawComicPanel(20, 18, 280, 88);
288
+ print('THE VERDICT', 90, 38, rgba8(255, 255, 255, 255));
289
+ print('A 3D Noir Comic Adventure', 46, 58, rgba8(255, 255, 255, 255));
290
+ print('Uncover the truth...', 90, 76, rgba8(255, 255, 255, 255));
291
+
292
+ // Pulsing prompt — with dark bg for visibility
293
+ rectfill(40, 205, 240, 18, rgba8(0, 0, 0, 220));
294
+ const pulse = Math.floor((Math.sin(sceneTime * 5) * 0.5 + 0.5) * 200 + 55);
295
+ print('SPACE to begin investigation', 50, 210, rgba8(255, 255, 255, pulse));
296
+ } else if (state === 'explore') {
297
+ const distToDesk = Math.hypot(playerPos.x, playerPos.z);
298
+ const distToSuspect = Math.hypot(playerPos.x - 3, playerPos.z + 2);
299
+
300
+ // Context prompt
301
+ if (distToDesk < 2.5 && !hasEvidence) {
302
+ drawComicPanel(93, 198, 134, 22);
303
+ print('[SPACE] Inspect Desk', 98, 204, rgba8(255, 255, 255, 255));
304
+ } else if (distToSuspect < 2.5) {
305
+ drawComicPanel(85, 198, 150, 22);
306
+ print('[SPACE] Talk to Suspect', 90, 204, rgba8(255, 255, 255, 255));
307
+ }
308
+
309
+ // Status
310
+ rectfill(2, 2, 100, 14, rgba8(0, 0, 0, 220));
311
+ print('WASD \x97 Move', 6, 6, rgba8(255, 255, 255, 255));
312
+ if (hasEvidence) {
313
+ rectfill(2, 16, 150, 14, rgba8(0, 0, 0, 220));
314
+ print('EVIDENCE COLLECTED', 6, 20, rgba8(255, 255, 255, 255));
315
+ }
316
+ } else if (state === 'dialogue') {
317
+ // Cinematic letterbox bars
318
+ rectfill(0, 0, W, 28, 0x000000);
319
+ rectfill(0, H - 68, W, 68, 0x000000);
320
+
321
+ // Speaker badge
322
+ const speakerColor =
323
+ speaker === 'Detective'
324
+ ? rgba8(68, 102, 221, 255)
325
+ : speaker === 'Suspect'
326
+ ? rgba8(221, 51, 51, 255)
327
+ : rgba8(51, 170, 85, 255);
328
+ const badgeX = speaker === 'Suspect' ? W - 92 : 8;
329
+ rectfill(badgeX, H - 82, 84, 18, speakerColor);
330
+ print(speaker.toUpperCase(), badgeX + 4, H - 77, rgba8(255, 255, 255, 255));
331
+
332
+ // Dialogue text
333
+ const display = currentText.substring(0, Math.floor(textScroll));
334
+ print(display, 14, H - 56, rgba8(255, 255, 255, 255));
335
+
336
+ // Advance prompt
337
+ if (textScroll >= currentText.length) {
338
+ const blink = Math.floor(sceneTime * 4) % 2 === 0;
339
+ if (blink) print('\u25BC SPACE', W - 50, H - 18, rgba8(255, 255, 255, 255));
340
+ }
341
+ }
342
+ }
@@ -0,0 +1,150 @@
1
+ // examples/audio-lab/code.js
2
+ // Interactive spatial 3D audio playground.
3
+ // Spawn floating sound emitters and walk around them to hear positional audio.
4
+ // Press keys 1-5 to trigger different sound effects. Press B to spawn an emitter.
5
+
6
+ const PRESETS = [
7
+ { key: 'Digit1', label: '1: Jump', opts: { wave: 'sine', freq: 440, dur: 0.15, sweep: 200 } },
8
+ { key: 'Digit2', label: '2: Coin', opts: { wave: 'square', freq: 880, dur: 0.1, sweep: -200 } },
9
+ {
10
+ key: 'Digit3',
11
+ label: '3: Laser',
12
+ opts: { wave: 'sawtooth', freq: 660, dur: 0.2, sweep: -400 },
13
+ },
14
+ { key: 'Digit4', label: '4: Explosion', opts: { wave: 'noise', freq: 80, dur: 0.4, vol: 0.6 } },
15
+ {
16
+ key: 'Digit5',
17
+ label: '5: Power Up',
18
+ opts: { wave: 'triangle', freq: 220, dur: 0.3, sweep: 660 },
19
+ },
20
+ ];
21
+
22
+ const EMITTER_COLORS = [0xff4400, 0x44ff00, 0x0088ff, 0xff00ff, 0xffaa00];
23
+
24
+ let player = { x: 0, y: 1, z: 0 };
25
+ let playerMesh;
26
+ let ground;
27
+ let emitters = []; // { mesh, x, z, color, pulse }
28
+ let sfxCDs; // cooldown set for sound triggers
29
+ let volume = 0.5;
30
+ let spawnCD;
31
+
32
+ export function init() {
33
+ setCameraPosition(0, 6, 10);
34
+ setCameraTarget(0, 0, 0);
35
+ setAmbientLight(0x334466, 1.2);
36
+ setFog(0x050510, 15, 40);
37
+
38
+ ground = createPlane(40, 40, 0x111133, [0, 0, 0]);
39
+ rotateMesh(ground, -Math.PI / 2, 0, 0);
40
+
41
+ playerMesh = createCube(0.6, 0xffffff, [0, 1, 0], { material: 'emissive', emissive: 0xffffff });
42
+ if (typeof setVolume === 'function') setVolume(volume);
43
+
44
+ // Initialize cooldowns for sound triggers
45
+ const cdDefs = {};
46
+ PRESETS.forEach(p => {
47
+ cdDefs[p.key] = 0.15;
48
+ });
49
+ sfxCDs = createCooldownSet(cdDefs);
50
+ spawnCD = createCooldown(0.5);
51
+ }
52
+
53
+ export function update(dt) {
54
+ const speed = 5;
55
+
56
+ // WASD movement
57
+ if (key('KeyW')) player.z -= speed * dt;
58
+ if (key('KeyS')) player.z += speed * dt;
59
+ if (key('KeyA')) player.x -= speed * dt;
60
+ if (key('KeyD')) player.x += speed * dt;
61
+
62
+ // Clamp to arena
63
+ player.x = Math.max(-18, Math.min(18, player.x));
64
+ player.z = Math.max(-18, Math.min(18, player.z));
65
+
66
+ setPosition(playerMesh, player.x, player.y, player.z);
67
+ setCameraPosition(player.x, player.y + 5, player.z + 8);
68
+ setCameraTarget(player.x, player.y, player.z);
69
+
70
+ // Volume: Q/E
71
+ if (keyp('KeyQ')) {
72
+ volume = Math.max(0, volume - 0.1);
73
+ if (typeof setVolume === 'function') setVolume(volume);
74
+ }
75
+ if (keyp('KeyE')) {
76
+ volume = Math.min(1, volume + 0.1);
77
+ if (typeof setVolume === 'function') setVolume(volume);
78
+ }
79
+
80
+ // Sound trigger keys 1-5
81
+ updateCooldowns(sfxCDs, dt);
82
+ PRESETS.forEach(({ key: k, opts }) => {
83
+ if (keyp(k) && useCooldown(sfxCDs[k])) {
84
+ if (typeof sfx === 'function') sfx(opts);
85
+ }
86
+ });
87
+
88
+ // B key: spawn emitter at player position
89
+ updateCooldown(spawnCD, dt);
90
+ if (keyp('KeyB') && emitters.length < 5 && useCooldown(spawnCD)) {
91
+ const color = EMITTER_COLORS[emitters.length % EMITTER_COLORS.length];
92
+ const mesh = createSphere(0.5, color, [player.x, 1.5, player.z], 12, {
93
+ material: 'holographic',
94
+ emissive: color,
95
+ emissiveIntensity: 0.6,
96
+ });
97
+ emitters.push({ mesh, x: player.x, z: player.z, color, pulse: Math.random() * Math.PI * 2 });
98
+ if (typeof sfx === 'function') sfx({ wave: 'sine', freq: 660, dur: 0.2, sweep: 220 });
99
+ }
100
+
101
+ // Animate emitter pulse and trigger proximity sfx
102
+ emitters.forEach(e => {
103
+ e.pulse += dt * 2;
104
+ const s = 1 + Math.sin(e.pulse) * 0.15;
105
+ setScale(e.mesh, s, s, s);
106
+
107
+ // Proximity sound: if player steps close, play a soft tone
108
+ const dist = Math.hypot(player.x - e.x, player.z - e.z);
109
+ if (dist < 1.5 && typeof sfx === 'function') {
110
+ sfx({ wave: 'sine', freq: 880, dur: 0.05 });
111
+ }
112
+ });
113
+ }
114
+
115
+ export function draw() {
116
+ // Header bar
117
+ rect(0, 0, 320, 18, rgba8(10, 10, 40, 255), true);
118
+ printCentered('AUDIO LAB', 4, 0xffffff);
119
+
120
+ // Preset strip
121
+ print('SFX:', 4, 22, 0xaaaaff);
122
+ PRESETS.forEach(({ label }, i) => {
123
+ print(label, 4 + i * 62, 30, 0x88aadd);
124
+ });
125
+
126
+ // Volume bar
127
+ print('VOL', 4, 42, 0xaaaaff);
128
+ const volW = Math.round(volume * 80);
129
+ rect(26, 42, 80, 7, rgba8(30, 30, 60, 200), true);
130
+ rect(26, 42, volW, 7, rgba8(80, 200, 100, 255), true);
131
+ rect(26, 42, 80, 7, rgba8(80, 100, 180, 180), false);
132
+ print('Q/E to adjust', 112, 43, 0x555577);
133
+
134
+ // Emitter count
135
+ print(`Emitters: ${emitters.length}/5 (B to spawn)`, 4, 54, 0xdddddd);
136
+
137
+ // Emitter list with color dots
138
+ emitters.forEach((e, i) => {
139
+ const dist = Math.round(Math.hypot(player.x - e.x, player.z - e.z) * 10) / 10;
140
+ const r = (e.color >> 16) & 0xff;
141
+ const g = (e.color >> 8) & 0xff;
142
+ const b = e.color & 0xff;
143
+ rect(4, 64 + i * 10, 6, 6, rgba8(r, g, b, 255), true);
144
+ print(`Emitter ${i + 1} dist: ${dist}m`, 14, 65 + i * 10, 0xaaaacc);
145
+ });
146
+
147
+ // Controls footer
148
+ rect(0, 170, 320, 10, rgba8(10, 10, 40, 255), true);
149
+ print('WASD: move 1-5: SFX B: spawn emitter Q/E: volume', 2, 172, 0x444466);
150
+ }