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,1169 @@
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
+
5
+ // Game state
6
+ let gameTime = 0;
7
+ let gameState = 'start'; // 'start', 'playing', 'gameOver'
8
+ let startScreenTime = 0;
9
+ let uiButtons = [];
10
+ let score = 0;
11
+ let level = 1;
12
+ let combo = 0;
13
+ let comboTimer = 0;
14
+
15
+ // 3D Game objects
16
+ let playerKnight = null;
17
+ let platforms = [];
18
+ let enemies = [];
19
+ let coins = [];
20
+ let particles = [];
21
+ let environment = [];
22
+
23
+ // Player state - NINJA POWERS!
24
+ let player = {
25
+ x: 0, y: 0, z: 0,
26
+ vx: 0, vy: 0, vz: 0,
27
+ onGround: false,
28
+ health: 100,
29
+ energy: 100,
30
+ coins: 0,
31
+ shuriken: 20,
32
+ facingRight: true,
33
+ jumpPower: 10,
34
+ speed: 8,
35
+ doubleJump: true,
36
+ attackCooldown: 0,
37
+ // Ninja abilities
38
+ wallRunning: false,
39
+ wallRunTime: 0,
40
+ maxWallRunTime: 1.5,
41
+ dashCooldown: 0,
42
+ dashDuration: 0,
43
+ crouching: false,
44
+ stealth: false,
45
+ grapplePoints: [],
46
+ grappling: false,
47
+ grappleTarget: null,
48
+ airDashAvailable: true,
49
+ slideDuration: 0
50
+ };
51
+
52
+ // Camera state
53
+ let camera = {
54
+ x: 0, y: 8, z: 15,
55
+ targetX: 0, targetY: 5, targetZ: 0,
56
+ smoothing: 0.1
57
+ };
58
+
59
+ export function init() {
60
+ // Clear all arrays to prevent mesh errors
61
+ enemies = []
62
+ coins = []
63
+ particles = []
64
+ platforms = []
65
+ environment = []
66
+ playerKnight = null
67
+
68
+ // Reset player state completely
69
+ player = {
70
+ pos: vec3(0, 5, 0),
71
+ vel: vec3(0, 0, 0),
72
+ yaw: 0,
73
+ grounded: false,
74
+ attacking: false,
75
+ attackTime: 0,
76
+ health: 100,
77
+ maxHealth: 100,
78
+ stamina: 100,
79
+ maxStamina: 100,
80
+ isDashing: false,
81
+ dashTime: 0,
82
+ wallRunning: false,
83
+ wallRunTime: 0,
84
+ wallSide: 0,
85
+ doubleJumpAvailable: true,
86
+ grappling: false,
87
+ grappleTarget: null,
88
+ sliding: false,
89
+ slideTime: 0,
90
+ shurikenCount: 10
91
+ }
92
+
93
+ score = 0
94
+ combo = 0
95
+ comboTime = 0
96
+ gameTime = 0
97
+ level = 1
98
+ gameState = 'start'
99
+ startScreenTime = 0
100
+
101
+ // Force canvas focus for keyboard events
102
+ console.log('🎮 Focusing canvas for input...')
103
+ const canvas = document.querySelector('canvas')
104
+ if (canvas) {
105
+ canvas.focus()
106
+ canvas.tabIndex = 1
107
+ }
108
+
109
+ createNinjaPlayer()
110
+ createNinjaWorld()
111
+ createPlatforms()
112
+ createGrapplePoints()
113
+ spawnEnemies()
114
+ spawnCoins()
115
+ }
116
+
117
+ function initStartScreen() {
118
+ uiButtons = [];
119
+ console.log('🥷 Start screen initialized');
120
+ }
121
+
122
+ export function update() {
123
+ const dt = 1/60;
124
+
125
+ // Handle start screen
126
+ if (gameState === 'start') {
127
+ startScreenTime += dt;
128
+
129
+ // KEYBOARD FALLBACK: Press ENTER or SPACE to start
130
+ if (isKeyPressed('Enter') || isKeyPressed(' ') || isKeyPressed('Space')) {
131
+ console.log('🥷 Starting Shadow Ninja 3D via keyboard!');
132
+ gameState = 'playing';
133
+ return;
134
+ }
135
+
136
+ // Check for any button press to start
137
+ for (let i = 0; i < 10; i++) {
138
+ if (btnp(i)) {
139
+ console.log(`🥷 Starting Shadow Ninja 3D via button ${i}!`);
140
+ gameState = 'playing';
141
+ return;
142
+ }
143
+ }
144
+
145
+ return;
146
+ }
147
+
148
+ // Playing state - main game loop
149
+ gameTime += dt;
150
+
151
+ // Update input
152
+ updateInput(dt);
153
+
154
+ // Update player
155
+ updatePlayer(dt);
156
+
157
+ // Update enemies
158
+ updateEnemies(dt);
159
+
160
+ // Update coins
161
+ updateCoins(dt);
162
+
163
+ // Update particles
164
+ updateParticles(dt);
165
+
166
+ // Update combo timer
167
+ if (comboTime > 0) {
168
+ comboTime -= dt;
169
+ if (comboTime <= 0) {
170
+ combo = 0;
171
+ }
172
+ }
173
+
174
+ // Check for player death
175
+ if (player.health <= 0) {
176
+ gameState = 'gameover';
177
+ console.log('💀 Game Over! Final Score:', score);
178
+ }
179
+ }
180
+
181
+ export function draw() {
182
+ if (gameState === 'start') {
183
+
184
+ export function draw() {
185
+ if (gameState === 'start') {
186
+ drawStartScreen();
187
+ return;
188
+ }
189
+
190
+ // 3D scene is automatically rendered by GPU backend
191
+ // Draw UI overlay using 2D API
192
+ drawUI();
193
+ }
194
+
195
+ function drawStartScreen() {
196
+ // Dark background
197
+ rect(0, 0, 640, 360, rgba8(10, 5, 30, 255), true);
198
+
199
+ // Title with glow effect
200
+ print('SHADOW NINJA 3D', 180, 70, rgba8(180, 80, 255, 255));
201
+
202
+ // Subtitle
203
+ const pulse = Math.sin(startScreenTime * 3) * 0.25 + 0.75;
204
+ print('Strider-Style 3.5D Ninja Platformer', 160, 100, rgba8(200, 150, 255, Math.floor(pulse * 255)));
205
+
206
+ // Controls panel
207
+ rect(100, 140, 440, 160, rgba8(20, 10, 40, 220), true);
208
+ rect(100, 140, 440, 160, rgba8(150, 80, 255, 255), false);
209
+
210
+ // Controls title
211
+ print('NINJA ABILITIES:', 220, 155, rgba8(255, 200, 255, 255));
212
+
213
+ // Controls - two columns
214
+ print('ARROWS = Move', 120, 180, rgba8(200, 200, 255, 255));
215
+ print('DOWN = Slide', 120, 200, rgba8(200, 200, 255, 255));
216
+ print('UP = Jump/Double Jump', 120, 220, rgba8(200, 200, 255, 255));
217
+ print('Z = Attack', 120, 240, rgba8(200, 200, 255, 255));
218
+ print('X = Dash/Air Dash', 120, 260, rgba8(200, 200, 255, 255));
219
+
220
+ print('C = Throw Shuriken', 340, 180, rgba8(200, 200, 255, 255));
221
+ print('G = Grappling Hook', 340, 200, rgba8(200, 200, 255, 255));
222
+ print('Wall Running Active', 340, 220, rgba8(150, 255, 150, 255));
223
+ print('Combo System', 340, 240, rgba8(255, 255, 150, 255));
224
+ print('Energy Management', 340, 260, rgba8(150, 200, 255, 255));
225
+
226
+ // Pulsing prompt - larger and centered
227
+ const alpha = Math.floor((Math.sin(startScreenTime * 5) * 0.5 + 0.5) * 255);
228
+ print('CLICK SCREEN OR PRESS ANY KEY', 180, 320, rgba8(255, 180, 255, alpha));
229
+
230
+ // Debug helper - show if keys are being detected
231
+ if (startScreenTime > 1) {
232
+ print('(Click on game screen to focus)', 190, 335, rgba8(150, 150, 150, 200));
233
+ }
234
+ }
235
+
236
+ function createNinjaPlayer() {
237
+ // Create ninja body - sleek and dark
238
+ const body = createCube(0.6, 0x2a2a3a, [0, 0, 0]);
239
+ setScale(body, 0.7, 1.1, 0.5);
240
+
241
+ // Create ninja mask/head - mysterious
242
+ const head = createCube(0.5, 0x1a1a2a, [0, 0.75, 0]);
243
+ setScale(head, 0.6, 0.5, 0.5);
244
+
245
+ // Create eyes - glowing
246
+ const eyes = createCube(0.1, 0x00ffff, [0, 0.8, 0.25]);
247
+ setScale(eyes, 0.4, 0.1, 0.1);
248
+
249
+ // Create katana - sleek blade
250
+ const katana = createCube(0.1, 0xccccff, [0.7, 0.5, 0]);
251
+ setScale(katana, 0.1, 1.3, 0.1);
252
+
253
+ // Create scarf - flowing
254
+ const scarf = createCube(0.3, 0x8833aa, [0, 0.3, -0.5]);
255
+ setScale(scarf, 0.8, 0.6, 0.3);
256
+
257
+ return { body, head, eyes, katana, scarf };
258
+ }
259
+
260
+ async function createNinjaWorld() {
261
+ // Create dark ground - rooftop aesthetic
262
+ const ground = createPlane(150, 150, 0x1a1a2a, [0, -1, 0]);
263
+ setRotation(ground, -Math.PI/2, 0, 0);
264
+
265
+ // Create cyberpunk city buildings in background
266
+ for (let i = 0; i < 12; i++) {
267
+ const building = createCube(6 + Math.random() * 4, 15 + Math.random() * 20, 0x2a2a4a, [
268
+ (i - 6) * 18 + (Math.random() - 0.5) * 8,
269
+ 8,
270
+ -50 - Math.random() * 30
271
+ ]);
272
+ setScale(building, 1, 1, 1);
273
+
274
+ // Add neon accents on buildings
275
+ if (Math.random() > 0.5) {
276
+ const neon = createCube(0.3, 1, 0xff00ff, [
277
+ (i - 6) * 18 + (Math.random() - 0.5) * 8,
278
+ 12 + Math.random() * 10,
279
+ -50 - Math.random() * 30
280
+ ]);
281
+ setScale(neon, 4, 0.2, 0.1);
282
+ }
283
+ }
284
+
285
+ // Create vertical wall structures - for wall running
286
+ for (let i = 0; i < 8; i++) {
287
+ const wall = createCube(0.5, 8, 0x3a3a5a, [
288
+ (i - 4) * 15,
289
+ 4,
290
+ -20 + (Math.random() - 0.5) * 10
291
+ ]);
292
+ setScale(wall, 4, 1, 0.5);
293
+ }
294
+
295
+ // Create neon light posts
296
+ for (let i = 0; i < 10; i++) {
297
+ const post = createCube(0.2, 4, 0x4a4a6a, [
298
+ (Math.random() - 0.5) * 100,
299
+ 2,
300
+ (Math.random() - 0.5) * 80
301
+ ]);
302
+ setScale(post, 1, 1, 1);
303
+
304
+ // Neon light on top
305
+ const light = createSphere(0.3, 0x00ffff, [
306
+ (Math.random() - 0.5) * 100,
307
+ 4.5,
308
+ (Math.random() - 0.5) * 80
309
+ ]);
310
+ }
311
+ }
312
+
313
+ function createPlatforms() {
314
+ platforms = [];
315
+
316
+ // Ground level platforms - rooftop tiles
317
+ for (let i = -6; i <= 6; i++) {
318
+ const platform = {
319
+ mesh: createCube(4, 0.4, 0x3a3a4a, [i * 7, -0.2, 0]),
320
+ x: i * 7, y: -0.2, z: 0,
321
+ width: 4, height: 0.4, depth: 5,
322
+ type: 'ground'
323
+ };
324
+ setScale(platform.mesh, 1, 1, 5);
325
+ platforms.push(platform);
326
+ }
327
+
328
+ // Floating platforms - ninja parkour course with varied heights
329
+ const platformData = [
330
+ { x: 12, y: 3, z: 8, color: 0x4a4a6a },
331
+ { x: 22, y: 6, z: 12, color: 0x5a3a7a },
332
+ { x: 32, y: 9, z: 10, color: 0x4a4a6a },
333
+ { x: 42, y: 12, z: 5, color: 0x5a3a7a },
334
+ { x: 28, y: 15, z: -5, color: 0x4a4a6a },
335
+ { x: 15, y: 18, z: -12, color: 0x5a3a7a },
336
+ { x: 0, y: 20, z: -15, color: 0x4a4a6a },
337
+ { x: -15, y: 17, z: -18, color: 0x5a3a7a },
338
+ { x: -28, y: 13, z: -12, color: 0x4a4a6a },
339
+ { x: -35, y: 9, z: -5, color: 0x5a3a7a },
340
+ { x: -42, y: 6, z: 5, color: 0x4a4a6a },
341
+ { x: -30, y: 4, z: 10, color: 0x5a3a7a }
342
+ ];
343
+
344
+ platformData.forEach((data, i) => {
345
+ const platform = {
346
+ mesh: createCube(3.5, 0.6, data.color, [data.x, data.y, data.z]),
347
+ x: data.x, y: data.y, z: data.z,
348
+ width: 3.5, height: 0.6, depth: 3.5,
349
+ type: 'floating',
350
+ id: i
351
+ };
352
+ setScale(platform.mesh, 1, 1, 1);
353
+ platforms.push(platform);
354
+
355
+ // Add neon edge lights to floating platforms
356
+ const edgeLight = createCube(3.6, 0.1, 0xff00ff, [data.x, data.y - 0.3, data.z]);
357
+ setScale(edgeLight, 1, 1, 1);
358
+ });
359
+ }
360
+
361
+ function createGrapplePoints() {
362
+ player.grapplePoints = [];
363
+
364
+ // Add grapple points in strategic locations
365
+ const grappleData = [
366
+ { x: 18, y: 12, z: 5 },
367
+ { x: 35, y: 15, z: 8 },
368
+ { x: 20, y: 22, z: -8 },
369
+ { x: -5, y: 24, z: -18 },
370
+ { x: -30, y: 18, z: -10 },
371
+ { x: -40, y: 12, z: 2 }
372
+ ];
373
+
374
+ grappleData.forEach(data => {
375
+ const point = {
376
+ mesh: createSphere(0.4, 0x00ffff, [data.x, data.y, data.z]),
377
+ x: data.x, y: data.y, z: data.z,
378
+ active: true
379
+ };
380
+ player.grapplePoints.push(point);
381
+ });
382
+ }
383
+
384
+ function spawnEnemies() {
385
+ enemies = [];
386
+
387
+ // Ground patrol enemies
388
+ for (let i = 0; i < 5; i++) {
389
+ const enemy = {
390
+ mesh: createCube(0.6, 0xff4444, [(i - 2) * 15, 0.5, 8]),
391
+ x: (i - 2) * 15, y: 0.5, z: 8,
392
+ vx: (Math.random() > 0.5 ? 1 : -1) * 2,
393
+ health: 3,
394
+ type: 'patrol',
395
+ patrolRange: 10,
396
+ originalX: (i - 2) * 15,
397
+ attackCooldown: 0
398
+ };
399
+ setScale(enemy.mesh, 0.8, 0.8, 0.8);
400
+ enemies.push(enemy);
401
+ }
402
+
403
+ // Flying enemies
404
+ for (let i = 0; i < 3; i++) {
405
+ const enemy = {
406
+ mesh: createSphere(0.4, 0xff8844, [i * 20 - 20, 8 + i * 2, 0]),
407
+ x: i * 20 - 20, y: 8 + i * 2, z: 0,
408
+ vx: Math.sin(i) * 3, vy: 0, vz: Math.cos(i) * 2,
409
+ health: 2,
410
+ type: 'flyer',
411
+ orbitCenter: { x: i * 20 - 20, y: 8 + i * 2, z: 0 },
412
+ orbitRadius: 5,
413
+ orbitAngle: i * 2,
414
+ attackCooldown: 0
415
+ };
416
+ enemies.push(enemy);
417
+ }
418
+ }
419
+
420
+ function spawnCoins() {
421
+ coins = [];
422
+
423
+ // Place coins on platforms
424
+ platforms.forEach((platform, i) => {
425
+ if (i > 8) { // Skip ground platforms
426
+ const coin = {
427
+ mesh: createSphere(0.3, 0xffdd00, [platform.x, platform.y + 1.5, platform.z]),
428
+ x: platform.x, y: platform.y + 1.5, z: platform.z,
429
+ collected: false,
430
+ rotationY: 0,
431
+ bobOffset: Math.random() * Math.PI * 2
432
+ };
433
+ coins.push(coin);
434
+ }
435
+ });
436
+
437
+ // Bonus coins in hard to reach places
438
+ for (let i = 0; i < 8; i++) {
439
+ const coin = {
440
+ mesh: createSphere(0.25, 0xffaa00, [
441
+ (Math.random() - 0.5) * 60,
442
+ 5 + Math.random() * 10,
443
+ (Math.random() - 0.5) * 30
444
+ ]),
445
+ x: 0, y: 0, z: 0,
446
+ collected: false,
447
+ rotationY: 0,
448
+ bobOffset: Math.random() * Math.PI * 2
449
+ };
450
+ const pos = getPosition(coin.mesh);
451
+ coin.x = pos[0]; coin.y = pos[1]; coin.z = pos[2];
452
+ coins.push(coin);
453
+ }
454
+ }
455
+
456
+ function updateInput(dt) {
457
+ const moveSpeed = player.dashDuration > 0 ? player.speed * 2.5 : player.speed;
458
+
459
+ // Update cooldowns
460
+ player.attackCooldown -= dt;
461
+ player.dashCooldown -= dt;
462
+ player.dashDuration -= dt;
463
+ player.slideDuration -= dt;
464
+
465
+ // Sliding
466
+ if (player.slideDuration > 0) {
467
+ player.vx = (player.facingRight ? 1 : -1) * moveSpeed * 1.5;
468
+ player.crouching = true;
469
+ return; // Override other controls during slide
470
+ } else {
471
+ player.crouching = false;
472
+ }
473
+
474
+ // Grappling hook movement
475
+ if (player.grappling && player.grappleTarget) {
476
+ const dx = player.grappleTarget.x - player.x;
477
+ const dy = player.grappleTarget.y - player.y;
478
+ const dz = player.grappleTarget.z - player.z;
479
+ const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
480
+
481
+ if (dist > 1.5) {
482
+ const pullSpeed = 20;
483
+ player.vx = (dx / dist) * pullSpeed;
484
+ player.vy = (dy / dist) * pullSpeed;
485
+ player.vz = (dz / dist) * pullSpeed;
486
+ } else {
487
+ player.grappling = false;
488
+ player.grappleTarget = null;
489
+ }
490
+ return; // Override other controls during grapple
491
+ }
492
+
493
+ // Horizontal movement
494
+ if (btn(0)) { // Left
495
+ player.vx = -moveSpeed;
496
+ player.facingRight = false;
497
+ } else if (btn(1)) { // Right
498
+ player.vx = moveSpeed;
499
+ player.facingRight = true;
500
+ } else {
501
+ player.vx *= 0.85; // Friction
502
+ }
503
+
504
+ // Forward/backward movement
505
+ if (btn(3) && player.onGround && !player.crouching) { // Down + on ground = slide
506
+ player.slideDuration = 0.5;
507
+ player.energy -= 5;
508
+ createSlideParticles();
509
+ } else if (btn(3)) { // Down in air
510
+ player.vz = moveSpeed * 0.6;
511
+ } else {
512
+ player.vz *= 0.85;
513
+ }
514
+
515
+ // Jump and double jump
516
+ if (btnp(2) && player.onGround) { // Up - jump when on ground
517
+ player.vy = player.jumpPower;
518
+ player.onGround = false;
519
+ player.doubleJump = true;
520
+ player.airDashAvailable = true;
521
+ createJumpParticles();
522
+ } else if (btnp(2) && player.doubleJump && player.energy >= 15) { // Double jump
523
+ player.vy = player.jumpPower * 0.9;
524
+ player.doubleJump = false;
525
+ player.energy -= 15;
526
+ createDoubleJumpParticles();
527
+ }
528
+
529
+ // Dash (X key) - ground dash or air dash
530
+ if (btnp(5) && player.dashCooldown <= 0 && player.energy >= 20) {
531
+ if (player.onGround) {
532
+ // Ground dash
533
+ player.dashDuration = 0.3;
534
+ player.dashCooldown = 0.8;
535
+ player.energy -= 20;
536
+ createDashParticles();
537
+ } else if (player.airDashAvailable) {
538
+ // Air dash
539
+ player.vx = (player.facingRight ? 1 : -1) * player.speed * 3;
540
+ player.vy = 0;
541
+ player.airDashAvailable = false;
542
+ player.dashCooldown = 1.0;
543
+ player.energy -= 25;
544
+ createAirDashParticles();
545
+ }
546
+ }
547
+
548
+ // Shuriken throw (C key)
549
+ if (btnp(6) && player.shuriken > 0 && player.energy >= 5) {
550
+ throwShuriken();
551
+ player.shuriken--;
552
+ player.energy -= 5;
553
+ }
554
+
555
+ // Grapple hook (G key or Space)
556
+ if (btnp(7) || btnp(4)) {
557
+ const nearestGrapple = findNearestGrapplePoint();
558
+ if (nearestGrapple && !player.grappling) {
559
+ player.grappling = true;
560
+ player.grappleTarget = nearestGrapple;
561
+ createGrappleEffect();
562
+ }
563
+ }
564
+
565
+ // Melee attack (Z key)
566
+ if (btn(4) && player.attackCooldown <= 0) {
567
+ performAttack();
568
+ player.attackCooldown = 0.4;
569
+ }
570
+ }
571
+
572
+ function updatePlayer(dt) {
573
+ // Update combo timer
574
+ if (comboTimer > 0) {
575
+ comboTimer -= dt;
576
+ if (comboTimer <= 0) {
577
+ combo = 0;
578
+ }
579
+ }
580
+
581
+ // Check for wall running
582
+ player.wallRunning = false;
583
+ if (!player.onGround && Math.abs(player.vx) > 5) {
584
+ // Check if near any wall structure
585
+ for (let i = 0; i < 8; i++) {
586
+ const wallX = (i - 4) * 15;
587
+ const wallZ = i % 2 === 0 ? -20 : -30;
588
+
589
+ const distToWall = Math.sqrt((player.x - wallX) ** 2 + (player.z - wallZ) ** 2);
590
+ if (distToWall < 3) {
591
+ player.wallRunning = true;
592
+ player.wallRunTime += dt;
593
+
594
+ // Wall running slows vertical fall and provides slight upward boost
595
+ player.vy = Math.max(player.vy, -5);
596
+ if (player.wallRunTime < player.maxWallRunTime) {
597
+ player.vy += 3 * dt; // Slight upward boost
598
+ }
599
+ break;
600
+ }
601
+ }
602
+ }
603
+
604
+ // Reset wall run time if not wall running
605
+ if (!player.wallRunning) {
606
+ player.wallRunTime = 0;
607
+ }
608
+
609
+ // Apply gravity (reduced during wall run)
610
+ const gravityMultiplier = player.wallRunning ? 0.3 : 1.0;
611
+ player.vy -= 25 * gravityMultiplier * dt;
612
+
613
+ // Update position
614
+ player.x += player.vx * dt;
615
+ player.y += player.vy * dt;
616
+ player.z += player.vz * dt;
617
+
618
+ // Platform collision detection
619
+ player.onGround = false;
620
+
621
+ for (const platform of platforms) {
622
+ // Simple AABB collision - adjust for crouching
623
+ const hitboxHeight = player.crouching ? 0.8 : 1.0;
624
+
625
+ if (player.x > platform.x - platform.width/2 &&
626
+ player.x < platform.x + platform.width/2 &&
627
+ player.z > platform.z - platform.depth/2 &&
628
+ player.z < platform.z + platform.depth/2) {
629
+
630
+ if (player.y <= platform.y + platform.height &&
631
+ player.y + player.vy * dt > platform.y + platform.height) {
632
+ player.y = platform.y + platform.height;
633
+ player.vy = 0;
634
+ player.onGround = true;
635
+ player.doubleJump = true;
636
+ player.airDashAvailable = true; // Reset air dash on landing
637
+ }
638
+ }
639
+ }
640
+
641
+ // World boundaries
642
+ if (player.x < -50) player.x = -50;
643
+ if (player.x > 50) player.x = 50;
644
+ if (player.z < -30) player.z = -30;
645
+ if (player.z > 30) player.z = 30;
646
+
647
+ // Fall reset
648
+ if (player.y < -10) {
649
+ player.x = 0;
650
+ player.y = 2;
651
+ player.z = 0;
652
+ player.vx = 0;
653
+ player.vy = 0;
654
+ player.vz = 0;
655
+ player.health -= 20;
656
+ combo = 0;
657
+ comboTimer = 0;
658
+ }
659
+
660
+ // Update ninja meshes
661
+ updatePlayerMeshes();
662
+
663
+ // Regenerate energy
664
+ if (player.energy < 100) {
665
+ player.energy += 25 * dt;
666
+ }
667
+ }
668
+
669
+ function updatePlayerMeshes() {
670
+ // Safety check - make sure player meshes exist
671
+ if (!playerKnight || !playerKnight.body || !playerKnight.head ||
672
+ !playerKnight.eyes || !playerKnight.katana || !playerKnight.scarf) {
673
+ return;
674
+ }
675
+
676
+ const bodyY = player.crouching ? player.y - 0.2 : player.y;
677
+
678
+ setPosition(playerKnight.body, player.x, bodyY, player.z);
679
+ setPosition(playerKnight.head, player.x, bodyY + 0.75, player.z);
680
+ setPosition(playerKnight.eyes, player.x, bodyY + 0.8, player.z + (player.facingRight ? 0.25 : -0.25));
681
+
682
+ // Katana position based on facing direction
683
+ const katanaX = player.facingRight ? player.x + 0.7 : player.x - 0.7;
684
+ const katanaRotY = player.facingRight ? -0.3 : Math.PI + 0.3;
685
+ setPosition(playerKnight.katana, katanaX, bodyY + 0.5, player.z);
686
+ setRotation(playerKnight.katana, 0, katanaRotY, 0);
687
+
688
+ // Animate based on movement
689
+ const speed = Math.sqrt(player.vx * player.vx + player.vz * player.vz);
690
+ const walkCycle = Math.sin(gameTime * (speed + 1) * 2) * 0.08;
691
+ const facing = player.facingRight ? 0 : Math.PI;
692
+ setRotation(playerKnight.body, walkCycle, facing, 0);
693
+
694
+ // Scarf physics - flowing behind ninja
695
+ const scarfSwing = Math.sin(gameTime * 5) * 0.25;
696
+ const scarfZ = player.z + (player.facingRight ? -0.5 : 0.5);
697
+ setPosition(playerKnight.scarf, player.x, bodyY + 0.3, scarfZ);
698
+ setRotation(playerKnight.scarf, scarfSwing, facing, 0);
699
+
700
+ // Dash effect - tilt body
701
+ if (player.dashDuration > 0) {
702
+ const tilt = player.facingRight ? -0.4 : 0.4;
703
+ setRotation(playerKnight.body, tilt, facing, 0);
704
+ }
705
+ }
706
+
707
+ function performAttack() {
708
+ // Create katana slash effect - purple arc
709
+ const slashAngle = player.facingRight ? 0 : Math.PI;
710
+ const slashX = player.x + (player.facingRight ? 1.5 : -1.5);
711
+
712
+ // Multiple slash particles for arc effect
713
+ for (let i = 0; i < 8; i++) {
714
+ const angle = slashAngle + (i - 4) * 0.3;
715
+ const offset = 1.5;
716
+ particles.push({
717
+ mesh: createSphere(0.15, 0xaa44ff, [
718
+ slashX + Math.cos(angle) * offset,
719
+ player.y + 0.5 + Math.sin(i * 0.5) * 0.5,
720
+ player.z
721
+ ]),
722
+ vx: Math.cos(angle) * 3,
723
+ vy: Math.sin(i * 0.5) * 2,
724
+ vz: 0,
725
+ life: 0.3,
726
+ maxLife: 0.3
727
+ });
728
+ }
729
+
730
+ // Check for enemy hits
731
+ enemies.forEach(enemy => {
732
+ const dx = enemy.x - player.x;
733
+ const dy = enemy.y - player.y;
734
+ const dz = enemy.z - player.z;
735
+ const distance = Math.sqrt(dx*dx + dy*dy + dz*dz);
736
+
737
+ if (distance < 3) {
738
+ // Combo system - more damage with higher combo
739
+ const damage = 2 + Math.floor(combo / 5);
740
+ enemy.health -= damage;
741
+
742
+ // Award combo
743
+ combo++;
744
+ comboTimer = 2.0;
745
+
746
+ createHitParticles(enemy.x, enemy.y, enemy.z);
747
+
748
+ if (enemy.health <= 0) {
749
+ destroyMesh(enemy.mesh);
750
+ enemy.dead = true;
751
+ score += 100 * (1 + Math.floor(combo / 10)); // Bonus score for combos
752
+ }
753
+ }
754
+ });
755
+ }
756
+
757
+ function updateEnemies(dt) {
758
+ enemies.forEach(enemy => {
759
+ if (enemy.dead) return;
760
+
761
+ enemy.attackCooldown -= dt;
762
+
763
+ switch (enemy.type) {
764
+ case 'patrol':
765
+ // Patrol back and forth
766
+ enemy.x += enemy.vx * dt;
767
+
768
+ if (Math.abs(enemy.x - enemy.originalX) > enemy.patrolRange) {
769
+ enemy.vx *= -1;
770
+ }
771
+
772
+ setPosition(enemy.mesh, enemy.x, enemy.y, enemy.z);
773
+
774
+ // Attack player if close
775
+ const distToPlayer = Math.sqrt(
776
+ Math.pow(enemy.x - player.x, 2) +
777
+ Math.pow(enemy.z - player.z, 2)
778
+ );
779
+
780
+ if (distToPlayer < 4 && enemy.attackCooldown <= 0) {
781
+ // Simple attack - damage player
782
+ if (distToPlayer < 2) {
783
+ player.health -= 10;
784
+ enemy.attackCooldown = 2;
785
+ }
786
+ }
787
+ break;
788
+
789
+ case 'flyer':
790
+ // Orbit around center point
791
+ enemy.orbitAngle += dt * 2;
792
+ enemy.x = enemy.orbitCenter.x + Math.cos(enemy.orbitAngle) * enemy.orbitRadius;
793
+ enemy.z = enemy.orbitCenter.z + Math.sin(enemy.orbitAngle) * enemy.orbitRadius;
794
+ enemy.y = enemy.orbitCenter.y + Math.sin(enemy.orbitAngle * 2) * 2;
795
+
796
+ setPosition(enemy.mesh, enemy.x, enemy.y, enemy.z);
797
+ rotateMesh(enemy.mesh, 0, dt * 3, 0);
798
+ break;
799
+ }
800
+ });
801
+
802
+ // Remove dead enemies
803
+ enemies = enemies.filter(enemy => !enemy.dead);
804
+ }
805
+
806
+ function updateCoins(dt) {
807
+ coins.forEach(coin => {
808
+ if (coin.collected) return;
809
+
810
+ // Animate coins
811
+ coin.rotationY += dt * 4;
812
+ coin.bobOffset += dt * 3;
813
+ const newY = coin.y + Math.sin(coin.bobOffset) * 0.3;
814
+
815
+ setPosition(coin.mesh, coin.x, newY, coin.z);
816
+ setRotation(coin.mesh, 0, coin.rotationY, 0);
817
+
818
+ // Check collection
819
+ const distance = Math.sqrt(
820
+ Math.pow(coin.x - player.x, 2) +
821
+ Math.pow(newY - player.y, 2) +
822
+ Math.pow(coin.z - player.z, 2)
823
+ );
824
+
825
+ if (distance < 1.5) {
826
+ coin.collected = true;
827
+ destroyMesh(coin.mesh);
828
+ player.coins += 10;
829
+ score += 50;
830
+ createCoinParticles(coin.x, coin.y, coin.z);
831
+ }
832
+ });
833
+ }
834
+
835
+ function updateParticles(dt) {
836
+ for (let i = particles.length - 1; i >= 0; i--) {
837
+ const particle = particles[i];
838
+ particle.life -= dt;
839
+
840
+ const pos = getPosition(particle.mesh);
841
+ pos[0] += particle.vx * dt;
842
+ pos[1] += particle.vy * dt;
843
+ pos[2] += particle.vz * dt;
844
+
845
+ // Shuriken don't have gravity and spin
846
+ if (particle.isShuriken) {
847
+ particle.rotation += dt * 20;
848
+ setRotation(particle.mesh, 0, 0, particle.rotation);
849
+ } else {
850
+ particle.vy -= 10 * dt; // Gravity for normal particles
851
+ }
852
+
853
+ setPosition(particle.mesh, pos[0], pos[1], pos[2]);
854
+
855
+ if (!particle.isShuriken) {
856
+ const scale = particle.life / particle.maxLife;
857
+ setScale(particle.mesh, scale, scale, scale);
858
+ }
859
+
860
+ if (particle.life <= 0) {
861
+ destroyMesh(particle.mesh);
862
+ particles.splice(i, 1);
863
+ }
864
+ }
865
+ }
866
+
867
+ function updateCamera(dt) {
868
+ // Smooth camera following
869
+ camera.targetX = player.x;
870
+ camera.targetY = player.y + 5;
871
+ camera.targetZ = player.z;
872
+
873
+ camera.x += (player.x - camera.x) * camera.smoothing;
874
+ camera.y += (player.y + 8 - camera.y) * camera.smoothing;
875
+ camera.z += (player.z + 15 - camera.z) * camera.smoothing;
876
+
877
+ setCameraPosition(camera.x, camera.y, camera.z);
878
+ setCameraTarget(camera.targetX, camera.targetY, camera.targetZ);
879
+ }
880
+
881
+ function checkCollisions(dt) {
882
+ // Already handled in updatePlayer and updateEnemies
883
+ }
884
+
885
+ function updateGameLogic(dt) {
886
+ // Check for level completion
887
+ const remainingCoins = coins.filter(coin => !coin.collected).length;
888
+ if (remainingCoins === 0) {
889
+ level++;
890
+ score += 1000;
891
+ // Could spawn new level here
892
+ }
893
+
894
+ // Game over check
895
+ if (player.health <= 0) {
896
+ gameState = 'gameOver';
897
+ }
898
+ }
899
+
900
+ function createJumpParticles() {
901
+ for (let i = 0; i < 5; i++) {
902
+ const particle = {
903
+ mesh: createSphere(0.1, 0x88ccff, [player.x, player.y - 0.5, player.z]),
904
+ vx: (Math.random() - 0.5) * 4,
905
+ vy: Math.random() * 3,
906
+ vz: (Math.random() - 0.5) * 4,
907
+ life: 1,
908
+ maxLife: 1
909
+ };
910
+ particles.push(particle);
911
+ }
912
+ }
913
+
914
+ function createDoubleJumpParticles() {
915
+ for (let i = 0; i < 8; i++) {
916
+ const particle = {
917
+ mesh: createSphere(0.15, 0xffff44, [player.x, player.y, player.z]),
918
+ vx: (Math.random() - 0.5) * 6,
919
+ vy: (Math.random() - 0.5) * 6,
920
+ vz: (Math.random() - 0.5) * 6,
921
+ life: 1.5,
922
+ maxLife: 1.5
923
+ };
924
+ particles.push(particle);
925
+ }
926
+ }
927
+
928
+ function createHitParticles(x, y, z) {
929
+ for (let i = 0; i < 10; i++) {
930
+ const particle = {
931
+ mesh: createSphere(0.08, 0xff4444, [x, y, z]),
932
+ vx: (Math.random() - 0.5) * 8,
933
+ vy: Math.random() * 6,
934
+ vz: (Math.random() - 0.5) * 8,
935
+ life: 0.8,
936
+ maxLife: 0.8
937
+ };
938
+ particles.push(particle);
939
+ }
940
+ }
941
+
942
+ function createCoinParticles(x, y, z) {
943
+ for (let i = 0; i < 6; i++) {
944
+ const particle = {
945
+ mesh: createSphere(0.05, 0xffdd00, [x, y, z]),
946
+ vx: (Math.random() - 0.5) * 5,
947
+ vy: Math.random() * 4 + 2,
948
+ vz: (Math.random() - 0.5) * 5,
949
+ life: 1.2,
950
+ maxLife: 1.2
951
+ };
952
+ particles.push(particle);
953
+ }
954
+ }
955
+
956
+ function createDashParticles() {
957
+ for (let i = 0; i < 10; i++) {
958
+ const particle = {
959
+ mesh: createSphere(0.12, 0xaa44ff, [player.x, player.y + 0.5, player.z]),
960
+ vx: (Math.random() - 0.5) * 3 - (player.facingRight ? 5 : -5),
961
+ vy: (Math.random() - 0.5) * 2,
962
+ vz: (Math.random() - 0.5) * 3,
963
+ life: 0.5,
964
+ maxLife: 0.5
965
+ };
966
+ particles.push(particle);
967
+ }
968
+ }
969
+
970
+ function createAirDashParticles() {
971
+ for (let i = 0; i < 15; i++) {
972
+ const particle = {
973
+ mesh: createSphere(0.15, 0x00ffff, [player.x, player.y, player.z]),
974
+ vx: (Math.random() - 0.5) * 8,
975
+ vy: (Math.random() - 0.5) * 8,
976
+ vz: (Math.random() - 0.5) * 8,
977
+ life: 0.8,
978
+ maxLife: 0.8
979
+ };
980
+ particles.push(particle);
981
+ }
982
+ }
983
+
984
+ function createSlideParticles() {
985
+ for (let i = 0; i < 8; i++) {
986
+ const particle = {
987
+ mesh: createSphere(0.08, 0x8844aa, [player.x, player.y - 0.3, player.z]),
988
+ vx: (Math.random() - 0.5) * 4 - (player.facingRight ? 3 : -3),
989
+ vy: Math.random() * 2,
990
+ vz: (Math.random() - 0.5) * 4,
991
+ life: 0.6,
992
+ maxLife: 0.6
993
+ };
994
+ particles.push(particle);
995
+ }
996
+ }
997
+
998
+ function createGrappleEffect() {
999
+ for (let i = 0; i < 12; i++) {
1000
+ const t = i / 12;
1001
+ const x = player.x + (player.grappleTarget.x - player.x) * t;
1002
+ const y = player.y + (player.grappleTarget.y - player.y) * t;
1003
+ const z = player.z + (player.grappleTarget.z - player.z) * t;
1004
+
1005
+ const particle = {
1006
+ mesh: createSphere(0.1, 0x00ffff, [x, y, z]),
1007
+ vx: 0,
1008
+ vy: 0,
1009
+ vz: 0,
1010
+ life: 0.4,
1011
+ maxLife: 0.4
1012
+ };
1013
+ particles.push(particle);
1014
+ }
1015
+ }
1016
+
1017
+ function throwShuriken() {
1018
+ const shuriken = {
1019
+ mesh: createCube(0.3, 0xcccccc, [
1020
+ player.x + (player.facingRight ? 1 : -1),
1021
+ player.y + 0.5,
1022
+ player.z
1023
+ ]),
1024
+ vx: (player.facingRight ? 1 : -1) * 25,
1025
+ vy: 0,
1026
+ vz: 0,
1027
+ life: 2,
1028
+ maxLife: 2,
1029
+ rotation: 0,
1030
+ isShuriken: true
1031
+ };
1032
+ setScale(shuriken.mesh, 0.8, 0.1, 0.8);
1033
+ particles.push(shuriken);
1034
+
1035
+ // Check for enemy hits
1036
+ setTimeout(() => {
1037
+ enemies.forEach(enemy => {
1038
+ const dx = enemy.x - (player.x + (player.facingRight ? 5 : -5));
1039
+ const dy = enemy.y - player.y;
1040
+ const dz = enemy.z - player.z;
1041
+ const distance = Math.sqrt(dx*dx + dy*dy + dz*dz);
1042
+
1043
+ if (distance < 3 && !enemy.dead) {
1044
+ enemy.health -= 1;
1045
+ createHitParticles(enemy.x, enemy.y, enemy.z);
1046
+ combo++;
1047
+ comboTimer = 2;
1048
+
1049
+ if (enemy.health <= 0) {
1050
+ destroyMesh(enemy.mesh);
1051
+ enemy.dead = true;
1052
+ score += 150;
1053
+ }
1054
+ }
1055
+ });
1056
+ }, 100);
1057
+ }
1058
+
1059
+ function findNearestGrapplePoint() {
1060
+ let nearest = null;
1061
+ let minDist = 20; // Max grapple range
1062
+
1063
+ player.grapplePoints.forEach(point => {
1064
+ if (!point.active) return;
1065
+
1066
+ const dx = point.x - player.x;
1067
+ const dy = point.y - player.y;
1068
+ const dz = point.z - player.z;
1069
+ const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
1070
+
1071
+ if (dist < minDist) {
1072
+ minDist = dist;
1073
+ nearest = point;
1074
+ }
1075
+ });
1076
+
1077
+ return nearest;
1078
+ }
1079
+
1080
+ function drawUI() {
1081
+ // Ninja HUD Background - dark with purple/cyan accents
1082
+ rect(16, 16, 420, 100, rgba8(10, 10, 26, 200), true);
1083
+ rect(16, 16, 420, 100, rgba8(136, 51, 170, 180), false);
1084
+ rect(17, 17, 418, 98, rgba8(0, 255, 255, 80), false);
1085
+
1086
+ // Score and Level
1087
+ print(`SCORE: ${score.toString().padStart(8, '0')}`, 24, 24, rgba8(0, 255, 255, 255));
1088
+ print(`LEVEL: ${level}`, 24, 40, rgba8(170, 68, 255, 255));
1089
+ print(`COINS: ${player.coins}`, 24, 56, rgba8(255, 215, 0, 255));
1090
+
1091
+ // Shuriken count
1092
+ print(`SHURIKEN: ${player.shuriken}`, 24, 72, rgba8(200, 200, 200, 255));
1093
+
1094
+ // Combo meter
1095
+ if (combo > 0) {
1096
+ print(`COMBO x${combo}`, 24, 88, rgba8(255, 100, 255, 255));
1097
+ const comboBarWidth = Math.floor((comboTimer / 2.0) * 80);
1098
+ rect(100, 90, 80, 6, rgba8(50, 20, 60, 255), true);
1099
+ rect(100, 90, comboBarWidth, 6, rgba8(255, 100, 255, 255), true);
1100
+ }
1101
+
1102
+ // Health bar - red with dark background
1103
+ print('HEALTH:', 220, 24, rgba8(255, 255, 255, 255));
1104
+ rect(285, 22, 120, 10, rgba8(50, 0, 0, 255), true);
1105
+ rect(285, 22, Math.floor((player.health / 100) * 120), 10, rgba8(255, 0, 80, 255), true);
1106
+ rect(285, 22, 120, 10, rgba8(255, 0, 80, 100), false);
1107
+
1108
+ // Energy bar - cyan with dark background
1109
+ print('ENERGY:', 220, 42, rgba8(255, 255, 255, 255));
1110
+ rect(285, 40, 120, 10, rgba8(0, 20, 40, 255), true);
1111
+ rect(285, 40, Math.floor((player.energy / 100) * 120), 10, rgba8(0, 255, 255, 255), true);
1112
+ rect(285, 40, 120, 10, rgba8(0, 255, 255, 100), false);
1113
+
1114
+ // Ability indicators
1115
+ print('ABILITIES:', 220, 60, rgba8(200, 200, 200, 255));
1116
+
1117
+ // Dash indicator
1118
+ const dashReady = player.dashCooldown <= 0 && player.energy >= 20;
1119
+ rect(285, 58, 24, 8, dashReady ? rgba8(170, 68, 255, 255) : rgba8(50, 20, 60, 255), true);
1120
+ print('DSH', 288, 60, rgba8(255, 255, 255, 255));
1121
+
1122
+ // Air Dash indicator
1123
+ const airDashReady = player.airDashAvailable && player.energy >= 25;
1124
+ rect(312, 58, 24, 8, airDashReady ? rgba8(0, 255, 255, 255) : rgba8(20, 50, 60, 255), true);
1125
+ print('AIR', 315, 60, rgba8(255, 255, 255, 255));
1126
+
1127
+ // Grapple indicator
1128
+ const grappleReady = findNearestGrapplePoint() !== null;
1129
+ rect(339, 58, 24, 8, grappleReady ? rgba8(0, 255, 255, 255) : rgba8(20, 50, 60, 255), true);
1130
+ print('GRP', 342, 60, rgba8(255, 255, 255, 255));
1131
+
1132
+ // Shuriken indicator
1133
+ const shurikenReady = player.shuriken > 0 && player.energy >= 5;
1134
+ rect(366, 58, 24, 8, shurikenReady ? rgba8(200, 200, 200, 255) : rgba8(40, 40, 40, 255), true);
1135
+ print('SHR', 369, 60, rgba8(255, 255, 255, 255));
1136
+
1137
+ // 3D Stats - smaller and in corner
1138
+ const stats = get3DStats();
1139
+ if (stats) {
1140
+ print(`${stats.meshes || 0}m`, 580, 24, rgba8(100, 100, 100, 200));
1141
+ }
1142
+
1143
+ // Position info - debug mode
1144
+ // print(`POS: ${player.x.toFixed(1)}, ${player.y.toFixed(1)}, ${player.z.toFixed(1)}`, 220, 90, rgba8(100, 100, 100, 150));
1145
+
1146
+ // Controls hint
1147
+ print('X=DASH C=SHURIKEN G=GRAPPLE Z=ATTACK', 16, 340, rgba8(136, 51, 170, 200));
1148
+
1149
+ if (gameState === 'gameOver') {
1150
+ rect(0, 0, 640, 360, rgba8(0, 0, 0, 200), true);
1151
+ print('GAME OVER', 260, 150, rgba8(255, 50, 50, 255));
1152
+ print(`FINAL SCORE: ${score}`, 230, 180, rgba8(255, 255, 0, 255));
1153
+ print(`COINS COLLECTED: ${player.coins}`, 220, 200, rgba8(255, 215, 0, 255));
1154
+ print('PRESS R TO RESTART', 220, 240, rgba8(255, 255, 255, 255));
1155
+
1156
+ if (btnp(17)) { // R key
1157
+ // Reset game
1158
+ score = 0;
1159
+ level = 1;
1160
+ player.health = 100;
1161
+ player.energy = 100;
1162
+ player.coins = 0;
1163
+ player.x = 0; player.y = 2; player.z = 0;
1164
+ gameState = 'playing';
1165
+ clearScene();
1166
+ init();
1167
+ }
1168
+ }
1169
+ }