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,1117 @@
1
+ // DUNGEON DELVE 3D — Ultimate Roguelike Dungeon Crawler
2
+ // Procedurally generated dungeons with themed floors, boss fights, traps,
3
+ // particle effects, screen shake, and atmospheric lighting.
4
+
5
+ const TILE_SIZE = 2;
6
+ const MAP_W = 30;
7
+ const MAP_H = 30;
8
+ const TILE = { WALL: 0, FLOOR: 1, DOOR: 2, STAIRS: 3, CHEST: 4, TRAP: 5, TORCH: 6 };
9
+
10
+ // ── Floor themes ────────────────────────────────────────────────────────────
11
+ const THEMES = [
12
+ {
13
+ name: 'Stone Crypt',
14
+ wall: 0x444466,
15
+ floor: 0x2a2a3a,
16
+ fog: 0x0a0a18,
17
+ accent: 0x6666aa,
18
+ ambient: 0xccccee,
19
+ torch: 0xff8833,
20
+ },
21
+ {
22
+ name: 'Mossy Sewer',
23
+ wall: 0x335544,
24
+ floor: 0x223322,
25
+ fog: 0x081008,
26
+ accent: 0x44aa66,
27
+ ambient: 0x88ddaa,
28
+ torch: 0x66ff88,
29
+ },
30
+ {
31
+ name: 'Bone Catacombs',
32
+ wall: 0x665544,
33
+ floor: 0x332b22,
34
+ fog: 0x100c08,
35
+ accent: 0xccaa88,
36
+ ambient: 0xeeddcc,
37
+ torch: 0xffaa44,
38
+ },
39
+ {
40
+ name: 'Frozen Depths',
41
+ wall: 0x556688,
42
+ floor: 0x334455,
43
+ fog: 0x0a1020,
44
+ accent: 0x88bbff,
45
+ ambient: 0xaaccff,
46
+ torch: 0x44ccff,
47
+ },
48
+ {
49
+ name: 'Inferno Pits',
50
+ wall: 0x663322,
51
+ floor: 0x331a10,
52
+ fog: 0x180808,
53
+ accent: 0xff4422,
54
+ ambient: 0xff8866,
55
+ torch: 0xff3300,
56
+ },
57
+ {
58
+ name: 'The Void',
59
+ wall: 0x332244,
60
+ floor: 0x1a1128,
61
+ fog: 0x08041a,
62
+ accent: 0xaa44ff,
63
+ ambient: 0xcc88ff,
64
+ torch: 0xdd55ff,
65
+ },
66
+ ];
67
+
68
+ // ── State ───────────────────────────────────────────────────────────────────
69
+ let gameState = 'start';
70
+ let time = 0;
71
+ let floor = 1;
72
+ let map = [];
73
+ let rooms = [];
74
+ let mapMeshes = [];
75
+ let torchLights = [];
76
+
77
+ let hero = {
78
+ x: 0,
79
+ y: 0,
80
+ hp: 25,
81
+ maxHp: 25,
82
+ atk: 5,
83
+ def: 2,
84
+ xp: 0,
85
+ xpNext: 15,
86
+ level: 1,
87
+ gold: 0,
88
+ potions: 2,
89
+ weapon: 'Rusty Sword',
90
+ weaponBonus: 0,
91
+ kills: 0,
92
+ totalDmg: 0,
93
+ };
94
+ let heroMesh = null;
95
+ let heroGlow = null;
96
+
97
+ let enemies = [];
98
+ let enemyMeshes = new Map();
99
+
100
+ let items = [];
101
+ let itemMeshes = new Map();
102
+
103
+ let messages = [];
104
+ let floatingTexts;
105
+ let shake;
106
+ let potionCd;
107
+
108
+ let camCurrent = { x: 0, y: 0 };
109
+ let moveTimer = 0;
110
+ const MOVE_DELAY = 0.12;
111
+
112
+ let screenFlash = 0;
113
+ let screenFlashColor = [255, 50, 50];
114
+ let minimap = null;
115
+ let discoveredTiles = null;
116
+
117
+ // ── Theme helper ────────────────────────────────────────────────────────────
118
+ function getTheme() {
119
+ return THEMES[(floor - 1) % THEMES.length];
120
+ }
121
+
122
+ // ── Enemy templates ─────────────────────────────────────────────────────────
123
+ function getEnemyTypes() {
124
+ const f = floor;
125
+ return [
126
+ {
127
+ name: 'Slime',
128
+ hp: 6 + f * 2,
129
+ atk: 2 + f,
130
+ def: 0,
131
+ xp: 5 + f,
132
+ color: 0x44ff44,
133
+ shape: 'slime',
134
+ },
135
+ {
136
+ name: 'Skeleton',
137
+ hp: 10 + f * 2,
138
+ atk: 4 + f,
139
+ def: 1,
140
+ xp: 10 + f * 2,
141
+ color: 0xddddaa,
142
+ shape: 'skeleton',
143
+ },
144
+ {
145
+ name: 'Demon',
146
+ hp: 14 + f * 3,
147
+ atk: 6 + f,
148
+ def: 2,
149
+ xp: 20 + f * 3,
150
+ color: 0xff3333,
151
+ shape: 'demon',
152
+ },
153
+ {
154
+ name: 'Ghost',
155
+ hp: 8 + f * 2,
156
+ atk: 5 + f,
157
+ def: 0,
158
+ xp: 15 + f * 2,
159
+ color: 0x8888ff,
160
+ shape: 'ghost',
161
+ },
162
+ {
163
+ name: 'Lich',
164
+ hp: 18 + f * 4,
165
+ atk: 8 + f,
166
+ def: 3,
167
+ xp: 30 + f * 4,
168
+ color: 0xaa44ff,
169
+ shape: 'lich',
170
+ },
171
+ {
172
+ name: 'Dragon Whelp',
173
+ hp: 25 + f * 5,
174
+ atk: 10 + f,
175
+ def: 4,
176
+ xp: 50 + f * 5,
177
+ color: 0xff8800,
178
+ shape: 'dragon',
179
+ },
180
+ ];
181
+ }
182
+
183
+ function getBossTemplate() {
184
+ const f = floor;
185
+ const bosses = [
186
+ {
187
+ name: 'Goblin King',
188
+ hp: 40 + f * 8,
189
+ atk: 8 + f * 2,
190
+ def: 3,
191
+ xp: 80 + f * 10,
192
+ color: 0x88ff44,
193
+ shape: 'boss',
194
+ },
195
+ {
196
+ name: 'Shadow Lord',
197
+ hp: 60 + f * 10,
198
+ atk: 10 + f * 2,
199
+ def: 4,
200
+ xp: 120 + f * 10,
201
+ color: 0x6622cc,
202
+ shape: 'boss',
203
+ },
204
+ {
205
+ name: 'Bone Dragon',
206
+ hp: 80 + f * 12,
207
+ atk: 12 + f * 3,
208
+ def: 5,
209
+ xp: 200 + f * 15,
210
+ color: 0xffcc44,
211
+ shape: 'boss',
212
+ },
213
+ ];
214
+ return bosses[Math.floor(floor / 3) % bosses.length];
215
+ }
216
+
217
+ // ── Create enemy mesh ───────────────────────────────────────────────────────
218
+ function createEnemyMesh(e) {
219
+ const wx = e.x * TILE_SIZE;
220
+ const wz = e.y * TILE_SIZE;
221
+ let m;
222
+ const opts = { material: 'emissive', emissive: e.color };
223
+ switch (e.shape) {
224
+ case 'slime':
225
+ m = createSphere(0.45, e.color, [wx, 0.45, wz], 8, opts);
226
+ setScale(m, 1.2, 0.7, 1.2);
227
+ break;
228
+ case 'skeleton':
229
+ m = createCapsule(0.25, 0.9, e.color, [wx, 0.7, wz], opts);
230
+ break;
231
+ case 'demon':
232
+ m = createCone(0.4, 1.2, e.color, [wx, 0.6, wz], opts);
233
+ break;
234
+ case 'ghost':
235
+ m = createSphere(0.35, e.color, [wx, 0.8, wz], 8, {
236
+ material: 'emissive',
237
+ emissive: e.color,
238
+ });
239
+ break;
240
+ case 'lich':
241
+ m = createCylinder(0.15, 0.35, 1.2, e.color, [wx, 0.6, wz], opts);
242
+ break;
243
+ case 'dragon':
244
+ m = createCube(0.9, e.color, [wx, 0.6, wz], opts);
245
+ setScale(m, 1.2, 0.8, 1.5);
246
+ break;
247
+ case 'boss':
248
+ m = createCube(1.2, e.color, [wx, 0.9, wz], { material: 'emissive', emissive: e.color });
249
+ setScale(m, 1.5, 1.5, 1.5);
250
+ break;
251
+ default:
252
+ m = createSphere(0.4, e.color, [wx, 0.5, wz], 8, opts);
253
+ }
254
+ return m;
255
+ }
256
+
257
+ // ── Dungeon generation ──────────────────────────────────────────────────────
258
+ function generateDungeon() {
259
+ for (const m of mapMeshes) destroyMesh(m);
260
+ mapMeshes = [];
261
+ for (const [, m] of enemyMeshes) destroyMesh(m);
262
+ enemyMeshes.clear();
263
+ for (const [, m] of itemMeshes) destroyMesh(m);
264
+ itemMeshes.clear();
265
+ for (const l of torchLights) destroyMesh(l);
266
+ torchLights = [];
267
+ if (heroMesh) {
268
+ destroyMesh(heroMesh);
269
+ heroMesh = null;
270
+ }
271
+ if (heroGlow) {
272
+ destroyMesh(heroGlow);
273
+ heroGlow = null;
274
+ }
275
+
276
+ map = [];
277
+ for (let y = 0; y < MAP_H; y++) {
278
+ map[y] = [];
279
+ for (let x = 0; x < MAP_W; x++) map[y][x] = TILE.WALL;
280
+ }
281
+
282
+ discoveredTiles = [];
283
+ for (let y = 0; y < MAP_H; y++) {
284
+ discoveredTiles[y] = [];
285
+ for (let x = 0; x < MAP_W; x++) discoveredTiles[y][x] = false;
286
+ }
287
+
288
+ // Generate rooms
289
+ rooms = [];
290
+ const maxRooms = 8 + Math.min(floor, 6);
291
+ for (let i = 0; i < maxRooms * 4; i++) {
292
+ const w = 3 + Math.floor(Math.random() * 5);
293
+ const h = 3 + Math.floor(Math.random() * 5);
294
+ const rx = 1 + Math.floor(Math.random() * (MAP_W - w - 2));
295
+ const ry = 1 + Math.floor(Math.random() * (MAP_H - h - 2));
296
+ let overlap = false;
297
+ for (const r of rooms) {
298
+ if (rx - 1 < r.x + r.w && rx + w + 1 > r.x && ry - 1 < r.y + r.h && ry + h + 1 > r.y) {
299
+ overlap = true;
300
+ break;
301
+ }
302
+ }
303
+ if (overlap) continue;
304
+ rooms.push({ x: rx, y: ry, w, h, cx: Math.floor(rx + w / 2), cy: Math.floor(ry + h / 2) });
305
+ for (let dy = 0; dy < h; dy++)
306
+ for (let dx = 0; dx < w; dx++) map[ry + dy][rx + dx] = TILE.FLOOR;
307
+ if (rooms.length >= maxRooms) break;
308
+ }
309
+
310
+ // Corridors with doors
311
+ for (let i = 1; i < rooms.length; i++) {
312
+ const a = rooms[i - 1],
313
+ b = rooms[i];
314
+ let x = a.cx,
315
+ y = a.cy;
316
+ let placedDoor = false;
317
+ while (x !== b.cx) {
318
+ if (!placedDoor && map[y][x] === TILE.WALL) {
319
+ map[y][x] = TILE.DOOR;
320
+ placedDoor = true;
321
+ } else if (map[y][x] === TILE.WALL) map[y][x] = TILE.FLOOR;
322
+ x += x < b.cx ? 1 : -1;
323
+ }
324
+ placedDoor = false;
325
+ while (y !== b.cy) {
326
+ if (!placedDoor && map[y][x] === TILE.WALL) {
327
+ map[y][x] = TILE.DOOR;
328
+ placedDoor = true;
329
+ } else if (map[y][x] === TILE.WALL) map[y][x] = TILE.FLOOR;
330
+ y += y < b.cy ? 1 : -1;
331
+ }
332
+ }
333
+
334
+ // Stairs in last room
335
+ const lastRoom = rooms[rooms.length - 1];
336
+ map[lastRoom.cy][lastRoom.cx] = TILE.STAIRS;
337
+
338
+ // Chests
339
+ for (let i = 2; i < rooms.length - 1; i++) {
340
+ if (Math.random() < 0.45) {
341
+ const r = rooms[i];
342
+ const cx = r.x + 1 + Math.floor(Math.random() * Math.max(1, r.w - 2));
343
+ const cy = r.y + 1 + Math.floor(Math.random() * Math.max(1, r.h - 2));
344
+ if (map[cy][cx] === TILE.FLOOR) map[cy][cx] = TILE.CHEST;
345
+ }
346
+ }
347
+
348
+ // Traps (floor 2+)
349
+ if (floor >= 2) {
350
+ for (let i = 1; i < rooms.length; i++) {
351
+ if (Math.random() < 0.3) {
352
+ const r = rooms[i];
353
+ const tx = r.x + 1 + Math.floor(Math.random() * Math.max(1, r.w - 2));
354
+ const ty = r.y + 1 + Math.floor(Math.random() * Math.max(1, r.h - 2));
355
+ if (map[ty][tx] === TILE.FLOOR) map[ty][tx] = TILE.TRAP;
356
+ }
357
+ }
358
+ }
359
+
360
+ // Torches at room edges
361
+ for (let i = 0; i < rooms.length; i++) {
362
+ const r = rooms[i];
363
+ // Try corners adjacent to walls
364
+ const corners = [
365
+ [r.x - 1, r.y - 1],
366
+ [r.x + r.w, r.y - 1],
367
+ [r.x - 1, r.y + r.h],
368
+ [r.x + r.w, r.y + r.h],
369
+ ];
370
+ for (const [tx, ty] of corners) {
371
+ if (tx >= 0 && tx < MAP_W && ty >= 0 && ty < MAP_H && map[ty][tx] === TILE.WALL) {
372
+ map[ty][tx] = TILE.TORCH;
373
+ }
374
+ }
375
+ }
376
+
377
+ // Player in first room
378
+ hero.x = rooms[0].cx;
379
+ hero.y = rooms[0].cy;
380
+
381
+ // Enemies
382
+ enemies = [];
383
+ const types = getEnemyTypes();
384
+ const isBossFloor = floor % 3 === 0;
385
+
386
+ for (let i = 1; i < rooms.length; i++) {
387
+ const r = rooms[i];
388
+ if (isBossFloor && i === rooms.length - 1) {
389
+ const boss = getBossTemplate();
390
+ enemies.push({
391
+ ...boss,
392
+ x: r.cx,
393
+ y: r.cy + (r.h > 3 ? 1 : 0),
394
+ maxHp: boss.hp,
395
+ alive: true,
396
+ isBoss: true,
397
+ });
398
+ map[lastRoom.cy][lastRoom.cx] = TILE.FLOOR;
399
+ const sy = lastRoom.y + 1,
400
+ sx = lastRoom.x + 1;
401
+ if (sy < MAP_H && sx < MAP_W) map[sy][sx] = TILE.STAIRS;
402
+ continue;
403
+ }
404
+ const numEnemies = 1 + Math.floor(Math.random() * (1 + Math.floor(floor / 2)));
405
+ for (let e = 0; e < numEnemies; e++) {
406
+ const ex = r.x + 1 + Math.floor(Math.random() * Math.max(1, r.w - 2));
407
+ const ey = r.y + 1 + Math.floor(Math.random() * Math.max(1, r.h - 2));
408
+ if (map[ey][ex] !== TILE.FLOOR) continue;
409
+ const maxIdx = Math.min(Math.floor(1 + floor * 0.6), types.length - 1);
410
+ const t = types[Math.floor(Math.random() * (maxIdx + 1))];
411
+ enemies.push({ ...t, x: ex, y: ey, maxHp: t.hp, alive: true, isBoss: false });
412
+ }
413
+ }
414
+
415
+ // Items
416
+ items = [];
417
+ for (let i = 1; i < rooms.length; i++) {
418
+ if (Math.random() < 0.4) {
419
+ const r = rooms[i];
420
+ const ix = r.x + Math.floor(Math.random() * r.w);
421
+ const iy = r.y + Math.floor(Math.random() * r.h);
422
+ if (map[iy][ix] === TILE.FLOOR) {
423
+ items.push({
424
+ x: ix,
425
+ y: iy,
426
+ type: Math.random() < 0.55 ? 'gold' : 'potion',
427
+ amount: Math.random() < 0.55 ? 5 + Math.floor(Math.random() * 10 * floor) : 1,
428
+ collected: false,
429
+ });
430
+ }
431
+ }
432
+ }
433
+
434
+ buildMapMeshes();
435
+ revealAround(hero.x, hero.y, 4);
436
+ }
437
+
438
+ // ── Fog of war ──────────────────────────────────────────────────────────────
439
+ function revealAround(cx, cy, radius) {
440
+ for (let dy = -radius; dy <= radius; dy++) {
441
+ for (let dx = -radius; dx <= radius; dx++) {
442
+ const nx = cx + dx,
443
+ ny = cy + dy;
444
+ if (
445
+ nx >= 0 &&
446
+ nx < MAP_W &&
447
+ ny >= 0 &&
448
+ ny < MAP_H &&
449
+ Math.abs(dx) + Math.abs(dy) <= radius + 1
450
+ )
451
+ discoveredTiles[ny][nx] = true;
452
+ }
453
+ }
454
+ }
455
+
456
+ // ── Build 3D meshes ─────────────────────────────────────────────────────────
457
+ function buildMapMeshes() {
458
+ const theme = getTheme();
459
+
460
+ setAmbientLight(theme.ambient, 0.12);
461
+ setLightDirection(0, -1, 0.2);
462
+ setLightColor(theme.accent);
463
+ setFog(theme.fog, 4, 20);
464
+ enableBloom(0.6, 0.3, 0.4);
465
+ enableVignette(1.2, 0.9);
466
+
467
+ for (let y = 0; y < MAP_H; y++) {
468
+ for (let x = 0; x < MAP_W; x++) {
469
+ const wx = x * TILE_SIZE;
470
+ const wz = y * TILE_SIZE;
471
+ const tile = map[y][x];
472
+
473
+ if (tile === TILE.WALL || tile === TILE.TORCH) {
474
+ let adjacent = false;
475
+ for (let dy2 = -1; dy2 <= 1; dy2++) {
476
+ for (let dx2 = -1; dx2 <= 1; dx2++) {
477
+ const ny2 = y + dy2,
478
+ nx2 = x + dx2;
479
+ if (
480
+ ny2 >= 0 &&
481
+ ny2 < MAP_H &&
482
+ nx2 >= 0 &&
483
+ nx2 < MAP_W &&
484
+ map[ny2][nx2] !== TILE.WALL &&
485
+ map[ny2][nx2] !== TILE.TORCH
486
+ )
487
+ adjacent = true;
488
+ }
489
+ }
490
+ if (adjacent) {
491
+ const wallMesh = createCube(TILE_SIZE, theme.wall, [wx, TILE_SIZE * 0.75, wz], {
492
+ material: 'standard',
493
+ roughness: 0.9,
494
+ });
495
+ setScale(wallMesh, 1, 1.5, 1);
496
+ mapMeshes.push(wallMesh);
497
+ if (tile === TILE.TORCH) {
498
+ const torchMesh = createCube(0.15, theme.torch, [wx, TILE_SIZE * 1.6, wz], {
499
+ material: 'emissive',
500
+ emissive: theme.torch,
501
+ });
502
+ mapMeshes.push(torchMesh);
503
+ const light = createPointLight(theme.torch, 1.5, 8, wx, TILE_SIZE * 1.6, wz);
504
+ torchLights.push(light);
505
+ }
506
+ }
507
+ } else {
508
+ let floorColor = theme.floor;
509
+ if (tile === TILE.STAIRS) floorColor = 0xffdd44;
510
+ else if (tile === TILE.CHEST) floorColor = 0x886622;
511
+ else if (tile === TILE.DOOR) floorColor = 0x553311;
512
+
513
+ const floorMesh = createCube(TILE_SIZE, floorColor, [wx, -0.1, wz], {
514
+ material: 'standard',
515
+ roughness: 1.0,
516
+ });
517
+ setScale(floorMesh, 1, 0.1, 1);
518
+ mapMeshes.push(floorMesh);
519
+
520
+ if (tile === TILE.STAIRS) {
521
+ const stairGlow = createCylinder(0.15, 0.15, 2.5, 0xffdd44, [wx, 1.25, wz], {
522
+ material: 'emissive',
523
+ emissive: 0xffdd44,
524
+ });
525
+ mapMeshes.push(stairGlow);
526
+ const sl = createPointLight(0xffdd44, 2, 8, wx, 2, wz);
527
+ torchLights.push(sl);
528
+ }
529
+
530
+ if (tile === TILE.CHEST) {
531
+ const chest = createCube(0.7, 0xcc8833, [wx, 0.35, wz], {
532
+ material: 'standard',
533
+ roughness: 0.5,
534
+ });
535
+ setScale(chest, 1, 0.7, 0.7);
536
+ mapMeshes.push(chest);
537
+ const lid = createCube(0.7, 0xffcc44, [wx, 0.6, wz], {
538
+ material: 'emissive',
539
+ emissive: 0xffcc44,
540
+ });
541
+ setScale(lid, 1.05, 0.15, 0.75);
542
+ mapMeshes.push(lid);
543
+ }
544
+
545
+ if (tile === TILE.DOOR) {
546
+ const doorL = createCube(0.2, 0x553311, [wx - 0.7, 0.7, wz], {
547
+ material: 'standard',
548
+ roughness: 0.7,
549
+ });
550
+ setScale(doorL, 1, 7, 1);
551
+ mapMeshes.push(doorL);
552
+ const doorR = createCube(0.2, 0x553311, [wx + 0.7, 0.7, wz], {
553
+ material: 'standard',
554
+ roughness: 0.7,
555
+ });
556
+ setScale(doorR, 1, 7, 1);
557
+ mapMeshes.push(doorR);
558
+ const doorTop = createCube(0.2, 0x664422, [wx, 1.5, wz], {
559
+ material: 'standard',
560
+ roughness: 0.7,
561
+ });
562
+ setScale(doorTop, 8, 1, 1);
563
+ mapMeshes.push(doorTop);
564
+ }
565
+ }
566
+ }
567
+ }
568
+
569
+ // Hero
570
+ heroMesh = createCapsule(0.3, 0.8, 0x4488ff, [hero.x * TILE_SIZE, 0.7, hero.y * TILE_SIZE], {
571
+ material: 'standard',
572
+ roughness: 0.4,
573
+ });
574
+ heroGlow = createPointLight(0x4488ff, 1.0, 6, hero.x * TILE_SIZE, 1.5, hero.y * TILE_SIZE);
575
+ torchLights.push(heroGlow);
576
+
577
+ for (const e of enemies) enemyMeshes.set(e, createEnemyMesh(e));
578
+
579
+ for (const item of items) {
580
+ const color = item.type === 'gold' ? 0xffdd00 : 0xff4488;
581
+ itemMeshes.set(
582
+ item,
583
+ createSphere(0.2, color, [item.x * TILE_SIZE, 0.3, item.y * TILE_SIZE], 8, {
584
+ material: 'emissive',
585
+ emissive: color,
586
+ })
587
+ );
588
+ }
589
+
590
+ minimap = createMinimap({
591
+ x: 540,
592
+ y: 10,
593
+ width: 90,
594
+ height: 90,
595
+ shape: 'rect',
596
+ worldW: MAP_W,
597
+ worldH: MAP_H,
598
+ bgColor: rgba8(0, 0, 0, 200),
599
+ borderLight: rgba8(100, 100, 140, 255),
600
+ borderDark: rgba8(40, 40, 60, 255),
601
+ fogOfWar: 5,
602
+ tiles: (tx, ty) => {
603
+ if (!discoveredTiles[ty] || !discoveredTiles[ty][tx]) return null;
604
+ const t = map[ty][tx];
605
+ if (t === TILE.WALL || t === TILE.TORCH) return rgba8(60, 60, 80);
606
+ if (t === TILE.STAIRS) return rgba8(255, 255, 100);
607
+ if (t === TILE.CHEST) return rgba8(200, 150, 50);
608
+ if (t === TILE.DOOR) return rgba8(100, 70, 30);
609
+ return rgba8(40, 40, 55);
610
+ },
611
+ tileW: MAP_W,
612
+ tileH: MAP_H,
613
+ player: { x: hero.x, y: hero.y, color: rgba8(80, 160, 255), blink: true },
614
+ entities: [],
615
+ });
616
+ }
617
+
618
+ // ── Combat ──────────────────────────────────────────────────────────────────
619
+ function rollDmg(attacker, defender) {
620
+ return Math.max(1, attacker.atk - defender.def + Math.floor(Math.random() * 3) - 1);
621
+ }
622
+
623
+ function tryMove(dx, dy) {
624
+ const nx = hero.x + dx;
625
+ const ny = hero.y + dy;
626
+ if (nx < 0 || nx >= MAP_W || ny < 0 || ny >= MAP_H) return;
627
+ if (map[ny][nx] === TILE.WALL || map[ny][nx] === TILE.TORCH) return;
628
+
629
+ const enemy = enemies.find(e => e.alive && e.x === nx && e.y === ny);
630
+ if (enemy) {
631
+ const dmg = rollDmg(hero, enemy);
632
+ enemy.hp -= dmg;
633
+ hero.totalDmg += dmg;
634
+ triggerShake(shake, enemy.isBoss ? 6 : 3);
635
+ screenFlash = 0.12;
636
+ screenFlashColor = [255, 180, 50];
637
+ addMessage(`Hit ${enemy.name} for ${dmg}!`, 0xffaa44);
638
+ floatingTexts.spawn(`-${dmg}`, enemy.x * TILE_SIZE, 2.5, {
639
+ z: enemy.y * TILE_SIZE,
640
+ duration: 0.8,
641
+ color: 0xff8844,
642
+ });
643
+ sfx('hit');
644
+ if (enemy.hp <= 0) {
645
+ enemy.alive = false;
646
+ hero.xp += enemy.xp;
647
+ hero.kills++;
648
+ addMessage(`${enemy.name} slain! +${enemy.xp}XP`, enemy.isBoss ? 0xffdd44 : 0x44ff44);
649
+ if (enemyMeshes.has(enemy)) {
650
+ destroyMesh(enemyMeshes.get(enemy));
651
+ enemyMeshes.delete(enemy);
652
+ }
653
+ sfx('explosion');
654
+ if (Math.random() < 0.35) {
655
+ const drop = {
656
+ x: nx,
657
+ y: ny,
658
+ type: Math.random() < 0.5 ? 'gold' : 'potion',
659
+ amount: Math.random() < 0.5 ? 5 + Math.floor(Math.random() * 8 * floor) : 1,
660
+ collected: false,
661
+ };
662
+ items.push(drop);
663
+ const color = drop.type === 'gold' ? 0xffdd00 : 0xff4488;
664
+ itemMeshes.set(
665
+ drop,
666
+ createSphere(0.2, color, [drop.x * TILE_SIZE, 0.3, drop.y * TILE_SIZE], 8, {
667
+ material: 'emissive',
668
+ emissive: color,
669
+ })
670
+ );
671
+ }
672
+ checkLevelUp();
673
+ }
674
+ enemyTurn();
675
+ return;
676
+ }
677
+
678
+ hero.x = nx;
679
+ hero.y = ny;
680
+ setPosition(heroMesh, nx * TILE_SIZE, 0.7, ny * TILE_SIZE);
681
+ setPosition(heroGlow, nx * TILE_SIZE, 1.5, ny * TILE_SIZE);
682
+ revealAround(nx, ny, 4);
683
+
684
+ for (const item of items) {
685
+ if (!item.collected && item.x === nx && item.y === ny) {
686
+ item.collected = true;
687
+ if (item.type === 'gold') {
688
+ hero.gold += item.amount;
689
+ addMessage(`+${item.amount} gold`, 0xffdd00);
690
+ } else {
691
+ hero.potions += item.amount;
692
+ addMessage('+1 Potion', 0xff4488);
693
+ }
694
+ sfx('coin');
695
+ if (itemMeshes.has(item)) {
696
+ destroyMesh(itemMeshes.get(item));
697
+ itemMeshes.delete(item);
698
+ }
699
+ }
700
+ }
701
+
702
+ if (map[ny][nx] === TILE.CHEST) {
703
+ const loot = Math.floor(Math.random() * 25 * floor) + 10;
704
+ hero.gold += loot;
705
+ addMessage(`Chest! +${loot} gold`, 0xffaa00);
706
+ sfx('coin');
707
+ map[ny][nx] = TILE.FLOOR;
708
+ if (Math.random() < 0.35) {
709
+ hero.weaponBonus++;
710
+ const weapons = [
711
+ 'Iron Sword',
712
+ 'Steel Blade',
713
+ 'Flame Sword',
714
+ 'Shadow Katana',
715
+ 'Dragon Fang',
716
+ 'Void Cleaver',
717
+ ];
718
+ hero.weapon = weapons[Math.min(hero.weaponBonus - 1, weapons.length - 1)];
719
+ hero.atk += 2;
720
+ addMessage(`Found ${hero.weapon}! ATK+2`, 0xff8844);
721
+ sfx('powerup');
722
+ screenFlash = 0.2;
723
+ screenFlashColor = [255, 200, 50];
724
+ }
725
+ }
726
+
727
+ if (map[ny][nx] === TILE.TRAP) {
728
+ const trapDmg = 3 + Math.floor(floor * 1.5);
729
+ hero.hp -= trapDmg;
730
+ addMessage(`TRAP! -${trapDmg} HP`, 0xff2222);
731
+ screenFlash = 0.15;
732
+ screenFlashColor = [255, 50, 50];
733
+ triggerShake(shake, 4);
734
+ sfx('hit');
735
+ map[ny][nx] = TILE.FLOOR;
736
+ if (hero.hp <= 0) {
737
+ gameState = 'dead';
738
+ addMessage('Killed by a trap!', 0xff0000);
739
+ return;
740
+ }
741
+ }
742
+
743
+ if (map[ny][nx] === TILE.STAIRS) {
744
+ floor++;
745
+ addMessage(`Descending to Floor ${floor}...`, 0x8888ff);
746
+ hero.hp = Math.min(hero.hp + 8, hero.maxHp);
747
+ sfx('powerup');
748
+ generateDungeon();
749
+ return;
750
+ }
751
+
752
+ enemyTurn();
753
+ }
754
+
755
+ function enemyTurn() {
756
+ for (const e of enemies) {
757
+ if (!e.alive) continue;
758
+ const dx = hero.x - e.x;
759
+ const dy = hero.y - e.y;
760
+ const dist = Math.abs(dx) + Math.abs(dy);
761
+ if (dist <= 1) {
762
+ const dmg = rollDmg(e, hero);
763
+ hero.hp -= dmg;
764
+ screenFlash = 0.1;
765
+ screenFlashColor = [255, 50, 50];
766
+ triggerShake(shake, e.isBoss ? 5 : 2);
767
+ addMessage(`${e.name} hits you for ${dmg}!`, 0xff4444);
768
+ floatingTexts.spawn(`-${dmg}`, hero.x * TILE_SIZE, 2.5, {
769
+ z: hero.y * TILE_SIZE,
770
+ duration: 0.8,
771
+ color: 0xff4444,
772
+ });
773
+ sfx('hit');
774
+ if (hero.hp <= 0) {
775
+ gameState = 'dead';
776
+ addMessage('You have been slain!', 0xff0000);
777
+ sfx('explosion');
778
+ }
779
+ } else if (dist < 10) {
780
+ let mx = 0,
781
+ my = 0;
782
+ if (Math.abs(dx) > Math.abs(dy)) mx = dx > 0 ? 1 : -1;
783
+ else my = dy > 0 ? 1 : -1;
784
+ const enx = e.x + mx,
785
+ eny = e.y + my;
786
+ if (
787
+ enx >= 0 &&
788
+ enx < MAP_W &&
789
+ eny >= 0 &&
790
+ eny < MAP_H &&
791
+ map[eny][enx] !== TILE.WALL &&
792
+ map[eny][enx] !== TILE.TORCH
793
+ ) {
794
+ const blocked = enemies.some(o => o !== e && o.alive && o.x === enx && o.y === eny);
795
+ if (!blocked && !(enx === hero.x && eny === hero.y)) {
796
+ e.x = enx;
797
+ e.y = eny;
798
+ if (enemyMeshes.has(e))
799
+ setPosition(enemyMeshes.get(e), e.x * TILE_SIZE, e.isBoss ? 0.9 : 0.5, e.y * TILE_SIZE);
800
+ }
801
+ }
802
+ }
803
+ }
804
+ }
805
+
806
+ function checkLevelUp() {
807
+ while (hero.xp >= hero.xpNext) {
808
+ hero.xp -= hero.xpNext;
809
+ hero.level++;
810
+ hero.maxHp += 5;
811
+ hero.hp = hero.maxHp;
812
+ hero.atk += 1;
813
+ hero.def += 1;
814
+ hero.xpNext = Math.floor(hero.xpNext * 1.5);
815
+ addMessage(`LEVEL UP! Lv.${hero.level}!`, 0xffff00);
816
+ screenFlash = 0.3;
817
+ screenFlashColor = [255, 255, 100];
818
+ sfx('powerup');
819
+ }
820
+ }
821
+
822
+ function addMessage(text, color) {
823
+ messages.unshift({ text, color, timer: 4 });
824
+ if (messages.length > 8) messages.pop();
825
+ }
826
+
827
+ function usePotion() {
828
+ if (hero.potions > 0 && hero.hp < hero.maxHp) {
829
+ hero.potions--;
830
+ const heal = 10 + hero.level * 3;
831
+ hero.hp = Math.min(hero.hp + heal, hero.maxHp);
832
+ addMessage(`Healed ${heal} HP!`, 0xff88ff);
833
+ screenFlash = 0.1;
834
+ screenFlashColor = [100, 255, 150];
835
+ sfx('powerup');
836
+ enemyTurn();
837
+ }
838
+ }
839
+
840
+ // ── Init ────────────────────────────────────────────────────────────────────
841
+ export function init() {
842
+ hero = {
843
+ x: 0,
844
+ y: 0,
845
+ hp: 25,
846
+ maxHp: 25,
847
+ atk: 5,
848
+ def: 2,
849
+ xp: 0,
850
+ xpNext: 15,
851
+ level: 1,
852
+ gold: 0,
853
+ potions: 2,
854
+ weapon: 'Rusty Sword',
855
+ weaponBonus: 0,
856
+ kills: 0,
857
+ totalDmg: 0,
858
+ };
859
+ floor = 1;
860
+ messages = [];
861
+ floatingTexts = createFloatingTextSystem();
862
+ shake = createShake({ decay: 0.85, maxMag: 8 });
863
+ potionCd = createCooldown(0.5);
864
+ enemies = [];
865
+ items = [];
866
+ mapMeshes = [];
867
+ torchLights = [];
868
+ time = 0;
869
+ gameState = 'start';
870
+
871
+ enableFXAA();
872
+ generateDungeon();
873
+ }
874
+
875
+ // ── Update ──────────────────────────────────────────────────────────────────
876
+ export function update(dt) {
877
+ time += dt;
878
+ updateShake(shake, dt);
879
+ updateCooldown(potionCd, dt);
880
+
881
+ if (gameState === 'start') {
882
+ if (keyp('Space') || keyp('Enter')) {
883
+ gameState = 'playing';
884
+ sfx('confirm');
885
+ }
886
+ updateCamera(dt);
887
+ return;
888
+ }
889
+
890
+ if (gameState === 'dead') {
891
+ if (keyp('Space') || keyp('Enter')) init();
892
+ return;
893
+ }
894
+
895
+ moveTimer -= dt;
896
+ if (moveTimer <= 0) {
897
+ let moved = false;
898
+ const held = moveTimer < -0.3;
899
+ if (keyp('ArrowUp') || keyp('KeyW') || (held && (key('ArrowUp') || key('KeyW')))) {
900
+ tryMove(0, -1);
901
+ moved = true;
902
+ } else if (keyp('ArrowDown') || keyp('KeyS') || (held && (key('ArrowDown') || key('KeyS')))) {
903
+ tryMove(0, 1);
904
+ moved = true;
905
+ } else if (keyp('ArrowLeft') || keyp('KeyA') || (held && (key('ArrowLeft') || key('KeyA')))) {
906
+ tryMove(-1, 0);
907
+ moved = true;
908
+ } else if (keyp('ArrowRight') || keyp('KeyD') || (held && (key('ArrowRight') || key('KeyD')))) {
909
+ tryMove(1, 0);
910
+ moved = true;
911
+ }
912
+ if (moved) moveTimer = MOVE_DELAY;
913
+ }
914
+
915
+ if ((keyp('KeyP') || keyp('KeyQ')) && useCooldown(potionCd)) usePotion();
916
+
917
+ if (keyp('Space')) {
918
+ enemyTurn();
919
+ addMessage('Waiting...', 0x888899);
920
+ }
921
+
922
+ if (screenFlash > 0) screenFlash -= dt;
923
+
924
+ for (const m of messages) m.timer -= dt;
925
+ messages = messages.filter(m => m.timer > 0);
926
+ floatingTexts.update(dt);
927
+
928
+ for (const e of enemies) {
929
+ if (!e.alive) continue;
930
+ const mesh = enemyMeshes.get(e);
931
+ if (mesh) {
932
+ const bob = Math.sin(time * 3 + e.x * 2 + e.y * 3) * 0.12;
933
+ setPosition(mesh, e.x * TILE_SIZE, (e.isBoss ? 0.9 : 0.5) + bob, e.y * TILE_SIZE);
934
+ rotateMesh(mesh, 0, dt * (e.isBoss ? 0.5 : 1.5), 0);
935
+ }
936
+ }
937
+
938
+ if (heroMesh) {
939
+ const bob = Math.sin(time * 4) * 0.04;
940
+ setPosition(heroMesh, hero.x * TILE_SIZE, 0.7 + bob, hero.y * TILE_SIZE);
941
+ }
942
+
943
+ if (minimap) {
944
+ minimap.player.x = hero.x;
945
+ minimap.player.y = hero.y;
946
+ minimap.entities = enemies
947
+ .filter(e => e.alive && Math.abs(e.x - hero.x) + Math.abs(e.y - hero.y) <= 8)
948
+ .map(e => ({
949
+ x: e.x,
950
+ y: e.y,
951
+ color: e.isBoss ? rgba8(255, 220, 50) : rgba8(255, 60, 60),
952
+ size: e.isBoss ? 3 : 2,
953
+ }));
954
+ }
955
+
956
+ updateCamera(dt);
957
+ }
958
+
959
+ function updateCamera(dt) {
960
+ const tx = hero.x * TILE_SIZE;
961
+ const tz = hero.y * TILE_SIZE;
962
+ camCurrent.x += (tx - camCurrent.x) * 0.12;
963
+ camCurrent.y += (tz - camCurrent.y) * 0.12;
964
+ const sx = shake.offsetX || 0;
965
+ const sy = shake.offsetY || 0;
966
+ setCameraPosition(camCurrent.x + 1.5 + sx * 0.05, 14, camCurrent.y + 11 + sy * 0.05);
967
+ setCameraTarget(camCurrent.x + sx * 0.02, 0, camCurrent.y + sy * 0.02);
968
+ }
969
+
970
+ // ── Draw HUD ────────────────────────────────────────────────────────────────
971
+ export function draw() {
972
+ const theme = getTheme();
973
+
974
+ if (gameState === 'start') {
975
+ rectfill(0, 0, 640, 360, rgba8(0, 0, 0, 200));
976
+ drawGlowText('DUNGEON DELVE', 210, 50, rgba8(255, 200, 100), rgba8(200, 120, 40));
977
+ printCentered('A Roguelike Adventure', 320, 85, rgba8(180, 170, 160));
978
+ printCentered(
979
+ 'Descend the dungeon. Slay monsters. Find treasure.',
980
+ 320,
981
+ 130,
982
+ rgba8(180, 180, 200)
983
+ );
984
+ printCentered('Permadeath! When you die, you start over.', 320, 150, rgba8(255, 100, 100));
985
+ rectfill(160, 180, 320, 70, rgba8(20, 20, 30, 200));
986
+ rect(160, 180, 320, 70, rgba8(100, 100, 150), false);
987
+ printCentered('WASD / Arrows = Move & Attack', 320, 190, rgba8(160, 160, 200));
988
+ printCentered('P / Q = Use Potion', 320, 206, rgba8(160, 160, 200));
989
+ printCentered('SPACE = Wait (skip turn)', 320, 222, rgba8(160, 160, 200));
990
+ printCentered('Walk into enemies to attack!', 320, 238, rgba8(200, 200, 160));
991
+ const pulse = Math.sin(time * 3) * 0.5 + 0.5;
992
+ printCentered(
993
+ 'PRESS SPACE TO DELVE',
994
+ 320,
995
+ 280,
996
+ rgba8(255, 255, 100, Math.floor(100 + pulse * 155))
997
+ );
998
+ return;
999
+ }
1000
+
1001
+ if (gameState === 'dead') {
1002
+ rectfill(0, 0, 640, 360, rgba8(60, 0, 0, 220));
1003
+ drawGlowText('YOU HAVE PERISHED', 185, 60, rgba8(255, 50, 50), rgba8(180, 0, 0));
1004
+ printCentered(
1005
+ `Floor ${floor} | Level ${hero.level} | ${hero.kills} Kills | ${hero.gold} Gold`,
1006
+ 320,
1007
+ 110,
1008
+ rgba8(200, 200, 200)
1009
+ );
1010
+ printCentered(`Total Damage Dealt: ${hero.totalDmg}`, 320, 130, rgba8(180, 180, 180));
1011
+ const rating =
1012
+ hero.kills > 30
1013
+ ? 'LEGENDARY'
1014
+ : hero.kills > 20
1015
+ ? 'HEROIC'
1016
+ : hero.kills > 10
1017
+ ? 'CHAMPION'
1018
+ : hero.kills > 5
1019
+ ? 'BRAVE'
1020
+ : 'NOVICE';
1021
+ drawGlowText(rating, 260, 170, rgba8(255, 215, 0), rgba8(180, 150, 0));
1022
+ const pulse = Math.sin(time * 2) * 0.5 + 0.5;
1023
+ printCentered(
1024
+ 'PRESS SPACE TO TRY AGAIN',
1025
+ 320,
1026
+ 240,
1027
+ rgba8(200, 150, 150, Math.floor(120 + pulse * 135))
1028
+ );
1029
+ return;
1030
+ }
1031
+
1032
+ // Screen flash
1033
+ if (screenFlash > 0) {
1034
+ const a = Math.floor(screenFlash * 500);
1035
+ rectfill(
1036
+ 0,
1037
+ 0,
1038
+ 640,
1039
+ 360,
1040
+ rgba8(screenFlashColor[0], screenFlashColor[1], screenFlashColor[2], Math.min(a, 200))
1041
+ );
1042
+ }
1043
+
1044
+ // Stats panel
1045
+ rectfill(6, 6, 170, 108, rgba8(10, 10, 18, 220));
1046
+ rect(6, 6, 170, 108, rgba8(80, 80, 120, 200), false);
1047
+ drawPixelBorder(6, 6, 170, 108, rgba8(90, 90, 130), rgba8(30, 30, 50));
1048
+ print(`FLOOR ${floor} Lv.${hero.level}`, 14, 14, rgba8(255, 200, 100));
1049
+ print(
1050
+ `${theme.name}`,
1051
+ 14,
1052
+ 26,
1053
+ rgba8((theme.accent >> 16) & 0xff, (theme.accent >> 8) & 0xff, theme.accent & 0xff, 200)
1054
+ );
1055
+ print('HP', 14, 40, rgba8(220, 220, 220));
1056
+ drawHealthBar(36, 40, 128, 8, hero.hp, hero.maxHp);
1057
+ print(`${hero.hp}/${hero.maxHp}`, 80, 40, rgba8(255, 255, 255));
1058
+ print('XP', 14, 54, rgba8(220, 220, 220));
1059
+ drawProgressBar(
1060
+ 36,
1061
+ 54,
1062
+ 128,
1063
+ 8,
1064
+ hero.xp / hero.xpNext,
1065
+ rgba8(100, 100, 255),
1066
+ rgba8(30, 30, 50),
1067
+ rgba8(80, 80, 150)
1068
+ );
1069
+ print(`ATK:${hero.atk} DEF:${hero.def}`, 14, 68, rgba8(180, 180, 210));
1070
+ print(`${hero.weapon}`, 14, 80, rgba8(255, 180, 100));
1071
+ print(`Gold:${hero.gold} Pot:${hero.potions}`, 14, 94, rgba8(255, 220, 80));
1072
+
1073
+ // Messages
1074
+ for (let i = 0; i < messages.length; i++) {
1075
+ const m = messages[i];
1076
+ const alpha = Math.min(255, Math.floor(m.timer * 85));
1077
+ const r = (m.color >> 16) & 0xff,
1078
+ g = (m.color >> 8) & 0xff,
1079
+ b = m.color & 0xff;
1080
+ rectfill(188, 318 - i * 14, m.text.length * 8 + 8, 13, rgba8(0, 0, 0, Math.floor(alpha * 0.6)));
1081
+ print(m.text, 192, 320 - i * 14, rgba8(r, g, b, alpha));
1082
+ }
1083
+
1084
+ // Minimap
1085
+ if (minimap) drawMinimap(minimap, time);
1086
+
1087
+ // Enemy HP bars
1088
+ for (const e of enemies) {
1089
+ if (!e.alive) continue;
1090
+ const dist = Math.abs(e.x - hero.x) + Math.abs(e.y - hero.y);
1091
+ if (dist > 6) continue;
1092
+ const sx = 320 + (e.x * TILE_SIZE - hero.x * TILE_SIZE) * 8;
1093
+ const sy = 150 - (e.y * TILE_SIZE - hero.y * TILE_SIZE) * 4;
1094
+ if (sx < 20 || sx > 620 || sy < 20 || sy > 340) continue;
1095
+ const barW = e.isBoss ? 50 : 30;
1096
+ const hpPct = e.hp / e.maxHp;
1097
+ rectfill(sx - barW / 2, sy - 14, barW, 4, rgba8(40, 40, 40, 180));
1098
+ const hpCol =
1099
+ hpPct > 0.5 ? rgba8(50, 200, 50) : hpPct > 0.25 ? rgba8(220, 200, 50) : rgba8(220, 50, 50);
1100
+ rectfill(sx - barW / 2, sy - 14, Math.floor(barW * hpPct), 4, hpCol);
1101
+ print(e.name, sx - e.name.length * 4, sy - 22, rgba8(255, 255, 255, 180));
1102
+ if (e.isBoss) print('BOSS', sx - 16, sy - 30, rgba8(255, 220, 50));
1103
+ }
1104
+
1105
+ drawFloatingTexts3D(floatingTexts, (x, y, z) => [
1106
+ Math.floor(320 + (x - hero.x * TILE_SIZE) * 8),
1107
+ Math.floor(180 - y * 6 - (z - hero.y * TILE_SIZE) * 4),
1108
+ ]);
1109
+
1110
+ rectfill(0, 348, 400, 12, rgba8(0, 0, 0, 120));
1111
+ print(
1112
+ 'WASD=Move P=Potion SPACE=Wait Walk into enemies to attack!',
1113
+ 8,
1114
+ 348,
1115
+ rgba8(140, 140, 170, 200)
1116
+ );
1117
+ }