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,744 @@
1
+ // NEO-DOOM: FAST, BRIGHT, FUN ARENA SHOOTER
2
+ // 3 levels, 4 enemy types, pickups, boss fights
3
+
4
+ let gameTime = 0;
5
+ let gameState = 'start'; // start, playing, gameover, levelclear, victory
6
+
7
+ let player = {
8
+ x: 2,
9
+ y: 1.5,
10
+ z: 2,
11
+ yaw: 0,
12
+ pitch: 0,
13
+ health: 100,
14
+ armor: 0,
15
+ score: 0,
16
+ ammo: 50,
17
+ };
18
+
19
+ let level = 1;
20
+ let kills = 0;
21
+ let totalEnemies = 0;
22
+ let levelClearTimer = 0;
23
+
24
+ let entities = {
25
+ walls: [],
26
+ enemies: [],
27
+ bullets: [],
28
+ particles: [],
29
+ pickups: [],
30
+ enemyBullets: [],
31
+ };
32
+
33
+ // 3 maps with different layouts and enemy compositions
34
+ const MAPS = [
35
+ // Level 1: Classic maze (grunts only)
36
+ [
37
+ '########################',
38
+ '#..E.......#...........#',
39
+ '#.###.####.#.#.#####.E.#',
40
+ '#.#......#.#.#.#...#...#',
41
+ '#.#..E...#...#.#...###.#',
42
+ '#.######.#####.#E......#',
43
+ '#..............#...###.#',
44
+ '###.############...#...#',
45
+ '#......#...E...#...###.#',
46
+ '#.####.#.#####.#E......#',
47
+ '#.#....#.#...#.#.###.###',
48
+ '#.#E...#.#.E.#.#...#...#',
49
+ '#.######.#.###.###.#...#',
50
+ '#......................#',
51
+ '########################',
52
+ ],
53
+ // Level 2: Open arena with pillars (grunts + shooters)
54
+ [
55
+ '########################',
56
+ '#......................#',
57
+ '#..##..S.......##..S..#',
58
+ '#..##..........##.....#',
59
+ '#......E..............#',
60
+ '#...........##........#',
61
+ '#..S........##....S...#',
62
+ '#.........E.......E...#',
63
+ '#...##................#',
64
+ '#...##.....S....##....#',
65
+ '#.........E.....##....#',
66
+ '#..E..............S...#',
67
+ '#.......##............#',
68
+ '#.......##.....E......#',
69
+ '########################',
70
+ ],
71
+ // Level 3: Corridors (grunts + shooters + tanks + boss)
72
+ [
73
+ '########################',
74
+ '#..T...#......#...S...#',
75
+ '#.####.#.####.#.####..#',
76
+ '#......#.#..#.#.#.....#',
77
+ '#.##.###.#..#...#.###.#',
78
+ '#....S...#..#####.T...#',
79
+ '#.######.#............#',
80
+ '#........####.####.####',
81
+ '#.####.#.......#...S..#',
82
+ '#.T....#.#####.#.####.#',
83
+ '#.######.#...#.#......#',
84
+ '#........#.B.#...####.#',
85
+ '#.####.###...###......#',
86
+ '#.S....................#',
87
+ '########################',
88
+ ],
89
+ ];
90
+
91
+ const MAT = {
92
+ floor: { material: 'standard', color: 0x223344, roughness: 1.0 },
93
+ wall: { material: 'standard', color: 0xdddddd, roughness: 0.8 },
94
+ wallColor: { material: 'standard', color: 0x00aaff, roughness: 0.8 },
95
+ wall2: { material: 'standard', color: 0xff6600, roughness: 0.8 },
96
+ wall3: { material: 'standard', color: 0xcc00ff, roughness: 0.8 },
97
+ enemy: { material: 'emissive', color: 0xff3300, intensity: 2.0 },
98
+ enemyEye: { material: 'emissive', color: 0xffffff, intensity: 3.0 },
99
+ shooter: { material: 'emissive', color: 0x00ff44, intensity: 2.0 },
100
+ tank: { material: 'emissive', color: 0x9933ff, intensity: 2.0 },
101
+ boss: { material: 'emissive', color: 0xffcc00, intensity: 3.0 },
102
+ bullet: { material: 'emissive', color: 0x00ffcc, intensity: 4.0 },
103
+ enemyBullet: { material: 'emissive', color: 0xff4400, intensity: 3.0 },
104
+ particle: { material: 'emissive', color: 0xffaa00, intensity: 2.0 },
105
+ healthPickup: { material: 'emissive', color: 0x00ff44, intensity: 3.0 },
106
+ ammoPickup: { material: 'emissive', color: 0xffcc00, intensity: 3.0 },
107
+ armorPickup: { material: 'emissive', color: 0x4488ff, intensity: 3.0 },
108
+ };
109
+
110
+ let mouseInit = false;
111
+
112
+ export function init() {
113
+ setFog(0x001122, 5, 80);
114
+ setAmbientLight(0xffffff, 0.6);
115
+ setDirectionalLight([-1, -2, -1], 0xffffee, 1.2);
116
+
117
+ let floor = createPlane(200, 200, 0x112233, [0, 0, 0], MAT.floor);
118
+ setRotation(floor, -Math.PI / 2, 0, 0);
119
+
120
+ if (!mouseInit) {
121
+ mouseInit = true;
122
+ document.addEventListener('mousedown', () => {
123
+ if (!document.pointerLockElement) {
124
+ document.body.requestPointerLock().catch(() => {});
125
+ }
126
+ if (gameState === 'start' || gameState === 'gameover' || gameState === 'victory') {
127
+ startGame(1);
128
+ }
129
+ });
130
+ document.addEventListener('mousemove', e => {
131
+ if (document.pointerLockElement && gameState === 'playing') {
132
+ player.yaw -= e.movementX * 0.003;
133
+ player.pitch -= e.movementY * 0.003;
134
+ if (player.pitch > 1.2) player.pitch = 1.2;
135
+ if (player.pitch < -1.2) player.pitch = -1.2;
136
+ }
137
+ });
138
+ }
139
+ }
140
+
141
+ function cleanupLevel() {
142
+ for (let w of entities.walls) destroyMesh(w.m);
143
+ for (let e of entities.enemies) {
144
+ destroyMesh(e.m1);
145
+ destroyMesh(e.m2);
146
+ }
147
+ for (let b of entities.bullets) destroyMesh(b.m);
148
+ for (let b of entities.enemyBullets) destroyMesh(b.m);
149
+ for (let p of entities.particles) destroyMesh(p.m);
150
+ for (let p of entities.pickups) destroyMesh(p.m);
151
+ entities = { walls: [], enemies: [], bullets: [], particles: [], pickups: [], enemyBullets: [] };
152
+ }
153
+
154
+ function startGame(lvl) {
155
+ cleanupLevel();
156
+ gameState = 'playing';
157
+ gameTime = 0;
158
+ level = lvl;
159
+ kills = 0;
160
+ totalEnemies = 0;
161
+ levelClearTimer = 0;
162
+
163
+ // Full reset on level 1, keep stats on level transitions
164
+ if (lvl === 1) {
165
+ player.health = 100;
166
+ player.armor = 0;
167
+ player.score = 0;
168
+ player.ammo = 50;
169
+ }
170
+ // Bonus ammo each level
171
+ player.ammo += lvl > 1 ? 25 : 0;
172
+
173
+ const map = MAPS[lvl - 1];
174
+ const SIZE = 4;
175
+ let accentMat = lvl === 1 ? MAT.wallColor : lvl === 2 ? MAT.wall2 : MAT.wall3;
176
+
177
+ for (let z = 0; z < map.length; z++) {
178
+ for (let x = 0; x < map[z].length; x++) {
179
+ let char = map[z][x];
180
+ let px = (x - 12) * SIZE;
181
+ let pz = (z - 7) * SIZE;
182
+
183
+ if (char === '#') {
184
+ let isColor = (x + z) % 3 === 0;
185
+ let m = createCube(
186
+ SIZE,
187
+ isColor ? accentMat.color : MAT.wall.color,
188
+ [px, 3, pz],
189
+ isColor ? accentMat : MAT.wall
190
+ );
191
+ setScale(m, 1, 1.5, 1);
192
+ entities.walls.push({ m, x: px, z: pz, r: SIZE / 2 });
193
+ } else if (char === 'E') {
194
+ spawnEnemy(px, pz, 'grunt');
195
+ totalEnemies++;
196
+ } else if (char === 'S') {
197
+ spawnEnemy(px, pz, 'shooter');
198
+ totalEnemies++;
199
+ } else if (char === 'T') {
200
+ spawnEnemy(px, pz, 'tank');
201
+ totalEnemies++;
202
+ } else if (char === 'B') {
203
+ spawnEnemy(px, pz, 'boss');
204
+ totalEnemies++;
205
+ }
206
+ }
207
+ }
208
+
209
+ player.x = -11 * SIZE;
210
+ player.z = -6 * SIZE;
211
+ player.yaw = Math.PI / 4;
212
+ player.pitch = 0;
213
+ }
214
+
215
+ function spawnEnemy(x, z, type) {
216
+ let mat, hp, spd, size, dmg;
217
+ switch (type) {
218
+ case 'shooter':
219
+ mat = MAT.shooter;
220
+ hp = 5;
221
+ spd = 0.1 + Math.random() * 0.05;
222
+ size = 1.5;
223
+ dmg = 8;
224
+ break;
225
+ case 'tank':
226
+ mat = MAT.tank;
227
+ hp = 12;
228
+ spd = 0.08;
229
+ size = 2.2;
230
+ dmg = 20;
231
+ break;
232
+ case 'boss':
233
+ mat = MAT.boss;
234
+ hp = 40 + level * 10;
235
+ spd = 0.12;
236
+ size = 3.5;
237
+ dmg = 15;
238
+ break;
239
+ default: // grunt
240
+ mat = MAT.enemy;
241
+ hp = 3;
242
+ spd = 0.15 + Math.random() * 0.1;
243
+ size = 1.5;
244
+ dmg = 10;
245
+ break;
246
+ }
247
+ let m1 = createCube(size, mat.color, [x, 2, z], mat);
248
+ let m2 = createCube(size * 0.5, MAT.enemyEye.color, [x, 2.5, z + 0.5], MAT.enemyEye);
249
+ entities.enemies.push({
250
+ m1,
251
+ m2,
252
+ x,
253
+ z,
254
+ y: 2,
255
+ health: hp,
256
+ maxHealth: hp,
257
+ speed: spd,
258
+ type,
259
+ dmg,
260
+ size,
261
+ fireTimer: 0,
262
+ });
263
+ }
264
+
265
+ function getWallCollision(nx, nz, radius) {
266
+ for (let w of entities.walls) {
267
+ if (Math.abs(nx - w.x) < w.r + radius && Math.abs(nz - w.z) < w.r + radius) return true;
268
+ }
269
+ return false;
270
+ }
271
+
272
+ function shoot() {
273
+ if (player.ammo <= 0) return;
274
+ player.ammo--;
275
+
276
+ let fx = Math.sin(player.yaw) * Math.cos(player.pitch);
277
+ let fy = Math.sin(player.pitch);
278
+ let fz = Math.cos(player.yaw) * Math.cos(player.pitch);
279
+
280
+ // Spawn bullet from camera/head height, offset slightly down and right for gun position
281
+ let headY = player.y + 1.0;
282
+ let rx = Math.cos(player.yaw);
283
+ let rz = -Math.sin(player.yaw);
284
+ let bx = player.x + fx * 1.5 + rx * 0.3;
285
+ let by = headY + fy * 1.5 - 0.3;
286
+ let bz = player.z + fz * 1.5 + rz * 0.3;
287
+
288
+ let m = createCube(0.2, MAT.bullet.color, [bx, by, bz], MAT.bullet);
289
+ setScale(m, 1, 1, 6);
290
+ setRotation(m, -player.pitch, player.yaw, 0);
291
+
292
+ entities.bullets.push({ m, x: bx, y: by, z: bz, vx: fx * 3, vy: fy * 3, vz: fz * 3, life: 30 });
293
+ sfx('laser');
294
+ }
295
+
296
+ function enemyShoot(e, angleOffset) {
297
+ let dx = player.x - e.x;
298
+ let dy = player.y + 1 - e.y;
299
+ let dz = player.z - e.z;
300
+ let hDist = Math.hypot(dx, dz);
301
+ if (hDist < 0.1) return;
302
+
303
+ let baseAngle = Math.atan2(dx, dz) + (angleOffset || 0);
304
+ let dist3d = Math.hypot(dx, dy, dz);
305
+ let spd = e.type === 'boss' ? 2.0 : 1.5;
306
+ let bx = e.x,
307
+ by = e.y + 0.5,
308
+ bz = e.z;
309
+ let m = createCube(0.3, MAT.enemyBullet.color, [bx, by, bz], MAT.enemyBullet);
310
+
311
+ entities.enemyBullets.push({
312
+ m,
313
+ x: bx,
314
+ y: by,
315
+ z: bz,
316
+ vx: Math.sin(baseAngle) * spd,
317
+ vy: (dy / dist3d) * spd,
318
+ vz: Math.cos(baseAngle) * spd,
319
+ life: 60,
320
+ dmg: e.type === 'boss' ? 12 : 8,
321
+ });
322
+ }
323
+
324
+ function spawnPickup(x, y, z) {
325
+ let r = Math.random();
326
+ let type, mat;
327
+ if (r < 0.4) {
328
+ type = 'ammo';
329
+ mat = MAT.ammoPickup;
330
+ } else if (r < 0.75) {
331
+ type = 'health';
332
+ mat = MAT.healthPickup;
333
+ } else {
334
+ type = 'armor';
335
+ mat = MAT.armorPickup;
336
+ }
337
+
338
+ let m = createCube(0.6, mat.color, [x, y, z], mat);
339
+ entities.pickups.push({ m, x, y, z, type, life: 600 });
340
+ }
341
+
342
+ function spawnGibs(cx, cy, cz, color, count) {
343
+ for (let i = 0; i < count; i++) {
344
+ let m = createCube(0.3, color, [cx, cy, cz], { material: 'emissive', color, intensity: 2 });
345
+ entities.particles.push({
346
+ m,
347
+ x: cx,
348
+ y: cy,
349
+ z: cz,
350
+ vx: (Math.random() - 0.5) * 1.5,
351
+ vy: Math.random() * 1.5,
352
+ vz: (Math.random() - 0.5) * 1.5,
353
+ life: 20 + Math.random() * 10,
354
+ });
355
+ }
356
+ }
357
+
358
+ function applyDamage(dmg) {
359
+ if (player.armor > 0) {
360
+ let absorbed = Math.min(player.armor, Math.floor(dmg * 0.6));
361
+ player.armor -= absorbed;
362
+ dmg -= absorbed;
363
+ }
364
+ player.health -= dmg;
365
+ setFog(0xff0000, 1, 20);
366
+ sfx('hit');
367
+ if (player.health <= 0) {
368
+ gameState = 'gameover';
369
+ sfx('death');
370
+ if (document.pointerLockElement) document.exitPointerLock();
371
+ }
372
+ }
373
+
374
+ export function update() {
375
+ gameTime++;
376
+
377
+ // Level clear transition
378
+ if (gameState === 'levelclear') {
379
+ levelClearTimer--;
380
+ if (levelClearTimer <= 0) {
381
+ if (level >= 3) {
382
+ gameState = 'victory';
383
+ if (document.pointerLockElement) document.exitPointerLock();
384
+ } else {
385
+ startGame(level + 1);
386
+ }
387
+ }
388
+ let headY = player.y + 1.0;
389
+ setCameraPosition(player.x, headY, player.z);
390
+ setCameraTarget(
391
+ player.x + Math.sin(player.yaw),
392
+ headY + Math.sin(player.pitch),
393
+ player.z + Math.cos(player.yaw)
394
+ );
395
+ return;
396
+ }
397
+
398
+ if (gameState !== 'playing') {
399
+ let r = 40;
400
+ setCameraPosition(Math.sin(gameTime * 0.005) * r, 30, Math.cos(gameTime * 0.005) * r);
401
+ setCameraTarget(0, 0, 0);
402
+ return;
403
+ }
404
+
405
+ // Player Movement
406
+ let speed = 0.35;
407
+ let fx = Math.sin(player.yaw);
408
+ let fz = Math.cos(player.yaw);
409
+ let rx = Math.cos(player.yaw);
410
+ let rz = -Math.sin(player.yaw);
411
+
412
+ let dx = 0,
413
+ dz = 0;
414
+ if (key('KeyW') || key('ArrowUp')) {
415
+ dx += fx;
416
+ dz += fz;
417
+ }
418
+ if (key('KeyS') || key('ArrowDown')) {
419
+ dx -= fx;
420
+ dz -= fz;
421
+ }
422
+ if (key('KeyA') || key('ArrowLeft')) {
423
+ dx += rx;
424
+ dz += rz;
425
+ }
426
+ if (key('KeyD') || key('ArrowRight')) {
427
+ dx -= rx;
428
+ dz -= rz;
429
+ }
430
+
431
+ let len = Math.hypot(dx, dz);
432
+ if (len > 0) {
433
+ dx = (dx / len) * speed;
434
+ dz = (dz / len) * speed;
435
+ }
436
+
437
+ let plrRadius = 1.0;
438
+ if (!getWallCollision(player.x + dx, player.z, plrRadius)) player.x += dx;
439
+ if (!getWallCollision(player.x, player.z + dz, plrRadius)) player.z += dz;
440
+
441
+ if (len > 0) player.y = 1.5 + Math.sin(gameTime * 0.4) * 0.2;
442
+ else player.y = 1.5 + Math.sin(gameTime * 0.05) * 0.05;
443
+
444
+ // Camera
445
+ let headY = player.y + 1.0;
446
+ let tx = player.x + Math.sin(player.yaw) * Math.cos(player.pitch);
447
+ let ty = headY + Math.sin(player.pitch);
448
+ let tz = player.z + Math.cos(player.yaw) * Math.cos(player.pitch);
449
+ setCameraPosition(player.x, headY, player.z);
450
+ setCameraTarget(tx, ty, tz);
451
+ setCameraFOV(100);
452
+
453
+ // Shooting
454
+ if ((mouseDown() || key('Space') || btn('A')) && gameTime % 4 === 0) {
455
+ shoot();
456
+ if (player.ammo > 0) player.pitch += 0.02;
457
+ }
458
+
459
+ // Update Player Bullets
460
+ for (let i = entities.bullets.length - 1; i >= 0; i--) {
461
+ let b = entities.bullets[i];
462
+ b.x += b.vx;
463
+ b.y += b.vy;
464
+ b.z += b.vz;
465
+ setPosition(b.m, b.x, b.y, b.z);
466
+ b.life--;
467
+ let hit = false;
468
+
469
+ for (let j = entities.enemies.length - 1; j >= 0; j--) {
470
+ let e = entities.enemies[j];
471
+ let hitR = e.size * 0.6;
472
+ if (Math.hypot(b.x - e.x, b.z - e.z) < hitR + 0.5 && Math.abs(b.y - e.y) < hitR + 1.0) {
473
+ hit = true;
474
+ e.health -= 1;
475
+ spawnGibs(b.x, b.y, b.z, 0xffaa00, 3);
476
+ if (e.health <= 0) {
477
+ destroyMesh(e.m1);
478
+ destroyMesh(e.m2);
479
+ entities.enemies.splice(j, 1);
480
+ kills++;
481
+ let pts =
482
+ e.type === 'boss' ? 1000 : e.type === 'tank' ? 300 : e.type === 'shooter' ? 200 : 100;
483
+ player.score += pts;
484
+ let gibColor =
485
+ e.type === 'boss'
486
+ ? 0xffcc00
487
+ : e.type === 'tank'
488
+ ? 0x9933ff
489
+ : e.type === 'shooter'
490
+ ? 0x00ff44
491
+ : 0xff3300;
492
+ spawnGibs(e.x, e.y, e.z, gibColor, 15);
493
+ sfx('explosion');
494
+ if (Math.random() < 0.6) spawnPickup(e.x, 1, e.z);
495
+ } else {
496
+ sfx('hit');
497
+ }
498
+ break;
499
+ }
500
+ }
501
+
502
+ if (!hit && getWallCollision(b.x, b.z, 0)) {
503
+ hit = true;
504
+ spawnGibs(b.x, b.y, b.z, 0x00ffff, 4);
505
+ }
506
+ if (b.life <= 0 || hit) {
507
+ destroyMesh(b.m);
508
+ entities.bullets.splice(i, 1);
509
+ }
510
+ }
511
+
512
+ // Update Enemy Bullets
513
+ for (let i = entities.enemyBullets.length - 1; i >= 0; i--) {
514
+ let b = entities.enemyBullets[i];
515
+ b.x += b.vx;
516
+ b.y += b.vy;
517
+ b.z += b.vz;
518
+ setPosition(b.m, b.x, b.y, b.z);
519
+ b.life--;
520
+ let hit = false;
521
+
522
+ if (Math.hypot(b.x - player.x, b.z - player.z) < 1.5 && Math.abs(b.y - player.y) < 2.0) {
523
+ hit = true;
524
+ applyDamage(b.dmg);
525
+ }
526
+ if (!hit && getWallCollision(b.x, b.z, 0)) hit = true;
527
+ if (b.life <= 0 || hit) {
528
+ destroyMesh(b.m);
529
+ entities.enemyBullets.splice(i, 1);
530
+ }
531
+ }
532
+
533
+ // Update Enemies
534
+ for (let i = entities.enemies.length - 1; i >= 0; i--) {
535
+ let e = entities.enemies[i];
536
+ let dist = Math.hypot(player.x - e.x, player.z - e.z);
537
+
538
+ // Movement - shooters keep distance, others charge
539
+ let minDist = e.type === 'shooter' ? 8 : 1.5;
540
+ if (dist < 30 && dist > minDist) {
541
+ let ex = e.x + ((player.x - e.x) / dist) * e.speed;
542
+ let ez = e.z + ((player.z - e.z) / dist) * e.speed;
543
+ if (!getWallCollision(ex, e.z, 1.2)) e.x = ex;
544
+ if (!getWallCollision(e.x, ez, 1.2)) e.z = ez;
545
+ }
546
+ // Shooters retreat if too close
547
+ if (e.type === 'shooter' && dist < 6 && dist > 0.1) {
548
+ let ex = e.x - ((player.x - e.x) / dist) * e.speed * 0.5;
549
+ let ez = e.z - ((player.z - e.z) / dist) * e.speed * 0.5;
550
+ if (!getWallCollision(ex, e.z, 1.2)) e.x = ex;
551
+ if (!getWallCollision(e.x, ez, 1.2)) e.z = ez;
552
+ }
553
+
554
+ // Animate
555
+ let t = gameTime * 0.1 + i;
556
+ e.y = e.type === 'boss' ? 3 + Math.sin(t) * 0.8 : 2 + Math.sin(t) * 0.5;
557
+ setPosition(e.m1, e.x, e.y, e.z);
558
+ setRotation(e.m1, t, t, 0);
559
+
560
+ let eyeYaw = Math.atan2(player.x - e.x, player.z - e.z);
561
+ let eyeOff = e.size * 0.5;
562
+ setPosition(
563
+ e.m2,
564
+ e.x + Math.sin(eyeYaw) * eyeOff,
565
+ e.y + e.size * 0.3,
566
+ e.z + Math.cos(eyeYaw) * eyeOff
567
+ );
568
+ setRotation(e.m2, 0, eyeYaw, 0);
569
+
570
+ // Ranged attacks (shooters + boss)
571
+ if ((e.type === 'shooter' || e.type === 'boss') && dist < 25) {
572
+ e.fireTimer++;
573
+ let fireRate = e.type === 'boss' ? 45 : 90;
574
+ if (e.fireTimer >= fireRate) {
575
+ e.fireTimer = 0;
576
+ enemyShoot(e, 0);
577
+ if (e.type === 'boss') {
578
+ enemyShoot(e, -0.15);
579
+ enemyShoot(e, 0.15);
580
+ }
581
+ }
582
+ }
583
+
584
+ // Melee attack (not shooters)
585
+ if (e.type !== 'shooter' && dist < 2.0 && gameTime % 15 === 0) {
586
+ applyDamage(e.dmg);
587
+ }
588
+ }
589
+
590
+ // Update Pickups
591
+ for (let i = entities.pickups.length - 1; i >= 0; i--) {
592
+ let p = entities.pickups[i];
593
+ p.life--;
594
+ let py = p.y + Math.sin(gameTime * 0.1 + i) * 0.3;
595
+ setPosition(p.m, p.x, py, p.z);
596
+ setRotation(p.m, 0, gameTime * 0.05, 0);
597
+
598
+ // Collect
599
+ if (Math.hypot(player.x - p.x, player.z - p.z) < 2.0) {
600
+ let picked = false;
601
+ if (p.type === 'health' && player.health < 100) {
602
+ player.health = Math.min(100, player.health + 25);
603
+ sfx('powerup');
604
+ picked = true;
605
+ } else if (p.type === 'ammo') {
606
+ player.ammo += 20;
607
+ sfx('coin');
608
+ picked = true;
609
+ } else if (p.type === 'armor' && player.armor < 100) {
610
+ player.armor = Math.min(100, player.armor + 25);
611
+ sfx('powerup');
612
+ picked = true;
613
+ }
614
+ if (picked) {
615
+ destroyMesh(p.m);
616
+ entities.pickups.splice(i, 1);
617
+ continue;
618
+ }
619
+ }
620
+ if (p.life <= 0) {
621
+ destroyMesh(p.m);
622
+ entities.pickups.splice(i, 1);
623
+ }
624
+ }
625
+
626
+ // Particles
627
+ for (let i = entities.particles.length - 1; i >= 0; i--) {
628
+ let p = entities.particles[i];
629
+ p.x += p.vx;
630
+ p.y += p.vy;
631
+ p.z += p.vz;
632
+ p.vy -= 0.1;
633
+ if (p.y < 0) {
634
+ p.y = 0;
635
+ p.vy *= -0.5;
636
+ }
637
+ p.life--;
638
+ setPosition(p.m, p.x, p.y, p.z);
639
+ let s = p.life / 20;
640
+ setScale(p.m, s, s, s);
641
+ if (p.life <= 0) {
642
+ destroyMesh(p.m);
643
+ entities.particles.splice(i, 1);
644
+ }
645
+ }
646
+
647
+ // Level clear check
648
+ if (entities.enemies.length === 0 && kills >= totalEnemies && totalEnemies > 0) {
649
+ gameState = 'levelclear';
650
+ levelClearTimer = 180;
651
+ player.score += level * 500;
652
+ sfx('powerup');
653
+ }
654
+
655
+ // Restore fog (color per level)
656
+ if (player.health > 0) {
657
+ let fogColor = level === 1 ? 0x001122 : level === 2 ? 0x221100 : 0x110022;
658
+ setFog(fogColor, 5, 80);
659
+ }
660
+ }
661
+
662
+ export function draw() {
663
+ if (gameState === 'start') {
664
+ rectfill(0, 0, 640, 360, rgba8(0, 0, 0, 187));
665
+ print('NEO-DOOM', 100, 80, rgba8(0, 255, 204, 255));
666
+ print('3 LEVELS OF DEMON CARNAGE', 60, 110, rgba8(255, 100, 0, 255));
667
+ print('CLICK TO START AND LOCK MOUSE', 50, 150, rgba8(255, 255, 255, 255));
668
+ print('WASD Move | Mouse Aim | Click Shoot', 30, 180, rgba8(170, 170, 170, 255));
669
+ } else if (gameState === 'gameover') {
670
+ rectfill(0, 0, 640, 360, rgba8(187, 0, 0, 187));
671
+ print('YOU DIED', 120, 80, rgba8(255, 255, 255, 255));
672
+ print(`LEVEL ${level} KILLS: ${kills}/${totalEnemies}`, 80, 110, rgba8(255, 170, 0, 255));
673
+ print(`FINAL SCORE: ${player.score}`, 90, 140, rgba8(255, 170, 0, 255));
674
+ print('CLICK TO RESTART', 90, 190, rgba8(170, 170, 170, 255));
675
+ } else if (gameState === 'victory') {
676
+ rectfill(0, 0, 640, 360, rgba8(0, 50, 0, 200));
677
+ print('VICTORY!', 120, 70, rgba8(0, 255, 100, 255));
678
+ print('ALL DEMONS SLAIN', 90, 100, rgba8(255, 255, 255, 255));
679
+ print(`FINAL SCORE: ${player.score}`, 90, 140, rgba8(255, 170, 0, 255));
680
+ print('CLICK TO PLAY AGAIN', 80, 190, rgba8(170, 170, 170, 255));
681
+ } else if (gameState === 'levelclear') {
682
+ drawHUD();
683
+ rectfill(60, 90, 200, 50, rgba8(0, 0, 0, 200));
684
+ print(`LEVEL ${level} CLEAR!`, 95, 100, rgba8(0, 255, 100, 255));
685
+ print(`+${level * 500} BONUS`, 110, 120, rgba8(255, 170, 0, 255));
686
+ } else {
687
+ drawHUD();
688
+ }
689
+ }
690
+
691
+ function drawHUD() {
692
+ // Health bar
693
+ let hColor = player.health > 30 ? rgba8(0, 255, 100, 255) : rgba8(255, 0, 0, 255);
694
+ drawProgressBar(10, 220, 80, 8, Math.max(0, player.health) / 100, hColor, rgba8(50, 50, 50, 200));
695
+ print(`HP: ${Math.max(0, player.health)}`, 10, 210, hColor);
696
+
697
+ // Armor bar
698
+ if (player.armor > 0) {
699
+ drawProgressBar(
700
+ 10,
701
+ 200,
702
+ 80,
703
+ 6,
704
+ player.armor / 100,
705
+ rgba8(68, 136, 255, 255),
706
+ rgba8(50, 50, 50, 200)
707
+ );
708
+ print(`ARM: ${player.armor}`, 10, 190, rgba8(68, 136, 255, 255));
709
+ }
710
+
711
+ // Ammo
712
+ let ammoColor = player.ammo > 10 ? rgba8(255, 204, 0, 255) : rgba8(255, 50, 50, 255);
713
+ print(`AMMO: ${player.ammo}`, 260, 218, ammoColor);
714
+ if (player.ammo <= 0) print('NO AMMO!', 130, 135, rgba8(255, 50, 50, 255));
715
+
716
+ // Score + Level + Kills
717
+ print(`SCORE: ${player.score}`, 120, 5, rgba8(255, 170, 0, 255));
718
+ print(`LEVEL ${level}`, 10, 5, rgba8(0, 255, 204, 255));
719
+ print(`KILLS: ${kills}/${totalEnemies}`, 240, 5, rgba8(255, 255, 255, 255));
720
+
721
+ // Crosshair
722
+ rectfill(159, 119, 3, 3, rgba8(255, 255, 255, 200));
723
+ rectfill(151, 120, 6, 1, rgba8(255, 255, 255, 150));
724
+ rectfill(164, 120, 6, 1, rgba8(255, 255, 255, 150));
725
+ rectfill(160, 111, 1, 6, rgba8(255, 255, 255, 150));
726
+ rectfill(160, 124, 1, 6, rgba8(255, 255, 255, 150));
727
+
728
+ // Gun
729
+ let isMoving = key('KeyW') || key('KeyS') || key('KeyA') || key('KeyD');
730
+ let bobX = isMoving ? Math.sin(gameTime * 0.3) * 10 : 0;
731
+ let bobY = isMoving ? Math.abs(Math.sin(gameTime * 0.3)) * 10 : 0;
732
+ let recoil = mouseDown() && gameTime % 4 < 2 ? 15 : 0;
733
+ let gx = 160 + bobX;
734
+ let gy = 260 + bobY + recoil;
735
+
736
+ rectfill(gx - 15, gy - 60, 30, 100, rgba8(51, 51, 51, 255));
737
+ rectfill(gx - 20, gy - 20, 40, 80, rgba8(85, 85, 85, 255));
738
+ rectfill(gx - 5, gy - 70, 10, 60, rgba8(0, 255, 204, 255));
739
+
740
+ if (recoil) {
741
+ rectfill(gx - 25, gy - 90, 50, 40, rgba8(0, 255, 204, 170));
742
+ rectfill(gx - 10, gy - 80, 20, 20, rgba8(255, 255, 255, 255));
743
+ }
744
+ }