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,1081 @@
1
+ // 🎬 NOVA64 DEMOSCENE - TRON ODYSSEY
2
+ // An epic visual journey through a neon digital realm
3
+ // Showcasing: Bloom, Post-Processing, Particles, Shaders, and Dynamic Effects
4
+
5
+ /* eslint-disable no-undef */
6
+ // Nova64 runtime provides these globals: enableBloom, enableFXAA, setBloomStrength, etc.
7
+
8
+ let gameTime = 0;
9
+ let sceneTime = 0;
10
+ let currentScene = 0;
11
+ let transitioning = false;
12
+ let transitionProgress = 0;
13
+
14
+ // Scene objects
15
+ let gridFloor = null;
16
+ let tunnelSegments = [];
17
+ let dataStreams = [];
18
+ let pulseRings = [];
19
+ let lightCycles = [];
20
+ let digitalTowers = [];
21
+ let particleSystems = [];
22
+ let energyFields = [];
23
+
24
+ // Camera control
25
+ let camera = {
26
+ x: 0,
27
+ y: 15,
28
+ z: 35,
29
+ targetX: 0,
30
+ targetY: 0,
31
+ targetZ: 0,
32
+ fov: 75,
33
+ roll: 0,
34
+ };
35
+
36
+ // State management
37
+ let gameState = 'start';
38
+ let startScreenTime = 0;
39
+
40
+ // Scene definitions
41
+ const SCENES = [
42
+ { name: 'GRID AWAKENING', duration: 8, color: 0x00ffff },
43
+ { name: 'DATA TUNNEL', duration: 10, color: 0xff00ff },
44
+ { name: 'DIGITAL CITY', duration: 12, color: 0xffff00 },
45
+ { name: 'ENERGY CORE', duration: 10, color: 0xff0099 },
46
+ { name: 'THE VOID', duration: 8, color: 0x0099ff },
47
+ ];
48
+
49
+ // Color palettes for each scene
50
+ const COLORS = {
51
+ neonCyan: 0x00ffff,
52
+ neonMagenta: 0xff00ff,
53
+ neonYellow: 0xffff00,
54
+ neonPink: 0xff0099,
55
+ neonGreen: 0x00ff99,
56
+ neonOrange: 0xff6600,
57
+ electric: 0x0099ff,
58
+ pulse: 0xffffff,
59
+ };
60
+
61
+ export async function init() {
62
+ console.log('========================================');
63
+ console.log('🎬 NOVA64 DEMOSCENE - TRON ODYSSEY INIT');
64
+ console.log('========================================');
65
+ console.log('Initial gameState:', gameState);
66
+
67
+ // Initial camera setup
68
+ setCameraPosition(camera.x, camera.y, camera.z);
69
+ setCameraTarget(0, 0, 0);
70
+ setCameraFOV(camera.fov);
71
+
72
+ // Enable post-processing effects with BALANCED settings
73
+ console.log('🎨 Enabling post-processing effects...');
74
+ const bloomEnabled = enableBloom(1.2, 0.6, 0.3); // Balanced bloom: visible glow without washing out
75
+ console.log('✨ Bloom enabled:', bloomEnabled);
76
+ enableFXAA(); // Smooth edges
77
+ console.log('✨ FXAA enabled');
78
+
79
+ // Verify effects are enabled
80
+ if (typeof isEffectsEnabled === 'function') {
81
+ console.log('✅ Effects system active:', isEffectsEnabled());
82
+ }
83
+
84
+ // Additional effects
85
+ enableChromaticAberration(0.003);
86
+ enableVignette(1.5, 0.85);
87
+
88
+ // Scene lighting - Balanced for visibility with neon contrast
89
+ setLightDirection(-0.5, -0.8, -0.3);
90
+ setAmbientLight(0x1a1a2a); // Darker but not pitch black - you can see objects
91
+
92
+ // Dark fog for TRON aesthetic
93
+ setFog(0x000020, 30, 150);
94
+
95
+ // Build initial scene
96
+ await buildStartScene();
97
+
98
+ // Initialize start screen
99
+ initStartScreen();
100
+
101
+ console.log('✨ Demoscene initialized - Ready to journey!');
102
+ }
103
+
104
+ function initStartScreen() {
105
+ // Clear any existing buttons first
106
+ clearButtons();
107
+
108
+ console.log('🎬 Initializing start screen buttons...');
109
+
110
+ // Main start button - extra large and flashy
111
+ const startBtn = createButton(
112
+ centerX(280),
113
+ 180,
114
+ 280,
115
+ 70,
116
+ '▶ BEGIN ODYSSEY ▶',
117
+ () => {
118
+ console.log('🚀🚀🚀 START BUTTON CLICKED! 🚀🚀🚀');
119
+ console.log('Setting gameState from', gameState, 'to playing');
120
+ gameState = 'playing';
121
+ currentScene = 0;
122
+ sceneTime = 0;
123
+ console.log('gameState is now:', gameState);
124
+ },
125
+ {
126
+ normalColor: rgba8(0, 255, 255, 255),
127
+ hoverColor: rgba8(100, 255, 255, 255),
128
+ pressedColor: rgba8(0, 200, 200, 255),
129
+ }
130
+ );
131
+
132
+ console.log('✅ Start button created:', startBtn);
133
+
134
+ // Info button
135
+ const infoBtn = createButton(
136
+ centerX(240),
137
+ 270,
138
+ 240,
139
+ 50,
140
+ '💡 ABOUT DEMO',
141
+ () => {
142
+ console.log('ℹ️ ABOUT BUTTON CLICKED!');
143
+ console.log('TRON ODYSSEY - A visual showcase of Nova64 capabilities');
144
+ console.log('Features: Bloom, Particles, Shaders, Dynamic Scenes');
145
+ },
146
+ {
147
+ normalColor: rgba8(255, 0, 255, 255),
148
+ hoverColor: rgba8(255, 100, 255, 255),
149
+ pressedColor: rgba8(200, 0, 200, 255),
150
+ }
151
+ );
152
+
153
+ console.log('✅ Info button created:', infoBtn);
154
+ console.log('🎬 Start screen initialization complete');
155
+ }
156
+
157
+ async function buildStartScene() {
158
+ // Create a VIBRANT neon intro scene
159
+ // Glowing grid floor - subtle base glow
160
+ const gridSize = 80;
161
+ gridFloor = createAdvancedCube(
162
+ gridSize,
163
+ {
164
+ color: COLORS.neonCyan,
165
+ emissive: COLORS.neonCyan,
166
+ emissiveIntensity: 0.3, // Reduced from 0.8 - subtle foundation
167
+ flatShading: true,
168
+ },
169
+ [0, -0.5, 0]
170
+ );
171
+ setScale(gridFloor, 1, 0.01, 1); // Make it flat
172
+
173
+ // Grid lines with moderate emissive glow
174
+ for (let i = -40; i <= 40; i += 5) {
175
+ // Horizontal - alternating cyan and magenta
176
+ const hColor = i % 10 === 0 ? COLORS.neonCyan : COLORS.electric;
177
+ const hLine = createAdvancedCube(
178
+ 1,
179
+ {
180
+ color: hColor,
181
+ emissive: hColor,
182
+ emissiveIntensity: 0.6, // Reduced from 1.2
183
+ flatShading: true,
184
+ },
185
+ [0, 0.1, i]
186
+ );
187
+ setScale(hLine, gridSize, 0.15, 0.3);
188
+ tunnelSegments.push(hLine);
189
+
190
+ // Vertical - alternating colors
191
+ const vColor = i % 10 === 0 ? COLORS.neonMagenta : COLORS.neonPink;
192
+ const vLine = createAdvancedCube(
193
+ 1,
194
+ {
195
+ color: vColor,
196
+ emissive: vColor,
197
+ emissiveIntensity: 0.6, // Reduced from 1.2
198
+ flatShading: true,
199
+ },
200
+ [i, 0.1, 0]
201
+ );
202
+ setScale(vLine, 0.3, 0.15, gridSize);
203
+ tunnelSegments.push(vLine);
204
+ }
205
+
206
+ // Floating GLOWING crystalline structures - much brighter
207
+ for (let i = 0; i < 8; i++) {
208
+ const angle = (i / 8) * Math.PI * 2;
209
+ const radius = 25;
210
+ const x = Math.cos(angle) * radius;
211
+ const z = Math.sin(angle) * radius;
212
+ const size = 2 + Math.random() * 3;
213
+
214
+ // Rainbow of colors for crystals
215
+ const crystalColors = [
216
+ COLORS.neonCyan,
217
+ COLORS.neonMagenta,
218
+ COLORS.neonYellow,
219
+ COLORS.neonPink,
220
+ COLORS.neonGreen,
221
+ ];
222
+ const crystalColor = crystalColors[i % crystalColors.length];
223
+
224
+ const crystal = createAdvancedCube(
225
+ size,
226
+ {
227
+ color: crystalColor,
228
+ emissive: crystalColor,
229
+ emissiveIntensity: 0.8, // Reduced from 1.5 - bright but not blinding
230
+ flatShading: true,
231
+ },
232
+ [x, size, z]
233
+ );
234
+ setRotation(crystal, Math.PI / 4, angle, Math.PI / 6);
235
+ digitalTowers.push({ mesh: crystal, x, z, angle, rotSpeed: 0.5 + Math.random() });
236
+ }
237
+
238
+ // Particle system
239
+ await createParticleField();
240
+ }
241
+
242
+ async function createParticleField() {
243
+ // Ambient floating particles - BRIGHT and GLOWING
244
+ for (let i = 0; i < 150; i++) {
245
+ // More particles!
246
+ const x = (Math.random() - 0.5) * 100;
247
+ const y = Math.random() * 30;
248
+ const z = (Math.random() - 0.5) * 100;
249
+
250
+ const colors = [
251
+ COLORS.neonCyan,
252
+ COLORS.neonMagenta,
253
+ COLORS.neonYellow,
254
+ COLORS.neonPink,
255
+ COLORS.neonGreen,
256
+ COLORS.neonOrange,
257
+ ];
258
+ const color = colors[Math.floor(Math.random() * colors.length)];
259
+
260
+ const particle = createSphere(0.3, color, [x, y, z], 6, {
261
+ emissive: color,
262
+ emissiveIntensity: 0.7, // Reduced from 2.0 - visible sparkle without blinding
263
+ });
264
+
265
+ particleSystems.push({
266
+ mesh: particle,
267
+ x,
268
+ y,
269
+ z,
270
+ vx: (Math.random() - 0.5) * 2,
271
+ vy: Math.random() * 0.5,
272
+ vz: (Math.random() - 0.5) * 2,
273
+ life: 100,
274
+ color,
275
+ });
276
+ }
277
+ }
278
+
279
+ export function update(dt) {
280
+ gameTime += dt;
281
+
282
+ // Start screen state
283
+ if (gameState === 'start') {
284
+ startScreenTime += dt;
285
+
286
+ // Update buttons - this handles mouse clicks
287
+ const clicked = updateAllButtons();
288
+ if (clicked) {
289
+ console.log('🖱️ A button was clicked!');
290
+ // Extra safety: force state change if button was clicked but callback didn't fire
291
+ if (gameState === 'start') {
292
+ console.log('💡 Button clicked but state not changed, forcing...');
293
+ gameState = 'playing';
294
+ currentScene = 0;
295
+ sceneTime = 0;
296
+ }
297
+ }
298
+
299
+ // KEYBOARD SUPPORT: Press SPACE or ENTER to start (use isKeyDown for continuous detection)
300
+ if (isKeyDown('Space') || isKeyDown('Enter')) {
301
+ console.log('⌨️ Keyboard pressed! Starting demoscene journey...');
302
+ gameState = 'playing';
303
+ currentScene = 0;
304
+ sceneTime = 0;
305
+ clearButtons(); // Clear buttons when starting
306
+ }
307
+
308
+ // Animated camera orbit on start screen
309
+ const radius = 40;
310
+ camera.x = Math.cos(startScreenTime * 0.3) * radius;
311
+ camera.z = Math.sin(startScreenTime * 0.3) * radius;
312
+ camera.y = 20 + Math.sin(startScreenTime * 0.5) * 5;
313
+
314
+ setCameraPosition(camera.x, camera.y, camera.z);
315
+ setCameraTarget(0, 5, 0);
316
+
317
+ // Animate start scene objects
318
+ updateStartSceneAnimation(dt);
319
+ updateParticles(dt);
320
+
321
+ return;
322
+ }
323
+
324
+ // Main demo progression
325
+ sceneTime += dt;
326
+
327
+ // Check for scene transition
328
+ if (sceneTime >= SCENES[currentScene].duration && !transitioning) {
329
+ if (currentScene < SCENES.length - 1) {
330
+ startSceneTransition();
331
+ } else {
332
+ // End of demo - loop back
333
+ currentScene = 0;
334
+ sceneTime = 0;
335
+ transitionToNextScene();
336
+ }
337
+ }
338
+
339
+ // Handle transitions
340
+ if (transitioning) {
341
+ transitionProgress += dt * 0.5; // 2 second transitions
342
+ if (transitionProgress >= 1.0) {
343
+ transitioning = false;
344
+ transitionProgress = 0;
345
+ transitionToNextScene();
346
+ }
347
+ }
348
+
349
+ // Update current scene
350
+ updateCurrentScene(dt);
351
+ updateCamera(dt);
352
+ updateParticles(dt);
353
+ }
354
+
355
+ function updateStartSceneAnimation(dt) {
356
+ // Rotate crystals
357
+ digitalTowers.forEach(tower => {
358
+ tower.angle += tower.rotSpeed * dt;
359
+ setRotation(tower.mesh, Math.PI / 4 + Math.sin(gameTime * 2) * 0.2, tower.angle, Math.PI / 6);
360
+ });
361
+ }
362
+
363
+ function updateCurrentScene(dt) {
364
+ const progress = sceneTime / SCENES[currentScene].duration;
365
+
366
+ switch (currentScene) {
367
+ case 0: // GRID AWAKENING
368
+ updateGridAwakening(dt, progress);
369
+ break;
370
+ case 1: // DATA TUNNEL
371
+ updateDataTunnel(dt, progress);
372
+ break;
373
+ case 2: // DIGITAL CITY
374
+ updateDigitalCity(dt, progress);
375
+ break;
376
+ case 3: // ENERGY CORE
377
+ updateEnergyCore(dt, progress);
378
+ break;
379
+ case 4: // THE VOID
380
+ updateTheVoid(dt, progress);
381
+ break;
382
+ }
383
+ }
384
+
385
+ // Scene 0: GRID AWAKENING
386
+ function updateGridAwakening(dt, progress) {
387
+ // Rotate floating crystals
388
+ digitalTowers.forEach((tower, i) => {
389
+ const heightOffset = Math.sin(gameTime * 2 + i) * 3;
390
+ const rotSpeed = 1 + Math.sin(gameTime + i) * 0.5;
391
+ tower.angle += rotSpeed * dt;
392
+
393
+ setPosition(tower.mesh, tower.x, 4 + heightOffset, tower.z);
394
+ setRotation(tower.mesh, gameTime * 0.5, tower.angle, gameTime * 0.3);
395
+ });
396
+
397
+ // Spawn pulse rings periodically
398
+ if (Math.floor(gameTime * 2) !== Math.floor((gameTime - dt) * 2)) {
399
+ createPulseRing();
400
+ }
401
+
402
+ // Update pulse rings
403
+ updatePulseRings(dt);
404
+
405
+ // Camera movement - rising up
406
+ camera.y = 15 + progress * 10;
407
+ camera.z = 35 - progress * 15;
408
+ }
409
+
410
+ // Scene 1: DATA TUNNEL
411
+ function updateDataTunnel(dt, progress) {
412
+ // Create tunnel segments on the fly
413
+ if (tunnelSegments.length < 50 && Math.random() < 0.3) {
414
+ createTunnelSegment();
415
+ }
416
+
417
+ // Move tunnel segments
418
+ for (let i = tunnelSegments.length - 1; i >= 0; i--) {
419
+ const seg = tunnelSegments[i];
420
+ if (seg.z) {
421
+ seg.z += 20 * dt;
422
+ setPosition(seg.mesh, seg.x || 0, seg.y || 0, seg.z);
423
+
424
+ // Remove if behind camera
425
+ if (seg.z > 50) {
426
+ destroyMesh(seg.mesh);
427
+ tunnelSegments.splice(i, 1);
428
+ }
429
+ }
430
+ }
431
+
432
+ // Create data streams
433
+ if (dataStreams.length < 30 && Math.random() < 0.2) {
434
+ createDataStream();
435
+ }
436
+
437
+ // Update data streams
438
+ updateDataStreams(dt);
439
+
440
+ // Camera - flying forward through tunnel
441
+ camera.z = 35 - progress * 50;
442
+ camera.y = 10 + Math.sin(progress * Math.PI * 4) * 3;
443
+ camera.roll = Math.sin(progress * Math.PI * 2) * 0.2;
444
+ }
445
+
446
+ // Scene 2: DIGITAL CITY
447
+ function updateDigitalCity(dt, progress) {
448
+ // Build city towers as we go
449
+ if (digitalTowers.length < 40 && Math.random() < 0.1) {
450
+ createDigitalTower();
451
+ }
452
+
453
+ // Animate towers with pulsing effect
454
+ digitalTowers.forEach(tower => {
455
+ if (tower.pulsePhase !== undefined) {
456
+ tower.pulsePhase += dt * 3;
457
+ const scale = 1 + Math.sin(tower.pulsePhase) * 0.15;
458
+ const width = tower.width || 3;
459
+ const height = tower.height || 15;
460
+ setScale(tower.mesh, width * scale, height * scale, width * scale);
461
+ }
462
+ });
463
+
464
+ // Spawn light cycles
465
+ if (lightCycles.length < 6 && Math.random() < 0.1) {
466
+ createLightCycle();
467
+ }
468
+
469
+ // Update light cycles
470
+ updateLightCycles(dt);
471
+
472
+ // Camera - orbiting the city
473
+ const orbitRadius = 40 + Math.sin(progress * Math.PI) * 15;
474
+ const orbitAngle = progress * Math.PI * 2;
475
+ camera.x = Math.cos(orbitAngle) * orbitRadius;
476
+ camera.z = Math.sin(orbitAngle) * orbitRadius;
477
+ camera.y = 20 + Math.sin(progress * Math.PI * 4) * 5;
478
+ }
479
+
480
+ // Scene 3: ENERGY CORE
481
+ function updateEnergyCore(dt, progress) {
482
+ // Create energy fields
483
+ if (energyFields.length < 20 && Math.random() < 0.15) {
484
+ createEnergyField();
485
+ }
486
+
487
+ // Rotate and pulse energy fields
488
+ energyFields.forEach(field => {
489
+ field.rotation += field.rotSpeed * dt;
490
+ field.pulsePhase += dt * 4;
491
+
492
+ const scale = 1 + Math.sin(field.pulsePhase) * 0.3;
493
+ setScale(field.mesh, scale, scale, scale);
494
+ setRotation(field.mesh, field.rotation, field.rotation * 1.5, field.rotation * 0.5);
495
+ });
496
+
497
+ // Camera - spiraling into the core
498
+ const spiralRadius = 30 - progress * 20;
499
+ const spiralHeight = 20 - progress * 15;
500
+ const spiralAngle = progress * Math.PI * 6;
501
+
502
+ camera.x = Math.cos(spiralAngle) * spiralRadius;
503
+ camera.z = Math.sin(spiralAngle) * spiralRadius;
504
+ camera.y = spiralHeight;
505
+
506
+ // Increase bloom intensity for energy core climax
507
+ setBloomStrength(1.2 + progress * 1.0); // Goes from 1.2 to 2.2 - dramatic but visible
508
+ }
509
+
510
+ // Scene 4: THE VOID
511
+ function updateTheVoid(dt, progress) {
512
+ // Fade to darkness
513
+ const fogFar = 120 - progress * 100;
514
+ setFog(0x000000, 10, Math.max(20, fogFar));
515
+
516
+ // Create final particle explosion
517
+ if (progress > 0.5 && Math.random() < 0.5) {
518
+ createExplosionParticle();
519
+ }
520
+
521
+ // Camera - pulling back dramatically
522
+ camera.z = -20 + progress * 60;
523
+ camera.y = 5 + progress * 30;
524
+
525
+ // Gradually reduce bloom as we fade to void
526
+ setBloomStrength(1.2 - progress * 1.0); // Fades from 1.2 to 0.2
527
+ }
528
+
529
+ // Helper functions for creating scene elements
530
+ function createPulseRing() {
531
+ const ringColors = [COLORS.neonCyan, COLORS.neonMagenta, COLORS.neonYellow];
532
+ const color = ringColors[Math.floor(Math.random() * ringColors.length)];
533
+
534
+ const ring = createSphere(1, color, [0, 0.2, 0], 8, {
535
+ emissive: color,
536
+ emissiveIntensity: 1.0, // Reduced from 2.5 - noticeable pulse without washing out
537
+ });
538
+ pulseRings.push({
539
+ mesh: ring,
540
+ scale: 1,
541
+ life: 2,
542
+ maxLife: 2,
543
+ color,
544
+ });
545
+ }
546
+
547
+ function updatePulseRings(dt) {
548
+ for (let i = pulseRings.length - 1; i >= 0; i--) {
549
+ const ring = pulseRings[i];
550
+ ring.life -= dt;
551
+ ring.scale += dt * 15;
552
+
553
+ setScale(ring.mesh, ring.scale, 0.1, ring.scale);
554
+
555
+ if (ring.life <= 0) {
556
+ destroyMesh(ring.mesh);
557
+ pulseRings.splice(i, 1);
558
+ }
559
+ }
560
+ }
561
+
562
+ function createTunnelSegment() {
563
+ const z = -50 - Math.random() * 20;
564
+ const segments = 8;
565
+ const tunnelColors = [COLORS.neonCyan, COLORS.neonMagenta, COLORS.neonYellow, COLORS.neonPink];
566
+
567
+ for (let i = 0; i < segments; i++) {
568
+ const angle = (i / segments) * Math.PI * 2;
569
+ const radius = 15;
570
+ const x = Math.cos(angle) * radius;
571
+ const y = Math.sin(angle) * radius;
572
+
573
+ const color = tunnelColors[i % tunnelColors.length];
574
+ const seg = createAdvancedCube(
575
+ 1,
576
+ {
577
+ color: color,
578
+ emissive: color,
579
+ emissiveIntensity: 0.8, // Reduced from 1.5
580
+ flatShading: true,
581
+ },
582
+ [x, y, z]
583
+ );
584
+ setScale(seg, 1, 1, 2);
585
+
586
+ tunnelSegments.push({
587
+ mesh: seg,
588
+ x,
589
+ y,
590
+ z,
591
+ });
592
+ }
593
+ }
594
+
595
+ function createDataStream() {
596
+ const angle = Math.random() * Math.PI * 2;
597
+ const radius = 10 + Math.random() * 5;
598
+ const x = Math.cos(angle) * radius;
599
+ const y = Math.sin(angle) * radius;
600
+
601
+ const colors = [
602
+ COLORS.neonCyan,
603
+ COLORS.neonMagenta,
604
+ COLORS.neonYellow,
605
+ COLORS.neonGreen,
606
+ COLORS.neonOrange,
607
+ ];
608
+ const color = colors[Math.floor(Math.random() * colors.length)];
609
+
610
+ const stream = createAdvancedCube(
611
+ 1,
612
+ {
613
+ color: color,
614
+ emissive: color,
615
+ emissiveIntensity: 0.9, // Reduced from 1.8
616
+ flatShading: true,
617
+ },
618
+ [x, y, -60]
619
+ );
620
+ setScale(stream, 0.4, 0.4, 4);
621
+
622
+ dataStreams.push({
623
+ mesh: stream,
624
+ x,
625
+ y,
626
+ z: -60,
627
+ speed: 30 + Math.random() * 20,
628
+ color,
629
+ });
630
+ }
631
+
632
+ function updateDataStreams(dt) {
633
+ for (let i = dataStreams.length - 1; i >= 0; i--) {
634
+ const stream = dataStreams[i];
635
+ stream.z += stream.speed * dt;
636
+ setPosition(stream.mesh, stream.x, stream.y, stream.z);
637
+
638
+ if (stream.z > 50) {
639
+ destroyMesh(stream.mesh);
640
+ dataStreams.splice(i, 1);
641
+ }
642
+ }
643
+ }
644
+
645
+ function createDigitalTower() {
646
+ const x = (Math.random() - 0.5) * 80;
647
+ const z = (Math.random() - 0.5) * 80;
648
+
649
+ // Avoid center
650
+ if (Math.abs(x) < 15 && Math.abs(z) < 15) return;
651
+
652
+ const width = 2 + Math.random() * 3;
653
+ const height = 10 + Math.random() * 20;
654
+
655
+ const colors = [
656
+ COLORS.neonCyan,
657
+ COLORS.neonMagenta,
658
+ COLORS.neonYellow,
659
+ COLORS.neonPink,
660
+ COLORS.neonGreen,
661
+ COLORS.neonOrange,
662
+ ];
663
+ const color = colors[Math.floor(Math.random() * colors.length)];
664
+
665
+ const tower = createAdvancedCube(
666
+ 1,
667
+ {
668
+ color: color,
669
+ emissive: color,
670
+ emissiveIntensity: 0.7, // Reduced from 1.3
671
+ flatShading: true,
672
+ },
673
+ [x, height / 2, z]
674
+ );
675
+ setScale(tower, width, height, width);
676
+
677
+ digitalTowers.push({
678
+ mesh: tower,
679
+ x,
680
+ z,
681
+ height,
682
+ width,
683
+ baseScale: 1,
684
+ pulsePhase: Math.random() * Math.PI * 2,
685
+ color,
686
+ });
687
+ }
688
+
689
+ function createLightCycle() {
690
+ const angle = Math.random() * Math.PI * 2;
691
+ const radius = 30;
692
+ const x = Math.cos(angle) * radius;
693
+ const z = Math.sin(angle) * radius;
694
+
695
+ const cycleColors = [COLORS.neonCyan, COLORS.neonMagenta, COLORS.neonYellow, COLORS.neonOrange];
696
+ const bodyColor = cycleColors[Math.floor(Math.random() * cycleColors.length)];
697
+ const trailColor = bodyColor; // Matching trail
698
+
699
+ const body = createAdvancedCube(
700
+ 1,
701
+ {
702
+ color: bodyColor,
703
+ emissive: bodyColor,
704
+ emissiveIntensity: 0.8, // Reduced from 1.5
705
+ flatShading: true,
706
+ },
707
+ [x, 1, z]
708
+ );
709
+ setScale(body, 2, 0.5, 1);
710
+
711
+ const trail = createAdvancedCube(
712
+ 1,
713
+ {
714
+ color: trailColor,
715
+ emissive: trailColor,
716
+ emissiveIntensity: 0.6, // Reduced from 1.2
717
+ flatShading: true,
718
+ },
719
+ [x, 1, z]
720
+ );
721
+ setScale(trail, 0.5, 0.5, 8);
722
+
723
+ lightCycles.push({
724
+ body,
725
+ trail,
726
+ x,
727
+ z,
728
+ angle,
729
+ speed: 2 + Math.random(),
730
+ color: bodyColor,
731
+ });
732
+ }
733
+
734
+ function updateLightCycles(dt) {
735
+ lightCycles.forEach(cycle => {
736
+ cycle.angle += cycle.speed * dt;
737
+
738
+ const radius = 30;
739
+ cycle.x = Math.cos(cycle.angle) * radius;
740
+ cycle.z = Math.sin(cycle.angle) * radius;
741
+
742
+ setPosition(cycle.body, cycle.x, 1, cycle.z);
743
+ setRotation(cycle.body, 0, cycle.angle + Math.PI / 2, 0);
744
+
745
+ const trailX = cycle.x - Math.cos(cycle.angle + Math.PI / 2) * 4;
746
+ const trailZ = cycle.z - Math.sin(cycle.angle + Math.PI / 2) * 4;
747
+ setPosition(cycle.trail, trailX, 1, trailZ);
748
+ setRotation(cycle.trail, 0, cycle.angle + Math.PI / 2, 0);
749
+ });
750
+ }
751
+
752
+ function createEnergyField() {
753
+ const x = (Math.random() - 0.5) * 20;
754
+ const y = (Math.random() - 0.5) * 20;
755
+ const z = (Math.random() - 0.5) * 20;
756
+
757
+ const size = 1 + Math.random() * 2;
758
+ const colors = [
759
+ COLORS.neonMagenta,
760
+ COLORS.neonYellow,
761
+ COLORS.neonPink,
762
+ COLORS.neonCyan,
763
+ COLORS.neonGreen,
764
+ ];
765
+ const color = colors[Math.floor(Math.random() * colors.length)];
766
+
767
+ const field = createSphere(size, color, [x, y, z], 10, {
768
+ emissive: color,
769
+ emissiveIntensity: 1.0, // Reduced from 2.0 - bright but not blinding
770
+ });
771
+
772
+ energyFields.push({
773
+ mesh: field,
774
+ rotation: 0,
775
+ rotSpeed: 0.5 + Math.random(),
776
+ pulsePhase: Math.random() * Math.PI * 2,
777
+ color,
778
+ });
779
+ }
780
+
781
+ function createExplosionParticle() {
782
+ const x = (Math.random() - 0.5) * 40;
783
+ const y = (Math.random() - 0.5) * 40;
784
+ const z = (Math.random() - 0.5) * 40;
785
+
786
+ const colors = Object.values(COLORS);
787
+ const color = colors[Math.floor(Math.random() * colors.length)];
788
+
789
+ const particle = createSphere(0.5, color, [x, y, z], 6, {
790
+ emissive: color,
791
+ emissiveIntensity: 1.2, // Reduced from 2.5 - bright explosion without washing out
792
+ });
793
+
794
+ particleSystems.push({
795
+ mesh: particle,
796
+ x,
797
+ y,
798
+ z,
799
+ vx: (Math.random() - 0.5) * 20,
800
+ vy: (Math.random() - 0.5) * 20,
801
+ vz: (Math.random() - 0.5) * 20,
802
+ life: 3,
803
+ color,
804
+ });
805
+ }
806
+
807
+ function updateParticles(dt) {
808
+ for (let i = particleSystems.length - 1; i >= 0; i--) {
809
+ const particle = particleSystems[i];
810
+
811
+ particle.x += particle.vx * dt;
812
+ particle.y += particle.vy * dt;
813
+ particle.z += particle.vz * dt;
814
+
815
+ // Slight gravity
816
+ particle.vy -= dt * 2;
817
+
818
+ particle.life -= dt;
819
+
820
+ setPosition(particle.mesh, particle.x, particle.y, particle.z);
821
+
822
+ // Fade out
823
+ const scale = Math.max(0, particle.life / 3);
824
+ setScale(particle.mesh, scale, scale, scale);
825
+
826
+ if (particle.life <= 0 || scale <= 0) {
827
+ destroyMesh(particle.mesh);
828
+ particleSystems.splice(i, 1);
829
+ }
830
+ }
831
+ }
832
+
833
+ function updateCamera(_dt) {
834
+ setCameraPosition(camera.x, camera.y, camera.z);
835
+ setCameraTarget(camera.targetX, camera.targetY, camera.targetZ);
836
+
837
+ // Apply camera roll if needed
838
+ if (Math.abs(camera.roll) > 0.01) {
839
+ // Roll effect would be applied here if supported
840
+ }
841
+ }
842
+
843
+ function startSceneTransition() {
844
+ transitioning = true;
845
+ transitionProgress = 0;
846
+ console.log(`🎬 Transitioning to: ${SCENES[currentScene + 1].name}`);
847
+ }
848
+
849
+ function transitionToNextScene() {
850
+ // Clean up previous scene objects
851
+ cleanupScene();
852
+
853
+ // Move to next scene
854
+ currentScene++;
855
+ if (currentScene >= SCENES.length) {
856
+ currentScene = 0;
857
+ }
858
+
859
+ sceneTime = 0;
860
+
861
+ // Setup new scene
862
+ setupScene(currentScene);
863
+
864
+ console.log(`✨ Now showing: ${SCENES[currentScene].name}`);
865
+ }
866
+
867
+ function cleanupScene() {
868
+ // Remove all dynamic objects (keep start scene for now)
869
+ dataStreams.forEach(s => destroyMesh(s.mesh));
870
+ dataStreams = [];
871
+
872
+ pulseRings.forEach(r => destroyMesh(r.mesh));
873
+ pulseRings = [];
874
+
875
+ lightCycles.forEach(c => {
876
+ destroyMesh(c.body);
877
+ destroyMesh(c.trail);
878
+ });
879
+ lightCycles = [];
880
+
881
+ energyFields.forEach(f => destroyMesh(f.mesh));
882
+ energyFields = [];
883
+ }
884
+
885
+ function setupScene(_sceneIndex) {
886
+ // Set theme lighting based on scene - balanced darkness
887
+ setFog(0x000020, 30, 150);
888
+ setBloomStrength(1.2); // Balanced bloom setting
889
+
890
+ // Reset camera for new scene
891
+ camera.roll = 0;
892
+ camera.fov = 75;
893
+ setCameraFOV(camera.fov);
894
+ }
895
+
896
+ let drawCallCount = 0;
897
+
898
+ export function draw() {
899
+ // Log first few draw calls to verify it's working
900
+ if (drawCallCount < 3) {
901
+ console.log(`✏️ draw() called, gameState: ${gameState}, drawCallCount: ${drawCallCount}`);
902
+ drawCallCount++;
903
+ }
904
+
905
+ // Start screen
906
+ if (gameState === 'start') {
907
+ drawStartScreen();
908
+ return;
909
+ }
910
+
911
+ // Demo HUD
912
+ drawDemoHUD();
913
+ }
914
+
915
+ function drawStartScreen() {
916
+ // Dark gradient overlay
917
+ drawGradientRect(0, 0, 640, 360, rgba8(0, 10, 20, 220), rgba8(20, 0, 40, 240), true);
918
+
919
+ // Animated title
920
+ const pulse = Math.sin(startScreenTime * 3) * 0.3 + 0.7;
921
+ const bounce = Math.sin(startScreenTime * 2) * 8;
922
+
923
+ setFont('huge');
924
+ setTextAlign('center');
925
+
926
+ // Title with color shift
927
+ const r = Math.floor(128 + Math.sin(startScreenTime * 2) * 127);
928
+ const g = Math.floor(128 + Math.sin(startScreenTime * 2 + 2) * 127);
929
+ const b = Math.floor(128 + Math.sin(startScreenTime * 2 + 4) * 127);
930
+
931
+ drawTextShadow('NOVA64', 320, 50 + bounce, rgba8(r, g, b, 255), rgba8(0, 0, 0, 255), 6, 1);
932
+
933
+ setFont('large');
934
+ const cyan = rgba8(0, 255, 255, Math.floor(pulse * 255));
935
+ drawTextOutline('DEMOSCENE', 320, 110, cyan, rgba8(0, 0, 0, 255), 2);
936
+
937
+ // Subtitle
938
+ setFont('normal');
939
+ const magenta = rgba8(255, 0, 255, 255);
940
+ drawText('▶ TRON ODYSSEY ◀', 320, 145, magenta, 1);
941
+
942
+ // Info panel
943
+ const panel = createPanel(centerX(500), 210, 500, 200, {
944
+ bgColor: rgba8(10, 0, 20, 200),
945
+ borderColor: rgba8(0, 255, 255, 255),
946
+ borderWidth: 3,
947
+ shadow: true,
948
+ gradient: true,
949
+ gradientColor: rgba8(20, 0, 40, 200),
950
+ });
951
+ drawPanel(panel);
952
+
953
+ setFont('small');
954
+ setTextAlign('center');
955
+ drawText('A VISUAL SHOWCASE OF NOVA64 CAPABILITIES', 320, 225, uiColors.warning, 1);
956
+ drawText('', 320, 240, uiColors.light, 1);
957
+ drawText('✨ BLOOM & POST-PROCESSING EFFECTS', 320, 255, uiColors.light, 1);
958
+ drawText('🎨 DYNAMIC SHADER MATERIALS', 320, 270, uiColors.light, 1);
959
+ drawText('💫 GPU-ACCELERATED PARTICLES', 320, 285, uiColors.light, 1);
960
+ drawText('🎬 CINEMATIC CAMERA CHOREOGRAPHY', 320, 300, uiColors.light, 1);
961
+ drawText('🌈 PROCEDURAL NEON GEOMETRY', 320, 315, uiColors.light, 1);
962
+
963
+ setFont('tiny');
964
+ drawText(
965
+ 'Journey through 5 unique scenes showcasing the engine',
966
+ 320,
967
+ 335,
968
+ uiColors.secondary,
969
+ 1
970
+ );
971
+
972
+ // Draw buttons
973
+ drawAllButtons();
974
+
975
+ // Pulsing prompt
976
+ const alpha = Math.floor((Math.sin(startScreenTime * 5) * 0.5 + 0.5) * 255);
977
+ setFont('normal');
978
+ drawText('▶ PRESS BEGIN OR SPACEBAR TO START ◀', 320, 375, rgba8(0, 255, 255, alpha), 1);
979
+
980
+ // Credits
981
+ setFont('tiny');
982
+ drawText('CONTROLS: CLICK BUTTON OR PRESS SPACE/ENTER', 320, 395, rgba8(150, 150, 200, 200), 1);
983
+ drawText('NOVA64 - THE ULTIMATE FANTASY CONSOLE', 320, 410, rgba8(100, 100, 150, 180), 1);
984
+ }
985
+
986
+ function drawDemoHUD() {
987
+ // Minimal HUD during demo
988
+ const scene = SCENES[currentScene];
989
+ const progress = (sceneTime / scene.duration) * 100;
990
+
991
+ // Scene info panel - top left
992
+ const panelWidth = 280;
993
+ rect(16, 16, panelWidth, 90, rgba8(0, 0, 20, 200), true);
994
+ rect(16, 16, panelWidth, 90, scene.color, false);
995
+
996
+ setFont('normal');
997
+ setTextAlign('left');
998
+ print('🎬 DEMOSCENE', 24, 24, rgba8(255, 255, 255, 255));
999
+
1000
+ setFont('small');
1001
+ print(
1002
+ `Scene ${currentScene + 1}/${SCENES.length}: ${scene.name}`,
1003
+ 24,
1004
+ 45,
1005
+ rgba8(200, 200, 255, 255)
1006
+ );
1007
+
1008
+ // Progress bar
1009
+ const barWidth = panelWidth - 16;
1010
+ const barFill = (progress / 100) * barWidth;
1011
+
1012
+ rect(24, 62, barWidth, 8, rgba8(40, 40, 60, 200), true);
1013
+ rect(24, 62, barFill, 8, scene.color, true);
1014
+ rect(24, 62, barWidth, 8, rgba8(255, 255, 255, 100), false);
1015
+
1016
+ setFont('tiny');
1017
+ print(`${progress.toFixed(1)}%`, 24, 78, rgba8(150, 150, 200, 255));
1018
+ print(`Time: ${sceneTime.toFixed(1)}s / ${scene.duration}s`, 24, 90, rgba8(150, 150, 200, 255));
1019
+
1020
+ // Effect status - top right
1021
+ const statsX = 640 - 200;
1022
+ rect(statsX - 16, 16, 200, 65, rgba8(0, 0, 20, 200), true);
1023
+ rect(statsX - 16, 16, 200, 65, rgba8(255, 0, 255, 255), false);
1024
+
1025
+ setFont('tiny');
1026
+ setTextAlign('left');
1027
+ print('EFFECTS ACTIVE:', statsX - 8, 24, rgba8(255, 255, 255, 255));
1028
+ print('✓ BLOOM', statsX - 8, 37, rgba8(0, 255, 0, 255));
1029
+ print('✓ FXAA', statsX - 8, 48, rgba8(0, 255, 0, 255));
1030
+ print('✓ PARTICLES', statsX - 8, 59, rgba8(0, 255, 0, 255));
1031
+ print('✓ FOG', statsX - 8, 70, rgba8(0, 255, 0, 255));
1032
+
1033
+ // Scene description - bottom
1034
+ rect(16, 360 - 45, 640 - 32, 30, rgba8(0, 0, 20, 220), true);
1035
+
1036
+ setFont('small');
1037
+ setTextAlign('center');
1038
+ const desc = getSceneDescription(currentScene);
1039
+ print(desc, 320, 360 - 35, rgba8(255, 255, 100, 255));
1040
+
1041
+ // Nova64 watermark
1042
+ setFont('tiny');
1043
+ print('NOVA64 - POWERED BY THREE.JS', 320, 360 - 20, rgba8(100, 100, 150, 200));
1044
+
1045
+ // Transition overlay
1046
+ if (transitioning) {
1047
+ const alpha = Math.floor(Math.sin(transitionProgress * Math.PI) * 200);
1048
+ rect(0, 0, 640, 360, rgba8(0, 0, 0, alpha), true);
1049
+
1050
+ if (transitionProgress > 0.4 && transitionProgress < 0.6) {
1051
+ setFont('large');
1052
+ setTextAlign('center');
1053
+ const nextScene = SCENES[currentScene + 1] || SCENES[0];
1054
+ drawTextShadow(
1055
+ nextScene.name,
1056
+ 320,
1057
+ 180,
1058
+ rgba8(255, 255, 255, 255),
1059
+ rgba8(0, 0, 0, 255),
1060
+ 4,
1061
+ 1
1062
+ );
1063
+ }
1064
+ }
1065
+ }
1066
+
1067
+ function getSceneDescription(sceneIndex) {
1068
+ const descriptions = [
1069
+ 'Grid awakening - The digital realm comes to life with pulsing energy',
1070
+ 'Data tunnel - Racing through streams of information at lightspeed',
1071
+ 'Digital city - Towering structures of pure light and geometry',
1072
+ 'Energy core - Spiraling into the heart of the system',
1073
+ "The void - Journey's end, returning to infinite darkness",
1074
+ ];
1075
+ return descriptions[sceneIndex] || '';
1076
+ }
1077
+
1078
+ // Utility functions
1079
+ function centerX(width) {
1080
+ return (640 - width) / 2;
1081
+ }