nova64 0.2.5 → 0.2.7

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 (185) 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/dist/runtime/api-2d.js +1158 -0
  118. package/dist/runtime/api-3d/camera.js +73 -0
  119. package/dist/runtime/api-3d/instancing.js +180 -0
  120. package/dist/runtime/api-3d/lights.js +51 -0
  121. package/dist/runtime/api-3d/materials.js +47 -0
  122. package/dist/runtime/api-3d/models.js +84 -0
  123. package/dist/runtime/api-3d/particles.js +296 -0
  124. package/dist/runtime/api-3d/pbr.js +113 -0
  125. package/dist/runtime/api-3d/primitives.js +304 -0
  126. package/dist/runtime/api-3d/scene.js +169 -0
  127. package/dist/runtime/api-3d/transforms.js +161 -0
  128. package/dist/runtime/api-3d.js +166 -0
  129. package/dist/runtime/api-effects.js +840 -0
  130. package/dist/runtime/api-gameutils.js +476 -0
  131. package/dist/runtime/api-generative.js +610 -0
  132. package/dist/runtime/api-presets.js +85 -0
  133. package/dist/runtime/api-skybox.js +232 -0
  134. package/dist/runtime/api-sprites.js +100 -0
  135. package/dist/runtime/api-voxel.js +712 -0
  136. package/dist/runtime/api.js +201 -0
  137. package/dist/runtime/assets.js +27 -0
  138. package/dist/runtime/audio.js +114 -0
  139. package/dist/runtime/collision.js +47 -0
  140. package/dist/runtime/console.js +101 -0
  141. package/dist/runtime/editor.js +233 -0
  142. package/dist/runtime/font.js +233 -0
  143. package/dist/runtime/framebuffer.js +28 -0
  144. package/dist/runtime/fullscreen-button.js +185 -0
  145. package/dist/runtime/gpu-canvas2d.js +47 -0
  146. package/dist/runtime/gpu-threejs.js +643 -0
  147. package/dist/runtime/gpu-webgl2.js +310 -0
  148. package/dist/runtime/index.d.ts +682 -0
  149. package/dist/runtime/index.js +22 -0
  150. package/dist/runtime/input.js +225 -0
  151. package/dist/runtime/logger.js +60 -0
  152. package/dist/runtime/physics.js +101 -0
  153. package/dist/runtime/screens.js +213 -0
  154. package/dist/runtime/storage.js +38 -0
  155. package/dist/runtime/store.js +151 -0
  156. package/dist/runtime/textinput.js +68 -0
  157. package/dist/runtime/ui/buttons.js +124 -0
  158. package/dist/runtime/ui/panels.js +105 -0
  159. package/dist/runtime/ui/text.js +86 -0
  160. package/dist/runtime/ui/widgets.js +141 -0
  161. package/dist/runtime/ui.js +111 -0
  162. package/index.html +6 -1
  163. package/package.json +9 -2
  164. package/public/assets/sky/studio/nx.png +0 -0
  165. package/public/assets/sky/studio/ny.png +0 -0
  166. package/public/assets/sky/studio/nz.png +0 -0
  167. package/public/assets/sky/studio/px.png +0 -0
  168. package/public/assets/sky/studio/py.png +0 -0
  169. package/public/assets/sky/studio/pz.png +0 -0
  170. package/public/os9-shell/assets/index-KchE_ngx.js +483 -0
  171. package/public/os9-shell/assets/index-KchE_ngx.js.map +1 -0
  172. package/public/os9-shell/index.html +10 -1
  173. package/runtime/api-2d.js +301 -21
  174. package/runtime/api-3d/pbr.js +45 -1
  175. package/runtime/api-3d.js +1 -0
  176. package/runtime/api-effects.js +90 -3
  177. package/runtime/api-gameutils.js +476 -0
  178. package/runtime/api-generative.js +610 -0
  179. package/runtime/api-skybox.js +54 -0
  180. package/runtime/api-voxel.js +139 -28
  181. package/runtime/gpu-threejs.js +13 -9
  182. package/runtime/ui.js +2 -2
  183. package/src/main.js +20 -0
  184. package/public/os9-shell/assets/index-B1Uvacma.js +0 -32825
  185. package/public/os9-shell/assets/index-B1Uvacma.js.map +0 -1
@@ -2,10 +2,19 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
+ <base href="/os9-shell/" />
5
6
  <link rel="icon" type="image/svg+xml" href="./nova-icon.svg" />
6
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
8
  <title>nova64 OS</title>
8
- <script type="module" crossorigin src="./assets/index-B1Uvacma.js"></script>
9
+ <script type="importmap">
10
+ {
11
+ "imports": {
12
+ "three": "https://esm.sh/three@0.182.0",
13
+ "three/examples/jsm/": "https://esm.sh/three@0.182.0/examples/jsm/"
14
+ }
15
+ }
16
+ </script>
17
+ <script type="module" crossorigin src="./assets/index-KchE_ngx.js"></script>
9
18
  <link rel="stylesheet" crossorigin href="./assets/index-DIHfrTaW.css">
10
19
  </head>
11
20
  <body>
package/runtime/api-2d.js CHANGED
@@ -6,7 +6,7 @@
6
6
  // printCentered, printRight, drawScanlines, drawNoise, drawProgressBar,
7
7
  // drawGlowText, drawPixelBorder, drawCheckerboard, colorLerp, colorMix, hexColor,
8
8
  // drawStarburst, drawDiamond, drawTriangle, drawWave, drawSpiral, scrollingText,
9
- // drawPanel, drawHealthBar, drawMinimap, n64Palette
9
+ // drawPanel, drawHealthBar, createMinimap, drawMinimap, n64Palette
10
10
 
11
11
  import { rgba8 } from './api.js';
12
12
 
@@ -731,29 +731,272 @@ export function api2d(gpu) {
731
731
  }
732
732
  }
733
733
 
734
+ // ── Minimap System ─────────────────────────────────────────────────────────
735
+
734
736
  /**
735
- * drawMinimap(x, y, size, entities, bgColor)
736
- * entities: [{ x, y, color, worldW, worldH }] — dot map for players/enemies/items.
737
- * worldW/worldH are the world-space bounds to normalise against.
737
+ * createMinimap(opts) create a reusable minimap configuration.
738
+ *
739
+ * opts:
740
+ * x, y — screen position (default: bottom-right corner)
741
+ * width, height — pixel dimensions (default: 80×80)
742
+ * bgColor — background fill (default: semi-transparent black)
743
+ * borderLight, borderDark — border colours (null to disable border)
744
+ * shape — 'rect' | 'circle' (default: 'rect')
745
+ * follow — entity to center on (object with .x, .y in world coords)
746
+ * worldW, worldH — world-space bounds for coordinate mapping
747
+ * tileW, tileH — if set, map is tile-based (grid cells)
748
+ * tileScale — pixels per tile (default: 2)
749
+ * tiles — fn(tx,ty)→color|null or 2D array; null/0 = skip
750
+ * fogOfWar — if set, only reveal tiles within this radius of follow entity
751
+ * entities — array of { x, y, color, size?, label? }
752
+ * player — shorthand for the player entity { x, y, color?, blink? }
753
+ * sweep — { speed, color } for animated radar sweep line
754
+ * gridLines — number of grid divisions (0 = none)
755
+ * gridColor — colour for grid lines
756
+ *
757
+ * Returns a minimap object you pass to drawMinimap().
758
+ * You can mutate its properties between frames (e.g. update entities).
738
759
  */
739
- function drawMinimap(x, y, size, entities, bgColor) {
740
- const bg = bgColor ?? rgba8(0, 0, 0, 180);
741
- const bgc = _unpack(bg);
742
- // Fill minimap bg
743
- for (let py = y; py < y + size; py++)
744
- for (let px = x; px < x + size; px++) _blend(px, py, bgc.r, bgc.g, bgc.b, bgc.a);
745
- // Border
746
- drawPixelBorder(x, y, size, size, rgba8(150, 150, 150), rgba8(50, 50, 50), 1);
747
- // Dots
748
- for (const e of entities) {
749
- const ww = e.worldW ?? 100,
750
- wh = e.worldH ?? 100;
751
- const dx = (x + (e.x / ww) * size) | 0;
752
- const dy = (y + (e.y / wh) * size) | 0;
760
+ function createMinimap(opts = {}) {
761
+ return {
762
+ x: opts.x ?? W - 90,
763
+ y: opts.y ?? 10,
764
+ width: opts.width ?? 80,
765
+ height: opts.height ?? 80,
766
+ bgColor: opts.bgColor ?? rgba8(0, 0, 0, 180),
767
+ borderLight: opts.borderLight !== undefined ? opts.borderLight : rgba8(150, 150, 150),
768
+ borderDark: opts.borderDark !== undefined ? opts.borderDark : rgba8(50, 50, 50),
769
+ shape: opts.shape ?? 'rect',
770
+ follow: opts.follow ?? null,
771
+ worldW: opts.worldW ?? 100,
772
+ worldH: opts.worldH ?? 100,
773
+ tileW: opts.tileW ?? 0,
774
+ tileH: opts.tileH ?? 0,
775
+ tileScale: opts.tileScale ?? 2,
776
+ tiles: opts.tiles ?? null,
777
+ fogOfWar: opts.fogOfWar ?? 0,
778
+ entities: opts.entities ?? [],
779
+ player: opts.player ?? null,
780
+ sweep: opts.sweep ?? null,
781
+ gridLines: opts.gridLines ?? 0,
782
+ gridColor: opts.gridColor ?? rgba8(40, 60, 40, 120),
783
+ };
784
+ }
785
+
786
+ /** Internal: check if pixel is inside a circle */
787
+ function _inCircle(px, py, cx, cy, r) {
788
+ const dx = px - cx,
789
+ dy = py - cy;
790
+ return dx * dx + dy * dy <= r * r;
791
+ }
792
+
793
+ /**
794
+ * drawMinimap(minimap, time?)
795
+ *
796
+ * Accepts EITHER a createMinimap() object, OR the legacy signature:
797
+ * drawMinimap(x, y, size, entities, bgColor)
798
+ *
799
+ * time is optional — only needed for sweep animation and player blinking.
800
+ */
801
+ function drawMinimap(minimapOrX, timeOrY, sizeArg, entitiesArg, bgColorArg) {
802
+ // Legacy compat: drawMinimap(x, y, size, entities, bgColor)
803
+ let mm;
804
+ let time = 0;
805
+ if (typeof minimapOrX === 'number') {
806
+ mm = createMinimap({
807
+ x: minimapOrX,
808
+ y: timeOrY,
809
+ width: sizeArg,
810
+ height: sizeArg,
811
+ worldW: 100,
812
+ worldH: 100,
813
+ bgColor: bgColorArg,
814
+ });
815
+ // Convert legacy entities (they carry worldW/worldH per-entity)
816
+ if (Array.isArray(entitiesArg)) {
817
+ mm.entities = entitiesArg.map(e => ({
818
+ x: e.x,
819
+ y: e.y,
820
+ color: e.color ?? rgba8(255, 255, 255),
821
+ size: 2,
822
+ }));
823
+ // Use first entity's world bounds if provided
824
+ if (entitiesArg.length > 0) {
825
+ mm.worldW = entitiesArg[0].worldW ?? 100;
826
+ mm.worldH = entitiesArg[0].worldH ?? 100;
827
+ }
828
+ }
829
+ } else {
830
+ mm = minimapOrX;
831
+ time = timeOrY ?? 0;
832
+ }
833
+
834
+ const { x, y, width: mw, height: mh, shape } = mm;
835
+ const cx = x + mw / 2,
836
+ cy = y + mh / 2;
837
+ const isCircle = shape === 'circle';
838
+ const radius = Math.min(mw, mh) / 2;
839
+
840
+ // 1. Background fill
841
+ const bgc = _unpack(mm.bgColor);
842
+ for (let py = y; py < y + mh; py++) {
843
+ for (let px = x; px < x + mw; px++) {
844
+ if (isCircle && !_inCircle(px, py, cx, cy, radius)) continue;
845
+ _blend(px, py, bgc.r, bgc.g, bgc.b, bgc.a);
846
+ }
847
+ }
848
+
849
+ // 2. Tile map rendering
850
+ if (mm.tiles && mm.tileW > 0 && mm.tileH > 0) {
851
+ const ts = mm.tileScale;
852
+ const isFunc = typeof mm.tiles === 'function';
853
+ const followTX = mm.follow ? Math.floor(mm.follow.x) : 0;
854
+ const followTY = mm.follow ? Math.floor(mm.follow.y) : 0;
855
+
856
+ for (let ty = 0; ty < mm.tileH; ty++) {
857
+ for (let tx = 0; tx < mm.tileW; tx++) {
858
+ // Fog of war check
859
+ if (mm.fogOfWar > 0 && mm.follow) {
860
+ const dist = Math.abs(tx - followTX) + Math.abs(ty - followTY);
861
+ if (dist > mm.fogOfWar) continue;
862
+ }
863
+
864
+ const tileColor = isFunc ? mm.tiles(tx, ty) : mm.tiles[ty] ? mm.tiles[ty][tx] : null;
865
+ if (!tileColor) continue;
866
+
867
+ const tc = _unpack(tileColor);
868
+ const px0 = x + tx * ts;
869
+ const py0 = y + ty * ts;
870
+ for (let dy = 0; dy < ts; dy++) {
871
+ for (let dx = 0; dx < ts; dx++) {
872
+ const px = px0 + dx,
873
+ py = py0 + dy;
874
+ if (isCircle && !_inCircle(px, py, cx, cy, radius)) continue;
875
+ _blend(px, py, tc.r, tc.g, tc.b, tc.a);
876
+ }
877
+ }
878
+ }
879
+ }
880
+ }
881
+
882
+ // 3. Grid lines
883
+ if (mm.gridLines > 0) {
884
+ const gc = _unpack(mm.gridColor);
885
+ for (let i = 1; i < mm.gridLines; i++) {
886
+ // Vertical line
887
+ const gx = (x + (mw / mm.gridLines) * i) | 0;
888
+ for (let py2 = y; py2 < y + mh; py2++) {
889
+ if (isCircle && !_inCircle(gx, py2, cx, cy, radius)) continue;
890
+ _blend(gx, py2, gc.r, gc.g, gc.b, gc.a);
891
+ }
892
+ // Horizontal line
893
+ const gy = (y + (mh / mm.gridLines) * i) | 0;
894
+ for (let px2 = x; px2 < x + mw; px2++) {
895
+ if (isCircle && !_inCircle(px2, gy, cx, cy, radius)) continue;
896
+ _blend(px2, gy, gc.r, gc.g, gc.b, gc.a);
897
+ }
898
+ }
899
+ }
900
+
901
+ // 4. Radar sweep line
902
+ if (mm.sweep) {
903
+ const angle = time * (mm.sweep.speed ?? 2);
904
+ const sc = _unpack(mm.sweep.color ?? rgba8(0, 255, 0, 100));
905
+ const sx = Math.cos(angle) * radius;
906
+ const sy = Math.sin(angle) * radius;
907
+ // Bresenham-ish sweep from center
908
+ const steps = Math.max(Math.abs(sx), Math.abs(sy)) | 0;
909
+ if (steps > 0) {
910
+ for (let s = 0; s <= steps; s++) {
911
+ const t = s / steps;
912
+ const px = (cx + sx * t) | 0;
913
+ const py = (cy + sy * t) | 0;
914
+ if (px < x || px >= x + mw || py < y || py >= y + mh) continue;
915
+ if (isCircle && !_inCircle(px, py, cx, cy, radius)) continue;
916
+ _blend(px, py, sc.r, sc.g, sc.b, sc.a);
917
+ }
918
+ }
919
+ }
920
+
921
+ // Helper: convert world coords to screen pixel
922
+ function worldToScreen(wx, wy) {
923
+ if (mm.tiles && mm.tileW > 0) {
924
+ // Tile-based — wx/wy are tile coords
925
+ return [(x + wx * mm.tileScale) | 0, (y + wy * mm.tileScale) | 0];
926
+ }
927
+ // World-space normalised
928
+ let nx, ny;
929
+ if (mm.follow) {
930
+ // Center on follow entity
931
+ nx = 0.5 + (wx - mm.follow.x) / mm.worldW;
932
+ ny = 0.5 + (wy - mm.follow.y) / mm.worldH;
933
+ } else {
934
+ nx = wx / mm.worldW;
935
+ ny = wy / mm.worldH;
936
+ }
937
+ return [(x + nx * mw) | 0, (y + ny * mh) | 0];
938
+ }
939
+
940
+ // 5. Entity dots
941
+ for (const e of mm.entities) {
942
+ const [ex, ey] = worldToScreen(e.x, e.y);
943
+ const dotSize = e.size ?? 2;
944
+ if (ex < x - dotSize || ex >= x + mw + dotSize || ey < y - dotSize || ey >= y + mh + dotSize)
945
+ continue;
753
946
  const ec = _unpack(e.color ?? rgba8(255, 255, 255));
754
- // 2x2 dot
755
- for (let oy = 0; oy < 2; oy++)
756
- for (let ox = 0; ox < 2; ox++) _blend(dx + ox, dy + oy, ec.r, ec.g, ec.b, ec.a);
947
+ const half = (dotSize / 2) | 0;
948
+ for (let dy = -half; dy < dotSize - half; dy++) {
949
+ for (let dx = -half; dx < dotSize - half; dx++) {
950
+ const px = ex + dx,
951
+ py = ey + dy;
952
+ if (px < x || px >= x + mw || py < y || py >= y + mh) continue;
953
+ if (isCircle && !_inCircle(px, py, cx, cy, radius)) continue;
954
+ _blend(px, py, ec.r, ec.g, ec.b, ec.a);
955
+ }
956
+ }
957
+ }
958
+
959
+ // 6. Player marker (with optional blink)
960
+ if (mm.player) {
961
+ const blink = mm.player.blink !== false;
962
+ const visible = !blink || Math.sin(time * 8) > 0;
963
+ if (visible) {
964
+ const [px, py] = worldToScreen(mm.player.x, mm.player.y);
965
+ const pc = _unpack(mm.player.color ?? rgba8(50, 150, 255));
966
+ const ps = mm.player.size ?? 3;
967
+ const half = (ps / 2) | 0;
968
+ for (let dy = -half; dy < ps - half; dy++) {
969
+ for (let dx = -half; dx < ps - half; dx++) {
970
+ const ppx = px + dx,
971
+ ppy = py + dy;
972
+ if (ppx < x || ppx >= x + mw || ppy < y || ppy >= y + mh) continue;
973
+ if (isCircle && !_inCircle(ppx, ppy, cx, cy, radius)) continue;
974
+ _blend(ppx, ppy, pc.r, pc.g, pc.b, pc.a);
975
+ }
976
+ }
977
+ }
978
+ }
979
+
980
+ // 7. Border
981
+ if (mm.borderLight !== null) {
982
+ if (isCircle) {
983
+ // Circle border — draw a ring
984
+ const bc = _unpack(mm.borderLight);
985
+ const r2inner = (radius - 1) * (radius - 1);
986
+ const r2outer = radius * radius;
987
+ for (let py = y; py < y + mh; py++) {
988
+ for (let px = x; px < x + mw; px++) {
989
+ const dx = px - cx,
990
+ dy = py - cy;
991
+ const d2 = dx * dx + dy * dy;
992
+ if (d2 >= r2inner && d2 <= r2outer) {
993
+ _blend(px, py, bc.r, bc.g, bc.b, bc.a);
994
+ }
995
+ }
996
+ }
997
+ } else {
998
+ drawPixelBorder(x, y, mw, mh, mm.borderLight, mm.borderDark, 1);
999
+ }
757
1000
  }
758
1001
  }
759
1002
 
@@ -768,6 +1011,40 @@ export function api2d(gpu) {
768
1011
  _print(text, x, y, color, scale);
769
1012
  }
770
1013
 
1014
+ /**
1015
+ * drawFloatingTexts(system, offsetX?, offsetY?)
1016
+ * Render all active texts from a createFloatingTextSystem().
1017
+ * offsetX/offsetY allow camera offset (for screen shake, etc.)
1018
+ */
1019
+ function drawFloatingTexts(system, offsetX = 0, offsetY = 0) {
1020
+ const texts = system.getTexts();
1021
+ for (const t of texts) {
1022
+ const alpha = Math.min(255, Math.floor((t.timer / t.maxTimer) * 255));
1023
+ const r = (t.color >> 16) & 0xff;
1024
+ const g = (t.color >> 8) & 0xff;
1025
+ const b = t.color & 0xff;
1026
+ _print(t.text, (t.x + offsetX) | 0, (t.y + offsetY) | 0, rgba8(r, g, b, alpha), t.scale);
1027
+ }
1028
+ }
1029
+
1030
+ /**
1031
+ * drawFloatingTexts3D(system, projectFn)
1032
+ * Render 3D floating texts using a user-supplied projection function.
1033
+ * projectFn(x, y, z) should return [screenX, screenY].
1034
+ * Use with spawn(..., { z: worldZ }) for 3D world-space floating texts.
1035
+ */
1036
+ function drawFloatingTexts3D(system, projectFn) {
1037
+ const texts = system.getTexts();
1038
+ for (const t of texts) {
1039
+ const [sx, sy] = projectFn(t.x, t.y, t.z ?? 0);
1040
+ const alpha = Math.min(255, Math.floor((t.timer / t.maxTimer) * 255));
1041
+ const r = (t.color >> 16) & 0xff;
1042
+ const g = (t.color >> 8) & 0xff;
1043
+ const b = t.color & 0xff;
1044
+ _print(t.text, sx | 0, sy | 0, rgba8(r, g, b, alpha), t.scale);
1045
+ }
1046
+ }
1047
+
771
1048
  // ── Full-screen helpers ───────────────────────────────────────────────────────
772
1049
 
773
1050
  /** Full-screen vertical gradient — great for sky/title backgrounds */
@@ -870,7 +1147,10 @@ export function api2d(gpu) {
870
1147
  drawPixelBorder,
871
1148
  drawPanel,
872
1149
  drawCrosshair,
1150
+ createMinimap,
873
1151
  drawMinimap,
1152
+ drawFloatingTexts,
1153
+ drawFloatingTexts3D,
874
1154
  scrollingText,
875
1155
  });
876
1156
  },
@@ -65,5 +65,49 @@ export function pbrModule({ meshes }) {
65
65
  return true;
66
66
  }
67
67
 
68
- return { loadNormalMap, setNormalMap, setPBRMaps };
68
+ /**
69
+ * Set PBR scalar properties on a mesh without requiring texture maps.
70
+ * Clones the material so the change only affects this specific mesh.
71
+ *
72
+ * @param {number} meshId - ID returned by createSphere / createCube etc.
73
+ * @param {object} [opts]
74
+ * @param {number} [opts.metalness] - 0 (dielectric) → 1 (fully metallic)
75
+ * @param {number} [opts.roughness] - 0 (mirror-smooth) → 1 (fully rough)
76
+ * @param {number} [opts.envMapIntensity] - Strength of environment reflections
77
+ * @param {number} [opts.color] - Override surface colour (hex)
78
+ * @returns {boolean} true on success
79
+ */
80
+ function setPBRProperties(meshId, opts = {}) {
81
+ const mesh = meshes.get(meshId);
82
+ if (!mesh) return false;
83
+
84
+ // Replace with a fresh MeshStandardMaterial so we own it exclusively
85
+ // (avoids mutating a shared cached material that other meshes may use)
86
+ const old = mesh.material;
87
+ mesh.material = new THREE.MeshStandardMaterial({
88
+ color:
89
+ opts.color !== undefined
90
+ ? new THREE.Color(opts.color)
91
+ : (old.color?.clone() ?? new THREE.Color(0xffffff)),
92
+ map: old.map ?? null,
93
+ metalness:
94
+ opts.metalness !== undefined
95
+ ? Math.max(0, Math.min(1, opts.metalness))
96
+ : (old.metalness ?? 0.0),
97
+ roughness:
98
+ opts.roughness !== undefined
99
+ ? Math.max(0, Math.min(1, opts.roughness))
100
+ : (old.roughness ?? 0.6),
101
+ envMapIntensity: opts.envMapIntensity ?? old.envMapIntensity ?? 1.0,
102
+ emissive: old.emissive?.clone() ?? new THREE.Color(0),
103
+ emissiveIntensity: old.emissiveIntensity ?? 0,
104
+ transparent: old.transparent ?? false,
105
+ opacity: old.opacity ?? 1.0,
106
+ side: old.side ?? THREE.FrontSide,
107
+ });
108
+ mesh.material.needsUpdate = true;
109
+ return true;
110
+ }
111
+
112
+ return { loadNormalMap, setNormalMap, setPBRMaps, setPBRProperties };
69
113
  }
package/runtime/api-3d.js CHANGED
@@ -139,6 +139,7 @@ export function threeDApi(gpu) {
139
139
  loadNormalMap: ctx.loadNormalMap,
140
140
  setNormalMap: ctx.setNormalMap,
141
141
  setPBRMaps: ctx.setPBRMaps,
142
+ setPBRProperties: ctx.setPBRProperties,
142
143
 
143
144
  // GPU particle system
144
145
  createParticleSystem: ctx.createParticleSystem,
@@ -37,6 +37,58 @@ const ChromaticAberrationShader = {
37
37
  `,
38
38
  };
39
39
 
40
+ // Glitch/damage shader — scanline displacement, RGB split, block artifacts
41
+ const GlitchShader = {
42
+ uniforms: {
43
+ tDiffuse: { value: null },
44
+ intensity: { value: 0.0 },
45
+ time: { value: 0.0 },
46
+ },
47
+ vertexShader: `
48
+ varying vec2 vUv;
49
+ void main() {
50
+ vUv = uv;
51
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
52
+ }
53
+ `,
54
+ fragmentShader: `
55
+ uniform sampler2D tDiffuse;
56
+ uniform float intensity;
57
+ uniform float time;
58
+ varying vec2 vUv;
59
+
60
+ float rand(vec2 co) {
61
+ return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
62
+ }
63
+
64
+ void main() {
65
+ vec2 uv = vUv;
66
+
67
+ // Scanline displacement — horizontal bands shift left/right
68
+ float scanJitter = step(0.99 - intensity * 0.3, rand(vec2(time * 1.3, floor(uv.y * 40.0))))
69
+ * (rand(vec2(time, floor(uv.y * 40.0))) - 0.5) * intensity * 0.15;
70
+ uv.x += scanJitter;
71
+
72
+ // Block glitch — large rectangular regions shift
73
+ float blockY = floor(uv.y * 8.0);
74
+ float blockShift = step(0.97 - intensity * 0.15, rand(vec2(blockY, time * 0.7)))
75
+ * (rand(vec2(blockY + 1.0, time)) - 0.5) * intensity * 0.1;
76
+ uv.x += blockShift;
77
+
78
+ // RGB channel split
79
+ float rgbShift = intensity * 0.012;
80
+ float r = texture2D(tDiffuse, vec2(uv.x + rgbShift, uv.y + rgbShift * 0.5)).r;
81
+ float g = texture2D(tDiffuse, uv).g;
82
+ float b = texture2D(tDiffuse, vec2(uv.x - rgbShift, uv.y - rgbShift * 0.3)).b;
83
+
84
+ // Color corruption — random bright pixels
85
+ float noise = step(0.995 - intensity * 0.05, rand(uv + time)) * intensity;
86
+
87
+ gl_FragColor = vec4(r + noise, g, b + noise * 0.5, 1.0);
88
+ }
89
+ `,
90
+ };
91
+
40
92
  // Vignette shader
41
93
  const VignetteShader = {
42
94
  uniforms: {
@@ -81,9 +133,11 @@ export function effectsApi(gpu) {
81
133
  let fxaaPass = null;
82
134
  let chromaticAberrationPass = null;
83
135
  let vignettePass = null;
136
+ let glitchPass = null;
84
137
 
85
138
  // Effect states
86
139
  let effectsEnabled = false;
140
+ let glitchTime = 0;
87
141
 
88
142
  // Custom shader materials
89
143
  const customShaders = new Map();
@@ -102,7 +156,7 @@ export function effectsApi(gpu) {
102
156
  }
103
157
 
104
158
  // === BLOOM EFFECTS ===
105
- function enableBloom(strength = 1.0, radius = 0.5, threshold = 0.85) {
159
+ function enableBloom(strength = 1.0, radius = 0.5, threshold = 0.6) {
106
160
  initPostProcessing();
107
161
 
108
162
  if (bloomPass) {
@@ -187,7 +241,7 @@ export function effectsApi(gpu) {
187
241
  }
188
242
 
189
243
  // === VIGNETTE ===
190
- function enableVignette(darkness = 1.5, offset = 0.95) {
244
+ function enableVignette(darkness = 1.0, offset = 0.9) {
191
245
  initPostProcessing();
192
246
  if (vignettePass) {
193
247
  vignettePass.uniforms['darkness'].value = darkness;
@@ -207,6 +261,31 @@ export function effectsApi(gpu) {
207
261
  }
208
262
  }
209
263
 
264
+ // === GLITCH EFFECT ===
265
+ function enableGlitch(intensity = 0.5) {
266
+ initPostProcessing();
267
+ if (glitchPass) {
268
+ glitchPass.uniforms['intensity'].value = Math.max(0, Math.min(1, intensity));
269
+ return;
270
+ }
271
+ glitchPass = new ShaderPass(GlitchShader);
272
+ glitchPass.uniforms['intensity'].value = Math.max(0, Math.min(1, intensity));
273
+ composer.addPass(glitchPass);
274
+ }
275
+
276
+ function disableGlitch() {
277
+ if (glitchPass && composer) {
278
+ composer.removePass(glitchPass);
279
+ glitchPass = null;
280
+ }
281
+ }
282
+
283
+ function setGlitchIntensity(intensity) {
284
+ if (glitchPass) {
285
+ glitchPass.uniforms['intensity'].value = Math.max(0, Math.min(1, intensity));
286
+ }
287
+ }
288
+
210
289
  // === CUSTOM SHADERS ===
211
290
 
212
291
  // Holographic shader
@@ -651,7 +730,7 @@ export function effectsApi(gpu) {
651
730
  const bloom = opts.bloom !== undefined ? opts.bloom : {};
652
731
  if (bloom !== false) {
653
732
  const b = typeof bloom === 'object' ? bloom : {};
654
- enableBloom(b.strength ?? 1.5, b.radius ?? 0.4, b.threshold ?? 0.1);
733
+ enableBloom(b.strength ?? 1.0, b.radius ?? 0.4, b.threshold ?? 0.6);
655
734
  }
656
735
 
657
736
  // FXAA
@@ -701,6 +780,11 @@ export function effectsApi(gpu) {
701
780
  // Update effects (called every frame)
702
781
  function updateEffects(deltaTime) {
703
782
  updateShaderTime(deltaTime);
783
+ // Animate glitch time uniform for randomness
784
+ if (glitchPass) {
785
+ glitchTime += deltaTime;
786
+ glitchPass.uniforms['time'].value = glitchTime;
787
+ }
704
788
  }
705
789
 
706
790
  // === EXPOSE API ===
@@ -719,6 +803,9 @@ export function effectsApi(gpu) {
719
803
  disableChromaticAberration: disableChromaticAberration,
720
804
  enableVignette: enableVignette,
721
805
  disableVignette: disableVignette,
806
+ enableGlitch: enableGlitch,
807
+ disableGlitch: disableGlitch,
808
+ setGlitchIntensity: setGlitchIntensity,
722
809
 
723
810
  // Convenience
724
811
  enableRetroEffects: enableRetroEffects,