nova64 0.2.6 → 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 (47) hide show
  1. package/dist/examples/strider-demo-3d/fix-game.sh +0 -0
  2. package/dist/runtime/api-2d.js +1158 -0
  3. package/dist/runtime/api-3d/camera.js +73 -0
  4. package/dist/runtime/api-3d/instancing.js +180 -0
  5. package/dist/runtime/api-3d/lights.js +51 -0
  6. package/dist/runtime/api-3d/materials.js +47 -0
  7. package/dist/runtime/api-3d/models.js +84 -0
  8. package/dist/runtime/api-3d/particles.js +296 -0
  9. package/dist/runtime/api-3d/pbr.js +113 -0
  10. package/dist/runtime/api-3d/primitives.js +304 -0
  11. package/dist/runtime/api-3d/scene.js +169 -0
  12. package/dist/runtime/api-3d/transforms.js +161 -0
  13. package/dist/runtime/api-3d.js +166 -0
  14. package/dist/runtime/api-effects.js +840 -0
  15. package/dist/runtime/api-gameutils.js +476 -0
  16. package/dist/runtime/api-generative.js +610 -0
  17. package/dist/runtime/api-presets.js +85 -0
  18. package/dist/runtime/api-skybox.js +232 -0
  19. package/dist/runtime/api-sprites.js +100 -0
  20. package/dist/runtime/api-voxel.js +712 -0
  21. package/dist/runtime/api.js +201 -0
  22. package/dist/runtime/assets.js +27 -0
  23. package/dist/runtime/audio.js +114 -0
  24. package/dist/runtime/collision.js +47 -0
  25. package/dist/runtime/console.js +101 -0
  26. package/dist/runtime/editor.js +233 -0
  27. package/dist/runtime/font.js +233 -0
  28. package/dist/runtime/framebuffer.js +28 -0
  29. package/dist/runtime/fullscreen-button.js +185 -0
  30. package/dist/runtime/gpu-canvas2d.js +47 -0
  31. package/dist/runtime/gpu-threejs.js +643 -0
  32. package/dist/runtime/gpu-webgl2.js +310 -0
  33. package/dist/runtime/index.d.ts +682 -0
  34. package/dist/runtime/index.js +22 -0
  35. package/dist/runtime/input.js +225 -0
  36. package/dist/runtime/logger.js +60 -0
  37. package/dist/runtime/physics.js +101 -0
  38. package/dist/runtime/screens.js +213 -0
  39. package/dist/runtime/storage.js +38 -0
  40. package/dist/runtime/store.js +151 -0
  41. package/dist/runtime/textinput.js +68 -0
  42. package/dist/runtime/ui/buttons.js +124 -0
  43. package/dist/runtime/ui/panels.js +105 -0
  44. package/dist/runtime/ui/text.js +86 -0
  45. package/dist/runtime/ui/widgets.js +141 -0
  46. package/dist/runtime/ui.js +111 -0
  47. package/package.json +34 -32
@@ -0,0 +1,225 @@
1
+ // runtime/input.js
2
+ // Gamepad button mapping (standard gamepad layout)
3
+ const GAMEPAD_BUTTONS = {
4
+ 0: 12, // A button → Start (button 12)
5
+ 1: 13, // B button → Select (button 13)
6
+ 2: 4, // X button → button 4
7
+ 3: 5, // Y button → button 5
8
+ 4: 6, // LB → button 6
9
+ 5: 7, // RB → button 7
10
+ 6: 8, // LT → button 8
11
+ 7: 9, // RT → button 9
12
+ 8: 13, // Select → button 13
13
+ 9: 12, // Start → button 12
14
+ 12: 0, // D-pad Up → button 0
15
+ 13: 3, // D-pad Down → button 3
16
+ 14: 1, // D-pad Left → button 1
17
+ 15: 2, // D-pad Right → button 2
18
+ };
19
+
20
+ const KEYMAP = {
21
+ // arrows + Z X C V
22
+ 0: 'ArrowLeft', // left
23
+ 1: 'ArrowRight', // right
24
+ 2: 'ArrowUp', // up
25
+ 3: 'ArrowDown', // down
26
+ 4: 'KeyZ',
27
+ 5: 'KeyX',
28
+ 6: 'KeyC',
29
+ 7: 'KeyV',
30
+ 8: 'KeyA',
31
+ 9: 'KeyS',
32
+ 10: 'KeyQ',
33
+ 11: 'KeyW',
34
+ 12: 'Enter', // Start
35
+ 13: 'Space', // Select/Action
36
+ };
37
+
38
+ class Input {
39
+ constructor() {
40
+ this.keys = new Map();
41
+ this.prev = new Map();
42
+ this.mouse = { x: 0, y: 0, down: false, prevDown: false };
43
+ this.uiCallbacks = { setMousePosition: null, setMouseButton: null };
44
+
45
+ // Gamepad state
46
+ this.gamepadButtons = new Map();
47
+ this.gamepadPrev = new Map();
48
+ this.gamepadAxes = { leftX: 0, leftY: 0, rightX: 0, rightY: 0 };
49
+ this.gamepadDeadzone = 0.15;
50
+
51
+ if (typeof window !== 'undefined') {
52
+ window.addEventListener('keydown', e => {
53
+ this.keys.set(e.code, true);
54
+ });
55
+ window.addEventListener('keyup', e => {
56
+ this.keys.set(e.code, false);
57
+ });
58
+ window.addEventListener('blur', () => {
59
+ this.keys.clear();
60
+ });
61
+
62
+ // Mouse event listeners
63
+ window.addEventListener('mousemove', e => {
64
+ const canvas = document.querySelector('canvas');
65
+ if (canvas) {
66
+ const rect = canvas.getBoundingClientRect();
67
+ // Scale mouse position to Nova64's 640x360 resolution
68
+ this.mouse.x = Math.floor(((e.clientX - rect.left) / rect.width) * 640);
69
+ this.mouse.y = Math.floor(((e.clientY - rect.top) / rect.height) * 360);
70
+
71
+ // Update UI system if connected
72
+ if (this.uiCallbacks.setMousePosition) {
73
+ this.uiCallbacks.setMousePosition(this.mouse.x, this.mouse.y);
74
+ }
75
+ }
76
+ });
77
+
78
+ window.addEventListener('mousedown', _e => {
79
+ this.mouse.down = true;
80
+ if (this.uiCallbacks.setMouseButton) {
81
+ this.uiCallbacks.setMouseButton(true);
82
+ }
83
+ });
84
+
85
+ window.addEventListener('mouseup', _e => {
86
+ this.mouse.down = false;
87
+ if (this.uiCallbacks.setMouseButton) {
88
+ this.uiCallbacks.setMouseButton(false);
89
+ }
90
+ });
91
+
92
+ // Gamepad connection events
93
+ window.addEventListener('gamepadconnected', _e => {
94
+ // Gamepad connected
95
+ });
96
+
97
+ window.addEventListener('gamepaddisconnected', _e => {
98
+ this.gamepadButtons.clear();
99
+ this.gamepadPrev.clear();
100
+ });
101
+ }
102
+ }
103
+
104
+ pollGamepad() {
105
+ const gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
106
+ const gamepad = gamepads[0]; // Use first connected gamepad
107
+
108
+ if (gamepad) {
109
+ // Read button states
110
+ gamepad.buttons.forEach((button, index) => {
111
+ const mappedButton = GAMEPAD_BUTTONS[index];
112
+ if (mappedButton !== undefined) {
113
+ this.gamepadButtons.set(mappedButton, button.pressed);
114
+ }
115
+ });
116
+
117
+ // Read axes with deadzone
118
+ if (gamepad.axes.length >= 4) {
119
+ this.gamepadAxes.leftX =
120
+ Math.abs(gamepad.axes[0]) > this.gamepadDeadzone ? gamepad.axes[0] : 0;
121
+ this.gamepadAxes.leftY =
122
+ Math.abs(gamepad.axes[1]) > this.gamepadDeadzone ? gamepad.axes[1] : 0;
123
+ this.gamepadAxes.rightX =
124
+ Math.abs(gamepad.axes[2]) > this.gamepadDeadzone ? gamepad.axes[2] : 0;
125
+ this.gamepadAxes.rightY =
126
+ Math.abs(gamepad.axes[3]) > this.gamepadDeadzone ? gamepad.axes[3] : 0;
127
+ }
128
+ }
129
+ }
130
+
131
+ // Connect UI system callbacks
132
+ connectUI(setMousePosition, setMouseButton) {
133
+ this.uiCallbacks.setMousePosition = setMousePosition;
134
+ this.uiCallbacks.setMouseButton = setMouseButton;
135
+ }
136
+ step() {
137
+ this.prev = new Map(this.keys);
138
+ this.mouse.prevDown = this.mouse.down;
139
+ this.gamepadPrev = new Map(this.gamepadButtons);
140
+ this.pollGamepad(); // Poll gamepad state every frame
141
+ }
142
+ btn(i) {
143
+ // Check keyboard OR gamepad
144
+ return !!this.keys.get(KEYMAP[i | 0] || '') || !!this.gamepadButtons.get(i | 0);
145
+ }
146
+ btnp(i) {
147
+ const code = KEYMAP[i | 0] || '';
148
+ const keyPressed = !!this.keys.get(code) && !this.prev.get(code);
149
+ const gamepadPressed = !!this.gamepadButtons.get(i | 0) && !this.gamepadPrev.get(i | 0);
150
+ return keyPressed || gamepadPressed;
151
+ }
152
+ key(code) {
153
+ return !!this.keys.get(code);
154
+ } // Direct key code checking — is currently held
155
+ keyp(code) {
156
+ return !!this.keys.get(code) && !this.prev.get(code);
157
+ } // just-pressed this frame
158
+
159
+ // Gamepad-specific functions
160
+ getGamepadAxis(axisName) {
161
+ return this.gamepadAxes[axisName] || 0;
162
+ }
163
+
164
+ isGamepadConnected() {
165
+ const gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
166
+ return gamepads[0] !== null && gamepads[0] !== undefined;
167
+ }
168
+
169
+ // Helper functions for easier key checking
170
+ isKeyDown(keyCode) {
171
+ // Space must be checked before single-char conversion (' ' → 'Key ' is wrong)
172
+ if (keyCode === ' ') keyCode = 'Space';
173
+ // Handle single character keys by converting to KeyCode format
174
+ if (keyCode.length === 1) {
175
+ keyCode = 'Key' + keyCode.toUpperCase();
176
+ }
177
+ return !!this.keys.get(keyCode);
178
+ }
179
+
180
+ isKeyPressed(keyCode) {
181
+ // Space must be checked before single-char conversion (' ' → 'Key ' is wrong)
182
+ if (keyCode === ' ') keyCode = 'Space';
183
+ // Handle single character keys by converting to KeyCode format
184
+ if (keyCode.length === 1) {
185
+ keyCode = 'Key' + keyCode.toUpperCase();
186
+ }
187
+ return !!this.keys.get(keyCode) && !this.prev.get(keyCode);
188
+ }
189
+ }
190
+
191
+ export const input = new Input();
192
+
193
+ export function inputApi() {
194
+ return {
195
+ exposeTo(target) {
196
+ Object.assign(target, {
197
+ btn: i => input.btn(i),
198
+ btnp: i => input.btnp(i),
199
+ key: code => input.key(code),
200
+ keyp: code => input.keyp(code),
201
+ isKeyDown: code => input.isKeyDown(code),
202
+ isKeyPressed: code => input.isKeyPressed(code),
203
+ // Mouse functions
204
+ mouseX: () => input.mouse.x,
205
+ mouseY: () => input.mouse.y,
206
+ mouseDown: () => input.mouse.down,
207
+ mousePressed: () => input.mouse.down && !input.mouse.prevDown,
208
+ // Gamepad functions
209
+ gamepadAxis: axisName => input.getGamepadAxis(axisName),
210
+ gamepadConnected: () => input.isGamepadConnected(),
211
+ // Axis aliases for convenience
212
+ leftStickX: () => input.getGamepadAxis('leftX'),
213
+ leftStickY: () => input.getGamepadAxis('leftY'),
214
+ rightStickX: () => input.getGamepadAxis('rightX'),
215
+ rightStickY: () => input.getGamepadAxis('rightY'),
216
+ });
217
+ },
218
+ step() {
219
+ input.step();
220
+ },
221
+ connectUI(setMousePosition, setMouseButton) {
222
+ input.connectUI(setMousePosition, setMouseButton);
223
+ },
224
+ };
225
+ }
@@ -0,0 +1,60 @@
1
+ // runtime/logger.js
2
+ // Configurable logging system for Nova64.
3
+ // Replaces scattered console.log calls with leveled, history-tracked output.
4
+ // In production builds the level is automatically raised to WARN.
5
+
6
+ export const LogLevel = Object.freeze({ DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, NONE: 4 });
7
+
8
+ class Logger {
9
+ constructor(initialLevel = LogLevel.INFO) {
10
+ this.level = initialLevel;
11
+ this.history = [];
12
+ this.maxHistory = 200;
13
+ }
14
+
15
+ _record(level, args) {
16
+ this.history.push({ level, args, ts: Date.now() });
17
+ if (this.history.length > this.maxHistory) this.history.shift();
18
+ }
19
+
20
+ debug(...args) {
21
+ if (this.level > LogLevel.DEBUG) return;
22
+ console.log('[Nova64:DEBUG]', ...args);
23
+ this._record('DEBUG', args);
24
+ }
25
+
26
+ info(...args) {
27
+ if (this.level > LogLevel.INFO) return;
28
+ console.log('[Nova64:INFO]', ...args);
29
+ this._record('INFO', args);
30
+ }
31
+
32
+ warn(...args) {
33
+ if (this.level > LogLevel.WARN) return;
34
+ console.warn('[Nova64:WARN]', ...args);
35
+ this._record('WARN', args);
36
+ }
37
+
38
+ error(...args) {
39
+ if (this.level > LogLevel.ERROR) return;
40
+ console.error('[Nova64:ERROR]', ...args);
41
+ this._record('ERROR', args);
42
+ }
43
+
44
+ setLevel(level) {
45
+ this.level = level;
46
+ }
47
+ getHistory() {
48
+ return [...this.history];
49
+ }
50
+ clearHistory() {
51
+ this.history = [];
52
+ }
53
+ }
54
+
55
+ export const logger = new Logger(
56
+ // Raise to WARN in production so debug noise is suppressed
57
+ typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.PROD
58
+ ? LogLevel.WARN
59
+ : LogLevel.INFO
60
+ );
@@ -0,0 +1,101 @@
1
+ // runtime/physics.js
2
+
3
+ export function physicsApi() {
4
+ const bodies = new Set();
5
+ let gravity = 500; // px/s^2
6
+ let tileSize = 8;
7
+ let solidFn = (_tx, _ty) => false;
8
+
9
+ function setGravity(g) {
10
+ gravity = g;
11
+ }
12
+ function setTileSize(ts) {
13
+ tileSize = ts | 0;
14
+ }
15
+ function setTileSolidFn(fn) {
16
+ solidFn = fn;
17
+ }
18
+ const setCollisionMap = setTileSolidFn; // friendlier alias
19
+
20
+ function createBody(x, y, w, h, opts = {}) {
21
+ const b = Object.assign(
22
+ { x, y, w, h, vx: 0, vy: 0, restitution: 0, friction: 0.9, onGround: false },
23
+ opts
24
+ );
25
+ bodies.add(b);
26
+ return b;
27
+ }
28
+ function destroyBody(b) {
29
+ bodies.delete(b);
30
+ }
31
+
32
+ function _sweepAABB(b, dt) {
33
+ // Integrate velocity
34
+ b.vy += gravity * dt;
35
+ let nx = b.x + b.vx * dt;
36
+ let ny = b.y + b.vy * dt;
37
+ b.onGround = false;
38
+
39
+ // Resolve X axis
40
+ const xdir = Math.sign(b.vx);
41
+ if (xdir !== 0) {
42
+ const ahead = xdir > 0 ? nx + b.w : nx;
43
+ const top = Math.floor(b.y / tileSize);
44
+ const bottom = Math.floor((b.y + b.h - 1) / tileSize);
45
+ for (let ty = top; ty <= bottom; ty++) {
46
+ const tx = Math.floor(ahead / tileSize);
47
+ if (solidFn(tx, ty)) {
48
+ if (xdir > 0) nx = tx * tileSize - b.w - 0.01;
49
+ else nx = (tx + 1) * tileSize + 0.01;
50
+ b.vx = -b.vx * b.restitution;
51
+ break;
52
+ }
53
+ }
54
+ }
55
+
56
+ // Resolve Y axis
57
+ const ydir = Math.sign(b.vy);
58
+ if (ydir !== 0) {
59
+ const ahead = ydir > 0 ? ny + b.h : ny;
60
+ const left = Math.floor(nx / tileSize);
61
+ const right = Math.floor((nx + b.w - 1) / tileSize);
62
+ for (let tx = left; tx <= right; tx++) {
63
+ const ty = Math.floor(ahead / tileSize);
64
+ if (solidFn(tx, ty)) {
65
+ if (ydir > 0) {
66
+ ny = ty * tileSize - b.h - 0.01;
67
+ b.onGround = true;
68
+ } else {
69
+ ny = (ty + 1) * tileSize + 0.01;
70
+ }
71
+ b.vy = -b.vy * b.restitution;
72
+ break;
73
+ }
74
+ }
75
+ }
76
+
77
+ // Friction on ground
78
+ if (b.onGround) b.vx *= b.friction;
79
+
80
+ b.x = nx;
81
+ b.y = ny;
82
+ }
83
+
84
+ function step(dt) {
85
+ for (const b of bodies) _sweepAABB(b, dt);
86
+ }
87
+
88
+ return {
89
+ exposeTo(target) {
90
+ Object.assign(target, {
91
+ createBody,
92
+ destroyBody,
93
+ stepPhysics: step,
94
+ setGravity,
95
+ setTileSize,
96
+ setTileSolidFn,
97
+ setCollisionMap,
98
+ });
99
+ },
100
+ };
101
+ }
@@ -0,0 +1,213 @@
1
+ // runtime/screens.js
2
+ // Nova64 Screen Management System
3
+ // Screen state machine with animated transitions (fade, slide-left, slide-right, wipe)
4
+ import { logger } from './logger.js';
5
+
6
+ export class ScreenManager {
7
+ constructor() {
8
+ this.screens = new Map();
9
+ this.currentScreen = null;
10
+ this.defaultScreen = null;
11
+ // Transition state
12
+ this._tr = null; // { type, progress, duration, fromName, toName, data, onEnd }
13
+ }
14
+
15
+ addScreen(name, screenDefinition) {
16
+ if (typeof screenDefinition === 'function') {
17
+ const Cls = screenDefinition;
18
+ this.screens.set(name, new Cls());
19
+ } else {
20
+ this.screens.set(name, screenDefinition);
21
+ }
22
+ if (!this.defaultScreen) this.defaultScreen = name;
23
+ return this;
24
+ }
25
+
26
+ // Immediate switch (no animation)
27
+ switchTo(screenName, data = {}) {
28
+ const screen = this.screens.get(screenName);
29
+ if (!screen) {
30
+ logger.warn("Screen '" + screenName + "' not found");
31
+ return false;
32
+ }
33
+ if (this.currentScreen) {
34
+ const c = this.screens.get(this.currentScreen);
35
+ if (c && typeof c.exit === 'function') c.exit();
36
+ }
37
+ this.currentScreen = screenName;
38
+ if (typeof screen.enter === 'function') screen.enter(data);
39
+ return true;
40
+ }
41
+
42
+ // Animated transition
43
+ // type: 'fade' | 'slide-left' | 'slide-right' | 'wipe' | 'instant'
44
+ transitionTo(screenName, type = 'fade', duration = 0.4, data = {}) {
45
+ if (this._tr && this._tr.active) return;
46
+ const toScreen = this.screens.get(screenName);
47
+ if (!toScreen) {
48
+ logger.warn("Screen '" + screenName + "' not found");
49
+ return;
50
+ }
51
+ this._tr = {
52
+ active: true,
53
+ type,
54
+ duration,
55
+ progress: 0,
56
+ fromName: this.currentScreen,
57
+ toName: screenName,
58
+ data,
59
+ onEnd: null,
60
+ };
61
+ if (typeof toScreen.enter === 'function') toScreen.enter(data);
62
+ }
63
+
64
+ onTransitionEnd(cb) {
65
+ if (this._tr) this._tr.onEnd = cb;
66
+ }
67
+
68
+ start(screenName = null) {
69
+ this.switchTo(screenName || this.defaultScreen);
70
+ }
71
+
72
+ update(dt) {
73
+ if (this._tr && this._tr.active) {
74
+ this._tr.progress += dt / this._tr.duration;
75
+ if (this._tr.progress >= 1) {
76
+ this._tr.progress = 1;
77
+ this._finishTransition();
78
+ }
79
+ const from = this._tr && this._tr.fromName ? this.screens.get(this._tr.fromName) : null;
80
+ const to = this._tr ? this.screens.get(this._tr.toName) : null;
81
+ if (from && typeof from.update === 'function') from.update(dt);
82
+ if (to && typeof to.update === 'function') to.update(dt);
83
+ return;
84
+ }
85
+ if (this.currentScreen) {
86
+ const s = this.screens.get(this.currentScreen);
87
+ if (s && typeof s.update === 'function') s.update(dt);
88
+ }
89
+ }
90
+
91
+ _finishTransition() {
92
+ const tr = this._tr;
93
+ if (tr.fromName) {
94
+ const old = this.screens.get(tr.fromName);
95
+ if (old && typeof old.exit === 'function') old.exit();
96
+ }
97
+ this.currentScreen = tr.toName;
98
+ this._tr = null;
99
+ if (typeof tr.onEnd === 'function') tr.onEnd();
100
+ }
101
+
102
+ draw() {
103
+ if (this._tr && this._tr.active) {
104
+ this._drawTransition();
105
+ return;
106
+ }
107
+ if (this.currentScreen) {
108
+ const s = this.screens.get(this.currentScreen);
109
+ if (s && typeof s.draw === 'function') s.draw();
110
+ }
111
+ }
112
+
113
+ _drawTransition() {
114
+ const { type, progress, fromName, toName } = this._tr;
115
+ const from = fromName ? this.screens.get(fromName) : null;
116
+ const to = this.screens.get(toName);
117
+ const t = Math.max(0, Math.min(1, progress));
118
+ const e = t * t * (3 - 2 * t); // smoothstep
119
+
120
+ if (type === 'fade') {
121
+ // Cross-fade via black
122
+ const midBlack = t < 0.5 ? Math.round(e * 510) : 0;
123
+ if (t < 0.5) {
124
+ if (from && typeof from.draw === 'function') from.draw();
125
+ if (midBlack > 0 && typeof globalThis.rect === 'function')
126
+ globalThis.rect(0, 0, 640, 360, globalThis.rgba8(0, 0, 0, Math.min(255, midBlack)), true);
127
+ } else {
128
+ if (to && typeof to.draw === 'function') to.draw();
129
+ const fadeIn = Math.round((1 - e) * 510);
130
+ if (fadeIn > 0 && typeof globalThis.rect === 'function')
131
+ globalThis.rect(0, 0, 640, 360, globalThis.rgba8(0, 0, 0, Math.min(255, fadeIn)), true);
132
+ }
133
+ } else if (type === 'slide-left' || type === 'slide-right') {
134
+ const dir = type === 'slide-left' ? -1 : 1;
135
+ const off = Math.round(e * 640);
136
+ const setC = typeof globalThis.setCamera === 'function' ? globalThis.setCamera : null;
137
+ if (from && typeof from.draw === 'function') {
138
+ if (setC) setC(dir * off, 0);
139
+ from.draw();
140
+ }
141
+ if (to && typeof to.draw === 'function') {
142
+ if (setC) setC(dir * off - dir * 640, 0);
143
+ to.draw();
144
+ }
145
+ if (setC) setC(0, 0);
146
+ } else if (type === 'wipe') {
147
+ if (to && typeof to.draw === 'function') to.draw();
148
+ if (from && typeof from.draw === 'function') {
149
+ const wipeX = Math.round(e * 640);
150
+ if (typeof globalThis.setCamera === 'function') globalThis.setCamera(wipeX, 0);
151
+ from.draw();
152
+ if (typeof globalThis.setCamera === 'function') globalThis.setCamera(0, 0);
153
+ }
154
+ } else {
155
+ if (to && typeof to.draw === 'function') to.draw();
156
+ }
157
+ }
158
+
159
+ getCurrentScreen() {
160
+ return this.currentScreen;
161
+ }
162
+ getCurrentScreenObject() {
163
+ return this.currentScreen ? this.screens.get(this.currentScreen) : null;
164
+ }
165
+ isTransitioning() {
166
+ return !!(this._tr && this._tr.active);
167
+ }
168
+
169
+ reset() {
170
+ if (this.currentScreen) {
171
+ const s = this.screens.get(this.currentScreen);
172
+ if (s && typeof s.exit === 'function') s.exit();
173
+ }
174
+ this.screens = new Map();
175
+ this.currentScreen = null;
176
+ this.defaultScreen = null;
177
+ this._tr = null;
178
+ }
179
+ }
180
+
181
+ // Base Screen class for class-based patterns
182
+ export class Screen {
183
+ constructor() {
184
+ this.data = {};
185
+ }
186
+ enter(data = {}) {
187
+ this.data = { ...this.data, ...data };
188
+ }
189
+ exit() {}
190
+ update(_dt) {}
191
+ draw() {}
192
+ }
193
+
194
+ // Factory
195
+ export function screenApi() {
196
+ const manager = new ScreenManager();
197
+ return {
198
+ manager,
199
+ exposeTo(target) {
200
+ target.ScreenManager = ScreenManager;
201
+ target.Screen = Screen;
202
+ target.screens = manager;
203
+ target.addScreen = (name, def) => manager.addScreen(name, def);
204
+ target.switchToScreen = (name, data) => manager.switchTo(name, data);
205
+ target.switchScreen = (name, data) => manager.switchTo(name, data);
206
+ target.transitionTo = (name, type, dur, data) => manager.transitionTo(name, type, dur, data);
207
+ target.onTransitionEnd = cb => manager.onTransitionEnd(cb);
208
+ target.isTransitioning = () => manager.isTransitioning();
209
+ target.getCurrentScreen = () => manager.getCurrentScreen();
210
+ target.startScreens = initial => manager.start(initial);
211
+ },
212
+ };
213
+ }
@@ -0,0 +1,38 @@
1
+ // runtime/storage.js
2
+ export function storageApi(namespace = 'nova64') {
3
+ function _k(k) {
4
+ return namespace + ':' + k;
5
+ }
6
+ function saveJSON(key, obj) {
7
+ try {
8
+ localStorage.setItem(_k(key), JSON.stringify(obj));
9
+ return true;
10
+ } catch (e) {
11
+ return false;
12
+ }
13
+ }
14
+ function loadJSON(key, fallback = null) {
15
+ try {
16
+ const s = localStorage.getItem(_k(key));
17
+ return s ? JSON.parse(s) : fallback;
18
+ } catch (e) {
19
+ return fallback;
20
+ }
21
+ }
22
+ function remove(key) {
23
+ try {
24
+ localStorage.removeItem(_k(key));
25
+ } catch (e) {
26
+ /* ignore */
27
+ }
28
+ }
29
+ // Canonical names match docs (saveData/loadData) — saveJSON/loadJSON kept as aliases
30
+ const saveData = saveJSON;
31
+ const loadData = loadJSON;
32
+ const deleteData = remove;
33
+ return {
34
+ exposeTo(target) {
35
+ Object.assign(target, { saveData, loadData, deleteData, saveJSON, loadJSON, remove });
36
+ },
37
+ };
38
+ }