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.
- package/dist/examples/strider-demo-3d/fix-game.sh +0 -0
- 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
|
@@ -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
|
+
}
|