nova64 0.2.6 → 0.2.8
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.
- package/dist/examples/strider-demo-3d/fix-game.sh +0 -0
- package/dist/os9-shell/assets/index-3Hr_q5dj.js +483 -0
- package/dist/os9-shell/assets/index-3Hr_q5dj.js.map +1 -0
- package/dist/os9-shell/index.html +2 -1
- package/dist/runtime/api-2d.js +1158 -0
- package/dist/runtime/api-3d/camera.js +73 -0
- package/dist/runtime/api-3d/instancing.js +180 -0
- package/dist/runtime/api-3d/lights.js +51 -0
- package/dist/runtime/api-3d/materials.js +47 -0
- package/dist/runtime/api-3d/models.js +84 -0
- package/dist/runtime/api-3d/particles.js +296 -0
- package/dist/runtime/api-3d/pbr.js +113 -0
- package/dist/runtime/api-3d/primitives.js +304 -0
- package/dist/runtime/api-3d/scene.js +169 -0
- package/dist/runtime/api-3d/transforms.js +161 -0
- package/dist/runtime/api-3d.js +166 -0
- package/dist/runtime/api-effects.js +840 -0
- package/dist/runtime/api-gameutils.js +476 -0
- package/dist/runtime/api-generative.js +610 -0
- package/dist/runtime/api-presets.js +85 -0
- package/dist/runtime/api-skybox.js +232 -0
- package/dist/runtime/api-sprites.js +100 -0
- package/dist/runtime/api-voxel.js +712 -0
- package/dist/runtime/api.js +201 -0
- package/dist/runtime/assets.js +27 -0
- package/dist/runtime/audio.js +114 -0
- package/dist/runtime/collision.js +47 -0
- package/dist/runtime/console.js +101 -0
- package/dist/runtime/editor.js +233 -0
- package/dist/runtime/font.js +233 -0
- package/dist/runtime/framebuffer.js +28 -0
- package/dist/runtime/fullscreen-button.js +185 -0
- package/dist/runtime/gpu-canvas2d.js +47 -0
- package/dist/runtime/gpu-threejs.js +643 -0
- package/dist/runtime/gpu-webgl2.js +310 -0
- package/dist/runtime/index.d.ts +682 -0
- package/dist/runtime/index.js +22 -0
- package/dist/runtime/input.js +225 -0
- package/dist/runtime/logger.js +60 -0
- package/dist/runtime/physics.js +101 -0
- package/dist/runtime/screens.js +213 -0
- package/dist/runtime/storage.js +38 -0
- package/dist/runtime/store.js +151 -0
- package/dist/runtime/textinput.js +68 -0
- package/dist/runtime/ui/buttons.js +124 -0
- package/dist/runtime/ui/panels.js +105 -0
- package/dist/runtime/ui/text.js +86 -0
- package/dist/runtime/ui/widgets.js +141 -0
- package/dist/runtime/ui.js +111 -0
- package/package.json +34 -32
- package/public/os9-shell/assets/index-3Hr_q5dj.js +483 -0
- package/public/os9-shell/assets/index-3Hr_q5dj.js.map +1 -0
- package/public/os9-shell/index.html +2 -1
|
@@ -0,0 +1,1158 @@
|
|
|
1
|
+
// runtime/api-2d.js
|
|
2
|
+
// Nova64 Enhanced 2D Drawing API
|
|
3
|
+
// All primitives operate on the framebuffer (the 2D overlay that composites over the 3D scene).
|
|
4
|
+
//
|
|
5
|
+
// Exposed globals: drawGradient, drawRadialGradient, drawRoundedRect, poly, tristrip,
|
|
6
|
+
// printCentered, printRight, drawScanlines, drawNoise, drawProgressBar,
|
|
7
|
+
// drawGlowText, drawPixelBorder, drawCheckerboard, colorLerp, colorMix, hexColor,
|
|
8
|
+
// drawStarburst, drawDiamond, drawTriangle, drawWave, drawSpiral, scrollingText,
|
|
9
|
+
// drawPanel, drawHealthBar, createMinimap, drawMinimap, n64Palette
|
|
10
|
+
|
|
11
|
+
import { rgba8 } from './api.js';
|
|
12
|
+
|
|
13
|
+
// ─── colour helpers ───────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/** Unpack an rgba8() BigInt into {r,g,b,a} 0-255 floats */
|
|
16
|
+
function _unpack(c) {
|
|
17
|
+
if (typeof c === 'bigint') {
|
|
18
|
+
return {
|
|
19
|
+
r: Number((c >> 48n) & 0xffffn) / 257,
|
|
20
|
+
g: Number((c >> 32n) & 0xffffn) / 257,
|
|
21
|
+
b: Number((c >> 16n) & 0xffffn) / 257,
|
|
22
|
+
a: Number(c & 0xffffn) / 257,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const bc = BigInt(Math.floor(c));
|
|
26
|
+
return {
|
|
27
|
+
r: Number((bc >> 48n) & 0xffffn) / 257,
|
|
28
|
+
g: Number((bc >> 32n) & 0xffffn) / 257,
|
|
29
|
+
b: Number((bc >> 16n) & 0xffffn) / 257,
|
|
30
|
+
a: Number(bc & 0xffffn) / 257,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Linearly interpolate two rgba8 colours. t = 0..1 */
|
|
35
|
+
function colorLerp(c1, c2, t) {
|
|
36
|
+
const a = _unpack(c1),
|
|
37
|
+
b = _unpack(c2);
|
|
38
|
+
return rgba8(
|
|
39
|
+
a.r + (b.r - a.r) * t,
|
|
40
|
+
a.g + (b.g - a.g) * t,
|
|
41
|
+
a.b + (b.b - a.b) * t,
|
|
42
|
+
a.a + (b.a - a.a) * t
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Multiply/tint a colour by a brightness factor 0..2 */
|
|
47
|
+
function colorMix(c, factor) {
|
|
48
|
+
const { r, g, b, a } = _unpack(c);
|
|
49
|
+
return rgba8(Math.min(255, r * factor), Math.min(255, g * factor), Math.min(255, b * factor), a);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Convert 0xRRGGBB hex number to rgba8 */
|
|
53
|
+
function hexColor(hex, alpha = 255) {
|
|
54
|
+
return rgba8((hex >> 16) & 0xff, (hex >> 8) & 0xff, hex & 0xff, alpha);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Convert HSL(0-360, 0-1, 0-1) to rgba8 */
|
|
58
|
+
function hslColor(h, s = 1, l = 0.5, alpha = 255) {
|
|
59
|
+
h = ((h % 360) + 360) % 360;
|
|
60
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
61
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
62
|
+
const m = l - c / 2;
|
|
63
|
+
let r, g, b;
|
|
64
|
+
if (h < 60) {
|
|
65
|
+
r = c;
|
|
66
|
+
g = x;
|
|
67
|
+
b = 0;
|
|
68
|
+
} else if (h < 120) {
|
|
69
|
+
r = x;
|
|
70
|
+
g = c;
|
|
71
|
+
b = 0;
|
|
72
|
+
} else if (h < 180) {
|
|
73
|
+
r = 0;
|
|
74
|
+
g = c;
|
|
75
|
+
b = x;
|
|
76
|
+
} else if (h < 240) {
|
|
77
|
+
r = 0;
|
|
78
|
+
g = x;
|
|
79
|
+
b = c;
|
|
80
|
+
} else if (h < 300) {
|
|
81
|
+
r = x;
|
|
82
|
+
g = 0;
|
|
83
|
+
b = c;
|
|
84
|
+
} else {
|
|
85
|
+
r = c;
|
|
86
|
+
g = 0;
|
|
87
|
+
b = x;
|
|
88
|
+
}
|
|
89
|
+
return rgba8(
|
|
90
|
+
Math.round((r + m) * 255),
|
|
91
|
+
Math.round((g + m) * 255),
|
|
92
|
+
Math.round((b + m) * 255),
|
|
93
|
+
alpha
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Math utilities ──────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/** Linear interpolation: lerp(a, b, t) → a + (b - a) * t */
|
|
100
|
+
function lerp(a, b, t) {
|
|
101
|
+
return a + (b - a) * t;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Clamp value between min and max */
|
|
105
|
+
function clamp(v, min = 0, max = 1) {
|
|
106
|
+
return v < min ? min : v > max ? max : v;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Random float in [min, max) */
|
|
110
|
+
function randRange(min, max) {
|
|
111
|
+
return min + Math.random() * (max - min);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Random integer in [min, max] inclusive */
|
|
115
|
+
function randInt(min, max) {
|
|
116
|
+
return (min + Math.random() * (max - min + 1)) | 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Distance between two 2D points */
|
|
120
|
+
function dist(x1, y1, x2, y2) {
|
|
121
|
+
const dx = x2 - x1,
|
|
122
|
+
dy = y2 - y1;
|
|
123
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Distance between two 3D points */
|
|
127
|
+
function dist3d(x1, y1, z1, x2, y2, z2) {
|
|
128
|
+
const dx = x2 - x1,
|
|
129
|
+
dy = y2 - y1,
|
|
130
|
+
dz = z2 - z1;
|
|
131
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Map a value from one range to another */
|
|
135
|
+
function remap(value, inMin, inMax, outMin, outMax) {
|
|
136
|
+
return outMin + ((value - inMin) * (outMax - outMin)) / (inMax - inMin);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Smooth pulse: returns 0-1-0 based on time with given frequency */
|
|
140
|
+
function pulse(time, frequency = 1) {
|
|
141
|
+
return Math.sin(time * frequency * Math.PI * 2) * 0.5 + 0.5;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Convert degrees to radians */
|
|
145
|
+
function deg2rad(d) {
|
|
146
|
+
return (d * Math.PI) / 180;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Convert radians to degrees */
|
|
150
|
+
function rad2deg(r) {
|
|
151
|
+
return (r * 180) / Math.PI;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Classic N64 / PS1 limited palette ───────────────────────────────────────
|
|
155
|
+
const n64Palette = {
|
|
156
|
+
black: rgba8(0, 0, 0),
|
|
157
|
+
white: rgba8(255, 255, 255),
|
|
158
|
+
red: rgba8(220, 30, 30),
|
|
159
|
+
green: rgba8(30, 200, 60),
|
|
160
|
+
blue: rgba8(30, 80, 220),
|
|
161
|
+
yellow: rgba8(255, 220, 0),
|
|
162
|
+
cyan: rgba8(0, 220, 220),
|
|
163
|
+
magenta: rgba8(200, 0, 200),
|
|
164
|
+
orange: rgba8(255, 140, 0),
|
|
165
|
+
purple: rgba8(120, 0, 200),
|
|
166
|
+
teal: rgba8(0, 160, 160),
|
|
167
|
+
brown: rgba8(140, 80, 30),
|
|
168
|
+
grey: rgba8(128, 128, 128),
|
|
169
|
+
darkGrey: rgba8(60, 60, 60),
|
|
170
|
+
lightGrey: rgba8(200, 200, 200),
|
|
171
|
+
sky: rgba8(70, 130, 200),
|
|
172
|
+
gold: rgba8(255, 210, 50),
|
|
173
|
+
silver: rgba8(192, 192, 210),
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// ─── actual drawing functions ─────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
export function api2d(gpu) {
|
|
179
|
+
const fb = gpu.getFramebuffer();
|
|
180
|
+
const W = fb.width; // 640
|
|
181
|
+
const H = fb.height; // 360
|
|
182
|
+
|
|
183
|
+
// cached camRef from std api - unused for 2D (no camera offset)
|
|
184
|
+
// const cam = { x: 0, y: 0 };
|
|
185
|
+
|
|
186
|
+
// helper: write a pixel with alpha blending
|
|
187
|
+
function _blend(x, y, r, g, b, a) {
|
|
188
|
+
if (x < 0 || y < 0 || x >= W || y >= H) return;
|
|
189
|
+
if (a >= 255) {
|
|
190
|
+
fb.pset(x, y, r * 257, g * 257, b * 257, 65535);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const i = (y * W + x) * 4;
|
|
194
|
+
const p = fb.pixels;
|
|
195
|
+
const af = a / 255;
|
|
196
|
+
const bf = 1 - af;
|
|
197
|
+
p[i] = Math.round(r * af + (p[i] / 257) * bf) * 257;
|
|
198
|
+
p[i + 1] = Math.round(g * af + (p[i + 1] / 257) * bf) * 257;
|
|
199
|
+
p[i + 2] = Math.round(b * af + (p[i + 2] / 257) * bf) * 257;
|
|
200
|
+
p[i + 3] = 65535;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** pset with color as rgba8 bigint */
|
|
204
|
+
function _pset(x, y, color) {
|
|
205
|
+
const c = _unpack(color);
|
|
206
|
+
_blend(x | 0, y | 0, c.r, c.g, c.b, c.a);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Gradient fills ──────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* drawGradient(x, y, w, h, c1, c2, dir)
|
|
213
|
+
* dir: 'v' vertical (top→bottom), 'h' horizontal (left→right), 'd' diagonal
|
|
214
|
+
*/
|
|
215
|
+
function drawGradient(x, y, w, h, c1, c2, dir = 'v') {
|
|
216
|
+
x |= 0;
|
|
217
|
+
y |= 0;
|
|
218
|
+
w |= 0;
|
|
219
|
+
h |= 0;
|
|
220
|
+
const a1 = _unpack(c1),
|
|
221
|
+
a2 = _unpack(c2);
|
|
222
|
+
const x1 = Math.max(0, x),
|
|
223
|
+
y1 = Math.max(0, y);
|
|
224
|
+
const x2 = Math.min(W, x + w),
|
|
225
|
+
y2 = Math.min(H, y + h);
|
|
226
|
+
for (let py = y1; py < y2; py++) {
|
|
227
|
+
for (let px = x1; px < x2; px++) {
|
|
228
|
+
let t;
|
|
229
|
+
if (dir === 'h') {
|
|
230
|
+
t = w > 0 ? (px - x) / w : 0;
|
|
231
|
+
} else if (dir === 'd') {
|
|
232
|
+
t = w + h > 0 ? (px - x + (py - y)) / (w + h) : 0;
|
|
233
|
+
} else {
|
|
234
|
+
t = h > 0 ? (py - y) / h : 0;
|
|
235
|
+
}
|
|
236
|
+
t = Math.max(0, Math.min(1, t));
|
|
237
|
+
_blend(
|
|
238
|
+
px,
|
|
239
|
+
py,
|
|
240
|
+
(a1.r + (a2.r - a1.r) * t) | 0,
|
|
241
|
+
(a1.g + (a2.g - a1.g) * t) | 0,
|
|
242
|
+
(a1.b + (a2.b - a1.b) * t) | 0,
|
|
243
|
+
(a1.a + (a2.a - a1.a) * t) | 0
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* drawRadialGradient(cx, cy, radius, innerColor, outerColor)
|
|
251
|
+
* Great for glows, halos, spot lights.
|
|
252
|
+
*/
|
|
253
|
+
function drawRadialGradient(cx, cy, radius, innerColor, outerColor) {
|
|
254
|
+
cx |= 0;
|
|
255
|
+
cy |= 0;
|
|
256
|
+
radius = radius | 0;
|
|
257
|
+
const ci = _unpack(innerColor),
|
|
258
|
+
co = _unpack(outerColor);
|
|
259
|
+
const x1 = Math.max(0, cx - radius),
|
|
260
|
+
y1 = Math.max(0, cy - radius);
|
|
261
|
+
const x2 = Math.min(W, cx + radius),
|
|
262
|
+
y2 = Math.min(H, cy + radius);
|
|
263
|
+
const r2 = radius * radius;
|
|
264
|
+
for (let py = y1; py < y2; py++) {
|
|
265
|
+
for (let px = x1; px < x2; px++) {
|
|
266
|
+
const dx = px - cx,
|
|
267
|
+
dy = py - cy;
|
|
268
|
+
const d2 = dx * dx + dy * dy;
|
|
269
|
+
if (d2 > r2) continue;
|
|
270
|
+
const t = Math.sqrt(d2) / radius;
|
|
271
|
+
_blend(
|
|
272
|
+
px,
|
|
273
|
+
py,
|
|
274
|
+
(ci.r + (co.r - ci.r) * t) | 0,
|
|
275
|
+
(ci.g + (co.g - ci.g) * t) | 0,
|
|
276
|
+
(ci.b + (co.b - ci.b) * t) | 0,
|
|
277
|
+
(ci.a + (co.a - ci.a) * t) | 0
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Rounded rect ────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* drawRoundedRect(x, y, w, h, radius, color, fill=true)
|
|
287
|
+
* PS1/N64-style rounded panel corners.
|
|
288
|
+
*/
|
|
289
|
+
function drawRoundedRect(x, y, w, h, radius, color, fill = true) {
|
|
290
|
+
x |= 0;
|
|
291
|
+
y |= 0;
|
|
292
|
+
w |= 0;
|
|
293
|
+
h |= 0;
|
|
294
|
+
radius = Math.min(radius | 0, Math.min(w, h) >> 1);
|
|
295
|
+
const { r, g, b, a } = _unpack(color);
|
|
296
|
+
|
|
297
|
+
if (fill) {
|
|
298
|
+
// Fill main body (three rects)
|
|
299
|
+
for (let py = y + radius; py < y + h - radius; py++) {
|
|
300
|
+
for (let px = x; px < x + w; px++) _blend(px, py, r, g, b, a);
|
|
301
|
+
}
|
|
302
|
+
for (let py = y; py < y + radius; py++) {
|
|
303
|
+
for (let px = x + radius; px < x + w - radius; px++) _blend(px, py, r, g, b, a);
|
|
304
|
+
}
|
|
305
|
+
for (let py = y + h - radius; py < y + h; py++) {
|
|
306
|
+
for (let px = x + radius; px < x + w - radius; px++) _blend(px, py, r, g, b, a);
|
|
307
|
+
}
|
|
308
|
+
// Fill corners with quarter-circles
|
|
309
|
+
const corners = [
|
|
310
|
+
[x + radius, y + radius],
|
|
311
|
+
[x + w - radius - 1, y + radius],
|
|
312
|
+
[x + radius, y + h - radius - 1],
|
|
313
|
+
[x + w - radius - 1, y + h - radius - 1],
|
|
314
|
+
];
|
|
315
|
+
for (const [cx, cy] of corners) {
|
|
316
|
+
for (let dy = -radius; dy <= radius; dy++) {
|
|
317
|
+
for (let dx = -radius; dx <= radius; dx++) {
|
|
318
|
+
if (dx * dx + dy * dy <= radius * radius) _blend(cx + dx, cy + dy, r, g, b, a);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
// Outline only — top/bottom edges
|
|
324
|
+
for (let px = x + radius; px < x + w - radius; px++) {
|
|
325
|
+
_blend(px, y, r, g, b, a);
|
|
326
|
+
_blend(px, y + h - 1, r, g, b, a);
|
|
327
|
+
}
|
|
328
|
+
// Left/right edges
|
|
329
|
+
for (let py = y + radius; py < y + h - radius; py++) {
|
|
330
|
+
_blend(x, py, r, g, b, a);
|
|
331
|
+
_blend(x + w - 1, py, r, g, b, a);
|
|
332
|
+
}
|
|
333
|
+
// Corners (just the arc outline)
|
|
334
|
+
const corners = [
|
|
335
|
+
[x + radius, y + radius, -1, -1],
|
|
336
|
+
[x + w - radius - 1, y + radius, 1, -1],
|
|
337
|
+
[x + radius, y + h - radius - 1, -1, 1],
|
|
338
|
+
[x + w - radius - 1, y + h - radius - 1, 1, 1],
|
|
339
|
+
];
|
|
340
|
+
for (const [cx, cy, sx, sy] of corners) {
|
|
341
|
+
for (let angle = 0; angle <= 90; angle += 1) {
|
|
342
|
+
const rad = (angle * Math.PI) / 180;
|
|
343
|
+
const px = (cx + sx * Math.cos(rad) * radius) | 0;
|
|
344
|
+
const py = (cy + sy * Math.sin(rad) * radius) | 0;
|
|
345
|
+
_blend(px, py, r, g, b, a);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ── Polygon ─────────────────────────────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* poly(points, color, fill=true)
|
|
355
|
+
* points: [[x0,y0],[x1,y1],...] — convex or concave polygon
|
|
356
|
+
* Uses scanline fill for filled, Bresenham for outline.
|
|
357
|
+
*/
|
|
358
|
+
function poly(points, color, fill = true) {
|
|
359
|
+
if (points.length < 3) return;
|
|
360
|
+
const { r, g, b, a } = _unpack(color);
|
|
361
|
+
|
|
362
|
+
if (fill) {
|
|
363
|
+
const minY = Math.max(0, Math.min(...points.map(p => p[1])) | 0);
|
|
364
|
+
const maxY = Math.min(H - 1, Math.max(...points.map(p => p[1])) | 0);
|
|
365
|
+
for (let py = minY; py <= maxY; py++) {
|
|
366
|
+
const intersections = [];
|
|
367
|
+
const n = points.length;
|
|
368
|
+
for (let i = 0; i < n; i++) {
|
|
369
|
+
const [x0, y0] = points[i];
|
|
370
|
+
const [x1, y1] = points[(i + 1) % n];
|
|
371
|
+
if ((y0 <= py && py < y1) || (y1 <= py && py < y0)) {
|
|
372
|
+
const t = (py - y0) / (y1 - y0);
|
|
373
|
+
intersections.push(x0 + t * (x1 - x0));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
intersections.sort((a, b) => a - b);
|
|
377
|
+
for (let i = 0; i + 1 < intersections.length; i += 2) {
|
|
378
|
+
const lx = Math.max(0, intersections[i] | 0);
|
|
379
|
+
const rx = Math.min(W - 1, intersections[i + 1] | 0);
|
|
380
|
+
for (let px = lx; px <= rx; px++) _blend(px, py, r, g, b, a);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
// Outline
|
|
385
|
+
const n = points.length;
|
|
386
|
+
for (let i = 0; i < n; i++) {
|
|
387
|
+
const [x0, y0] = points[i];
|
|
388
|
+
const [x1, y1] = points[(i + 1) % n];
|
|
389
|
+
// Bresenham
|
|
390
|
+
let dx = Math.abs(x1 - x0),
|
|
391
|
+
dy = -Math.abs(y1 - y0);
|
|
392
|
+
let sx = x0 < x1 ? 1 : -1,
|
|
393
|
+
sy = y0 < y1 ? 1 : -1;
|
|
394
|
+
let err = dx + dy,
|
|
395
|
+
px = x0 | 0,
|
|
396
|
+
py = y0 | 0;
|
|
397
|
+
for (;;) {
|
|
398
|
+
_blend(px, py, r, g, b, a);
|
|
399
|
+
if (px === (x1 | 0) && py === (y1 | 0)) break;
|
|
400
|
+
const e2 = 2 * err;
|
|
401
|
+
if (e2 >= dy) {
|
|
402
|
+
err += dy;
|
|
403
|
+
px += sx;
|
|
404
|
+
}
|
|
405
|
+
if (e2 <= dx) {
|
|
406
|
+
err += dx;
|
|
407
|
+
py += sy;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* drawTriangle(x0,y0, x1,y1, x2,y2, color, fill=true)
|
|
416
|
+
*/
|
|
417
|
+
function drawTriangle(x0, y0, x1, y1, x2, y2, color, fill = true) {
|
|
418
|
+
poly(
|
|
419
|
+
[
|
|
420
|
+
[x0, y0],
|
|
421
|
+
[x1, y1],
|
|
422
|
+
[x2, y2],
|
|
423
|
+
],
|
|
424
|
+
color,
|
|
425
|
+
fill
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* drawDiamond(cx, cy, halfW, halfH, color, fill=true)
|
|
431
|
+
*/
|
|
432
|
+
function drawDiamond(cx, cy, halfW, halfH, color, fill = true) {
|
|
433
|
+
poly(
|
|
434
|
+
[
|
|
435
|
+
[cx, cy - halfH],
|
|
436
|
+
[cx + halfW, cy],
|
|
437
|
+
[cx, cy + halfH],
|
|
438
|
+
[cx - halfW, cy],
|
|
439
|
+
],
|
|
440
|
+
color,
|
|
441
|
+
fill
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ── Text helpers ─────────────────────────────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
// We reach into the gpu's exposed stdApi print via globalThis
|
|
448
|
+
function _print(text, x, y, color, scale) {
|
|
449
|
+
if (typeof globalThis.print === 'function') globalThis.print(text, x, y, color, scale);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function measureText(text, scale = 1) {
|
|
453
|
+
// 5px glyph + 1px spacing = 6px per character; 7px tall
|
|
454
|
+
const s = Math.max(1, Math.round(scale));
|
|
455
|
+
return { width: text.length * 6 * s, height: 7 * s };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** printCentered(text, cx, y, color, scale=1) — centre on x */
|
|
459
|
+
function printCentered(text, cx, y, color, scale = 1) {
|
|
460
|
+
const w = measureText(text, scale).width;
|
|
461
|
+
_print(text, (cx - w / 2) | 0, y, color, scale);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** printRight(text, rightX, y, color, scale=1) — right-align */
|
|
465
|
+
function printRight(text, rightX, y, color, scale = 1) {
|
|
466
|
+
const w = measureText(text, scale).width;
|
|
467
|
+
_print(text, (rightX - w) | 0, y, color, scale);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* drawGlowText(text, x, y, color, glowColor, scale=1)
|
|
472
|
+
* Prints text with a soft glow halo by drawing the glowColor in 8 surrounding offsets
|
|
473
|
+
* then the main color on top. Pure N64/Dreamcast HUD aesthetic.
|
|
474
|
+
*/
|
|
475
|
+
function drawGlowText(text, x, y, color, glowColor, scale = 1) {
|
|
476
|
+
const offsets = [
|
|
477
|
+
[-1, -1],
|
|
478
|
+
[0, -1],
|
|
479
|
+
[1, -1],
|
|
480
|
+
[-1, 0],
|
|
481
|
+
[1, 0],
|
|
482
|
+
[-1, 1],
|
|
483
|
+
[0, 1],
|
|
484
|
+
[1, 1],
|
|
485
|
+
];
|
|
486
|
+
for (const [dx, dy] of offsets) {
|
|
487
|
+
_print(text, x + dx, y + dy, glowColor, scale);
|
|
488
|
+
}
|
|
489
|
+
_print(text, x, y, color, scale);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* drawGlowTextCentered(text, cx, y, color, glowColor, scale=1)
|
|
494
|
+
*/
|
|
495
|
+
function drawGlowTextCentered(text, cx, y, color, glowColor, scale = 1) {
|
|
496
|
+
const w = measureText(text, scale).width;
|
|
497
|
+
drawGlowText(text, (cx - w / 2) | 0, y, color, glowColor, scale);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* drawPulsingText(text, cx, y, color, time, opts)
|
|
502
|
+
* Centered text that pulses opacity and/or scale over time.
|
|
503
|
+
* opts: { frequency, minAlpha, glowColor, scale }
|
|
504
|
+
*/
|
|
505
|
+
function drawPulsingText(text, cx, y, color, time, opts = {}) {
|
|
506
|
+
const freq = opts.frequency ?? 3;
|
|
507
|
+
const minAlpha = opts.minAlpha ?? 120;
|
|
508
|
+
const scale = opts.scale ?? 1;
|
|
509
|
+
const alpha = Math.floor((Math.sin(time * freq) * 0.5 + 0.5) * (255 - minAlpha) + minAlpha);
|
|
510
|
+
const { r, g, b } = _unpack(color);
|
|
511
|
+
const pulsedColor = rgba8(r, g, b, alpha);
|
|
512
|
+
if (opts.glowColor) {
|
|
513
|
+
drawGlowTextCentered(text, cx, y, pulsedColor, opts.glowColor, scale);
|
|
514
|
+
} else {
|
|
515
|
+
printCentered(text, cx, y, pulsedColor, scale);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ── Screen overlays ──────────────────────────────────────────────────────────
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* drawScanlines(alpha=80, spacing=2)
|
|
523
|
+
* CRT scanline overlay — horizontal dark bands every `spacing` rows.
|
|
524
|
+
*/
|
|
525
|
+
function drawScanlines(alpha = 80, spacing = 2) {
|
|
526
|
+
const p = fb.pixels;
|
|
527
|
+
const af = (255 - alpha) / 255;
|
|
528
|
+
for (let y = 0; y < H; y += spacing) {
|
|
529
|
+
for (let x = 0; x < W; x++) {
|
|
530
|
+
const i = (y * W + x) * 4;
|
|
531
|
+
p[i] = (p[i] * af) | 0;
|
|
532
|
+
p[i + 1] = (p[i + 1] * af) | 0;
|
|
533
|
+
p[i + 2] = (p[i + 2] * af) | 0;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* drawNoise(x, y, w, h, alpha=40, seed=0)
|
|
540
|
+
* Deterministic LCG grain/static overlay. Good for title card grit.
|
|
541
|
+
*/
|
|
542
|
+
function drawNoise(x, y, w, h, alpha = 40, seed = 0) {
|
|
543
|
+
x |= 0;
|
|
544
|
+
y |= 0;
|
|
545
|
+
w |= 0;
|
|
546
|
+
h |= 0;
|
|
547
|
+
let s = seed | 12345;
|
|
548
|
+
const x1 = Math.max(0, x),
|
|
549
|
+
y1 = Math.max(0, y);
|
|
550
|
+
const x2 = Math.min(W, x + w),
|
|
551
|
+
y2 = Math.min(H, y + h);
|
|
552
|
+
for (let py = y1; py < y2; py++) {
|
|
553
|
+
for (let px = x1; px < x2; px++) {
|
|
554
|
+
s = (s * 1664525 + 1013904223) & 0xffffffff;
|
|
555
|
+
const n = (s >>> 16) & 0xff;
|
|
556
|
+
_blend(px, py, n, n, n, ((n / 255) * alpha) | 0);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* drawCheckerboard(x, y, w, h, c1, c2, size=8)
|
|
563
|
+
* N64 loading pattern / retro background filler.
|
|
564
|
+
*/
|
|
565
|
+
function drawCheckerboard(x, y, w, h, c1, c2, size = 8) {
|
|
566
|
+
x |= 0;
|
|
567
|
+
y |= 0;
|
|
568
|
+
w |= 0;
|
|
569
|
+
h |= 0;
|
|
570
|
+
size |= 0;
|
|
571
|
+
const x1 = Math.max(0, x),
|
|
572
|
+
y1 = Math.max(0, y);
|
|
573
|
+
const x2 = Math.min(W, x + w),
|
|
574
|
+
y2 = Math.min(H, y + h);
|
|
575
|
+
for (let py = y1; py < y2; py++) {
|
|
576
|
+
for (let px = x1; px < x2; px++) {
|
|
577
|
+
const cell = ((((px - x) / size) | 0) + (((py - y) / size) | 0)) % 2;
|
|
578
|
+
_pset(px, py, cell === 0 ? c1 : c2);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ── Progress / HUD bars ───────────────────────────────────────────────────────
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* drawProgressBar(x, y, w, h, t, fgColor, bgColor, borderColor)
|
|
587
|
+
* t = 0..1 fill fraction.
|
|
588
|
+
*/
|
|
589
|
+
function drawProgressBar(x, y, w, h, t, fgColor, bgColor, borderColor) {
|
|
590
|
+
t = Math.max(0, Math.min(1, t));
|
|
591
|
+
// Background
|
|
592
|
+
if (bgColor !== undefined) {
|
|
593
|
+
const bg = _unpack(bgColor);
|
|
594
|
+
for (let py = y; py < y + h; py++)
|
|
595
|
+
for (let px = x; px < x + w; px++) _blend(px, py, bg.r, bg.g, bg.b, bg.a);
|
|
596
|
+
}
|
|
597
|
+
// Fill
|
|
598
|
+
const fill = (w * t) | 0;
|
|
599
|
+
const fg = _unpack(fgColor);
|
|
600
|
+
for (let py = y; py < y + h; py++)
|
|
601
|
+
for (let px = x; px < x + fill; px++) _blend(px, py, fg.r, fg.g, fg.b, fg.a);
|
|
602
|
+
// Border
|
|
603
|
+
if (borderColor !== undefined) {
|
|
604
|
+
const bc = _unpack(borderColor);
|
|
605
|
+
for (let px = x; px < x + w; px++) {
|
|
606
|
+
_blend(px, y, bc.r, bc.g, bc.b, bc.a);
|
|
607
|
+
_blend(px, y + h - 1, bc.r, bc.g, bc.b, bc.a);
|
|
608
|
+
}
|
|
609
|
+
for (let py = y; py < y + h; py++) {
|
|
610
|
+
_blend(x, py, bc.r, bc.g, bc.b, bc.a);
|
|
611
|
+
_blend(x + w - 1, py, bc.r, bc.g, bc.b, bc.a);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* drawHealthBar(x, y, w, h, current, max, opts)
|
|
618
|
+
* opts: { barColor, backgroundColor, borderColor, dangerColor, dangerThreshold }
|
|
619
|
+
* Auto-switches to dangerColor when hp < dangerThreshold (default 0.25).
|
|
620
|
+
*/
|
|
621
|
+
function drawHealthBar(x, y, w, h, current, max, opts = {}) {
|
|
622
|
+
const t = max > 0 ? current / max : 0;
|
|
623
|
+
const danger = opts.dangerThreshold ?? 0.25;
|
|
624
|
+
const fg =
|
|
625
|
+
t < danger ? (opts.dangerColor ?? rgba8(220, 40, 40)) : (opts.barColor ?? rgba8(50, 220, 80));
|
|
626
|
+
drawProgressBar(
|
|
627
|
+
x,
|
|
628
|
+
y,
|
|
629
|
+
w,
|
|
630
|
+
h,
|
|
631
|
+
t,
|
|
632
|
+
fg,
|
|
633
|
+
opts.backgroundColor ?? rgba8(30, 30, 30, 200),
|
|
634
|
+
opts.borderColor ?? rgba8(200, 200, 200, 180)
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ── PS1/N64 panel border ──────────────────────────────────────────────────────
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* drawPixelBorder(x, y, w, h, lightColor, darkColor, thickness=2)
|
|
642
|
+
* 3D-embossed bevel — PS1/SNES-style UI panel border.
|
|
643
|
+
*/
|
|
644
|
+
function drawPixelBorder(x, y, w, h, lightColor, darkColor, thickness = 2) {
|
|
645
|
+
const lc = _unpack(lightColor),
|
|
646
|
+
dc = _unpack(darkColor);
|
|
647
|
+
for (let i = 0; i < thickness; i++) {
|
|
648
|
+
// Top & left (light)
|
|
649
|
+
for (let px = x + i; px < x + w - i; px++) _blend(px, y + i, lc.r, lc.g, lc.b, lc.a);
|
|
650
|
+
for (let py = y + i; py < y + h - i; py++) _blend(x + i, py, lc.r, lc.g, lc.b, lc.a);
|
|
651
|
+
// Bottom & right (dark)
|
|
652
|
+
for (let px = x + i; px < x + w - i; px++) _blend(px, y + h - 1 - i, dc.r, dc.g, dc.b, dc.a);
|
|
653
|
+
for (let py = y + i; py < y + h - i; py++) _blend(x + w - 1 - i, py, dc.r, dc.g, dc.b, dc.a);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ── Decorative shapes ─────────────────────────────────────────────────────────
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* drawStarburst(cx, cy, outerR, innerR, spikes, color, fill=true)
|
|
661
|
+
* Classic star shape — great for score popups and collectible icons.
|
|
662
|
+
*/
|
|
663
|
+
function drawStarburst(cx, cy, outerR, innerR, spikes, color, fill = true) {
|
|
664
|
+
const pts = [];
|
|
665
|
+
for (let i = 0; i < spikes * 2; i++) {
|
|
666
|
+
const angle = (i * Math.PI) / spikes - Math.PI / 2;
|
|
667
|
+
const r = i % 2 === 0 ? outerR : innerR;
|
|
668
|
+
pts.push([cx + Math.cos(angle) * r, cy + Math.sin(angle) * r]);
|
|
669
|
+
}
|
|
670
|
+
poly(pts, color, fill);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* drawWave(x, y, w, amplitude, frequency, phase, color, thickness=2)
|
|
675
|
+
* Animated sine wave — good for water, plasma, audio visualizers.
|
|
676
|
+
* phase = time * speed for animation.
|
|
677
|
+
*/
|
|
678
|
+
function drawWave(x, y, w, amplitude, frequency, phase, color, thickness = 2) {
|
|
679
|
+
const { r, g, b, a } = _unpack(color);
|
|
680
|
+
for (let px = x; px < x + w && px < W; px++) {
|
|
681
|
+
const py = (y + Math.sin((px - x) * frequency + phase) * amplitude) | 0;
|
|
682
|
+
for (let t = -thickness >> 1; t <= thickness >> 1; t++) {
|
|
683
|
+
const ny = py + t;
|
|
684
|
+
if (ny >= 0 && ny < H) _blend(px, ny, r, g, b, a);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* drawSpiral(cx, cy, turns, spacing, color)
|
|
691
|
+
* Decorative Archimedean spiral — psychedelic demoscene / loading screen art.
|
|
692
|
+
*/
|
|
693
|
+
function drawSpiral(cx, cy, turns, spacing, color) {
|
|
694
|
+
const { r, g, b, a } = _unpack(color);
|
|
695
|
+
const steps = turns * 360;
|
|
696
|
+
for (let i = 0; i < steps; i++) {
|
|
697
|
+
const angle = (i / 180) * Math.PI;
|
|
698
|
+
const rad = (i / 360) * spacing;
|
|
699
|
+
const px = (cx + Math.cos(angle) * rad) | 0;
|
|
700
|
+
const py = (cy + Math.sin(angle) * rad) | 0;
|
|
701
|
+
if (px >= 0 && px < W && py >= 0 && py < H) _blend(px, py, r, g, b, a);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ── Composite widgets ─────────────────────────────────────────────────────────
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* drawPanel(x, y, w, h, opts)
|
|
709
|
+
* A complete PS1/N64-style info panel with background, border, and optional title.
|
|
710
|
+
* opts: { bgColor, borderLight, borderDark, title, titleColor, padding, alpha }
|
|
711
|
+
*/
|
|
712
|
+
function drawPanel(x, y, w, h, opts = {}) {
|
|
713
|
+
const bg = opts.bgColor ?? rgba8(10, 10, 30, 200);
|
|
714
|
+
const bl = opts.borderLight ?? rgba8(180, 180, 220, 255);
|
|
715
|
+
const bd = opts.borderDark ?? rgba8(30, 30, 60, 255);
|
|
716
|
+
// opts.alpha available for callers but not used in fill path
|
|
717
|
+
|
|
718
|
+
// Fill panel (re-use opts.bgColor with alpha override)
|
|
719
|
+
const bgc = _unpack(bg);
|
|
720
|
+
for (let py = y + 2; py < y + h - 2; py++)
|
|
721
|
+
for (let px = x + 2; px < x + w - 2; px++) _blend(px, py, bgc.r, bgc.g, bgc.b, bgc.a);
|
|
722
|
+
|
|
723
|
+
// 3D border
|
|
724
|
+
drawPixelBorder(x, y, w, h, bl, bd, 2);
|
|
725
|
+
|
|
726
|
+
// Optional title text at top
|
|
727
|
+
if (opts.title) {
|
|
728
|
+
const tc = opts.titleColor ?? rgba8(255, 255, 255, 255);
|
|
729
|
+
const pad = opts.padding ?? 6;
|
|
730
|
+
printCentered(opts.title, x + w / 2, y + pad, tc);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ── Minimap System ─────────────────────────────────────────────────────────
|
|
735
|
+
|
|
736
|
+
/**
|
|
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).
|
|
759
|
+
*/
|
|
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;
|
|
946
|
+
const ec = _unpack(e.color ?? rgba8(255, 255, 255));
|
|
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
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* scrollingText(text, y, speed, time, color, scale=1, width=640)
|
|
1005
|
+
* Marquee text scrolling left across the screen.
|
|
1006
|
+
* time = accumulated seconds (from novaStore or cart-local variable).
|
|
1007
|
+
*/
|
|
1008
|
+
function scrollingText(text, y, speed, time, color, scale = 1, width = 640) {
|
|
1009
|
+
const tw = measureText(text, scale).width;
|
|
1010
|
+
const x = (width - ((time * speed) % (width + tw))) | 0;
|
|
1011
|
+
_print(text, x, y, color, scale);
|
|
1012
|
+
}
|
|
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
|
+
|
|
1048
|
+
// ── Full-screen helpers ───────────────────────────────────────────────────────
|
|
1049
|
+
|
|
1050
|
+
/** Full-screen vertical gradient — great for sky/title backgrounds */
|
|
1051
|
+
function drawSkyGradient(topColor, bottomColor) {
|
|
1052
|
+
drawGradient(0, 0, W, H, topColor, bottomColor, 'v');
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/** Full-screen flash — use alpha 0..255 for a fade effect */
|
|
1056
|
+
function drawFlash(color) {
|
|
1057
|
+
const { r, g, b, a } = _unpack(color);
|
|
1058
|
+
for (let py = 0; py < H; py++) for (let px = 0; px < W; px++) _blend(px, py, r, g, b, a);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Draw a crosshair / reticle at (cx, cy).
|
|
1063
|
+
* @param {number} cx - centre X in screen pixels
|
|
1064
|
+
* @param {number} cy - centre Y in screen pixels
|
|
1065
|
+
* @param {number} [size=8] - half-length of each arm in pixels
|
|
1066
|
+
* @param {number} [color=0xffffffff] - rgba8 or 0xRRGGBB colour
|
|
1067
|
+
* @param {'cross'|'dot'|'circle'} [style='cross'] - reticle style
|
|
1068
|
+
*/
|
|
1069
|
+
function drawCrosshair(cx, cy, size = 8, color = 0xffffffff, style = 'cross') {
|
|
1070
|
+
if (style === 'cross' || style === 'dot') {
|
|
1071
|
+
// Horizontal arm
|
|
1072
|
+
for (let x = cx - size; x <= cx + size; x++) _pset(x, cy, color);
|
|
1073
|
+
// Vertical arm
|
|
1074
|
+
for (let y = cy - size; y <= cy + size; y++) _pset(cx, y, color);
|
|
1075
|
+
}
|
|
1076
|
+
if (style === 'circle') {
|
|
1077
|
+
// Thin circle
|
|
1078
|
+
const steps = Math.max(32, size * 4);
|
|
1079
|
+
for (let i = 0; i < steps; i++) {
|
|
1080
|
+
const a = (i / steps) * Math.PI * 2;
|
|
1081
|
+
_pset(Math.round(cx + Math.cos(a) * size), Math.round(cy + Math.sin(a) * size), color);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
if (style === 'dot') {
|
|
1085
|
+
// Centre dot (extra pixels around the intersection)
|
|
1086
|
+
_pset(cx - 1, cy, color);
|
|
1087
|
+
_pset(cx + 1, cy, color);
|
|
1088
|
+
_pset(cx, cy - 1, color);
|
|
1089
|
+
_pset(cx, cy + 1, color);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// ─── expose ──────────────────────────────────────────────────────────────────
|
|
1094
|
+
return {
|
|
1095
|
+
exposeTo(target) {
|
|
1096
|
+
Object.assign(target, {
|
|
1097
|
+
// Colour helpers
|
|
1098
|
+
colorLerp,
|
|
1099
|
+
colorMix,
|
|
1100
|
+
hexColor,
|
|
1101
|
+
hslColor,
|
|
1102
|
+
n64Palette,
|
|
1103
|
+
|
|
1104
|
+
// Math utilities
|
|
1105
|
+
lerp,
|
|
1106
|
+
clamp,
|
|
1107
|
+
randRange,
|
|
1108
|
+
randInt,
|
|
1109
|
+
dist,
|
|
1110
|
+
dist3d,
|
|
1111
|
+
remap,
|
|
1112
|
+
pulse,
|
|
1113
|
+
deg2rad,
|
|
1114
|
+
rad2deg,
|
|
1115
|
+
|
|
1116
|
+
// Gradient fills
|
|
1117
|
+
drawGradient,
|
|
1118
|
+
drawRadialGradient,
|
|
1119
|
+
drawSkyGradient,
|
|
1120
|
+
drawFlash,
|
|
1121
|
+
|
|
1122
|
+
// Shapes
|
|
1123
|
+
drawRoundedRect,
|
|
1124
|
+
poly,
|
|
1125
|
+
drawTriangle,
|
|
1126
|
+
drawDiamond,
|
|
1127
|
+
drawStarburst,
|
|
1128
|
+
drawWave,
|
|
1129
|
+
drawSpiral,
|
|
1130
|
+
drawCheckerboard,
|
|
1131
|
+
|
|
1132
|
+
// Text
|
|
1133
|
+
measureText,
|
|
1134
|
+
printCentered,
|
|
1135
|
+
printRight,
|
|
1136
|
+
drawGlowText,
|
|
1137
|
+
drawGlowTextCentered,
|
|
1138
|
+
drawPulsingText,
|
|
1139
|
+
|
|
1140
|
+
// Overlays
|
|
1141
|
+
drawScanlines,
|
|
1142
|
+
drawNoise,
|
|
1143
|
+
|
|
1144
|
+
// HUD Widgets
|
|
1145
|
+
drawProgressBar,
|
|
1146
|
+
drawHealthBar,
|
|
1147
|
+
drawPixelBorder,
|
|
1148
|
+
drawPanel,
|
|
1149
|
+
drawCrosshair,
|
|
1150
|
+
createMinimap,
|
|
1151
|
+
drawMinimap,
|
|
1152
|
+
drawFloatingTexts,
|
|
1153
|
+
drawFloatingTexts3D,
|
|
1154
|
+
scrollingText,
|
|
1155
|
+
});
|
|
1156
|
+
},
|
|
1157
|
+
};
|
|
1158
|
+
}
|