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,1537 @@
1
+ // SHADOW NINJA 3D - True 3.5D Ninja Platformer
2
+ // Nintendo 64 / PlayStation style 3D ninja action with parkour and stealth
3
+ // Inspired by Strider, Shinobi, and Ninja Gaiden
4
+ // VERSION: v006-NUCLEAR-CACHE-BUST-${Date.now()}
5
+
6
+ // 🚨 CACHE DETECTION: If you see logs repeating in console, browser cache is stuck!
7
+ const CACHE_BUSTER_V006 = 'FRESH_CODE_LOADED_' + Date.now();
8
+ console.log('🚀🚀🚀 CACHE BUSTER V006 LOADED:', CACHE_BUSTER_V006);
9
+
10
+ // Helper function for 3D vectors
11
+ function vec3(x, y, z) {
12
+ return { x: x, y: y, z: z };
13
+ }
14
+
15
+ // Game state
16
+ let gameTime = 0;
17
+ let gameState = 'start'; // 'start', 'playing', 'gameOver'
18
+ let startScreenTime = 0;
19
+ let uiButtons = [];
20
+ let score = 0;
21
+ let level = 1;
22
+ let combo = 0;
23
+ let comboTimer = 0;
24
+
25
+ // 🔥 DEFINE BUTTON CALLBACK AT MODULE LEVEL TO AVOID SCOPE ISSUES
26
+ const startGameCallback = () => {
27
+ console.log('🎯🎯🎯 START BUTTON CLICKED V006! FRESH CODE! 🎯🎯🎯');
28
+ console.log('📊 BEFORE: gameState =', gameState);
29
+ gameState = 'playing';
30
+ console.log('📊 AFTER: gameState =', gameState);
31
+ console.log('✅✅✅ STATE CHANGED TO PLAYING! ✅✅✅');
32
+ };
33
+
34
+ // 3D Game objects
35
+ let playerKnight = null;
36
+ let platforms = [];
37
+ let enemies = [];
38
+ let coins = [];
39
+ let particles = [];
40
+ let environment = [];
41
+
42
+ // Player state - NINJA POWERS!
43
+ let player = {
44
+ x: 0,
45
+ y: 0,
46
+ z: 0,
47
+ vx: 0,
48
+ vy: 0,
49
+ vz: 0,
50
+ onGround: false,
51
+ health: 100,
52
+ energy: 100,
53
+ coins: 0,
54
+ shuriken: 20,
55
+ facingRight: true,
56
+ jumpPower: 10,
57
+ speed: 8,
58
+ doubleJump: true,
59
+ attackCooldown: 0,
60
+ // Ninja abilities
61
+ wallRunning: false,
62
+ wallRunTime: 0,
63
+ maxWallRunTime: 1.5,
64
+ dashCooldown: 0,
65
+ dashDuration: 0,
66
+ crouching: false,
67
+ stealth: false,
68
+ grapplePoints: [],
69
+ grappling: false,
70
+ grappleTarget: null,
71
+ airDashAvailable: true,
72
+ slideDuration: 0,
73
+ };
74
+
75
+ // Camera state
76
+ let camera = {
77
+ x: 0,
78
+ y: 8,
79
+ z: 15,
80
+ targetX: 0,
81
+ targetY: 5,
82
+ targetZ: 0,
83
+ smoothing: 0.1,
84
+ };
85
+
86
+ export function init() {
87
+ console.log('��� KNIGHT PLATFORMER V006 INIT START 🚀🚀🚀');
88
+ console.log('📦 gameState BEFORE init:', gameState);
89
+ console.log('�️ Clearing arrays. Current particles:', particles.length);
90
+
91
+ // Clear all arrays to prevent mesh errors
92
+ enemies = [];
93
+ coins = [];
94
+ particles = [];
95
+ platforms = [];
96
+ environment = [];
97
+ playerKnight = null;
98
+
99
+ // Reset player state completely
100
+ // Start ON the starting platform - PERFECT ONBOARDING!
101
+ player = {
102
+ pos: vec3(-10, 2, 0), // Start on LEFT side of ground platform at comfortable height
103
+ vel: vec3(0, 0, 0),
104
+ yaw: 0, // Face RIGHT to see the level ahead
105
+ grounded: true, // Start grounded so player doesn't fall!
106
+ attacking: false,
107
+ attackTime: 0,
108
+ health: 100,
109
+ maxHealth: 100,
110
+ stamina: 100,
111
+ maxStamina: 100,
112
+ isDashing: false,
113
+ dashTime: 0,
114
+ wallRunning: false,
115
+ wallRunTime: 0,
116
+ wallSide: 0,
117
+ doubleJumpAvailable: true,
118
+ grappling: false,
119
+ grappleTarget: null,
120
+ sliding: false,
121
+ slideTime: 0,
122
+ shurikenCount: 10,
123
+ };
124
+
125
+ score = 0;
126
+ combo = 0;
127
+ comboTimer = 0;
128
+ gameTime = 0;
129
+ level = 1;
130
+ // 🔥 DON'T RESET gameState HERE - button callback may have already changed it!
131
+ // gameState = 'start' // ← REMOVED - was resetting state after button click!
132
+ startScreenTime = 0;
133
+
134
+ // Create start button - callback is defined at module level
135
+ uiButtons = [];
136
+
137
+ uiButtons.push(
138
+ createButton(210, 310, 220, 50, '▶ START GAME', startGameCallback, {
139
+ normalColor: rgba8(150, 80, 255, 255),
140
+ hoverColor: rgba8(180, 110, 255, 255),
141
+ pressedColor: rgba8(120, 60, 220, 255),
142
+ })
143
+ );
144
+
145
+ // Force canvas focus for keyboard events
146
+ console.log('🎮 Focusing canvas for input...');
147
+ const canvas = document.querySelector('canvas');
148
+ if (canvas) {
149
+ canvas.focus();
150
+ canvas.tabIndex = 1;
151
+ }
152
+
153
+ // Create game objects and store references
154
+ playerKnight = createNinjaPlayer();
155
+ createNinjaWorld();
156
+ createPlatforms();
157
+ createGrapplePoints();
158
+ spawnEnemies();
159
+ spawnCoins();
160
+
161
+ // 📷 OPTIMAL SIDE-SCROLLER CAMERA (like Strider/Shinobi)
162
+ // Camera positioned for perfect gameplay view!
163
+ camera.x = player.pos.x;
164
+ camera.y = 6; // Eye-level height for immersive side-scrolling
165
+ camera.z = 18; // Closer for better visibility and control
166
+ camera.targetX = player.pos.x;
167
+ camera.targetY = 5; // Look slightly ahead
168
+ camera.targetZ = 0; // Straight ahead on the action plane
169
+
170
+ // Set initial camera position
171
+ setCameraPosition(camera.x, camera.y, camera.z);
172
+ setCameraTarget(camera.targetX, camera.targetY, camera.targetZ);
173
+
174
+ // 💡 MAXIMUM BRIGHTNESS - Crystal clear visibility!
175
+ setAmbientLight(0xffffff); // FULL WHITE ambient - no shadows!
176
+ setLightColor(0xffffff); // FULL WHITE directional light
177
+ setLightDirection(0.5, -0.5, 0.8); // From front and above for maximum visibility
178
+
179
+ console.log('✅✅✅ KNIGHT PLATFORMER V006 INIT COMPLETE ✅✅✅');
180
+ console.log('📦 gameState AFTER init:', gameState);
181
+ console.log('📷 Camera initialized at:', camera.x, camera.y, camera.z);
182
+ }
183
+
184
+ function initStartScreen() {
185
+ uiButtons = [];
186
+ console.log('🥷 Start screen initialized');
187
+ }
188
+
189
+ export function update() {
190
+ const dt = 1 / 60;
191
+
192
+ console.log(
193
+ '🔄 UPDATE called - gameState:',
194
+ gameState,
195
+ 'playerKnight:',
196
+ playerKnight ? 'exists' : 'null'
197
+ );
198
+
199
+ // Safety check: Don't update if game objects aren't initialized yet
200
+ if (playerKnight === null) {
201
+ // Game not initialized yet, skip this frame
202
+ console.log('⚠️ UPDATE: playerKnight is null, returning early');
203
+ return;
204
+ }
205
+
206
+ // Handle start screen
207
+ if (gameState === 'start') {
208
+ startScreenTime += dt;
209
+
210
+ // DEBUG: Log keyboard state
211
+ const enterDown = isKeyDown('Enter');
212
+ const spaceDown = isKeyDown('Space');
213
+ if (enterDown || spaceDown) {
214
+ console.log('⌨️ Keyboard detected! Enter:', enterDown, 'Space:', spaceDown);
215
+ }
216
+
217
+ // Update UI buttons (may change gameState via callback)
218
+ updateAllButtons();
219
+
220
+ // ✨ CRITICAL: Re-check gameState IMMEDIATELY after updateAllButtons()
221
+ // The button callback executes SYNCHRONOUSLY and may have changed gameState
222
+ if (gameState !== 'start') {
223
+ console.log('🎮🎮🎮 Button changed gameState to:', gameState, '- starting game!');
224
+ // DON'T return - fall through to playing state below
225
+ } else {
226
+ // Still on start screen - check for keyboard/gamepad input
227
+ // KEYBOARD FALLBACK: Press ENTER or SPACE to start
228
+ // Use isKeyDown (held) instead of isKeyPressed (transition) for more reliable detection
229
+ if (isKeyDown('Enter') || isKeyDown(' ') || isKeyDown('Space') || btnp(0) || btnp(1)) {
230
+ console.log('🥷🥷🥷 Starting Shadow Ninja 3D via keyboard or button!');
231
+ gameState = 'playing';
232
+ console.log('🎮🎮🎮 Game started! State:', gameState);
233
+ // Fall through to playing state
234
+ } else {
235
+ // Check for any OTHER button press to start
236
+ for (let i = 2; i < 10; i++) {
237
+ if (btnp(i)) {
238
+ console.log(`🥷 Starting Shadow Ninja 3D via button ${i}!`);
239
+ gameState = 'playing';
240
+ console.log('🎮 Game started! State:', gameState);
241
+ break;
242
+ }
243
+ }
244
+
245
+ // ✨ CRITICAL FIX: Only return if STILL on start screen after ALL checks (including UI button callback)
246
+ if (gameState === 'start') {
247
+ console.log('📺 UPDATE: Start screen done, returning');
248
+ return;
249
+ } else {
250
+ console.log('🚀 gameState changed to:', gameState, '- continuing to game!');
251
+ }
252
+ }
253
+ }
254
+ }
255
+
256
+ // If we reach here and still on start screen somehow, return
257
+ if (gameState === 'start') {
258
+ return;
259
+ }
260
+
261
+ // Playing state - main game loop
262
+ gameTime += dt;
263
+
264
+ // Update input
265
+ updateInput(dt);
266
+
267
+ // Update player
268
+ updatePlayer(dt);
269
+
270
+ // Update enemies
271
+ updateEnemies(dt);
272
+
273
+ // Update coins
274
+ updateCoins(dt);
275
+
276
+ // Update particles
277
+ updateParticles(dt);
278
+
279
+ // Update camera to follow player (CRITICAL for 2.5D side-scroller view)
280
+ updateCamera(dt);
281
+
282
+ // Update combo timer
283
+ if (comboTimer > 0) {
284
+ comboTimer -= dt;
285
+ if (comboTimer <= 0) {
286
+ combo = 0;
287
+ }
288
+ }
289
+
290
+ // Check for player death
291
+ if (player.health <= 0) {
292
+ gameState = 'gameover';
293
+ console.log('💀 Game Over! Final Score:', score);
294
+ }
295
+ }
296
+
297
+ export function draw() {
298
+ if (gameState === 'start') {
299
+ drawStartScreen();
300
+ return;
301
+ }
302
+
303
+ // 3D scene is automatically rendered by GPU backend
304
+ // Draw UI overlay using 2D API
305
+ drawUI();
306
+ }
307
+
308
+ function drawStartScreen() {
309
+ // Dark background
310
+ rect(0, 0, 640, 360, rgba8(10, 5, 30, 255), true);
311
+
312
+ // Title with glow effect
313
+ print('SHADOW NINJA 3D', 180, 70, rgba8(180, 80, 255, 255));
314
+
315
+ // Subtitle
316
+ const pulse = Math.sin(startScreenTime * 3) * 0.25 + 0.75;
317
+ print(
318
+ 'Strider-Style 3.5D Ninja Platformer',
319
+ 160,
320
+ 100,
321
+ rgba8(200, 150, 255, Math.floor(pulse * 255))
322
+ );
323
+
324
+ // START PROMPT - Make it obvious
325
+ const promptPulse = Math.sin(startScreenTime * 5) * 0.5 + 0.5;
326
+ print(
327
+ 'PRESS ENTER OR SPACE TO START',
328
+ 190,
329
+ 120,
330
+ rgba8(255, 255, 100, Math.floor(promptPulse * 255))
331
+ );
332
+
333
+ // Controls panel
334
+ rect(100, 140, 440, 160, rgba8(20, 10, 40, 220), true);
335
+ rect(100, 140, 440, 160, rgba8(150, 80, 255, 255), false);
336
+
337
+ // Controls title
338
+ print('NINJA ABILITIES:', 220, 155, rgba8(255, 200, 255, 255));
339
+
340
+ // Controls - two columns
341
+ print('ARROWS = Move', 120, 180, rgba8(200, 200, 255, 255));
342
+ print('DOWN = Slide', 120, 200, rgba8(200, 200, 255, 255));
343
+ print('UP = Jump/Double Jump', 120, 220, rgba8(200, 200, 255, 255));
344
+ print('Z = Attack', 120, 240, rgba8(200, 200, 255, 255));
345
+ print('X = Dash/Air Dash', 120, 260, rgba8(200, 200, 255, 255));
346
+
347
+ print('C = Throw Shuriken', 340, 180, rgba8(200, 200, 255, 255));
348
+ print('G = Grappling Hook', 340, 200, rgba8(200, 200, 255, 255));
349
+ print('Wall Running Active', 340, 220, rgba8(150, 255, 150, 255));
350
+ print('Combo System', 340, 240, rgba8(255, 255, 150, 255));
351
+ print('Energy Management', 340, 260, rgba8(150, 200, 255, 255));
352
+
353
+ // Draw start button
354
+ drawAllButtons();
355
+ }
356
+
357
+ function createNinjaPlayer() {
358
+ // 🥷 BRIGHT NINJA - Highly visible and COOL!
359
+
360
+ // Main body - BRIGHT BLUE ninja suit
361
+ const body = createCube(1, 0x4466ff, [0, 0, 0]); // Bright blue suit
362
+ setScale(body, 1.2, 2, 0.9); // Tall and athletic
363
+
364
+ // Chest armor - BRIGHT CYAN GLOWING plate
365
+ const chest = createCube(0.8, 0x44ffff, [0, 0.4, 0.46]); // Bright cyan chest plate
366
+ setScale(chest, 1.1, 1.2, 0.1);
367
+
368
+ // Head - BRIGHT GRAY helmet
369
+ const head = createCube(0.7, 0x888888, [0, 1.5, 0]); // Gray ninja helmet
370
+ setScale(head, 0.9, 0.9, 0.9);
371
+
372
+ // GLOWING YELLOW VISOR - highly visible eyes
373
+ const visor = createCube(0.8, 0xffff44, [0, 1.5, 0.36]); // Yellow glowing visor
374
+ setScale(visor, 0.85, 0.3, 0.05);
375
+
376
+ // BRIGHT RED HEADBAND - flowing behind
377
+ const headband = createCube(1.5, 0xff4444, [0, 1.7, -0.4]); // Bright red ribbon
378
+ setScale(headband, 0.15, 0.1, 1.2);
379
+
380
+ // BRIGHT PURPLE ENERGY KATANA - glowing blade
381
+ const katana = createCube(0.08, 0xdd88ff, [-0.5, 0.9, -0.4]); // Bright purple katana
382
+ setScale(katana, 0.08, 2, 0.08);
383
+ setRotation(katana, 0, 0, -0.4); // Angled on back
384
+
385
+ // Katana handle - BRIGHT SILVER
386
+ const katanaHandle = createCube(0.12, 0xdddddd, [-0.5, -0.3, -0.4]);
387
+ setScale(katanaHandle, 0.12, 0.4, 0.12);
388
+ setRotation(katanaHandle, 0, 0, -0.4);
389
+
390
+ // BRIGHT CYAN SCARF - flowing behind
391
+ const scarf = createCube(0.4, 0x44ddff, [0, 1, -0.6]); // Bright cyan trail
392
+ setScale(scarf, 0.7, 1.2, 0.15);
393
+
394
+ // Arms - BRIGHT BLUE with energy
395
+ const leftArm = createCube(0.3, 0x4466ff, [-0.7, 0.2, 0]);
396
+ setScale(leftArm, 0.3, 1.3, 0.3);
397
+
398
+ const rightArm = createCube(0.3, 0x4466ff, [0.7, 0.2, 0]);
399
+ setScale(rightArm, 0.3, 1.3, 0.3);
400
+
401
+ // BRIGHT arm guards - YELLOW ENERGY
402
+ const leftGuard = createCube(0.32, 0xffff44, [-0.7, -0.3, 0]);
403
+ setScale(leftGuard, 0.35, 0.25, 0.35);
404
+
405
+ const rightGuard = createCube(0.32, 0xffff44, [0.7, -0.3, 0]);
406
+ setScale(rightGuard, 0.35, 0.25, 0.35);
407
+
408
+ // Legs - BRIGHT BLUE pants
409
+ const leftLeg = createCube(0.35, 0x4466ff, [-0.4, -1.3, 0]);
410
+ setScale(leftLeg, 0.35, 1.4, 0.35);
411
+
412
+ const rightLeg = createCube(0.35, 0x4466ff, [0.4, -1.3, 0]);
413
+ setScale(rightLeg, 0.35, 1.4, 0.35);
414
+
415
+ // Knee guards - BRIGHT YELLOW
416
+ const leftKnee = createCube(0.36, 0xffff44, [-0.4, -1, 0]);
417
+ setScale(leftKnee, 0.38, 0.2, 0.38);
418
+
419
+ const rightKnee = createCube(0.36, 0xffff44, [0.4, -1, 0]);
420
+ setScale(rightKnee, 0.38, 0.2, 0.38);
421
+
422
+ // NO AURA - it blocks visibility!
423
+
424
+ return {
425
+ body,
426
+ chest,
427
+ head,
428
+ visor,
429
+ headband,
430
+ katana,
431
+ katanaHandle,
432
+ scarf,
433
+ leftArm,
434
+ rightArm,
435
+ leftGuard,
436
+ rightGuard,
437
+ leftLeg,
438
+ rightLeg,
439
+ leftKnee,
440
+ rightKnee,
441
+ };
442
+ }
443
+
444
+ async function createNinjaWorld() {
445
+ // 🌈 BRIGHT COLORFUL WORLD - Maximum visibility!
446
+
447
+ // BRIGHT BLUE ground - easy to see!
448
+ const ground = createPlane(400, 400, 0x6699ff, [25, -3, 0]);
449
+ setRotation(ground, -Math.PI / 2, 0, 0);
450
+
451
+ // BRIGHT YELLOW grid lines on ground for depth perception
452
+ for (let i = -10; i <= 20; i++) {
453
+ const gridLine = createCube(200, 0.15, 0xffee44, [i * 5, -2.8, 0]);
454
+ setScale(gridLine, 1, 1, 1);
455
+ setRotation(gridLine, -Math.PI / 2, 0, 0);
456
+ }
457
+
458
+ // COLORFUL BUILDINGS - Bright and visible!
459
+ const buildingColors = [
460
+ 0xff6688, // Bright pink
461
+ 0x66ff88, // Bright green
462
+ 0x6688ff, // Bright blue
463
+ 0xffaa66, // Bright orange
464
+ 0xaa66ff, // Bright purple
465
+ ];
466
+
467
+ // Background buildings - BRIGHT colors!
468
+ for (let i = 0; i < 12; i++) {
469
+ const color = buildingColors[i % buildingColors.length];
470
+ const x = (i - 6) * 15;
471
+ const height = 20 + Math.random() * 15;
472
+
473
+ // Main building - BRIGHT!
474
+ const building = createCube(8, height, color, [x, height / 2 - 3, -40]);
475
+ setScale(building, 1, 1, 1);
476
+
477
+ // WHITE accent strips for contrast
478
+ for (let j = 0; j < 3; j++) {
479
+ const strip = createCube(8.3, 1, 0xffffff, [x, (height / 4) * (j + 1) - 3, -39.7]);
480
+ setScale(strip, 1, 1, 1);
481
+ }
482
+ }
483
+
484
+ // BRIGHT FLOATING PLATFORMS in background
485
+ const bgPlatforms = [
486
+ { x: -15, y: 8, color: 0xff88ff },
487
+ { x: 5, y: 10, color: 0x88ffff },
488
+ { x: 25, y: 9, color: 0xffff88 },
489
+ { x: 40, y: 11, color: 0x88ff88 },
490
+ ];
491
+
492
+ bgPlatforms.forEach(plat => {
493
+ const platform = createCube(6, 1, plat.color, [plat.x, plat.y, -25]);
494
+ setScale(platform, 1, 1, 1);
495
+ });
496
+
497
+ // BRIGHT DECORATIVE SPHERES
498
+ for (let i = 0; i < 15; i++) {
499
+ const colors = [0xff8888, 0x88ff88, 0x8888ff, 0xffff88, 0xff88ff, 0x88ffff];
500
+ const orbColor = colors[i % colors.length];
501
+ createSphere(0.8, orbColor, [-20 + i * 5, 4 + Math.sin(i) * 3, -15 - Math.cos(i) * 5]);
502
+ }
503
+
504
+ // BRIGHT YELLOW SUN/LIGHT SOURCE in sky
505
+ createSphere(10, 0xffffaa, [0, 30, -70]);
506
+ }
507
+
508
+ function createPlatforms() {
509
+ platforms = [];
510
+
511
+ // 🏗️ MAIN GROUND - Long continuous platform for easy side-scrolling!
512
+ const mainGround = {
513
+ mesh: createCube(100, 1, 0x6644ff, [-10, 0, 0]), // BRIGHT PURPLE main platform!
514
+ x: -10,
515
+ y: 0,
516
+ z: 0,
517
+ width: 100,
518
+ height: 1,
519
+ depth: 8,
520
+ type: 'ground',
521
+ };
522
+ setScale(mainGround.mesh, 1, 1, 1);
523
+ platforms.push(mainGround);
524
+
525
+ // Add BRIGHT YELLOW glowing edge to main platform
526
+ const mainEdge = createCube(100, 0.2, 0xffff00, [-10, 1.1, 0]);
527
+ setScale(mainEdge, 1, 1, 1);
528
+
529
+ // EASY ACCESSIBLE PLATFORMS - Progressive difficulty on the same Z plane (Z=0)!
530
+ const easyPlatforms = [
531
+ // Tutorial section - easy jumps
532
+ { x: -5, y: 3, w: 6, color: 0x88aa66 },
533
+ { x: 2, y: 4.5, w: 5, color: 0x6688aa },
534
+ { x: 8, y: 6, w: 5, color: 0xaa6688 },
535
+ { x: 14, y: 7.5, w: 4.5, color: 0x66aa88 },
536
+
537
+ // Mid-section - gaining height
538
+ { x: 20, y: 9, w: 5, color: 0xaa8866 },
539
+ { x: 26, y: 10.5, w: 4, color: 0x88aa66 },
540
+ { x: 31, y: 12, w: 4, color: 0x6688aa },
541
+
542
+ // Upper section - skill jumps
543
+ { x: 37, y: 13, w: 4.5, color: 0xaa6688 },
544
+ { x: 43, y: 14, w: 4, color: 0x66aa88 },
545
+ { x: 48, y: 15, w: 5, color: 0xaa8866 },
546
+ ];
547
+
548
+ easyPlatforms.forEach((data, i) => {
549
+ const platform = {
550
+ mesh: createCube(data.w, 0.8, data.color, [data.x, data.y, 0]), // All at Z=0!
551
+ x: data.x,
552
+ y: data.y,
553
+ z: 0,
554
+ width: data.w,
555
+ height: 0.8,
556
+ depth: 4,
557
+ type: 'floating',
558
+ id: i,
559
+ };
560
+ setScale(platform.mesh, 1, 1, 1);
561
+ platforms.push(platform);
562
+
563
+ // Add BRIGHT WHITE edge to each platform
564
+ const edgeLight = createCube(data.w + 0.1, 0.15, 0xffffff, [data.x, data.y + 0.48, 0]);
565
+ setScale(edgeLight, 1, 1, 1);
566
+
567
+ // Add BRIGHT YELLOW underglow
568
+ const underglow = createCube(data.w - 0.2, 0.12, 0xffee44, [data.x, data.y - 0.42, 0]);
569
+ setScale(underglow, 1, 1, 1);
570
+ });
571
+
572
+ // COLLECTIBLE PLATFORMS - coins trail
573
+ const coinPlatforms = [
574
+ { x: 5, y: 2, w: 3 },
575
+ { x: 11, y: 3.5, w: 2.5 },
576
+ { x: 17, y: 5, w: 3 },
577
+ { x: 23, y: 6.5, w: 2.5 },
578
+ ];
579
+
580
+ coinPlatforms.forEach((data, i) => {
581
+ const platform = {
582
+ mesh: createCube(data.w, 0.5, 0xffaa44, [data.x, data.y, 0]), // BRIGHT ORANGE platforms!
583
+ x: data.x,
584
+ y: data.y,
585
+ z: 0,
586
+ width: data.w,
587
+ height: 0.5,
588
+ depth: 3,
589
+ type: 'coin',
590
+ id: i + 100,
591
+ };
592
+ setScale(platform.mesh, 1, 1, 1);
593
+ platforms.push(platform);
594
+
595
+ // BRIGHT YELLOW glow for coin platforms
596
+ const glow = createCube(data.w + 0.2, 0.12, 0xffff44, [data.x, data.y + 0.32, 0]);
597
+ setScale(glow, 1, 1, 1);
598
+ });
599
+ }
600
+
601
+ function createGrapplePoints() {
602
+ player.grapplePoints = [];
603
+
604
+ // Add grapple points in strategic locations
605
+ const grappleData = [
606
+ { x: 18, y: 12, z: 5 },
607
+ { x: 35, y: 15, z: 8 },
608
+ { x: 20, y: 22, z: -8 },
609
+ { x: -5, y: 24, z: -18 },
610
+ { x: -30, y: 18, z: -10 },
611
+ { x: -40, y: 12, z: 2 },
612
+ ];
613
+
614
+ grappleData.forEach(data => {
615
+ const point = {
616
+ mesh: createSphere(0.4, 0x00ffff, [data.x, data.y, data.z]),
617
+ x: data.x,
618
+ y: data.y,
619
+ z: data.z,
620
+ active: true,
621
+ };
622
+ player.grapplePoints.push(point);
623
+ });
624
+ }
625
+
626
+ function spawnEnemies() {
627
+ enemies = [];
628
+
629
+ // Ground patrol enemies - ON THE SAME Z PLANE as player!
630
+ const enemyPositions = [
631
+ { x: 10, y: 1.5 },
632
+ { x: 25, y: 1.5 },
633
+ { x: 35, y: 1.5 },
634
+ { x: 50, y: 1.5 },
635
+ ];
636
+
637
+ enemyPositions.forEach((pos, i) => {
638
+ const enemy = {
639
+ mesh: createCube(1, 0xff3355, [pos.x, pos.y, 0]), // Red cyber-enemies at Z=0!
640
+ x: pos.x,
641
+ y: pos.y,
642
+ z: 0,
643
+ vx: -2, // Move left
644
+ health: 3,
645
+ type: 'patrol',
646
+ patrolRange: 8,
647
+ originalX: pos.x,
648
+ attackCooldown: 0,
649
+ };
650
+ setScale(enemy.mesh, 1, 1.5, 1);
651
+ enemies.push(enemy);
652
+
653
+ // Red glowing eyes
654
+ const leftEye = createSphere(0.15, 0xff0000, [pos.x - 0.3, pos.y + 0.5, 0.51]);
655
+ const rightEye = createSphere(0.15, 0xff0000, [pos.x + 0.3, pos.y + 0.5, 0.51]);
656
+ });
657
+
658
+ // Flying drones - hovering enemies on same Z plane
659
+ const dronePositions = [
660
+ { x: 18, y: 8 },
661
+ { x: 33, y: 10 },
662
+ { x: 46, y: 12 },
663
+ ];
664
+
665
+ dronePositions.forEach((pos, i) => {
666
+ const enemy = {
667
+ mesh: createSphere(0.6, 0xff8844, [pos.x, pos.y, 0]), // Orange drones at Z=0!
668
+ x: pos.x,
669
+ y: pos.y,
670
+ z: 0,
671
+ vx: 0,
672
+ vy: 0,
673
+ vz: 0,
674
+ health: 2,
675
+ type: 'flyer',
676
+ orbitCenter: { x: pos.x, y: pos.y, z: 0 },
677
+ orbitRadius: 3,
678
+ orbitAngle: i * 2,
679
+ attackCooldown: 0,
680
+ };
681
+ enemies.push(enemy);
682
+
683
+ // Cyan propeller effect
684
+ const propeller = createCube(1.2, 0.1, 0x00ffff, [pos.x, pos.y + 0.7, 0]);
685
+ setScale(propeller, 1, 1, 1);
686
+ });
687
+ }
688
+
689
+ function spawnCoins() {
690
+ coins = [];
691
+
692
+ // Place coins on platforms
693
+ platforms.forEach((platform, i) => {
694
+ if (i > 8) {
695
+ // Skip ground platforms
696
+ const coin = {
697
+ mesh: createSphere(0.3, 0xffdd00, [platform.x, platform.y + 1.5, platform.z]),
698
+ x: platform.x,
699
+ y: platform.y + 1.5,
700
+ z: platform.z,
701
+ collected: false,
702
+ rotationY: 0,
703
+ bobOffset: Math.random() * Math.PI * 2,
704
+ };
705
+ coins.push(coin);
706
+ }
707
+ });
708
+
709
+ // Bonus coins in hard to reach places
710
+ for (let i = 0; i < 8; i++) {
711
+ const coin = {
712
+ mesh: createSphere(0.25, 0xffaa00, [
713
+ (Math.random() - 0.5) * 60,
714
+ 5 + Math.random() * 10,
715
+ (Math.random() - 0.5) * 30,
716
+ ]),
717
+ x: 0,
718
+ y: 0,
719
+ z: 0,
720
+ collected: false,
721
+ rotationY: 0,
722
+ bobOffset: Math.random() * Math.PI * 2,
723
+ };
724
+ const pos = getPosition(coin.mesh);
725
+ coin.x = pos[0];
726
+ coin.y = pos[1];
727
+ coin.z = pos[2];
728
+ coins.push(coin);
729
+ }
730
+ }
731
+
732
+ function updateInput(dt) {
733
+ const moveSpeed = 15; // Base movement speed
734
+ const dashMultiplier = player.isDashing && player.dashTime > 0 ? 2.5 : 1.0;
735
+
736
+ // Update timers
737
+ if (player.attackTime > 0) player.attackTime -= dt;
738
+ if (player.dashTime > 0) player.dashTime -= dt;
739
+ if (player.slideTime > 0) player.slideTime -= dt;
740
+
741
+ // Sliding
742
+ if (player.sliding && player.slideTime > 0) {
743
+ const slideDir = Math.cos(player.yaw);
744
+ player.vel.x = slideDir * moveSpeed * 1.5;
745
+ return; // Override other controls during slide
746
+ } else {
747
+ player.sliding = false;
748
+ }
749
+
750
+ // Grappling hook movement
751
+ if (player.grappling && player.grappleTarget) {
752
+ const dx = player.grappleTarget.x - player.pos.x;
753
+ const dy = player.grappleTarget.y - player.pos.y;
754
+ const dz = player.grappleTarget.z - player.pos.z;
755
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
756
+
757
+ if (dist > 1.5) {
758
+ const pullSpeed = 20;
759
+ player.vel.x = (dx / dist) * pullSpeed;
760
+ player.vel.y = (dy / dist) * pullSpeed;
761
+ player.vel.z = (dz / dist) * pullSpeed;
762
+ } else {
763
+ player.grappling = false;
764
+ player.grappleTarget = null;
765
+ }
766
+ return; // Override other controls during grapple
767
+ }
768
+
769
+ // 🎮 SIDE-SCROLLER MOVEMENT - Simple and responsive!
770
+ // Debug - log key states
771
+ const leftPressed = isKeyDown('ArrowLeft');
772
+ const rightPressed = isKeyDown('ArrowRight');
773
+ const spacePressed = isKeyDown('Space');
774
+
775
+ if (leftPressed || rightPressed || spacePressed) {
776
+ console.log('⌨️ KEYS:', 'Left:', leftPressed, 'Right:', rightPressed, 'Space:', spacePressed);
777
+ }
778
+
779
+ // Horizontal movement - LEFT/RIGHT arrows
780
+ if (leftPressed) {
781
+ player.vel.x = -moveSpeed * dashMultiplier;
782
+ player.yaw = Math.PI; // Face left
783
+ console.log('⬅️ MOVING LEFT at speed', player.vel.x);
784
+ } else if (rightPressed) {
785
+ player.vel.x = moveSpeed * dashMultiplier;
786
+ player.yaw = 0; // Face right
787
+ console.log('➡️ MOVING RIGHT at speed', player.vel.x);
788
+ } else {
789
+ player.vel.x *= 0.8; // Friction - stop quickly
790
+ }
791
+
792
+ // Z-axis movement locked for true side-scroller feel
793
+ // Player stays on single plane like Strider
794
+ player.vel.z *= 0.9; // Dampen any z-movement
795
+
796
+ // Down arrow = crouch/slide
797
+ if (isKeyDown('ArrowDown') && player.grounded && !player.sliding) {
798
+ player.sliding = true;
799
+ player.slideTime = 0.5;
800
+ player.stamina -= 5;
801
+ }
802
+
803
+ // Jump - Space or Z
804
+ if ((isKeyPressed('Space') || isKeyPressed('KeyZ')) && player.grounded) {
805
+ player.vel.y = 18; // Jump power
806
+ player.grounded = false;
807
+ player.doubleJumpAvailable = true;
808
+ } else if (
809
+ (isKeyPressed('Space') || isKeyPressed('KeyZ')) &&
810
+ player.doubleJumpAvailable &&
811
+ player.stamina >= 15
812
+ ) {
813
+ // Double jump
814
+ player.vel.y = 16;
815
+ player.doubleJumpAvailable = false;
816
+ player.stamina -= 15;
817
+ }
818
+
819
+ // Dash - X key
820
+ if (isKeyPressed('KeyX') && player.stamina >= 20) {
821
+ player.isDashing = true;
822
+ player.dashTime = 0.3;
823
+ player.stamina -= 20;
824
+ }
825
+
826
+ // Attack - Z key
827
+ if (isKeyPressed('KeyZ') && player.attackTime <= 0) {
828
+ performAttack();
829
+ player.attackTime = 0.4;
830
+ player.attacking = true;
831
+ } else if (player.attackTime <= 0) {
832
+ player.attacking = false;
833
+ }
834
+
835
+ // Shuriken - C key
836
+ if (isKeyPressed('KeyC') && player.shurikenCount > 0 && player.stamina >= 5) {
837
+ throwShuriken();
838
+ player.shurikenCount--;
839
+ player.stamina -= 5;
840
+ }
841
+
842
+ // Grapple - G key
843
+ if (isKeyPressed('KeyG')) {
844
+ const nearestGrapple = findNearestGrapplePoint();
845
+ if (nearestGrapple && !player.grappling) {
846
+ player.grappling = true;
847
+ player.grappleTarget = nearestGrapple;
848
+ }
849
+ }
850
+ }
851
+
852
+ function updatePlayer(dt) {
853
+ // Update combo timer
854
+ if (comboTimer > 0) {
855
+ comboTimer -= dt;
856
+ if (comboTimer <= 0) {
857
+ combo = 0;
858
+ }
859
+ }
860
+
861
+ // Check for wall running
862
+ player.wallRunning = false;
863
+ if (!player.grounded && Math.abs(player.vel.x) > 5) {
864
+ // Check if near any wall structure
865
+ for (let i = 0; i < 8; i++) {
866
+ const wallX = (i - 4) * 15;
867
+ const wallZ = i % 2 === 0 ? -20 : -30;
868
+
869
+ const distToWall = Math.sqrt((player.pos.x - wallX) ** 2 + (player.pos.z - wallZ) ** 2);
870
+ if (distToWall < 3) {
871
+ player.wallRunning = true;
872
+ player.wallRunTime += dt;
873
+
874
+ // Wall running slows vertical fall and provides slight upward boost
875
+ player.vel.y = Math.max(player.vel.y, -5);
876
+ if (player.wallRunTime < 10) {
877
+ // max wall run time
878
+ player.vel.y += 3 * dt; // Slight upward boost
879
+ }
880
+ break;
881
+ }
882
+ }
883
+ }
884
+
885
+ // Reset wall run time if not wall running
886
+ if (!player.wallRunning) {
887
+ player.wallRunTime = 0;
888
+ }
889
+
890
+ // Apply gravity (reduced during wall run)
891
+ const gravityMultiplier = player.wallRunning ? 0.3 : 1.0;
892
+ player.vel.y -= 25 * gravityMultiplier * dt;
893
+
894
+ // Update position
895
+ player.pos.x += player.vel.x * dt;
896
+ player.pos.y += player.vel.y * dt;
897
+ player.pos.z += player.vel.z * dt;
898
+
899
+ // Platform collision detection
900
+ player.grounded = false;
901
+
902
+ for (const platform of platforms) {
903
+ // Simple AABB collision
904
+ if (
905
+ player.pos.x > platform.x - platform.width / 2 &&
906
+ player.pos.x < platform.x + platform.width / 2 &&
907
+ player.pos.z > platform.z - platform.depth / 2 &&
908
+ player.pos.z < platform.z + platform.depth / 2
909
+ ) {
910
+ if (
911
+ player.pos.y <= platform.y + platform.height &&
912
+ player.pos.y + player.vel.y * dt > platform.y + platform.height
913
+ ) {
914
+ player.pos.y = platform.y + platform.height;
915
+ player.vel.y = 0;
916
+ player.grounded = true;
917
+ player.doubleJumpAvailable = true;
918
+ }
919
+ }
920
+ }
921
+
922
+ // World boundaries
923
+ if (player.pos.x < -50) player.pos.x = -50;
924
+ if (player.pos.x > 50) player.pos.x = 50;
925
+ if (player.pos.z < -30) player.pos.z = -30;
926
+ if (player.pos.z > 30) player.pos.z = 30;
927
+
928
+ // Fall reset
929
+ if (player.pos.y < -10) {
930
+ player.pos.x = 0;
931
+ player.pos.y = 2;
932
+ player.pos.z = 0;
933
+ player.vel.x = 0;
934
+ player.vel.y = 0;
935
+ player.vel.z = 0;
936
+ player.health -= 20;
937
+ combo = 0;
938
+ comboTimer = 0;
939
+ }
940
+
941
+ // Update ninja meshes
942
+ updatePlayerMeshes();
943
+
944
+ // Regenerate stamina
945
+ if (player.stamina < player.maxStamina) {
946
+ player.stamina += 25 * dt;
947
+ }
948
+ }
949
+
950
+ function updatePlayerMeshes() {
951
+ // Safety check
952
+ if (!playerKnight || !playerKnight.body) {
953
+ console.log('⚠️ Player meshes missing!');
954
+ return;
955
+ }
956
+
957
+ // Calculate facing direction for rotation
958
+ const facingDir = player.yaw;
959
+
960
+ // BODY - main torso (NO AURA - keeps ninja visible!)
961
+ if (playerKnight.body) {
962
+ setPosition(playerKnight.body, player.pos.x, player.pos.y, player.pos.z);
963
+ setRotation(playerKnight.body, 0, facingDir, 0);
964
+ }
965
+
966
+ // CHEST ARMOR - cyan plate
967
+ if (playerKnight.chest) {
968
+ const chestZ = player.pos.z + Math.sin(facingDir) * 0.46;
969
+ setPosition(playerKnight.chest, player.pos.x, player.pos.y + 0.4, chestZ);
970
+ setRotation(playerKnight.chest, 0, facingDir, 0);
971
+ }
972
+
973
+ // HEAD - helmet
974
+ if (playerKnight.head) {
975
+ setPosition(playerKnight.head, player.pos.x, player.pos.y + 1.5, player.pos.z);
976
+ setRotation(playerKnight.head, 0, facingDir, 0);
977
+ }
978
+
979
+ // VISOR - cyan glowing eyes
980
+ if (playerKnight.visor) {
981
+ const visorZ = player.pos.z + Math.sin(facingDir) * 0.36;
982
+ setPosition(playerKnight.visor, player.pos.x, player.pos.y + 1.5, visorZ);
983
+ setRotation(playerKnight.visor, 0, facingDir, 0);
984
+ }
985
+
986
+ // HEADBAND - red energy ribbon flowing behind
987
+ if (playerKnight.headband) {
988
+ const headbandZ = player.pos.z - Math.sin(facingDir) * 0.4;
989
+ const flowOffset = Math.sin(gameTime * 5) * 0.1;
990
+ setPosition(playerKnight.headband, player.pos.x, player.pos.y + 1.7 + flowOffset, headbandZ);
991
+ setRotation(playerKnight.headband, 0, facingDir, Math.sin(gameTime * 3) * 0.1);
992
+ }
993
+
994
+ // KATANA - purple energy blade on back
995
+ if (playerKnight.katana) {
996
+ const katanaX = player.pos.x - Math.cos(facingDir) * 0.5;
997
+ const katanaZ = player.pos.z - Math.sin(facingDir) * 0.4;
998
+ setPosition(playerKnight.katana, katanaX, player.pos.y + 0.9, katanaZ);
999
+ setRotation(playerKnight.katana, 0, facingDir, -0.4);
1000
+ }
1001
+
1002
+ // KATANA HANDLE
1003
+ if (playerKnight.katanaHandle) {
1004
+ const handleX = player.pos.x - Math.cos(facingDir) * 0.5;
1005
+ const handleZ = player.pos.z - Math.sin(facingDir) * 0.4;
1006
+ setPosition(playerKnight.katanaHandle, handleX, player.pos.y - 0.3, handleZ);
1007
+ setRotation(playerKnight.katanaHandle, 0, facingDir, -0.4);
1008
+ }
1009
+
1010
+ // SCARF - flowing cyan energy trail
1011
+ if (playerKnight.scarf) {
1012
+ const scarfX = player.pos.x - Math.cos(facingDir) * 0.5;
1013
+ const scarfZ = player.pos.z - Math.sin(facingDir) * 0.6;
1014
+ const flowY = Math.sin(gameTime * 4) * 0.15;
1015
+ setPosition(playerKnight.scarf, scarfX, player.pos.y + 1 + flowY, scarfZ);
1016
+ setRotation(playerKnight.scarf, 0, facingDir, Math.sin(gameTime * 2) * 0.15);
1017
+ }
1018
+
1019
+ // ARMS - with running animation
1020
+ const armSwing = Math.abs(player.vel.x) > 1 ? Math.sin(gameTime * 12) * 0.4 : 0;
1021
+ if (playerKnight.leftArm) {
1022
+ setPosition(playerKnight.leftArm, player.pos.x - 0.7, player.pos.y + 0.2, player.pos.z);
1023
+ setRotation(playerKnight.leftArm, armSwing, facingDir, 0);
1024
+ }
1025
+ if (playerKnight.rightArm) {
1026
+ setPosition(playerKnight.rightArm, player.pos.x + 0.7, player.pos.y + 0.2, player.pos.z);
1027
+ setRotation(playerKnight.rightArm, -armSwing, facingDir, 0);
1028
+ }
1029
+
1030
+ // GLOWING ARM GUARDS
1031
+ if (playerKnight.leftGuard) {
1032
+ setPosition(playerKnight.leftGuard, player.pos.x - 0.7, player.pos.y - 0.3, player.pos.z);
1033
+ }
1034
+ if (playerKnight.rightGuard) {
1035
+ setPosition(playerKnight.rightGuard, player.pos.x + 0.7, player.pos.y - 0.3, player.pos.z);
1036
+ }
1037
+
1038
+ // LEGS - with running animation
1039
+ const legSwing = Math.abs(player.vel.x) > 1 ? Math.sin(gameTime * 14) * 0.3 : 0;
1040
+ if (playerKnight.leftLeg) {
1041
+ setPosition(playerKnight.leftLeg, player.pos.x - 0.4, player.pos.y - 1.3, player.pos.z);
1042
+ setRotation(playerKnight.leftLeg, -legSwing, facingDir, 0);
1043
+ }
1044
+ if (playerKnight.rightLeg) {
1045
+ setPosition(playerKnight.rightLeg, player.pos.x + 0.4, player.pos.y - 1.3, player.pos.z);
1046
+ setRotation(playerKnight.rightLeg, legSwing, facingDir, 0);
1047
+ }
1048
+
1049
+ // KNEE GUARDS - cyan glow
1050
+ if (playerKnight.leftKnee) {
1051
+ setPosition(playerKnight.leftKnee, player.pos.x - 0.4, player.pos.y - 1, player.pos.z);
1052
+ }
1053
+ if (playerKnight.rightKnee) {
1054
+ setPosition(playerKnight.rightKnee, player.pos.x + 0.4, player.pos.y - 1, player.pos.z);
1055
+ }
1056
+ }
1057
+
1058
+ function performAttack() {
1059
+ // Create katana slash effect - purple arc
1060
+ const slashAngle = player.yaw; // Use yaw for direction
1061
+ const slashDir = Math.cos(player.yaw);
1062
+ const slashX = player.pos.x + slashDir * 1.5;
1063
+
1064
+ // Multiple slash particles for arc effect
1065
+ for (let i = 0; i < 8; i++) {
1066
+ const angle = slashAngle + (i - 4) * 0.3;
1067
+ const offset = 1.5;
1068
+ particles.push({
1069
+ mesh: createSphere(0.15, 0xaa44ff, [
1070
+ slashX + Math.cos(angle) * offset,
1071
+ player.pos.y + 0.5 + Math.sin(i * 0.5) * 0.5,
1072
+ player.pos.z,
1073
+ ]),
1074
+ vx: Math.cos(angle) * 3,
1075
+ vy: Math.sin(i * 0.5) * 2,
1076
+ vz: 0,
1077
+ life: 0.3,
1078
+ maxLife: 0.3,
1079
+ });
1080
+ }
1081
+
1082
+ // Check for enemy hits
1083
+ enemies.forEach(enemy => {
1084
+ const dx = enemy.x - player.pos.x;
1085
+ const dy = enemy.y - player.pos.y;
1086
+ const dz = enemy.z - player.pos.z;
1087
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
1088
+
1089
+ if (distance < 3) {
1090
+ // Combo system - more damage with higher combo
1091
+ const damage = 2 + Math.floor(combo / 5);
1092
+ enemy.health -= damage;
1093
+
1094
+ // Award combo
1095
+ combo++;
1096
+ comboTimer = 2.0;
1097
+
1098
+ createHitParticles(enemy.x, enemy.y, enemy.z);
1099
+
1100
+ if (enemy.health <= 0) {
1101
+ destroyMesh(enemy.mesh);
1102
+ enemy.dead = true;
1103
+ score += 100 * (1 + Math.floor(combo / 10)); // Bonus score for combos
1104
+ }
1105
+ }
1106
+ });
1107
+ }
1108
+
1109
+ function updateEnemies(dt) {
1110
+ enemies.forEach(enemy => {
1111
+ if (enemy.dead) return;
1112
+
1113
+ enemy.attackCooldown -= dt;
1114
+
1115
+ switch (enemy.type) {
1116
+ case 'patrol':
1117
+ // Patrol back and forth
1118
+ enemy.x += enemy.vx * dt;
1119
+
1120
+ if (Math.abs(enemy.x - enemy.originalX) > enemy.patrolRange) {
1121
+ enemy.vx *= -1;
1122
+ }
1123
+
1124
+ setPosition(enemy.mesh, enemy.x, enemy.y, enemy.z);
1125
+
1126
+ // Attack player if close
1127
+ {
1128
+ const distToPlayer = Math.sqrt(
1129
+ Math.pow(enemy.x - player.x, 2) + Math.pow(enemy.z - player.z, 2)
1130
+ );
1131
+
1132
+ if (distToPlayer < 4 && enemy.attackCooldown <= 0) {
1133
+ // Simple attack - damage player
1134
+ if (distToPlayer < 2) {
1135
+ player.health -= 10;
1136
+ enemy.attackCooldown = 2;
1137
+ }
1138
+ }
1139
+ }
1140
+ break;
1141
+
1142
+ case 'flyer':
1143
+ // Orbit around center point
1144
+ enemy.orbitAngle += dt * 2;
1145
+ enemy.x = enemy.orbitCenter.x + Math.cos(enemy.orbitAngle) * enemy.orbitRadius;
1146
+ enemy.z = enemy.orbitCenter.z + Math.sin(enemy.orbitAngle) * enemy.orbitRadius;
1147
+ enemy.y = enemy.orbitCenter.y + Math.sin(enemy.orbitAngle * 2) * 2;
1148
+
1149
+ setPosition(enemy.mesh, enemy.x, enemy.y, enemy.z);
1150
+ rotateMesh(enemy.mesh, 0, dt * 3, 0);
1151
+ break;
1152
+ }
1153
+ });
1154
+
1155
+ // Remove dead enemies
1156
+ enemies = enemies.filter(enemy => !enemy.dead);
1157
+ }
1158
+
1159
+ function updateCoins(dt) {
1160
+ coins.forEach(coin => {
1161
+ if (coin.collected) return;
1162
+
1163
+ // Animate coins
1164
+ coin.rotationY += dt * 4;
1165
+ coin.bobOffset += dt * 3;
1166
+ const newY = coin.y + Math.sin(coin.bobOffset) * 0.3;
1167
+
1168
+ setPosition(coin.mesh, coin.x, newY, coin.z);
1169
+ setRotation(coin.mesh, 0, coin.rotationY, 0);
1170
+
1171
+ // Check collection
1172
+ const distance = Math.sqrt(
1173
+ Math.pow(coin.x - player.x, 2) + Math.pow(newY - player.y, 2) + Math.pow(coin.z - player.z, 2)
1174
+ );
1175
+
1176
+ if (distance < 1.5) {
1177
+ coin.collected = true;
1178
+ destroyMesh(coin.mesh);
1179
+ player.coins += 10;
1180
+ score += 50;
1181
+ createCoinParticles(coin.x, coin.y, coin.z);
1182
+ }
1183
+ });
1184
+ }
1185
+
1186
+ function updateParticles(dt) {
1187
+ for (let i = particles.length - 1; i >= 0; i--) {
1188
+ const particle = particles[i];
1189
+ if (!particle || !particle.mesh) {
1190
+ particles.splice(i, 1);
1191
+ continue;
1192
+ }
1193
+
1194
+ particle.life -= dt;
1195
+
1196
+ const pos = getPosition(particle.mesh);
1197
+ if (!pos) {
1198
+ // Mesh doesn't exist anymore, remove particle
1199
+ particles.splice(i, 1);
1200
+ continue;
1201
+ }
1202
+
1203
+ pos[0] += particle.vx * dt;
1204
+ pos[1] += particle.vy * dt;
1205
+ pos[2] += particle.vz * dt;
1206
+
1207
+ // Shuriken don't have gravity and spin
1208
+ if (particle.isShuriken) {
1209
+ particle.rotation += dt * 20;
1210
+ setRotation(particle.mesh, 0, 0, particle.rotation);
1211
+ } else {
1212
+ particle.vy -= 10 * dt; // Gravity for normal particles
1213
+ }
1214
+
1215
+ setPosition(particle.mesh, pos[0], pos[1], pos[2]);
1216
+
1217
+ if (!particle.isShuriken) {
1218
+ const scale = particle.life / particle.maxLife;
1219
+ setScale(particle.mesh, scale, scale, scale);
1220
+ }
1221
+
1222
+ if (particle.life <= 0) {
1223
+ destroyMesh(particle.mesh);
1224
+ particles.splice(i, 1);
1225
+ }
1226
+ }
1227
+ }
1228
+
1229
+ function updateCamera(_dt) {
1230
+ // 📷 PERFECT SIDE-SCROLLER CAMERA (like Strider, Shinobi, Ninja Gaiden)
1231
+ // Camera follows player smoothly - optimal for gameplay!
1232
+
1233
+ camera.targetX = player.pos.x + 3; // Look slightly ahead
1234
+ camera.targetY = 5; // Center on action
1235
+ camera.targetZ = 0; // LOCKED - straight ahead view
1236
+
1237
+ // Smooth horizontal following only
1238
+ camera.x += (player.pos.x - camera.x) * 0.1;
1239
+ camera.y = 6; // Perfect eye-level view!
1240
+ camera.z = 18; // Close enough to see details, far enough to see platforms!
1241
+
1242
+ setCameraPosition(camera.x, camera.y, camera.z);
1243
+ setCameraTarget(camera.targetX, camera.targetY, camera.targetZ);
1244
+ }
1245
+
1246
+ function checkCollisions(dt) {
1247
+ // Already handled in updatePlayer and updateEnemies
1248
+ }
1249
+
1250
+ function updateGameLogic(dt) {
1251
+ // Check for level completion
1252
+ const remainingCoins = coins.filter(coin => !coin.collected).length;
1253
+ if (remainingCoins === 0) {
1254
+ level++;
1255
+ score += 1000;
1256
+ // Could spawn new level here
1257
+ }
1258
+
1259
+ // Game over check
1260
+ if (player.health <= 0) {
1261
+ gameState = 'gameOver';
1262
+ }
1263
+ }
1264
+
1265
+ function createJumpParticles() {
1266
+ for (let i = 0; i < 5; i++) {
1267
+ const particle = {
1268
+ mesh: createSphere(0.1, 0x88ccff, [player.x, player.y - 0.5, player.z]),
1269
+ vx: (Math.random() - 0.5) * 4,
1270
+ vy: Math.random() * 3,
1271
+ vz: (Math.random() - 0.5) * 4,
1272
+ life: 1,
1273
+ maxLife: 1,
1274
+ };
1275
+ particles.push(particle);
1276
+ }
1277
+ }
1278
+
1279
+ function createDoubleJumpParticles() {
1280
+ for (let i = 0; i < 8; i++) {
1281
+ const particle = {
1282
+ mesh: createSphere(0.15, 0xffff44, [player.x, player.y, player.z]),
1283
+ vx: (Math.random() - 0.5) * 6,
1284
+ vy: (Math.random() - 0.5) * 6,
1285
+ vz: (Math.random() - 0.5) * 6,
1286
+ life: 1.5,
1287
+ maxLife: 1.5,
1288
+ };
1289
+ particles.push(particle);
1290
+ }
1291
+ }
1292
+
1293
+ function createHitParticles(x, y, z) {
1294
+ for (let i = 0; i < 10; i++) {
1295
+ const particle = {
1296
+ mesh: createSphere(0.08, 0xff4444, [x, y, z]),
1297
+ vx: (Math.random() - 0.5) * 8,
1298
+ vy: Math.random() * 6,
1299
+ vz: (Math.random() - 0.5) * 8,
1300
+ life: 0.8,
1301
+ maxLife: 0.8,
1302
+ };
1303
+ particles.push(particle);
1304
+ }
1305
+ }
1306
+
1307
+ function createCoinParticles(x, y, z) {
1308
+ for (let i = 0; i < 6; i++) {
1309
+ const particle = {
1310
+ mesh: createSphere(0.05, 0xffdd00, [x, y, z]),
1311
+ vx: (Math.random() - 0.5) * 5,
1312
+ vy: Math.random() * 4 + 2,
1313
+ vz: (Math.random() - 0.5) * 5,
1314
+ life: 1.2,
1315
+ maxLife: 1.2,
1316
+ };
1317
+ particles.push(particle);
1318
+ }
1319
+ }
1320
+
1321
+ function createDashParticles() {
1322
+ for (let i = 0; i < 10; i++) {
1323
+ const particle = {
1324
+ mesh: createSphere(0.12, 0xaa44ff, [player.x, player.y + 0.5, player.z]),
1325
+ vx: (Math.random() - 0.5) * 3 - (player.facingRight ? 5 : -5),
1326
+ vy: (Math.random() - 0.5) * 2,
1327
+ vz: (Math.random() - 0.5) * 3,
1328
+ life: 0.5,
1329
+ maxLife: 0.5,
1330
+ };
1331
+ particles.push(particle);
1332
+ }
1333
+ }
1334
+
1335
+ function createAirDashParticles() {
1336
+ for (let i = 0; i < 15; i++) {
1337
+ const particle = {
1338
+ mesh: createSphere(0.15, 0x00ffff, [player.x, player.y, player.z]),
1339
+ vx: (Math.random() - 0.5) * 8,
1340
+ vy: (Math.random() - 0.5) * 8,
1341
+ vz: (Math.random() - 0.5) * 8,
1342
+ life: 0.8,
1343
+ maxLife: 0.8,
1344
+ };
1345
+ particles.push(particle);
1346
+ }
1347
+ }
1348
+
1349
+ function createSlideParticles() {
1350
+ for (let i = 0; i < 8; i++) {
1351
+ const particle = {
1352
+ mesh: createSphere(0.08, 0x8844aa, [player.x, player.y - 0.3, player.z]),
1353
+ vx: (Math.random() - 0.5) * 4 - (player.facingRight ? 3 : -3),
1354
+ vy: Math.random() * 2,
1355
+ vz: (Math.random() - 0.5) * 4,
1356
+ life: 0.6,
1357
+ maxLife: 0.6,
1358
+ };
1359
+ particles.push(particle);
1360
+ }
1361
+ }
1362
+
1363
+ function createGrappleEffect() {
1364
+ for (let i = 0; i < 12; i++) {
1365
+ const t = i / 12;
1366
+ const x = player.x + (player.grappleTarget.x - player.x) * t;
1367
+ const y = player.y + (player.grappleTarget.y - player.y) * t;
1368
+ const z = player.z + (player.grappleTarget.z - player.z) * t;
1369
+
1370
+ const particle = {
1371
+ mesh: createSphere(0.1, 0x00ffff, [x, y, z]),
1372
+ vx: 0,
1373
+ vy: 0,
1374
+ vz: 0,
1375
+ life: 0.4,
1376
+ maxLife: 0.4,
1377
+ };
1378
+ particles.push(particle);
1379
+ }
1380
+ }
1381
+
1382
+ function throwShuriken() {
1383
+ const shuriken = {
1384
+ mesh: createCube(0.3, 0xcccccc, [
1385
+ player.x + (player.facingRight ? 1 : -1),
1386
+ player.y + 0.5,
1387
+ player.z,
1388
+ ]),
1389
+ vx: (player.facingRight ? 1 : -1) * 25,
1390
+ vy: 0,
1391
+ vz: 0,
1392
+ life: 2,
1393
+ maxLife: 2,
1394
+ rotation: 0,
1395
+ isShuriken: true,
1396
+ };
1397
+ setScale(shuriken.mesh, 0.8, 0.1, 0.8);
1398
+ particles.push(shuriken);
1399
+
1400
+ // Check for enemy hits
1401
+ setTimeout(() => {
1402
+ enemies.forEach(enemy => {
1403
+ const dx = enemy.x - (player.x + (player.facingRight ? 5 : -5));
1404
+ const dy = enemy.y - player.y;
1405
+ const dz = enemy.z - player.z;
1406
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
1407
+
1408
+ if (distance < 3 && !enemy.dead) {
1409
+ enemy.health -= 1;
1410
+ createHitParticles(enemy.x, enemy.y, enemy.z);
1411
+ combo++;
1412
+ comboTimer = 2;
1413
+
1414
+ if (enemy.health <= 0) {
1415
+ destroyMesh(enemy.mesh);
1416
+ enemy.dead = true;
1417
+ score += 150;
1418
+ }
1419
+ }
1420
+ });
1421
+ }, 100);
1422
+ }
1423
+
1424
+ function findNearestGrapplePoint() {
1425
+ let nearest = null;
1426
+ let minDist = 20; // Max grapple range
1427
+
1428
+ player.grapplePoints.forEach(point => {
1429
+ if (!point.active) return;
1430
+
1431
+ const dx = point.x - player.x;
1432
+ const dy = point.y - player.y;
1433
+ const dz = point.z - player.z;
1434
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
1435
+
1436
+ if (dist < minDist) {
1437
+ minDist = dist;
1438
+ nearest = point;
1439
+ }
1440
+ });
1441
+
1442
+ return nearest;
1443
+ }
1444
+
1445
+ function drawUI() {
1446
+ // Ninja HUD Background - dark with purple/cyan accents
1447
+ rect(16, 16, 420, 100, rgba8(10, 10, 26, 200), true);
1448
+ rect(16, 16, 420, 100, rgba8(136, 51, 170, 180), false);
1449
+ rect(17, 17, 418, 98, rgba8(0, 255, 255, 80), false);
1450
+
1451
+ // Score and Level
1452
+ print(`SCORE: ${score.toString().padStart(8, '0')}`, 24, 24, rgba8(0, 255, 255, 255));
1453
+ print(`LEVEL: ${level}`, 24, 40, rgba8(170, 68, 255, 255));
1454
+ print(`COINS: ${player.coins}`, 24, 56, rgba8(255, 215, 0, 255));
1455
+
1456
+ // Shuriken count
1457
+ print(`SHURIKEN: ${player.shuriken}`, 24, 72, rgba8(200, 200, 200, 255));
1458
+
1459
+ // Combo meter
1460
+ if (combo > 0) {
1461
+ print(`COMBO x${combo}`, 24, 88, rgba8(255, 100, 255, 255));
1462
+ const comboBarWidth = Math.floor((comboTimer / 2.0) * 80);
1463
+ rect(100, 90, 80, 6, rgba8(50, 20, 60, 255), true);
1464
+ rect(100, 90, comboBarWidth, 6, rgba8(255, 100, 255, 255), true);
1465
+ }
1466
+
1467
+ // Health bar - red with dark background
1468
+ print('HEALTH:', 220, 24, rgba8(255, 255, 255, 255));
1469
+ rect(285, 22, 120, 10, rgba8(50, 0, 0, 255), true);
1470
+ rect(285, 22, Math.floor((player.health / 100) * 120), 10, rgba8(255, 0, 80, 255), true);
1471
+ rect(285, 22, 120, 10, rgba8(255, 0, 80, 100), false);
1472
+
1473
+ // Energy bar - cyan with dark background
1474
+ print('ENERGY:', 220, 42, rgba8(255, 255, 255, 255));
1475
+ rect(285, 40, 120, 10, rgba8(0, 20, 40, 255), true);
1476
+ rect(285, 40, Math.floor((player.energy / 100) * 120), 10, rgba8(0, 255, 255, 255), true);
1477
+ rect(285, 40, 120, 10, rgba8(0, 255, 255, 100), false);
1478
+
1479
+ // Ability indicators
1480
+ print('ABILITIES:', 220, 60, rgba8(200, 200, 200, 255));
1481
+
1482
+ // Dash indicator
1483
+ const dashReady = player.dashCooldown <= 0 && player.energy >= 20;
1484
+ rect(285, 58, 24, 8, dashReady ? rgba8(170, 68, 255, 255) : rgba8(50, 20, 60, 255), true);
1485
+ print('DSH', 288, 60, rgba8(255, 255, 255, 255));
1486
+
1487
+ // Air Dash indicator
1488
+ const airDashReady = player.airDashAvailable && player.energy >= 25;
1489
+ rect(312, 58, 24, 8, airDashReady ? rgba8(0, 255, 255, 255) : rgba8(20, 50, 60, 255), true);
1490
+ print('AIR', 315, 60, rgba8(255, 255, 255, 255));
1491
+
1492
+ // Grapple indicator
1493
+ const grappleReady = findNearestGrapplePoint() !== null;
1494
+ rect(339, 58, 24, 8, grappleReady ? rgba8(0, 255, 255, 255) : rgba8(20, 50, 60, 255), true);
1495
+ print('GRP', 342, 60, rgba8(255, 255, 255, 255));
1496
+
1497
+ // Shuriken indicator
1498
+ const shurikenReady = player.shuriken > 0 && player.energy >= 5;
1499
+ rect(366, 58, 24, 8, shurikenReady ? rgba8(200, 200, 200, 255) : rgba8(40, 40, 40, 255), true);
1500
+ print('SHR', 369, 60, rgba8(255, 255, 255, 255));
1501
+
1502
+ // 3D Stats - smaller and in corner
1503
+ const stats = get3DStats();
1504
+ if (stats) {
1505
+ print(`${stats.meshes || 0}m`, 580, 24, rgba8(100, 100, 100, 200));
1506
+ }
1507
+
1508
+ // Position info - debug mode
1509
+ // print(`POS: ${player.x.toFixed(1)}, ${player.y.toFixed(1)}, ${player.z.toFixed(1)}`, 220, 90, rgba8(100, 100, 100, 150));
1510
+
1511
+ // Controls hint
1512
+ print('X=DASH C=SHURIKEN G=GRAPPLE Z=ATTACK', 16, 340, rgba8(136, 51, 170, 200));
1513
+
1514
+ if (gameState === 'gameOver') {
1515
+ rect(0, 0, 640, 360, rgba8(0, 0, 0, 200), true);
1516
+ print('GAME OVER', 260, 150, rgba8(255, 50, 50, 255));
1517
+ print(`FINAL SCORE: ${score}`, 230, 180, rgba8(255, 255, 0, 255));
1518
+ print(`COINS COLLECTED: ${player.coins}`, 220, 200, rgba8(255, 215, 0, 255));
1519
+ print('PRESS R TO RESTART', 220, 240, rgba8(255, 255, 255, 255));
1520
+
1521
+ if (btnp(17)) {
1522
+ // R key
1523
+ // Reset game
1524
+ score = 0;
1525
+ level = 1;
1526
+ player.health = 100;
1527
+ player.energy = 100;
1528
+ player.coins = 0;
1529
+ player.x = 0;
1530
+ player.y = 2;
1531
+ player.z = 0;
1532
+ gameState = 'playing';
1533
+ clearScene();
1534
+ init();
1535
+ }
1536
+ }
1537
+ }