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,1203 @@
1
+ // MYSTICAL REALM 3D - Creative Nintendo 64/PlayStation style fantasy world
2
+ // Showcases advanced 3D features: dynamic lighting, particle systems, procedural generation
3
+
4
+ // Game state management
5
+ let gameState = 'start'; // 'start', 'playing', 'paused', 'gameover'
6
+ let startScreenTime = 0;
7
+ let uiButtons = [];
8
+ let score = 0;
9
+ let crystalsCollected = 0;
10
+ let creaturesKept = 0;
11
+ let playTime = 0;
12
+ let magicBolts = []; // { mesh, x, y, z, vx, vy, vz, life }
13
+
14
+ let world = {
15
+ terrain: [],
16
+ crystals: [],
17
+ particles: [],
18
+ creatures: [],
19
+ weather: { type: 'clear', intensity: 0 },
20
+ };
21
+
22
+ let player = {
23
+ x: 0,
24
+ y: 5,
25
+ z: 0,
26
+ rotation: 0,
27
+ speed: 8,
28
+ jumpVelocity: 0,
29
+ onGround: false,
30
+ health: 100,
31
+ maxHealth: 100,
32
+ magicCooldown: 0,
33
+ magicCharges: 3,
34
+ maxMagicCharges: 3,
35
+ magicRechargeTimer: 0,
36
+ };
37
+
38
+ let camera = {
39
+ offset: { x: 0, y: 12, z: 20 },
40
+ target: { x: 0, y: 0, z: 0 },
41
+ };
42
+
43
+ let shake;
44
+ let magicCD;
45
+
46
+ let time = 0;
47
+ let dayNightCycle = 0;
48
+ let lightningTimer = 0;
49
+
50
+ // GPU instancing for trees (new feature)
51
+ let treeTrunkInstanceId = null;
52
+ let treeCrownInstanceId = null;
53
+ let treeMeta = []; // [{x, z, swayPhase}]
54
+
55
+ export async function init() {
56
+ cls();
57
+
58
+ console.log('🏰 Initializing Mystical Realm 3D...');
59
+
60
+ shake = createShake({ decay: 5 });
61
+ magicCD = createCooldown(0.35);
62
+
63
+ // Setup camera
64
+ setCameraPosition(camera.offset.x, camera.offset.y, camera.offset.z);
65
+ setCameraTarget(0, 0, 0);
66
+ setCameraFOV(65);
67
+
68
+ // Enable all retro effects for maximum N64/PSX nostalgia
69
+ enablePixelation(1);
70
+ enableDithering(true);
71
+ enableBloom(0.7, 0.5, 0.5); // Soft magical glow
72
+ enableFXAA(); // Smooth edges
73
+ enableVignette(1.2, 0.9); // Cinematic border
74
+
75
+ // Generate the mystical world
76
+ await generateTerrain();
77
+ await spawnCrystals();
78
+ await createCreatures();
79
+
80
+ // Set initial lighting
81
+ updateLighting();
82
+
83
+ // Initialize start screen
84
+ initStartScreen();
85
+
86
+ console.log('✨ Mystical Realm 3D loaded! Explore the fantasy world!');
87
+ }
88
+
89
+ function initStartScreen() {
90
+ uiButtons = [];
91
+
92
+ // START button
93
+ uiButtons.push(
94
+ createButton(
95
+ centerX(220),
96
+ 150,
97
+ 220,
98
+ 55,
99
+ '▶ BEGIN QUEST',
100
+ () => {
101
+ console.log('🎯 BEGIN QUEST CLICKED! Changing gameState to playing...');
102
+ gameState = 'playing';
103
+ playTime = 0;
104
+ score = 0;
105
+ crystalsCollected = 0;
106
+ console.log('✅ gameState is now:', gameState);
107
+ console.log('🏰 Quest begun!');
108
+ },
109
+ {
110
+ normalColor: rgba8(100, 50, 200, 255),
111
+ hoverColor: rgba8(130, 70, 230, 255),
112
+ pressedColor: rgba8(70, 30, 160, 255),
113
+ }
114
+ )
115
+ );
116
+
117
+ // CONTROLS button
118
+ uiButtons.push(
119
+ createButton(
120
+ centerX(220),
121
+ 290,
122
+ 220,
123
+ 45,
124
+ '? CONTROLS',
125
+ () => {
126
+ console.log('🎮 WASD/Arrows = Move, SPACE = Jump, Collect crystals!');
127
+ },
128
+ {
129
+ normalColor: uiColors.primary,
130
+ hoverColor: rgba8(50, 150, 255, 255),
131
+ pressedColor: rgba8(20, 100, 200, 255),
132
+ }
133
+ )
134
+ );
135
+ }
136
+
137
+ async function generateTerrain() {
138
+ console.log('🌍 Generating mystical terrain...');
139
+
140
+ // Create main ground plane with texture-like pattern
141
+ const mainGround = createPlane(100, 100, 0x2a4a2a, [0, 0, 0]);
142
+ setRotation(mainGround, -Math.PI / 2, 0, 0);
143
+ world.terrain.push({ mesh: mainGround, type: 'ground' });
144
+
145
+ // Generate procedural hills and mountains
146
+ for (let i = 0; i < 20; i++) {
147
+ const x = (Math.random() - 0.5) * 80;
148
+ const z = (Math.random() - 0.5) * 80;
149
+ const height = 3 + Math.random() * 8;
150
+ const width = 4 + Math.random() * 6;
151
+
152
+ const hill = createCube(width, height, width, 0x3a5a3a, [x, height / 2, z]);
153
+ world.terrain.push({
154
+ mesh: hill,
155
+ type: 'hill',
156
+ originalY: height / 2,
157
+ bobPhase: Math.random() * Math.PI * 2,
158
+ });
159
+ }
160
+
161
+ // Create mystical stone circles
162
+ for (let circle = 0; circle < 3; circle++) {
163
+ const centerX = (Math.random() - 0.5) * 60;
164
+ const centerZ = (Math.random() - 0.5) * 60;
165
+ const radius = 8 + Math.random() * 4;
166
+
167
+ for (let i = 0; i < 8; i++) {
168
+ const angle = (i / 8) * Math.PI * 2;
169
+ const x = centerX + Math.cos(angle) * radius;
170
+ const z = centerZ + Math.sin(angle) * radius;
171
+ const height = 6 + Math.random() * 4;
172
+
173
+ const stone = createAdvancedCube(
174
+ 1.5,
175
+ {
176
+ color: 0x555555,
177
+ emissive: 0x111144,
178
+ emissiveIntensity: 0.4,
179
+ metallic: true,
180
+ animated: true,
181
+ },
182
+ [x, height / 2, z]
183
+ );
184
+
185
+ world.terrain.push({
186
+ mesh: stone,
187
+ type: 'monolith',
188
+ originalY: height / 2,
189
+ glowPhase: Math.random() * Math.PI * 2,
190
+ });
191
+ }
192
+ }
193
+
194
+ // Ancient trees — GPU instanced (30 draw calls → 2)
195
+ treeTrunkInstanceId = createInstancedMesh('cube', 15, 0x4a3a2a, { size: 1 });
196
+ treeCrownInstanceId = createInstancedMesh('sphere', 15, 0x2a5a2a, { size: 4, segments: 8 });
197
+ treeMeta = [];
198
+ for (let i = 0; i < 15; i++) {
199
+ const x = (Math.random() - 0.5) * 90;
200
+ const z = (Math.random() - 0.5) * 90;
201
+ treeMeta.push({ x, z, swayPhase: Math.random() * Math.PI * 2 });
202
+ setInstanceTransform(treeTrunkInstanceId, i, x, 4, z, 0, 0, 0, 1, 8, 1);
203
+ setInstanceTransform(treeCrownInstanceId, i, x, 10, z);
204
+ }
205
+ finalizeInstances(treeTrunkInstanceId);
206
+ finalizeInstances(treeCrownInstanceId);
207
+ }
208
+
209
+ async function spawnCrystals() {
210
+ console.log('💎 Spawning mystical crystals...');
211
+
212
+ for (let i = 0; i < 25; i++) {
213
+ const x = (Math.random() - 0.5) * 70;
214
+ const z = (Math.random() - 0.5) * 70;
215
+ const y = 2;
216
+
217
+ // Create stunning holographic crystals
218
+ const colors = [0xff4444, 0x44ff44, 0x4444ff, 0xffff44, 0xff44ff, 0x44ffff];
219
+ const emissiveColors = [0x331111, 0x113311, 0x111133, 0x333311, 0x331133, 0x113333];
220
+ const colorIndex = Math.floor(Math.random() * colors.length);
221
+
222
+ const crystal = createAdvancedSphere(
223
+ 0.8,
224
+ {
225
+ color: colors[colorIndex],
226
+ emissive: emissiveColors[colorIndex],
227
+ emissiveIntensity: 0.7,
228
+ holographic: true,
229
+ animated: true,
230
+ metallic: Math.random() > 0.5,
231
+ transparent: true,
232
+ opacity: 0.9,
233
+ },
234
+ [x, y, z],
235
+ 12
236
+ );
237
+
238
+ world.crystals.push({
239
+ mesh: crystal,
240
+ x,
241
+ y,
242
+ z,
243
+ color: colors[colorIndex],
244
+ rotationSpeed: 1 + Math.random() * 2,
245
+ bobPhase: Math.random() * Math.PI * 2,
246
+ glowIntensity: Math.random(),
247
+ collected: false,
248
+ });
249
+ }
250
+ }
251
+
252
+ async function createCreatures() {
253
+ console.log('🦋 Creating mystical creatures...');
254
+
255
+ // Flying magical orbs — passive, high altitude
256
+ for (let i = 0; i < 8; i++) {
257
+ const orb = createAdvancedSphere(
258
+ 0.5,
259
+ {
260
+ color: 0xffff88,
261
+ emissive: 0x888844,
262
+ emissiveIntensity: 1.0,
263
+ holographic: true,
264
+ animated: true,
265
+ transparent: true,
266
+ opacity: 0.8,
267
+ },
268
+ [0, 15, 0],
269
+ 16
270
+ );
271
+
272
+ world.creatures.push({
273
+ mesh: orb,
274
+ type: 'orb',
275
+ x: (Math.random() - 0.5) * 60,
276
+ y: 15 + Math.random() * 10,
277
+ z: (Math.random() - 0.5) * 60,
278
+ vx: (Math.random() - 0.5) * 2,
279
+ vy: (Math.random() - 0.5) * 1,
280
+ vz: (Math.random() - 0.5) * 2,
281
+ glowPhase: Math.random() * Math.PI * 2,
282
+ trail: [],
283
+ stunTimer: 0,
284
+ caught: false,
285
+ aggressive: false,
286
+ atk: 0,
287
+ });
288
+ }
289
+
290
+ // Shadow wisps — ground-level, aggressive, chase the player
291
+ for (let i = 0; i < 6; i++) {
292
+ const wisp = createAdvancedSphere(
293
+ 0.6,
294
+ {
295
+ color: 0x9933cc,
296
+ emissive: 0x440066,
297
+ emissiveIntensity: 0.9,
298
+ holographic: true,
299
+ animated: true,
300
+ transparent: true,
301
+ opacity: 0.85,
302
+ },
303
+ [0, 3, 0],
304
+ 12
305
+ );
306
+
307
+ world.creatures.push({
308
+ mesh: wisp,
309
+ type: 'wisp',
310
+ x: (Math.random() - 0.5) * 70,
311
+ y: 3,
312
+ z: (Math.random() - 0.5) * 70,
313
+ vx: 0,
314
+ vy: 0,
315
+ vz: 0,
316
+ glowPhase: Math.random() * Math.PI * 2,
317
+ trail: [],
318
+ stunTimer: 0,
319
+ caught: false,
320
+ aggressive: true,
321
+ atk: 8,
322
+ aggroRange: 18,
323
+ speed: 5 + Math.random() * 3,
324
+ attackCD: 0,
325
+ });
326
+ }
327
+
328
+ // Fire sprites — fast, aggressive, appear near monoliths
329
+ for (let i = 0; i < 4; i++) {
330
+ const sprite = createAdvancedSphere(
331
+ 0.4,
332
+ {
333
+ color: 0xff4400,
334
+ emissive: 0x882200,
335
+ emissiveIntensity: 1.0,
336
+ holographic: true,
337
+ animated: true,
338
+ transparent: true,
339
+ opacity: 0.9,
340
+ },
341
+ [0, 4, 0],
342
+ 10
343
+ );
344
+
345
+ world.creatures.push({
346
+ mesh: sprite,
347
+ type: 'fire',
348
+ x: (Math.random() - 0.5) * 50,
349
+ y: 4,
350
+ z: (Math.random() - 0.5) * 50,
351
+ vx: 0,
352
+ vy: 0,
353
+ vz: 0,
354
+ glowPhase: Math.random() * Math.PI * 2,
355
+ trail: [],
356
+ stunTimer: 0,
357
+ caught: false,
358
+ aggressive: true,
359
+ atk: 12,
360
+ aggroRange: 14,
361
+ speed: 8 + Math.random() * 3,
362
+ attackCD: 0,
363
+ });
364
+ }
365
+ }
366
+
367
+ export function update(dt) {
368
+ // Handle start screen
369
+ if (gameState === 'start') {
370
+ startScreenTime += dt;
371
+ updateAllButtons();
372
+ // Still update world animations in background
373
+ time += dt;
374
+ dayNightCycle += dt * 0.1;
375
+ updateTerrain(dt);
376
+ updateCrystals(dt);
377
+ updateLighting();
378
+ return;
379
+ }
380
+
381
+ // Handle game over
382
+ if (gameState === 'gameover') {
383
+ updateAllButtons();
384
+ return;
385
+ }
386
+
387
+ // Handle win
388
+ if (gameState === 'win') {
389
+ time += dt;
390
+ updateAllButtons();
391
+ return;
392
+ }
393
+
394
+ // Playing state
395
+ time += dt;
396
+ dayNightCycle += dt * 0.1;
397
+ playTime += dt;
398
+
399
+ // Update player movement
400
+ updatePlayer(dt);
401
+
402
+ // Update camera
403
+ updateCamera(dt);
404
+
405
+ // Update world elements
406
+ updateTerrain(dt);
407
+ updateCrystals(dt);
408
+ updateCreatures(dt);
409
+ updateWeather(dt);
410
+
411
+ // Update lighting based on time of day
412
+ updateLighting();
413
+
414
+ // Particle system
415
+ updateParticles(dt);
416
+
417
+ // Check for crystal collection
418
+ checkCrystalCollection();
419
+
420
+ // Magic bolts and creature catching
421
+ updateMagicBolts(dt);
422
+ checkCreatureCollection();
423
+ checkWinCondition();
424
+
425
+ // Check game over
426
+ if (player.health <= 0 && gameState === 'playing') {
427
+ gameState = 'gameover';
428
+ initGameOverScreen();
429
+ }
430
+ }
431
+
432
+ function updatePlayer(dt) {
433
+ // Simple physics
434
+ player.jumpVelocity -= 25 * dt; // gravity
435
+ player.y += player.jumpVelocity * dt;
436
+
437
+ // Ground collision
438
+ if (player.y <= 2) {
439
+ player.y = 2;
440
+ player.jumpVelocity = 0;
441
+ player.onGround = true;
442
+ } else {
443
+ player.onGround = false;
444
+ }
445
+
446
+ // Input handling
447
+ let inputX = 0,
448
+ inputZ = 0;
449
+
450
+ if (key('KeyW') || key('ArrowUp')) inputZ = -1;
451
+ if (key('KeyS') || key('ArrowDown')) inputZ = 1;
452
+ if (key('KeyA') || key('ArrowLeft')) inputX = -1;
453
+ if (key('KeyD') || key('ArrowRight')) inputX = 1;
454
+
455
+ if (key('Space') && player.onGround) {
456
+ player.jumpVelocity = 12;
457
+ player.onGround = false;
458
+ }
459
+
460
+ // Movement
461
+ if (inputX !== 0 || inputZ !== 0) {
462
+ const moveSpeed = player.speed * dt;
463
+ player.x += inputX * moveSpeed;
464
+ player.z += inputZ * moveSpeed;
465
+
466
+ // Rotation based on movement
467
+ if (inputX !== 0 || inputZ !== 0) {
468
+ player.rotation = Math.atan2(inputX, inputZ);
469
+ }
470
+ }
471
+
472
+ // Magic recharge
473
+ updateCooldown(magicCD, dt);
474
+ player.magicRechargeTimer -= dt;
475
+ if (player.magicRechargeTimer <= 0 && player.magicCharges < player.maxMagicCharges) {
476
+ player.magicCharges++;
477
+ player.magicRechargeTimer = 3;
478
+ }
479
+
480
+ // Cast magic bolt
481
+ if (keyp('KeyE') && player.magicCharges > 0 && useCooldown(magicCD)) {
482
+ fireMagicBolt();
483
+ sfx('jump');
484
+ }
485
+
486
+ // Keep player in bounds
487
+ player.x = Math.max(-45, Math.min(45, player.x));
488
+ player.z = Math.max(-45, Math.min(45, player.z));
489
+ }
490
+
491
+ function fireMagicBolt() {
492
+ const dx = -Math.sin(player.rotation);
493
+ const dz = -Math.cos(player.rotation);
494
+ const speed = 28;
495
+ magicBolts.push({
496
+ mesh: createSphere(0.35, 0xff44ff, [player.x + dx, player.y + 1.5, player.z + dz]),
497
+ x: player.x + dx,
498
+ y: player.y + 1.5,
499
+ z: player.z + dz,
500
+ vx: dx * speed,
501
+ vy: 1.5,
502
+ vz: dz * speed,
503
+ life: 2.5,
504
+ });
505
+ player.magicCharges--;
506
+ triggerShake(shake, 0.8);
507
+ }
508
+
509
+ function updateMagicBolts(dt) {
510
+ for (let i = magicBolts.length - 1; i >= 0; i--) {
511
+ const bolt = magicBolts[i];
512
+ bolt.life -= dt;
513
+ if (bolt.life <= 0) {
514
+ removeMesh(bolt.mesh);
515
+ magicBolts.splice(i, 1);
516
+ continue;
517
+ }
518
+ bolt.x += bolt.vx * dt;
519
+ bolt.y += bolt.vy * dt;
520
+ bolt.z += bolt.vz * dt;
521
+ bolt.vy -= 4 * dt; // gentle arc
522
+ setPosition(bolt.mesh, bolt.x, bolt.y, bolt.z);
523
+
524
+ // Check hit on creatures
525
+ world.creatures.forEach(creature => {
526
+ if (creature.caught || creature.stunTimer > 0) return;
527
+ const dx = bolt.x - creature.x;
528
+ const dy = bolt.y - creature.y;
529
+ const dz = bolt.z - creature.z;
530
+ if (Math.sqrt(dx * dx + dy * dy + dz * dz) < 2.5) {
531
+ creature.stunTimer = 5;
532
+ creature.vx = 0;
533
+ creature.vy = 0;
534
+ creature.vz = 0;
535
+ removeMesh(bolt.mesh);
536
+ magicBolts.splice(i, 1);
537
+ score += 10;
538
+ triggerShake(shake, 1.5);
539
+ sfx('explosion');
540
+ }
541
+ });
542
+ }
543
+ }
544
+
545
+ function checkCreatureCollection() {
546
+ world.creatures.forEach(creature => {
547
+ if (creature.caught || creature.stunTimer <= 0) return;
548
+ const dx = player.x - creature.x;
549
+ const dy = player.y - creature.y;
550
+ const dz = player.z - creature.z;
551
+ if (Math.sqrt(dx * dx + dy * dy + dz * dz) < 3) {
552
+ creature.caught = true;
553
+ setMeshVisible(creature.mesh, false);
554
+ creaturesKept++;
555
+ score += 50;
556
+ triggerShake(shake, 2);
557
+ sfx('coin');
558
+ // Particle burst
559
+ for (let i = 0; i < 8; i++) spawnParticle(creature.x, creature.y, creature.z, 0xff88ff);
560
+ }
561
+ });
562
+ }
563
+
564
+ function checkWinCondition() {
565
+ if (gameState !== 'playing') return;
566
+ const allCrystals = world.crystals.every(c => c.collected);
567
+ const allCreatures = world.creatures.every(c => c.caught);
568
+ if (allCrystals && allCreatures) {
569
+ gameState = 'win';
570
+ initWinScreen();
571
+ }
572
+ }
573
+
574
+ function initWinScreen() {
575
+ uiButtons = [];
576
+ uiButtons.push(
577
+ createButton(
578
+ centerX(200),
579
+ 340,
580
+ 200,
581
+ 50,
582
+ '↻ PLAY AGAIN',
583
+ () => {
584
+ resetGame();
585
+ gameState = 'playing';
586
+ },
587
+ {
588
+ normalColor: uiColors.success,
589
+ hoverColor: rgba8(60, 220, 120, 255),
590
+ pressedColor: rgba8(30, 160, 80, 255),
591
+ }
592
+ )
593
+ );
594
+ uiButtons.push(
595
+ createButton(
596
+ centerX(200),
597
+ 405,
598
+ 200,
599
+ 45,
600
+ '← MAIN MENU',
601
+ () => {
602
+ resetGame();
603
+ gameState = 'start';
604
+ initStartScreen();
605
+ },
606
+ {
607
+ normalColor: uiColors.primary,
608
+ hoverColor: rgba8(50, 150, 255, 255),
609
+ pressedColor: rgba8(20, 100, 200, 255),
610
+ }
611
+ )
612
+ );
613
+ }
614
+
615
+ function drawWinScreen() {
616
+ drawGradientRect(0, 0, 640, 480, rgba8(10, 30, 10, 220), rgba8(20, 80, 20, 240), true);
617
+ const glow = (Math.sin(time * 3) + 1) * 0.5;
618
+ setFont('huge');
619
+ setTextAlign('center');
620
+ drawTextShadow('QUEST COMPLETE!', 320, 60, rgba8(255, 215, 0, 255), rgba8(0, 0, 0, 255), 5, 1);
621
+ setFont('large');
622
+ drawText('The realm is saved!', 320, 130, rgba8(Math.floor(100 + 155 * glow), 255, 100, 255), 1);
623
+ const panel = createPanel(centerX(420), 170, 420, 140, {
624
+ bgColor: rgba8(20, 40, 20, 220),
625
+ borderColor: rgba8(100, 200, 100, 255),
626
+ borderWidth: 3,
627
+ shadow: true,
628
+ title: 'FINAL SCORE',
629
+ titleBgColor: rgba8(60, 160, 60, 255),
630
+ });
631
+ drawPanel(panel);
632
+ setFont('normal');
633
+ setTextAlign('center');
634
+ const minutes = Math.floor(playTime / 60);
635
+ const seconds = Math.floor(playTime % 60);
636
+ drawText(`Score: ${score}`, 320, 215, rgba8(255, 215, 0, 255), 1);
637
+ drawText(
638
+ `Crystals: ${crystalsCollected} / ${world.crystals.length}`,
639
+ 320,
640
+ 240,
641
+ uiColors.light,
642
+ 1
643
+ );
644
+ drawText(
645
+ `Creatures caught: ${creaturesKept} / ${world.creatures.length}`,
646
+ 320,
647
+ 265,
648
+ uiColors.light,
649
+ 1
650
+ );
651
+ drawText(`Time: ${minutes}m ${seconds}s`, 320, 290, uiColors.secondary, 1);
652
+ drawAllButtons();
653
+ }
654
+
655
+ function updateCamera(dt) {
656
+ // Smooth camera follow
657
+ const targetX = player.x + camera.offset.x;
658
+ const targetY = player.y + camera.offset.y;
659
+ const targetZ = player.z + camera.offset.z;
660
+
661
+ // Add camera shake for dramatic effect
662
+ updateShake(shake, dt);
663
+ const [shakeX, shakeY] = getShakeOffset(shake);
664
+
665
+ setCameraPosition(targetX + shakeX, targetY + shakeY, targetZ);
666
+ setCameraTarget(player.x, player.y + 2, player.z);
667
+ }
668
+
669
+ function updateTerrain(dt) {
670
+ world.terrain.forEach(element => {
671
+ if (element.type === 'hill') {
672
+ // Gentle bobbing motion
673
+ element.bobPhase += dt;
674
+ const bobY = Math.sin(element.bobPhase * 0.5) * 0.2;
675
+ setPosition(
676
+ element.mesh,
677
+ getPosition(element.mesh)[0],
678
+ element.originalY + bobY,
679
+ getPosition(element.mesh)[2]
680
+ );
681
+ } else if (element.type === 'monolith') {
682
+ // Mysterious glowing
683
+ element.glowPhase += dt * 3;
684
+ const glow = (Math.sin(element.glowPhase) + 1) * 0.5;
685
+ // Color intensity varies
686
+ }
687
+ });
688
+
689
+ // Animate instanced tree crowns (sway in wind)
690
+ if (treeCrownInstanceId !== null && treeMeta.length > 0) {
691
+ for (let i = 0; i < treeMeta.length; i++) {
692
+ const t = treeMeta[i];
693
+ t.swayPhase += dt * 2;
694
+ const swayX = Math.sin(t.swayPhase) * 0.3;
695
+ setInstanceTransform(treeCrownInstanceId, i, t.x + swayX, 10, t.z);
696
+ }
697
+ finalizeInstances(treeCrownInstanceId);
698
+ }
699
+ }
700
+
701
+ function updateCrystals(dt) {
702
+ world.crystals.forEach(crystal => {
703
+ if (crystal.collected) return;
704
+
705
+ // Rotation
706
+ rotateMesh(crystal.mesh, 0, dt * crystal.rotationSpeed, 0);
707
+
708
+ // Bobbing motion
709
+ crystal.bobPhase += dt * 2;
710
+ const bobY = Math.sin(crystal.bobPhase) * 0.5;
711
+ setPosition(crystal.mesh, crystal.x, crystal.y + bobY, crystal.z);
712
+
713
+ // Glowing effect
714
+ crystal.glowIntensity += dt * 2;
715
+ });
716
+ }
717
+
718
+ function updateCreatures(dt) {
719
+ world.creatures.forEach(creature => {
720
+ if (creature.caught) return;
721
+
722
+ if (creature.stunTimer > 0) {
723
+ // Stunned — blink and drift toward ground
724
+ creature.stunTimer -= dt;
725
+ creature.glowPhase += dt * 20;
726
+ setMeshVisible(creature.mesh, Math.sin(creature.glowPhase * 5) > 0);
727
+ const groundY = creature.type === 'orb' ? 4 : 2;
728
+ creature.y = Math.max(groundY, creature.y - 4 * dt);
729
+ setPosition(creature.mesh, creature.x, creature.y, creature.z);
730
+ return;
731
+ }
732
+
733
+ // Normal visible state
734
+ setMeshVisible(creature.mesh, true);
735
+ creature.glowPhase += dt * 5;
736
+
737
+ // Aggressive creatures chase and attack player
738
+ if (creature.aggressive && creature.attackCD > 0) {
739
+ creature.attackCD -= dt;
740
+ }
741
+
742
+ if (creature.type === 'orb') {
743
+ // Flying movement — passive
744
+ creature.x += creature.vx * dt;
745
+ creature.y += creature.vy * dt;
746
+ creature.z += creature.vz * dt;
747
+
748
+ if (Math.abs(creature.x) > 40) creature.vx *= -0.8;
749
+ if (creature.y < 10 || creature.y > 25) creature.vy *= -0.8;
750
+ if (Math.abs(creature.z) > 40) creature.vz *= -0.8;
751
+
752
+ if (Math.random() < 0.01) {
753
+ creature.vx += (Math.random() - 0.5) * 2;
754
+ creature.vy += (Math.random() - 0.5) * 1;
755
+ creature.vz += (Math.random() - 0.5) * 2;
756
+ }
757
+ } else if (creature.type === 'wisp' || creature.type === 'fire') {
758
+ // Aggressive ground creatures — chase player when in range
759
+ const dx = player.x - creature.x;
760
+ const dz = player.z - creature.z;
761
+ const dist = Math.sqrt(dx * dx + dz * dz);
762
+
763
+ if (dist < creature.aggroRange && dist > 1.5) {
764
+ const spd = creature.speed * dt;
765
+ creature.x += (dx / dist) * spd;
766
+ creature.z += (dz / dist) * spd;
767
+ } else if (dist >= creature.aggroRange) {
768
+ // Idle wander
769
+ if (Math.random() < 0.02) {
770
+ creature.vx = (Math.random() - 0.5) * 3;
771
+ creature.vz = (Math.random() - 0.5) * 3;
772
+ }
773
+ creature.x += creature.vx * dt;
774
+ creature.z += creature.vz * dt;
775
+ creature.vx *= 0.98;
776
+ creature.vz *= 0.98;
777
+ }
778
+
779
+ // Hover bob
780
+ const baseY = creature.type === 'wisp' ? 3 : 4;
781
+ creature.y = baseY + Math.sin(creature.glowPhase) * 0.5;
782
+
783
+ // Deal damage on contact
784
+ if (dist < 2.5 && creature.attackCD <= 0) {
785
+ player.health -= creature.atk;
786
+ creature.attackCD = 1.2;
787
+ triggerShake(shake, 2);
788
+ sfx('explosion');
789
+ for (let i = 0; i < 4; i++) spawnParticle(player.x, player.y + 1, player.z, 0xff2222);
790
+ }
791
+
792
+ // Keep in bounds
793
+ creature.x = Math.max(-44, Math.min(44, creature.x));
794
+ creature.z = Math.max(-44, Math.min(44, creature.z));
795
+ }
796
+
797
+ setPosition(creature.mesh, creature.x, creature.y, creature.z);
798
+ });
799
+ }
800
+
801
+ function updateWeather(dt) {
802
+ // Weather system
803
+ if (Math.random() < 0.001) {
804
+ // Random weather change
805
+ const weathers = ['clear', 'rain', 'storm', 'mystical'];
806
+ world.weather.type = weathers[Math.floor(Math.random() * weathers.length)];
807
+ world.weather.intensity = Math.random();
808
+ }
809
+
810
+ // Lightning effects during storms
811
+ if (world.weather.type === 'storm') {
812
+ lightningTimer += dt;
813
+ if (lightningTimer > 2 + Math.random() * 3) {
814
+ // Lightning flash
815
+ setLightColor(0xffffff);
816
+ triggerShake(shake, 3);
817
+ lightningTimer = 0;
818
+ }
819
+ }
820
+ }
821
+
822
+ function updateLighting() {
823
+ // Day/night cycle
824
+ const dayPhase = (Math.sin(dayNightCycle) + 1) * 0.5;
825
+
826
+ // Sunrise/sunset colors
827
+ let lightColor = 0xffffff;
828
+ let ambientColor = 0x404040;
829
+ let fogColor = 0x202040;
830
+
831
+ if (dayPhase < 0.3) {
832
+ // Night
833
+ lightColor = 0x4444aa;
834
+ ambientColor = 0x202040;
835
+ fogColor = 0x101030;
836
+ } else if (dayPhase < 0.5) {
837
+ // Dawn
838
+ lightColor = 0xffaa44;
839
+ ambientColor = 0x404030;
840
+ fogColor = 0x403020;
841
+ } else if (dayPhase < 0.8) {
842
+ // Day
843
+ lightColor = 0xffffdd;
844
+ ambientColor = 0x606060;
845
+ fogColor = 0x808080;
846
+ } else {
847
+ // Dusk
848
+ lightColor = 0xff6644;
849
+ ambientColor = 0x404020;
850
+ fogColor = 0x402010;
851
+ }
852
+
853
+ // Apply lighting
854
+ setLightDirection(-0.5, -1, -0.3);
855
+ setLightColor(lightColor);
856
+ setAmbientLight(ambientColor);
857
+ setFog(fogColor, 30, 80);
858
+ }
859
+
860
+ function updateParticles(dt) {
861
+ // Simple particle system for mystical effects
862
+ if (Math.random() < 0.1) {
863
+ // Add sparkle particles around crystals
864
+ world.crystals.forEach(crystal => {
865
+ if (!crystal.collected && Math.random() < 0.05) {
866
+ spawnParticle(crystal.x, crystal.y + 2, crystal.z, crystal.color);
867
+ }
868
+ });
869
+ }
870
+ }
871
+
872
+ function spawnParticle(x, y, z, color) {
873
+ const particle = createSphere(0.1, color, [x, y, z]);
874
+ world.particles.push({
875
+ mesh: particle,
876
+ x,
877
+ y,
878
+ z,
879
+ vx: (Math.random() - 0.5) * 4,
880
+ vy: Math.random() * 3 + 2,
881
+ vz: (Math.random() - 0.5) * 4,
882
+ life: 2,
883
+ maxLife: 2,
884
+ });
885
+ }
886
+
887
+ function checkCrystalCollection() {
888
+ world.crystals.forEach(crystal => {
889
+ if (crystal.collected) return;
890
+
891
+ const dx = player.x - crystal.x;
892
+ const dy = player.y - crystal.y;
893
+ const dz = player.z - crystal.z;
894
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
895
+
896
+ if (distance < 3) {
897
+ crystal.collected = true;
898
+ crystalsCollected++;
899
+ score += 25;
900
+ setPosition(crystal.mesh, -1000, -1000, -1000);
901
+ sfx('coin');
902
+
903
+ // Particle burst effect
904
+ for (let i = 0; i < 10; i++) {
905
+ spawnParticle(
906
+ crystal.x + (Math.random() - 0.5) * 2,
907
+ crystal.y + Math.random() * 2,
908
+ crystal.z + (Math.random() - 0.5) * 2,
909
+ crystal.color
910
+ );
911
+ }
912
+
913
+ triggerShake(shake, 1);
914
+ }
915
+ });
916
+ }
917
+
918
+ export function draw() {
919
+ // Handle start screen
920
+ if (gameState === 'start') {
921
+ drawStartScreen();
922
+ return;
923
+ }
924
+
925
+ // Handle game over
926
+ if (gameState === 'gameover') {
927
+ drawGameOverScreen();
928
+ return;
929
+ }
930
+
931
+ // Handle win
932
+ if (gameState === 'win') {
933
+ drawWinScreen();
934
+ return;
935
+ }
936
+
937
+ // Playing state - Atmospheric UI
938
+ const dayPhase = (Math.sin(dayNightCycle) + 1) * 0.5;
939
+ let timeOfDay = 'Day';
940
+ if (dayPhase < 0.3) timeOfDay = 'Night';
941
+ else if (dayPhase < 0.5) timeOfDay = 'Dawn';
942
+ else if (dayPhase > 0.8) timeOfDay = 'Dusk';
943
+
944
+ // Title and info
945
+ print('🏰 MYSTICAL REALM 3D', 8, 8, rgba8(255, 215, 0, 255));
946
+ print('Nintendo 64 / PlayStation Fantasy World', 8, 24, rgba8(200, 150, 255, 255));
947
+
948
+ // Game stats
949
+ const collectedCrystals = world.crystals.filter(c => c.collected).length;
950
+ print(`Time: ${timeOfDay} | Weather: ${world.weather.type}`, 8, 50, rgba8(150, 200, 255, 255));
951
+ print(
952
+ `Crystals: ${collectedCrystals}/${world.crystals.length}`,
953
+ 8,
954
+ 66,
955
+ rgba8(255, 200, 100, 255)
956
+ );
957
+ const caughtCount = world.creatures.filter(c => c.caught).length;
958
+ print(`Creatures: ${caughtCount}/${world.creatures.length}`, 8, 82, rgba8(255, 150, 255, 255));
959
+ print(
960
+ `Position: ${player.x.toFixed(1)}, ${player.y.toFixed(1)}, ${player.z.toFixed(1)}`,
961
+ 8,
962
+ 98,
963
+ rgba8(100, 255, 150, 255)
964
+ );
965
+
966
+ // 3D stats
967
+ const stats = get3DStats();
968
+ if (stats && stats.render) {
969
+ print(
970
+ `3D Objects: ${world.terrain.length + world.crystals.length + world.creatures.length}`,
971
+ 8,
972
+ 114,
973
+ rgba8(150, 150, 255, 255)
974
+ );
975
+ print(
976
+ `GPU: ${stats.renderer || 'Three.js'} | Effects: Bloom+Dither+Pixel`,
977
+ 8,
978
+ 130,
979
+ rgba8(150, 150, 255, 255)
980
+ );
981
+ }
982
+
983
+ // Magic charge indicator
984
+ print('Magic:', 8, 148, rgba8(255, 100, 255, 255));
985
+ for (let i = 0; i < player.maxMagicCharges; i++) {
986
+ const filled = i < player.magicCharges;
987
+ print(
988
+ filled ? '✦' : '✧',
989
+ 60 + i * 18,
990
+ 148,
991
+ filled ? rgba8(255, 100, 255, 255) : rgba8(100, 50, 100, 180)
992
+ );
993
+ }
994
+
995
+ // Controls
996
+ print('WASD: Move | Space: Jump | E: Magic Bolt', 8, 300, rgba8(200, 200, 200, 180));
997
+ print('Stun creatures with magic, walk into them to catch!', 8, 316, rgba8(255, 255, 100, 200));
998
+ print('Collect all crystals + catch all creatures to win!', 8, 332, rgba8(100, 255, 100, 180));
999
+
1000
+ // Weather indicator
1001
+ if (world.weather.type === 'storm') {
1002
+ print('⚡ STORM APPROACHING ⚡', 200, 8, rgba8(255, 255, 0, 255));
1003
+ } else if (world.weather.type === 'mystical') {
1004
+ print('✨ Mystical energies swirl... ✨', 200, 8, rgba8(255, 100, 255, 255));
1005
+ }
1006
+
1007
+ // Health bar
1008
+ const healthPct = player.health / player.maxHealth;
1009
+ const hpColor =
1010
+ healthPct > 0.5
1011
+ ? rgba8(50, 200, 50, 255)
1012
+ : healthPct > 0.25
1013
+ ? rgba8(220, 180, 30, 255)
1014
+ : rgba8(200, 40, 40, 255);
1015
+ drawProgressBar(16, 168, 160, 10, healthPct, hpColor, rgba8(20, 10, 30, 220));
1016
+ print(`HP ${player.health}/${player.maxHealth}`, 16, 158, rgba8(255, 200, 255, 220));
1017
+ }
1018
+
1019
+ function drawStartScreen() {
1020
+ // Mystical gradient background
1021
+ drawGradientRect(0, 0, 640, 360, rgba8(20, 10, 40, 220), rgba8(50, 20, 80, 240), true);
1022
+
1023
+ // Animated title with magical glow
1024
+ const glow = Math.sin(startScreenTime * 2) * 0.3 + 0.7;
1025
+ const glowColor = rgba8(
1026
+ Math.floor(200 * glow),
1027
+ Math.floor(100 * glow),
1028
+ Math.floor(255 * glow),
1029
+ 255
1030
+ );
1031
+
1032
+ setFont('huge');
1033
+ setTextAlign('center');
1034
+ const bounce = Math.sin(startScreenTime * 2) * 12;
1035
+ drawTextShadow('MYSTICAL', 320, 50 + bounce, glowColor, rgba8(0, 0, 0, 255), 5, 1);
1036
+ drawTextShadow('REALM', 320, 100 + bounce, rgba8(255, 215, 0, 255), rgba8(0, 0, 0, 255), 5, 1);
1037
+
1038
+ // Subtitle with pulse
1039
+ setFont('large');
1040
+ const pulse = Math.sin(startScreenTime * 3) * 0.2 + 0.8;
1041
+ drawTextOutline(
1042
+ '3D Fantasy Adventure',
1043
+ 320,
1044
+ 150,
1045
+ rgba8(150, 100, 255, Math.floor(pulse * 255)),
1046
+ rgba8(0, 0, 0, 255),
1047
+ 1
1048
+ );
1049
+
1050
+ // Info panel
1051
+ const panel = createPanel(centerX(420), 340, 420, 200, {
1052
+ bgColor: rgba8(20, 10, 40, 200),
1053
+ borderColor: rgba8(100, 50, 200, 255),
1054
+ borderWidth: 3,
1055
+ shadow: true,
1056
+ gradient: true,
1057
+ gradientColor: rgba8(40, 20, 60, 200),
1058
+ });
1059
+ drawPanel(panel);
1060
+
1061
+ // Quest description
1062
+ setFont('normal');
1063
+ setTextAlign('center');
1064
+ drawText('QUEST BRIEFING', 320, 185, rgba8(255, 215, 0, 255), 1);
1065
+
1066
+ setFont('small');
1067
+ drawText('A mystical realm awaits exploration', 320, 210, uiColors.light, 1);
1068
+ drawText('Collect magical crystals scattered across the land', 320, 225, uiColors.light, 1);
1069
+ drawText('Navigate through day, night, and mystical storms', 320, 240, uiColors.light, 1);
1070
+
1071
+ setFont('tiny');
1072
+ drawText('WASD = Move | Space = Jump | E = Magic Bolt', 320, 270, uiColors.secondary, 1);
1073
+ drawText(
1074
+ 'Stun creatures • Catch them • Collect all crystals to WIN!',
1075
+ 320,
1076
+ 284,
1077
+ rgba8(255, 100, 255, 180),
1078
+ 1
1079
+ );
1080
+
1081
+ // Draw buttons
1082
+ drawAllButtons();
1083
+
1084
+ // Pulsing start prompt
1085
+ const alpha = Math.floor((Math.sin(startScreenTime * 5) * 0.5 + 0.5) * 255);
1086
+ setFont('normal');
1087
+ drawText('▶ PRESS BEGIN QUEST TO START ◀', 320, 305, rgba8(200, 100, 255, alpha), 1);
1088
+
1089
+ // Mystical particles hint
1090
+ setFont('tiny');
1091
+ drawText('Nintendo 64 / PlayStation Style Graphics', 320, 340, rgba8(150, 150, 200, 150), 1);
1092
+ }
1093
+
1094
+ function drawGameOverScreen() {
1095
+ // Dark mystical overlay
1096
+ rect(0, 0, 640, 360, rgba8(10, 5, 20, 220), true);
1097
+
1098
+ // Game over title
1099
+ setFont('huge');
1100
+ setTextAlign('center');
1101
+ const flash = Math.floor(time * 2) % 2 === 0;
1102
+ const color = flash ? rgba8(200, 100, 255, 255) : rgba8(150, 50, 200, 255);
1103
+ drawTextShadow('QUEST ENDED', 320, 80, color, rgba8(0, 0, 0, 255), 5, 1);
1104
+
1105
+ // Stats panel
1106
+ const statsPanel = createPanel(centerX(420), centerY(220), 420, 220, {
1107
+ bgColor: rgba8(20, 10, 40, 220),
1108
+ borderColor: rgba8(100, 50, 200, 255),
1109
+ borderWidth: 3,
1110
+ shadow: true,
1111
+ title: 'FINAL STATISTICS',
1112
+ titleBgColor: rgba8(100, 50, 200, 255),
1113
+ });
1114
+ drawPanel(statsPanel);
1115
+
1116
+ // Stats
1117
+ setFont('large');
1118
+ setTextAlign('center');
1119
+ drawText(`Crystals Collected: ${crystalsCollected}`, 320, 200, rgba8(255, 215, 0, 255), 1);
1120
+
1121
+ setFont('normal');
1122
+ const minutes = Math.floor(playTime / 60);
1123
+ const seconds = Math.floor(playTime % 60);
1124
+ drawText(`Time Played: ${minutes}m ${seconds}s`, 320, 235, uiColors.secondary, 1);
1125
+ drawText(`Score: ${score}`, 320, 260, uiColors.success, 1);
1126
+
1127
+ // Draw buttons
1128
+ drawAllButtons();
1129
+ }
1130
+
1131
+ function initGameOverScreen() {
1132
+ uiButtons = [];
1133
+
1134
+ // Try again button
1135
+ uiButtons.push(
1136
+ createButton(
1137
+ centerX(200),
1138
+ 310,
1139
+ 200,
1140
+ 50,
1141
+ '↻ TRY AGAIN',
1142
+ () => {
1143
+ resetGame();
1144
+ gameState = 'playing';
1145
+ },
1146
+ {
1147
+ normalColor: uiColors.success,
1148
+ hoverColor: rgba8(60, 220, 120, 255),
1149
+ pressedColor: rgba8(30, 160, 80, 255),
1150
+ }
1151
+ )
1152
+ );
1153
+
1154
+ // Main menu button
1155
+ uiButtons.push(
1156
+ createButton(
1157
+ centerX(200),
1158
+ 375,
1159
+ 200,
1160
+ 45,
1161
+ '← MAIN MENU',
1162
+ () => {
1163
+ resetGame();
1164
+ gameState = 'start';
1165
+ initStartScreen();
1166
+ },
1167
+ {
1168
+ normalColor: uiColors.primary,
1169
+ hoverColor: rgba8(50, 150, 255, 255),
1170
+ pressedColor: rgba8(20, 100, 200, 255),
1171
+ }
1172
+ )
1173
+ );
1174
+ }
1175
+
1176
+ function resetGame() {
1177
+ player.health = player.maxHealth;
1178
+ player.x = 0;
1179
+ player.y = 5;
1180
+ player.z = 0;
1181
+ player.jumpVelocity = 0;
1182
+ player.magicCharges = player.maxMagicCharges;
1183
+ magicCD.remaining = 0;
1184
+ player.magicRechargeTimer = 0;
1185
+ playTime = 0;
1186
+ score = 0;
1187
+ crystalsCollected = 0;
1188
+ creaturesKept = 0;
1189
+
1190
+ // Reset crystals
1191
+ world.crystals.forEach(c => (c.collected = false));
1192
+
1193
+ // Reset creatures
1194
+ world.creatures.forEach(c => {
1195
+ c.stunTimer = 0;
1196
+ c.caught = false;
1197
+ setMeshVisible(c.mesh, true);
1198
+ });
1199
+
1200
+ // Remove stray magic bolts
1201
+ magicBolts.forEach(b => removeMesh(b.mesh));
1202
+ magicBolts = [];
1203
+ }