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.
- package/README.md +25 -8
- package/bin/nova64.js +165 -0
- package/dist/assets/console-CY_kygm3.js +14 -0
- package/dist/assets/console-CY_kygm3.js.map +1 -0
- package/dist/assets/main-l0sNRNKZ.js.map +1 -0
- package/dist/assets/sky/studio/nx.png +0 -0
- package/dist/assets/sky/studio/ny.png +0 -0
- package/dist/assets/sky/studio/nz.png +0 -0
- package/dist/assets/sky/studio/px.png +0 -0
- package/dist/assets/sky/studio/py.png +0 -0
- package/dist/assets/sky/studio/pz.png +0 -0
- package/dist/assets/vanilla-Dcuy32gi.js +2 -0
- package/dist/assets/vanilla-Dcuy32gi.js.map +1 -0
- package/dist/console.html +899 -0
- package/dist/docs/BENCHMARK.md +77 -0
- package/dist/docs/CHEATSHEET.md +255 -0
- package/dist/docs/EFFECTS_API_GUIDE.md +577 -0
- package/dist/docs/EFFECTS_QUICK_REFERENCE.md +331 -0
- package/dist/docs/FONT_CHARACTER_REFERENCE.md +219 -0
- package/dist/docs/FREE_GLB_ASSETS.md +330 -0
- package/dist/docs/FULLSCREEN_BUTTON_FEATURE.md +296 -0
- package/dist/docs/GAMEPAD_SUPPORT.md +348 -0
- package/dist/docs/GAME_IMPROVEMENTS.md +278 -0
- package/dist/docs/GAME_QUALITY_STATUS.md +300 -0
- package/dist/docs/MIGRATION_GUIDE.md +553 -0
- package/dist/docs/NOVA64_3D_API.md +356 -0
- package/dist/docs/NOVA64_API_REFERENCE.md +1406 -0
- package/dist/docs/NOVA64_UI_API.md +503 -0
- package/dist/docs/UI_SYSTEM_SUMMARY.md +445 -0
- package/dist/docs/VOXEL_ENGINE_GUIDE.md +662 -0
- package/dist/docs/VOXEL_QUICK_REFERENCE.md +386 -0
- package/dist/docs/api-3d.html +750 -0
- package/dist/docs/api-effects.html +385 -0
- package/dist/docs/api-improvements.md +121 -0
- package/dist/docs/api-skybox.html +407 -0
- package/dist/docs/api-sprites.html +321 -0
- package/dist/docs/api-voxel.html +337 -0
- package/dist/docs/api.html +543 -0
- package/dist/docs/assets.html +306 -0
- package/dist/docs/audio.html +340 -0
- package/dist/docs/blogs.html +286 -0
- package/dist/docs/collision.html +316 -0
- package/dist/docs/console.html +247 -0
- package/dist/docs/editor.html +297 -0
- package/dist/docs/font.html +247 -0
- package/dist/docs/framebuffer.html +247 -0
- package/dist/docs/fullscreen-button.html +297 -0
- package/dist/docs/gpu-systems.html +247 -0
- package/dist/docs/index.html +580 -0
- package/dist/docs/input.html +491 -0
- package/dist/docs/physics.html +311 -0
- package/dist/docs/screens.html +311 -0
- package/dist/docs/storage.html +311 -0
- package/dist/docs/textinput.html +332 -0
- package/dist/docs/ui.html +488 -0
- package/dist/examples/3d-advanced/code.js +695 -0
- package/dist/examples/adventure-comic-3d/code.js +342 -0
- package/dist/examples/audio-lab/code.js +150 -0
- package/dist/examples/boids-flocking/code.js +270 -0
- package/dist/examples/crystal-cathedral-3d/code.js +706 -0
- package/dist/examples/cyberpunk-city-3d/code.js +1383 -0
- package/dist/examples/demoscene/README.md +192 -0
- package/dist/examples/demoscene/code.js +1081 -0
- package/dist/examples/demoscene/meta.json +21 -0
- package/dist/examples/dungeon-crawler-3d/code.js +1117 -0
- package/dist/examples/f-zero-nova-3d/code.js +865 -0
- package/dist/examples/f-zero-nova-3d/code_old.js +1555 -0
- package/dist/examples/fps-demo-3d/code.js +744 -0
- package/dist/examples/game-of-life-3d/code.js +338 -0
- package/dist/examples/generative-art/code.js +632 -0
- package/dist/examples/hello-3d/code.js +325 -0
- package/dist/examples/hello-skybox/code.js +183 -0
- package/dist/examples/hello-world/code.js +19 -0
- package/dist/examples/input-showcase/code.js +109 -0
- package/dist/examples/instancing-demo/code.js +315 -0
- package/dist/examples/minecraft-demo/code.js +387 -0
- package/dist/examples/model-viewer-3d/code.js +114 -0
- package/dist/examples/mystical-realm-3d/code.js +1203 -0
- package/dist/examples/nature-explorer-3d/code.js +1318 -0
- package/dist/examples/particles-demo/code.js +522 -0
- package/dist/examples/pbr-showcase/code.js +140 -0
- package/dist/examples/physics-demo-3d/code.js +948 -0
- package/dist/examples/screen-demo/code.js +267 -0
- package/dist/examples/shooter-demo-3d/code.js +1286 -0
- package/dist/examples/space-combat-3d/IMPLEMENTATION_SUMMARY.md +109 -0
- package/dist/examples/space-combat-3d/README.md +135 -0
- package/dist/examples/space-combat-3d/code.js +1332 -0
- package/dist/examples/space-harrier-3d/code.js +923 -0
- package/dist/examples/star-fox-nova-3d/code.js +1116 -0
- package/dist/examples/star-fox-nova-3d/code_backup.js +410 -0
- package/dist/examples/star-fox-nova-3d/code_broken.js +1821 -0
- package/dist/examples/storage-quest/code.js +209 -0
- package/dist/examples/strider-demo-3d/IMPROVEMENT_OPTIONS.md +285 -0
- package/dist/examples/strider-demo-3d/cache-test.html +132 -0
- package/dist/examples/strider-demo-3d/code-fixed.js +582 -0
- package/dist/examples/strider-demo-3d/code-old.js +1537 -0
- package/dist/examples/strider-demo-3d/code.js +1462 -0
- package/dist/examples/strider-demo-3d/code.js.bak2 +1169 -0
- package/dist/examples/strider-demo-3d/fix-game.sh +53 -0
- package/dist/examples/super-plumber-64/README.md +128 -0
- package/dist/examples/super-plumber-64/code.js +1185 -0
- package/dist/examples/super-plumber-64/index.html +88 -0
- package/dist/examples/test-2d-overlay/code.js +32 -0
- package/dist/examples/test-font/code.js +51 -0
- package/dist/examples/test-minimal/code.js +21 -0
- package/dist/examples/ui-demo/code.js +306 -0
- package/dist/examples/wing-commander-space/README.md +180 -0
- package/dist/examples/wing-commander-space/code.js +1285 -0
- package/dist/examples/wizardry-3d/CHANGELOG.md +366 -0
- package/dist/examples/wizardry-3d/code.js +3928 -0
- package/dist/index.html +666 -0
- package/dist/os9-shell/assets/index-DIHfrTaW.css +1 -0
- package/dist/os9-shell/assets/index-KchE_ngx.js +483 -0
- package/dist/os9-shell/assets/index-KchE_ngx.js.map +1 -0
- package/dist/os9-shell/index.html +23 -0
- package/dist/os9-shell/nova-icon.svg +12 -0
- package/index.html +6 -1
- package/package.json +37 -32
- package/public/assets/sky/studio/nx.png +0 -0
- package/public/assets/sky/studio/ny.png +0 -0
- package/public/assets/sky/studio/nz.png +0 -0
- package/public/assets/sky/studio/px.png +0 -0
- package/public/assets/sky/studio/py.png +0 -0
- package/public/assets/sky/studio/pz.png +0 -0
- package/public/os9-shell/assets/index-KchE_ngx.js +483 -0
- package/public/os9-shell/assets/index-KchE_ngx.js.map +1 -0
- package/public/os9-shell/index.html +10 -1
- package/runtime/api-2d.js +301 -21
- package/runtime/api-3d/pbr.js +45 -1
- package/runtime/api-3d.js +1 -0
- package/runtime/api-effects.js +90 -3
- package/runtime/api-gameutils.js +476 -0
- package/runtime/api-generative.js +610 -0
- package/runtime/api-skybox.js +54 -0
- package/runtime/api-voxel.js +139 -28
- package/runtime/gpu-threejs.js +13 -9
- package/runtime/ui.js +2 -2
- package/src/main.js +24 -1
- package/public/os9-shell/assets/index-B1Uvacma.js +0 -32825
- 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
|
+
}
|