@moonbase-machines/player 0.10.0 → 0.12.0
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/README.md +23 -8
- package/bin/moonbase-play.mjs +18 -7
- package/disk/disk.js +5 -7
- package/dist/moonbase-player.js +16 -16
- package/dist/moonbase-player.js.map +4 -4
- package/engines/original.js +10 -8
- package/engines/scenes.js +29 -31
- package/index.js +2 -0
- package/package.json +6 -6
- package/play.js +59 -50
- package/provider.js +27 -0
- package/remake/README.md +138 -0
- package/settings.js +82 -0
- package/test/play.test.js +76 -2
package/engines/original.js
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
// and `studies`, files of its own for a Commodore 64 game's disk, a name to its bytes in base64, in the drive, not kept.
|
|
11
11
|
import { Speaker } from '../audio.js';
|
|
12
12
|
|
|
13
|
+
// the setting the Apple's engine acts on (play.js `setting`): its pictures drawn as vectors over the screen
|
|
14
|
+
const VECTORS = { name: 'vectors', type: 'boolean', default: false, owner: 'person', words: 'Draw the pictures as vectors' };
|
|
15
|
+
|
|
13
16
|
export async function play(g) {
|
|
14
17
|
g.stage.innerHTML = '<svg id="vector" viewBox="0 0 140 192" preserveAspectRatio="none"></svg><canvas id="screen" width="560" height="384" tabindex="0"></canvas>';
|
|
15
18
|
const canvas = g.stage.querySelector('canvas'), ctx = canvas.getContext('2d'), layer = g.stage.querySelector('#vector');
|
|
@@ -19,12 +22,11 @@ export async function play(g) {
|
|
|
19
22
|
// An Apple II game: the machine, the canvas, the keys, and the hooks for the vectors. With Apple's
|
|
20
23
|
// ROMs and the game's disk images, where the page gives them (`roms`), it is line 1, the Apple itself booting the
|
|
21
24
|
// disks; without them, moonbase's (line 2), the game started at once.
|
|
22
|
-
async function apple({ game, saves, roms: makers, status,
|
|
25
|
+
async function apple({ game, saves, roms: makers, status, setting }, { canvas, ctx, layer, speaker }) {
|
|
23
26
|
const [{ Machine }, { placed }, { screen }, { shapes }, { elements }] = await Promise.all([
|
|
24
27
|
import('@moonbase-machines/apple2e/machine.js'), import('@moonbase-machines/apple2e/prodos.js'),
|
|
25
28
|
import('@moonbase-machines/apple2e/screen.js'), import('@moonbase-machines/vector/pictures.js'), import('@moonbase-machines/vector/svg.js')]);
|
|
26
|
-
|
|
27
|
-
const vectorToggle = $('vectors');
|
|
29
|
+
const vectors = setting(VECTORS);
|
|
28
30
|
// the title's saves on its own volume, the player's disk's (play.js `saves`): on line 2 the machine's files; on line 1
|
|
29
31
|
// written onto that volume's disk before it boots, and kept as the title's DOS writes it
|
|
30
32
|
const saving = saves.data;
|
|
@@ -73,9 +75,9 @@ function frame() {
|
|
|
73
75
|
speaker.feed(sound ? m.clicks : [], m.cpu.cycles); m.clicks.length = 0;
|
|
74
76
|
if (!hooked) hook();
|
|
75
77
|
const text = m.sw.text;
|
|
76
|
-
screen(m, rgba, { cols, owner:
|
|
78
|
+
screen(m, rgba, { cols, owner: vectors.value && layer.innerHTML ? m.owner : null });
|
|
77
79
|
ctx.putImageData(image, 0, 0);
|
|
78
|
-
layer.style.visibility =
|
|
80
|
+
layer.style.visibility = vectors.value && !text ? 'visible' : 'hidden';
|
|
79
81
|
status.textContent = m.halted ? 'The game has stopped: ' + m.halted + '.' : (m.idle > 2000 ? 'Waiting for a key.' : 'Running.');
|
|
80
82
|
requestAnimationFrame(frame);
|
|
81
83
|
}
|
|
@@ -86,7 +88,7 @@ return {
|
|
|
86
88
|
run() { canvas.focus(); frame(); },
|
|
87
89
|
press(k) { console.log('key', k, 'at pc $' + m.cpu.pc.toString(16)); m.press(k); },
|
|
88
90
|
start() { speaker.start(); },
|
|
89
|
-
sound(on) { sound = on; },
|
|
91
|
+
sound(on) { sound = on; },
|
|
90
92
|
engine: m,
|
|
91
93
|
};
|
|
92
94
|
}
|
|
@@ -94,7 +96,7 @@ return {
|
|
|
94
96
|
// A C64 game: on moonbase's own ROMs (@moonbase-machines/c64/rom.js), as the site has it everywhere, or on
|
|
95
97
|
// Commodore's where the page gives them (`roms`) and asks for them; the machine autostarted
|
|
96
98
|
// with LOAD"name",8,1, and RUN for a BASIC program, typed into the KERNAL's buffer, the frame on a canvas of
|
|
97
|
-
// 384 by 272 doubled, the keys as the matrix
|
|
99
|
+
// 384 by 272 doubled, the keys as the matrix, the arrows its cursor keys, and a gamepad the joystick.
|
|
98
100
|
async function c64(g, { canvas, layer, speaker }) {
|
|
99
101
|
const { game, saves, roms: makers, studies, status, printer } = g;
|
|
100
102
|
const [{ C64 }, { keyboard, painter }, { SID }, { c64Roms, typed: startTyped }, { add }] = await Promise.all([
|
|
@@ -144,7 +146,7 @@ async function c64(g, { canvas, layer, speaker }) {
|
|
|
144
146
|
press(k) { m.type(String.fromCharCode(k)); }, // (into the KERNAL's buffer, as typed)
|
|
145
147
|
key(e) { for (const f of listeners[e.type]) f(e); },
|
|
146
148
|
start() { speaker.start(); },
|
|
147
|
-
sound(on) { sound = on; },
|
|
149
|
+
sound(on) { sound = on; },
|
|
148
150
|
engine: m,
|
|
149
151
|
// the disk as it now stands, its own files and those saved: a .d64 that VICE or a real 64 can use
|
|
150
152
|
// (@moonbase-machines/c64/d64.js `add`)
|
package/engines/scenes.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// The Remake's engine for a title of scenes, 'card' or 'vector' (play.js hands it the game): its scripts on the
|
|
2
2
|
// remake's engine (remake/engine.js), its events drawn by the view on the stage, the view the machine's (an Apple's
|
|
3
|
-
// hi-res page for a card title, CardView; a Commodore 64's, C64View; a vector title's pictures and text, View);
|
|
4
|
-
//
|
|
5
|
-
// for it. A run's events are drawn one after another, with a moment for each sound and flash, so that what the
|
|
3
|
+
// hi-res page for a card title, CardView; a Commodore 64's, C64View; a vector title's pictures and text, View); the
|
|
4
|
+
// settings it acts on (play.js `game.setting`): the old style text, where its view draws text of its own (View); the
|
|
5
|
+
// mini view, where the page gives one and the title has a place for it. A run's events are drawn one after another, with a moment for each sound and flash, so that what the
|
|
6
6
|
// original took time over takes time here too.
|
|
7
7
|
import { makeEngine } from '../remake/load.js';
|
|
8
|
+
|
|
9
|
+
// the settings this engine acts on (play.js `setting`): the old style text, a vector title's; the mini view
|
|
10
|
+
const OLD_STYLE = { name: 'old-style', type: 'boolean', default: false, owner: 'person', words: 'Old style text' };
|
|
11
|
+
const MINI = { name: 'mini', type: ['', 'live', 'pictures'], default: '', owner: 'person', words: 'Mini view', labels: ['Off', 'Live', 'Pictures'] };
|
|
8
12
|
import { Speaker } from '../audio.js';
|
|
9
13
|
import { SID } from '@moonbase-machines/c64/sid.js';
|
|
10
14
|
import { matrixOf } from '@moonbase-machines/c64/kernal.js';
|
|
@@ -18,8 +22,7 @@ export async function play(g) {
|
|
|
18
22
|
const { title, scripts, game: disk, stage, status, fail, saves, $ } = g;
|
|
19
23
|
const c64 = disk.kind === 'c64';
|
|
20
24
|
stage.innerHTML = `<svg id="screen" preserveAspectRatio="xMidYMid meet" tabindex="0" aria-label="The game's screen"></svg>`;
|
|
21
|
-
|
|
22
|
-
const svg = $('screen');
|
|
25
|
+
const svg = $('screen'), drawn = c64 ? 'c64' : title.engine === 'card' ? 'card' : 'vector';
|
|
23
26
|
|
|
24
27
|
// its view's own drawing, for a title that has one (its title.js `view`, load.js)
|
|
25
28
|
const extra = title.viewSteps ?? {};
|
|
@@ -35,52 +38,49 @@ export async function play(g) {
|
|
|
35
38
|
const engine = makeEngine(title, scripts);
|
|
36
39
|
if (title.files) saves.keep(title.files);
|
|
37
40
|
const queue = [];
|
|
38
|
-
// The view, and every event it has drawn, so that it can be drawn again from the start when
|
|
39
|
-
// the
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
41
|
+
// The view, and every event it has drawn, so that it can be drawn again from the start when the old style text is
|
|
42
|
+
// turned on or off: a setting only where the view draws text of its own, a vector title's (View), the others drawing
|
|
43
|
+
// the machine's own screen
|
|
44
|
+
const history = [];
|
|
45
|
+
const oldStyle = drawn === 'vector' ? g.setting(OLD_STYLE) : { value: false, on() { return this; } };
|
|
43
46
|
let onView = null; // (the mini view's, given each view made)
|
|
44
|
-
const makeView = () => { svg.replaceChildren(); const v = c64 ? new C64View(svg, { engine, chargen }) :
|
|
47
|
+
const makeView = () => { svg.replaceChildren(); const v = drawn === 'c64' ? new C64View(svg, { engine, chargen }) : drawn === 'card' ? new CardView(svg, { engine, inset: g.mini ? title.inset : null }) : new View(svg, { disk, volume: Object.keys(disk.volumes)[0], engine, extra }); v.oldStyle = oldStyle.value; onView?.(v); return v; };
|
|
45
48
|
let view = makeView();
|
|
46
49
|
engine.on(e => queue.push(view.snapshot({ ...e })));
|
|
47
50
|
// The mini view, where the page gives one (`mini`) and the title has a place for it (its title.js `inset`): Off,
|
|
48
|
-
// Live or Pictures,
|
|
49
|
-
//
|
|
50
|
-
const miniBox = $('mini');
|
|
51
|
+
// Live or Pictures, a setting; shown in the band at the top of the screen while the band is there. A picture only:
|
|
52
|
+
// the game's screen otherwise as it is
|
|
51
53
|
if (title.inset && g.mini) {
|
|
52
|
-
|
|
53
|
-
try { miniBox.value = localStorage.getItem('remake-mini') ?? ''; } catch {}
|
|
54
|
+
const choice = g.setting(MINI);
|
|
54
55
|
const { top, bottom, view: [left, right] } = title.inset, at = document.createElement('div');
|
|
55
56
|
at.className = 'mini'; at.hidden = true;
|
|
56
57
|
Object.assign(at.style, { left: `${left / 5.6}%`, top: `${top / 3.84}%`, width: `${(right - left) / 5.6}%`, height: `${(bottom - top + 1) / 3.84}%` });
|
|
57
58
|
svg.parentElement.append(at);
|
|
58
59
|
const mini = await g.mini(at, { memory: () => engine.mem });
|
|
59
60
|
// (each view's painting followed by the mini view shown or not, as the band is there or not)
|
|
60
|
-
onView = v => { v.insetOn = !!
|
|
61
|
+
onView = v => { v.insetOn = !!choice.value; const prompts = v.prompts.bind(v); v.prompts = () => { prompts(); at.hidden = !(choice.value && v.banded); }; };
|
|
61
62
|
onView(view);
|
|
62
|
-
const show = () => { view.insetOn = !!
|
|
63
|
-
|
|
63
|
+
const show = () => { view.insetOn = !!choice.value; mini.mode(choice.value); view.prompts(); };
|
|
64
|
+
choice.on(show);
|
|
64
65
|
show();
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
-
remember.set(oldStyleBox.checked);
|
|
67
|
+
oldStyle.on(() => {
|
|
68
68
|
view = makeView();
|
|
69
69
|
for (const e of history) view.apply(e);
|
|
70
70
|
view.prompts();
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
// the sound: the original's
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
73
|
+
// the sound: a title that keeps the original's time (a card title's, Engine.time) clicks the speaker as the original
|
|
74
|
+
// did, and the clicks are played as the emulator plays them (audio.js); a vector title's tones play where its own
|
|
75
|
+
// sound flag is on, the teacher's setting in the game, as on the original. The player's Sound setting (play.js) is
|
|
76
|
+
// the speaker, on or off, for both.
|
|
77
77
|
const speaker = engine.timed ? new Speaker() : null;
|
|
78
|
-
let sounding =
|
|
78
|
+
let sounding = true;
|
|
79
79
|
const hear = () => { if (!c64) speaker?.feed(sounding ? engine.clicks : [], engine.cycles); engine.clicks.length = 0; };
|
|
80
80
|
const V = title.map?.vars ?? { sound: 0, soundOn: 0 };
|
|
81
81
|
let audio = null;
|
|
82
82
|
const tone = (from, to, ms) => {
|
|
83
|
-
if (!(engine.mem[V.sound] & V.soundOn)) return;
|
|
83
|
+
if (!sounding || !(engine.mem[V.sound] & V.soundOn)) return;
|
|
84
84
|
audio ??= new AudioContext();
|
|
85
85
|
const o = audio.createOscillator(), gain = audio.createGain(), t = audio.currentTime;
|
|
86
86
|
o.type = 'square'; o.frequency.setValueAtTime(from, t); o.frequency.linearRampToValueAtTime(to, t + ms / 1000);
|
|
@@ -193,16 +193,14 @@ export async function play(g) {
|
|
|
193
193
|
// pressed
|
|
194
194
|
keys(list) { if (c64) engine.clock.ahead.push(...list); else for (const k of list) press(k); },
|
|
195
195
|
start() { audio?.resume(); speaker?.start(); },
|
|
196
|
-
|
|
197
|
-
sound(on) { if (speaker) sounding = on; else engine.mem[V.sound] = on ? V.soundOn : 0; },
|
|
198
|
-
soundOn: !!speaker,
|
|
196
|
+
sound(on) { sounding = on; }, // (the player's speaker: play.js)
|
|
199
197
|
quick(on) { quick = on; },
|
|
200
198
|
// A route the page gives (`route`, a workshop's pilots' route), played first as the pilots play it, each key when
|
|
201
199
|
// the game waits for one, off the clock, so that it goes where it went on both sides; then the game goes on as
|
|
202
200
|
// usual
|
|
203
201
|
route({ route, name, keys: { expand, give, petscii, pollsAt } }) {
|
|
204
202
|
title.seed = route.seed; // (the random numbers from the route's seed, as the pilots have them)
|
|
205
|
-
if (route.printer && title.printer) { title.printer.on = true; $('printer').checked = true; }
|
|
203
|
+
if (route.printer && title.printer) { title.printer.on = true; const box = $('printer'); if (box) box.checked = true; }
|
|
206
204
|
status.textContent = 'Playing the route ' + name + '.';
|
|
207
205
|
// (on a Commodore 64 a key behind the one pressed goes into the KERNAL's buffer, as the pilots hold a key)
|
|
208
206
|
const side = { press: k => engine.press(k), also: k => { const m = engine.mem, put = v => { if (m[0xc6] < 10) { m[0x277 + m[0xc6]] = v; m[0xc6]++; } }; if (engine.keys.length) put(engine.keys.shift()); put(k); } };
|
package/index.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
export * as play from './play.js';
|
|
6
6
|
export * as keys from './keys.js';
|
|
7
7
|
export * as saves from './saves.js';
|
|
8
|
+
export * as settings from './settings.js';
|
|
9
|
+
export * as provider from './provider.js';
|
|
8
10
|
export * as enginesOriginal from './engines/original.js';
|
|
9
11
|
export * as enginesScenes from './engines/scenes.js';
|
|
10
12
|
export * as audio from './audio.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonbase-machines/player",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "The player for Mark Wharton's titles of 1985 to 1995: the Original, a game's own disk on Moonbase's machines, and the Remake, its scenes on an engine; the player's own disk of saves in the browser.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
"moonbase-play": "./bin/moonbase-play.mjs"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@moonbase-machines/apple2e": "0.
|
|
18
|
-
"@moonbase-machines/c64": "0.
|
|
19
|
-
"@moonbase-machines/charset": "0.
|
|
20
|
-
"@moonbase-machines/mos6502": "0.
|
|
21
|
-
"@moonbase-machines/vector": "0.
|
|
17
|
+
"@moonbase-machines/apple2e": "0.12.0",
|
|
18
|
+
"@moonbase-machines/c64": "0.12.0",
|
|
19
|
+
"@moonbase-machines/charset": "0.12.0",
|
|
20
|
+
"@moonbase-machines/mos6502": "0.12.0",
|
|
21
|
+
"@moonbase-machines/vector": "0.12.0"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"test": "node --test test/*.test.js",
|
package/play.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// The player: one way in for every title and every way to play it, the Original (the game's own disk on the machine
|
|
2
2
|
// it was made for) and the Remake (its scenes on the remake's engine, or the title's own engine). The player makes its
|
|
3
3
|
// part of the page and owns it, the same for all: the section and its classes, the stage and its size, the options
|
|
4
|
-
// line and the status, the keys, the sound, the printer, the saves on the
|
|
5
|
-
// the engine plays the game on the stage and does nothing of the page's.
|
|
4
|
+
// line and the status, the keys, the sound, the printer, the saves on the title's disk, and what the page asks of it;
|
|
5
|
+
// the engine plays the game on the stage and does nothing of the page's. The page tells the player everything: the
|
|
6
|
+
// disk and the settings are the page's to give (its provider, provider.js), and the player reads no address and
|
|
7
|
+
// keeps nothing in the browser itself.
|
|
6
8
|
//
|
|
7
|
-
// await play({ way, slug, title, data, root, disk,
|
|
8
|
-
// roms, studies, downloadDisk })
|
|
9
|
+
// await play({ way, slug, title, data, root, disk, settings, preview, words, screen, watch, claim, pace, keys,
|
|
10
|
+
// route, mini, roms, studies, downloadDisk })
|
|
9
11
|
// way 'original' or 'remake' (the default)
|
|
10
12
|
// slug the title's name in addresses: its section's class, its disk
|
|
11
13
|
// title the title's folder, an absolute URL (title.js, its scenes, compiled/, its own modules), where it
|
|
@@ -13,8 +15,8 @@
|
|
|
13
15
|
// data its game's data, an absolute URL (<name>.json: its disks' files, its kind, its saves), or for a
|
|
14
16
|
// title of its own engine the folder its data is in
|
|
15
17
|
// root the page's section the player is made in
|
|
16
|
-
// disk the title's
|
|
17
|
-
//
|
|
18
|
+
// disk the title's disk (disk/disk.js `openDisk`, `title/<slug>`), the page's to open where it keeps files
|
|
19
|
+
// settings the person's settings, the page's typed store (settings.js): each declared setting's value, kept
|
|
18
20
|
// preview the preview's build: what only it has may be used
|
|
19
21
|
// words the page's words the player shows: { printer: { line, print, tear } }
|
|
20
22
|
// screen [width, height], the screen's shape in its own dots where the page draws its own over it
|
|
@@ -25,6 +27,7 @@
|
|
|
25
27
|
// pace how much of the original's waits: 1 all (the default), 0 none, between a part of each; a number or
|
|
26
28
|
// a function, read at each wait
|
|
27
29
|
// and what only a workshop has, given by its pages, never looked for here:
|
|
30
|
+
// keys keys to press at the start, by name (space, return, left...), to go straight to a scene
|
|
28
31
|
// route a route to play first, { name, route, keys }: its keys, and the pilots' key handling
|
|
29
32
|
// mini a mini view for a title with an `inset`, (at, { memory }) => { mode(m) }
|
|
30
33
|
// roms the makers' ROMs this machine has (engines/original.js; a Commodore 64 remake's { chargen })
|
|
@@ -32,17 +35,18 @@
|
|
|
32
35
|
// downloadDisk a Commodore 64 game's disk offered to download, with what is saved on it
|
|
33
36
|
//
|
|
34
37
|
// An engine (engines/original.js, engines/scenes.js, or the title's own, its title.js `engine` a module in its folder)
|
|
35
|
-
// exports `play(game)`: given `game`, the player's own for it (below), it
|
|
38
|
+
// exports `play(game)`: given `game`, the player's own for it (below), it declares the settings it acts on
|
|
39
|
+
// (`game.setting(declaration)`, settings.js, each shown only so), makes what it draws on the stage and returns
|
|
36
40
|
// its handle, or nothing where it cannot start (and has said why in the status):
|
|
37
|
-
// { run(), press(k)?, key(e, k)?, keys(list)?, route(route)?, start()?, sound(on)?,
|
|
41
|
+
// { run(), press(k)?, key(e, k)?, keys(list)?, route(route)?, start()?, sound(on)?, quick(on)?,
|
|
38
42
|
// engine?, diskImage()? }
|
|
39
43
|
// run() starts it; press(k) a key, the machine's code (none for a title of its own engine with no machine's keys,
|
|
40
44
|
// which reads the keyboard itself); key(e, k), for an engine that reads the keyboard itself, each
|
|
41
|
-
// keydown and keyup with its code (undefined where the machine has none); keys(list),
|
|
42
|
-
// page's route; start(), at the player's first gesture (sound may start only then); sound(on)
|
|
43
|
-
//
|
|
45
|
+
// keydown and keyup with its code (undefined where the machine has none); keys(list), the page's `keys` at the start;
|
|
46
|
+
// route, the page's route; start(), at the player's first gesture (sound may start only then); sound(on), the
|
|
47
|
+
// player's speaker on or off (the Sound setting; a game's own sound setting stays the game's); quick(on) the game at
|
|
48
|
+
// full speed; engine, what `watch` gives a page; diskImage(), the disk to download.
|
|
44
49
|
import { titleOf, loadTitle, read } from './remake/load.js';
|
|
45
|
-
import { openDisk, titleDisk } from './disk/disk.js';
|
|
46
50
|
import { savesFor } from './saves.js';
|
|
47
51
|
import { code, named } from './keys.js';
|
|
48
52
|
import { paperBox, phoneKeys, KEYS_MARKUP } from './parts.js';
|
|
@@ -50,24 +54,44 @@ import { paperBox, phoneKeys, KEYS_MARKUP } from './parts.js';
|
|
|
50
54
|
// each machine's screen, in its own dots: an Apple's hi-res page, a Commodore 64's frame, border and all
|
|
51
55
|
const SCREENS = { prodos: [280, 192], dos33: [280, 192], c64: [384, 272] };
|
|
52
56
|
const ENGINES = { original: () => import('./engines/original.js'), card: () => import('./engines/scenes.js'), vector: () => import('./engines/scenes.js') };
|
|
57
|
+
// the settings the player itself acts on (settings.js): the speaker; a Commodore 64's printer; a title's version with
|
|
58
|
+
// its bugs fixed
|
|
59
|
+
const SOUND = { name: 'sound', type: 'boolean', default: true, owner: 'person', words: 'Sound' };
|
|
60
|
+
const PRINTER = { name: 'printer', type: 'boolean', default: true, owner: 'person', words: 'Printer' };
|
|
61
|
+
const FIXED = { name: 'fixed', type: 'boolean', default: false, owner: 'person', words: 'Bugs fixed', title: 'The game with its bugs fixed: it starts again' };
|
|
53
62
|
|
|
54
63
|
export async function play(options) {
|
|
55
|
-
const { way = 'remake', slug, root, preview = false, words } = options;
|
|
64
|
+
const { way = 'remake', slug, root, disk, settings, preview = false, words } = options;
|
|
56
65
|
root.classList.add('player', way);
|
|
57
66
|
if (slug) root.classList.add(slug);
|
|
58
|
-
root.innerHTML = `<div class="stage"></div><p class="options"><span id="status">Loading.</span>${KEYS_MARKUP}
|
|
59
|
-
+ '<label hidden><input type="checkbox" id="sound" checked> Sound</label>'
|
|
60
|
-
+ '<label hidden><input type="checkbox" id="printer" checked> Printer</label>'
|
|
61
|
-
+ '<label hidden title="The game with its bugs fixed: it starts again"><input type="checkbox" id="fixed"> Bugs fixed</label>'
|
|
62
|
-
+ '<label hidden>Mini view <select id="mini"><option value="">Off</option><option value="live">Live</option><option value="pictures">Pictures</option></select></label>'
|
|
67
|
+
root.innerHTML = `<div class="stage"></div><p class="options"><span id="status">Loading.</span>${KEYS_MARKUP}<span class="settings"></span>`
|
|
63
68
|
+ '<button id="disk" type="button" hidden>Download the disk</button></p>';
|
|
64
69
|
root.append(paperBox(words.printer));
|
|
65
70
|
const $ = id => root.querySelector('#' + id);
|
|
66
|
-
const stage = root.querySelector('.stage'), status = $('status'),
|
|
71
|
+
const stage = root.querySelector('.stage'), status = $('status'), line = root.querySelector('.options .settings');
|
|
67
72
|
const fail = err => { status.textContent = 'The game stopped: ' + err.message; status.className = 'error'; console.error(err); };
|
|
73
|
+
// A setting the player or its engine acts on, by its declaration (settings.js): its value from the page's store,
|
|
74
|
+
// and its control on the options line where the page offers it, a box for a boolean, a menu for an enum, each
|
|
75
|
+
// change kept and told; `first`, before the others (the sound)
|
|
76
|
+
const setting = (def, { first = false } = {}) => {
|
|
77
|
+
const s = { value: settings.get(def), told: [], on(f) { this.told.push(f); return this; } };
|
|
78
|
+
if (!settings.offered(def)) return s;
|
|
79
|
+
const label = document.createElement('label'), choice = Array.isArray(def.type);
|
|
80
|
+
if (def.title) label.title = def.title;
|
|
81
|
+
const control = choice
|
|
82
|
+
? Object.assign(document.createElement('select'), { innerHTML: def.type.map((v, i) => `<option value="${v}">${def.labels[i]}</option>`).join('') })
|
|
83
|
+
: Object.assign(document.createElement('input'), { type: 'checkbox' });
|
|
84
|
+
control.id = def.name;
|
|
85
|
+
if (choice) { control.value = s.value; label.append(def.words + ' ', control); } else { control.checked = s.value; label.append(control, ' ' + def.words); }
|
|
86
|
+
control.addEventListener('change', () => { s.value = choice ? control.value : control.checked; settings.set(def, s.value); for (const f of s.told) f(s.value); });
|
|
87
|
+
line[first ? 'prepend' : 'append'](label);
|
|
88
|
+
return s;
|
|
89
|
+
};
|
|
68
90
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
91
|
+
if (!disk || !settings) throw new Error('the page gives the player its disk and its settings');
|
|
92
|
+
// the title with its bugs fixed, for one that has such a version (its title.js `fixed`), a setting: its scripts
|
|
93
|
+
// chosen as it loads, so a change loads the page again
|
|
94
|
+
const fixed = settings.get(FIXED);
|
|
71
95
|
const folder = options.title ? new URL(options.title) : null, data = new URL(options.data);
|
|
72
96
|
// the title, and for its remake its scripts; the game's data (none here for a title of its own engine, which
|
|
73
97
|
// reads its own from its folder)
|
|
@@ -82,52 +106,39 @@ export async function play(options) {
|
|
|
82
106
|
link.dataset.title = title.slug; document.head.append(link);
|
|
83
107
|
await new Promise(done => { link.onload = link.onerror = done; });
|
|
84
108
|
}
|
|
85
|
-
const disk = options.disk ?? await openDisk(titleDisk(slug), { backend: options.backend });
|
|
86
109
|
// (a save the disk's backend could not store, said, not lost)
|
|
87
110
|
disk.failed = err => { status.textContent = 'A file was not kept: ' + err.message; status.className = 'error'; };
|
|
88
111
|
const [dots, lines] = options.screen ?? title?.screen ?? SCREENS[kind] ?? SCREENS.prodos;
|
|
89
112
|
fit(stage, root, dots, lines);
|
|
90
113
|
|
|
91
|
-
// a printer on device 4, for a Commodore 64 game: on unless its box is off (the choice kept in this browser),
|
|
92
|
-
// what it prints drawn below the screen as a sheet of paper, until it is torn off
|
|
93
|
-
let printer = null;
|
|
94
114
|
const g = {
|
|
95
|
-
way, slug, title, scripts, game, folder, data, stage, status, fail, disk, preview, words, fixed, $,
|
|
115
|
+
way, slug, title, scripts, game, folder, data, stage, status, fail, disk, preview, words, fixed, $, setting,
|
|
96
116
|
saves: game ? savesFor(game, disk) : null, roms: options.roms, studies: options.studies, mini: options.mini ?? null,
|
|
97
117
|
chargen: null, // (the engine's Commodore 64 characters, for the printer's paper)
|
|
98
118
|
pace: () => { const p = options.pace; return typeof p === 'function' ? p() : p ?? 1; },
|
|
99
|
-
// an engine's own
|
|
100
|
-
control(html) {
|
|
101
|
-
// the engine's sound turned on or off by the game itself (Aztlan's Game > Sounds): the box with it
|
|
102
|
-
sounding(on) { soundBox.checked = on; },
|
|
119
|
+
// an engine's own part of the options line that is no setting (Aztlan's preview selector), after the settings
|
|
120
|
+
control(html) { line.insertAdjacentHTML('afterend', html); },
|
|
103
121
|
};
|
|
122
|
+
// a printer on device 4, for a Commodore 64 game: on unless the setting is off, what it prints drawn below the
|
|
123
|
+
// screen as a sheet of paper, until it is torn off
|
|
104
124
|
if (kind === 'c64') {
|
|
105
125
|
const { Printer, paper } = await import('@moonbase-machines/c64/printer.js');
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
box.addEventListener('change', () => { printer.on = box.checked; try { localStorage.setItem('printer', box.checked ? '1' : '0'); } catch {} });
|
|
126
|
+
const on = setting(PRINTER);
|
|
127
|
+
const printer = g.printer = new Printer({ on: on.value });
|
|
128
|
+
on.on(v => { printer.on = v; });
|
|
129
|
+
const sheet = $('paper'), shown = $('paper-box');
|
|
111
130
|
printer.changed = () => { shown.hidden = !printer.lines.length; if (printer.lines.length) paper(sheet, printer, g.chargen); };
|
|
112
131
|
$('tear').addEventListener('click', () => printer.tear());
|
|
113
132
|
$('print-paper').addEventListener('click', () => window.print()); // (the sheet alone: its print style)
|
|
114
133
|
}
|
|
115
|
-
|
|
116
|
-
if (way === 'remake' && title?.fixed) {
|
|
117
|
-
const box = $('fixed');
|
|
118
|
-
box.checked = fixed; box.parentElement.hidden = false;
|
|
119
|
-
box.addEventListener('change', () => { const u = new URL(location.href); if (box.checked) u.searchParams.set('fixed', ''); else u.searchParams.delete('fixed'); location.href = u.href.replace('fixed=&', 'fixed&').replace(/fixed=$/, 'fixed'); });
|
|
120
|
-
}
|
|
134
|
+
if (way === 'remake' && title?.fixed) setting(FIXED).on(() => location.reload());
|
|
121
135
|
|
|
122
136
|
const engine = way === 'original' ? await ENGINES.original() : title.own ?? await ENGINES[title.engine]();
|
|
123
137
|
const handle = await engine.play(g);
|
|
124
138
|
if (!handle) return;
|
|
125
139
|
|
|
126
|
-
// the sound, where the engine has it: the
|
|
127
|
-
if (handle.sound) {
|
|
128
|
-
soundBox.parentElement.hidden = false; soundBox.checked = handle.soundOn ?? true;
|
|
129
|
-
soundBox.addEventListener('change', () => handle.sound(soundBox.checked));
|
|
130
|
-
}
|
|
140
|
+
// the sound, where the engine has it: the player's speaker, on or off
|
|
141
|
+
if (handle.sound) { const on = setting(SOUND, { first: true }); handle.sound(on.value); on.on(v => handle.sound(v)); }
|
|
131
142
|
// the keys, one listener for every engine: the machine's code for each (keys.js), offered to the page first
|
|
132
143
|
// (`claim`), then the engine's: the event itself where it reads the keyboard, else the code pressed
|
|
133
144
|
const keyed = e => {
|
|
@@ -153,15 +164,13 @@ export async function play(options) {
|
|
|
153
164
|
}
|
|
154
165
|
// a page that follows the game: its engine, a key pressed as the player presses one, the game at full speed
|
|
155
166
|
options.watch?.({ engine: handle.engine, press: k => { handle.start?.(); handle.press?.(k); }, quick: on => handle.quick?.(on) });
|
|
156
|
-
// a route the page gives, played first; then the game; then
|
|
157
|
-
// scene
|
|
167
|
+
// a route the page gives, played first; then the game; then the page's keys, to go straight to a scene
|
|
158
168
|
if (options.route) {
|
|
159
169
|
if (!handle.route) throw new Error('a route cannot be played on the ' + way);
|
|
160
170
|
await handle.route(options.route);
|
|
161
171
|
}
|
|
162
172
|
handle.run();
|
|
163
|
-
const
|
|
164
|
-
if (start) { const list = start.split(',').map(n => named(n, kind)); if (handle.keys) handle.keys(list); else for (const k of list) handle.press?.(k); }
|
|
173
|
+
if (options.keys?.length) { const list = options.keys.map(n => named(n, kind)); if (handle.keys) handle.keys(list); else for (const k of list) handle.press?.(k); }
|
|
165
174
|
} catch (err) { fail(err); }
|
|
166
175
|
}
|
|
167
176
|
|
package/provider.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The provider: where a visit keeps a person's files and settings, chosen once, and every page's disks and settings
|
|
2
|
+
// from it. A site makes one a page from a module of its own, choosing the backend (this browser's; an account's, the
|
|
3
|
+
// site's API's, when signed in) and giving a visit's values and its policy; a page with no site (moonbase-play's) makes
|
|
4
|
+
// this one with nothing given. The player and the programs never choose: a page passes the title's disk and the
|
|
5
|
+
// settings to play() (play.js), and a program that saves opens its disks here. Nothing here knows of an address.
|
|
6
|
+
//
|
|
7
|
+
// const { disk, settings } = await provider({ backend, visit, fixed, hidden })
|
|
8
|
+
// backend where files are kept (disk/disk.js says what one is); none given, this browser's, IndexedDB
|
|
9
|
+
// (disk/local.js), or where the browser keeps nothing (a private window may not) memory, for this
|
|
10
|
+
// visit only, and the disks say so (`kept`)
|
|
11
|
+
// visit def => a setting's value for this visit, the site's to give (settings.js); fixed and hidden, the
|
|
12
|
+
// page's policy of the settings
|
|
13
|
+
// await disk(name) a disk by its name, on the backend: titleDisk(slug), programDisk(name) (disk/disk.js)
|
|
14
|
+
// settings the person's settings, the typed store (settings.js), on the disk settingsDisk('person')
|
|
15
|
+
import { openDisk } from './disk/disk.js';
|
|
16
|
+
import { local } from './disk/local.js';
|
|
17
|
+
import { memory } from './disk/memory.js';
|
|
18
|
+
import { settingsStore } from './settings.js';
|
|
19
|
+
|
|
20
|
+
export async function provider({ backend, visit, fixed = {}, hidden = [] } = {}) {
|
|
21
|
+
if (!backend) { try { backend = await local(); } catch { backend = memory({ kept: false }); } }
|
|
22
|
+
const disk = name => openDisk(name, { backend });
|
|
23
|
+
const settings = settingsStore({ disks: { person: await disk(settingsDisk('person')) }, visit, fixed, hidden });
|
|
24
|
+
return { disk, settings };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const settingsDisk = owner => `settings/${owner}`;
|
package/remake/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# The remake engine
|
|
2
|
+
|
|
3
|
+
A remake is not a game written again in JavaScript. It is the title's own code, lifted from the original into scene
|
|
4
|
+
scripts, compiled to JavaScript and run on this engine. The engine stands in for the machine and for the routines the
|
|
5
|
+
title called. The logic is the original's, statement for statement, so the remake does what the original did, and the
|
|
6
|
+
pilots can hold the two side by side, event by event.
|
|
7
|
+
|
|
8
|
+
A title of its own engine is the exception: its `title.js` names a module in its folder as its `engine`, and that
|
|
9
|
+
module plays the game itself (below).
|
|
10
|
+
|
|
11
|
+
## A title's folder
|
|
12
|
+
|
|
13
|
+
The page gives the player the title's folder (`../README.md` says what is in it). For a remake on this engine it holds:
|
|
14
|
+
|
|
15
|
+
- `title.js`: the engine, `'card'` or `'vector'`; the scripts and the one it starts with; the files that load as
|
|
16
|
+
scripts (`fileScripts`); the memory at the start; the entries other scripts jump into; and the `map`, the routines
|
|
17
|
+
the scenes call, by address, each with the name of its step.
|
|
18
|
+
- `<name>.scene`: the scripts, as lifted and as edited after. They are the source.
|
|
19
|
+
- `compiled/<name>.js`: each scene compiled by `moonbase-compile`, which the remake runs. These are always compiled
|
|
20
|
+
again from the scene and never edited.
|
|
21
|
+
- the title's own modules, where it has them: its problems (`modules`) and its view's drawing (`view`).
|
|
22
|
+
|
|
23
|
+
## A scene script
|
|
24
|
+
|
|
25
|
+
A scene is a step or a statement to a line (`script.js`), each line with the address in the original it came from:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
L0813: a label
|
|
29
|
+
0813 if a != 127 goto L0825 a branch on a compare, or on a flag: if zero goto L0825
|
|
30
|
+
0817 call L08BF a call into the script's own code
|
|
31
|
+
081C char 32 a step: one of the title's routines, with its inputs
|
|
32
|
+
0825 x = 0 a statement on the registers a, x and y, the flags, the stack and memory
|
|
33
|
+
data $08D7 00 00 80 80 bytes of the original's tables, at their addresses
|
|
34
|
+
; a comment
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
A statement is the original's own instruction, written so it can be read. The engine runs it on three registers,
|
|
38
|
+
four flags, a stack and 64K of memory, as the 6502 did. A step is a call into one of the title's routines, which the
|
|
39
|
+
title's `map` names by address (`0x0800: 'char'`). The engine does the routine's work in JavaScript and emits an
|
|
40
|
+
event. Everything a scene does is an event: the view draws the events, and a pilot reads them as the transcript to
|
|
41
|
+
hold against the original's.
|
|
42
|
+
|
|
43
|
+
### Code that rewrites itself
|
|
44
|
+
|
|
45
|
+
A scene is the code as text, lifted once, so it can't follow code that changes its own instructions. The 6502 does
|
|
46
|
+
follow it, because it reads each instruction's bytes from memory as it runs. A routine that keeps a pointer in the
|
|
47
|
+
address bytes of its own `LDA` and moves the pointer by incrementing them would be lifted as a read of one fixed
|
|
48
|
+
address. Microsoft's CHRGET in 6502 BASIC is the best-known case. Such a line needs either a step, the routine
|
|
49
|
+
answered in JavaScript, or a hand edit to read through the pointer instead (`a = [$007A]`, the byte at the address
|
|
50
|
+
held at `$7A` and `$7B`), with the reason beside it.
|
|
51
|
+
|
|
52
|
+
## Compiled to JavaScript
|
|
53
|
+
|
|
54
|
+
`moonbase-compile <title folder>` writes each scene as an ES module in `compiled/` (`compile.js`). The module's
|
|
55
|
+
default export is `{ labels, data, steps, run }`:
|
|
56
|
+
|
|
57
|
+
- `labels`, each label's step;
|
|
58
|
+
- `data`, the bytes the script puts in memory;
|
|
59
|
+
- `steps`, each step's address, its line in the scene and its text;
|
|
60
|
+
- `run`, a function for each block of the script, by the step it starts at.
|
|
61
|
+
|
|
62
|
+
A run of statements that only computes is one function, a loop where it branches back to its own start, each line
|
|
63
|
+
with the scene's line beside it as a comment. Anything that can stop the run stays a step of its own, so the title
|
|
64
|
+
waits where it waited: a routine's step, and a read or write that may reach the I/O page. A jump, a call or a return
|
|
65
|
+
ends a function.
|
|
66
|
+
|
|
67
|
+
The player loads the compiled modules with `import()` (`load.js` `loadTitle`). There is no JSON in between. The JSON
|
|
68
|
+
beside the title, `<name>.json`, is the game's data: its disks' files, its kind and where it saves. `loadTitle` with
|
|
69
|
+
`scenes` reads the `.scene` files instead and runs them line by line, which gives the same results more slowly.
|
|
70
|
+
|
|
71
|
+
## The two engines
|
|
72
|
+
|
|
73
|
+
The scripts are the same for every title. What differs is the routines their steps call, and there are two sets.
|
|
74
|
+
|
|
75
|
+
- **Card** (`card.js`, `cards.js`, `card-view.js`, `c64-view.js`): a title on the Apple IIe or the Commodore 64. A
|
|
76
|
+
toolkit's own code is lifted into scripts too, beside the scenes. What cannot be a script is JavaScript here: the
|
|
77
|
+
routines that take their data inline after the call (fill, copy, print), the Apple's ROM routines and ProDOS's
|
|
78
|
+
calls (`@moonbase-machines/apple2e`), the Commodore 64's KERNAL, I/O page, drive, printer and BASIC
|
|
79
|
+
(`@moonbase-machines/c64`), the keyboard and the soft switches. The screen is the machine's own, written byte for
|
|
80
|
+
byte as the original wrote it.
|
|
81
|
+
- **Vector** (`vector.js`, `layout.js`, `raster.js`, `problems.js`, `view.js`): a title on the vector engine. Its
|
|
82
|
+
routines (a text, a picture, a box, a wait, a key) are written again here by the original's rules for keys, random
|
|
83
|
+
numbers and waits. The view draws the pictures from their shapes as SVG (`@moonbase-machines/vector`), at any size.
|
|
84
|
+
A title's problems are its own module, rewritten in JavaScript as generators, run with the helpers in `problems.js`.
|
|
85
|
+
|
|
86
|
+
`load.js` makes the engine from the title and its scripts (`makeEngine`), with the routines of the title's `engine`.
|
|
87
|
+
`../engines/scenes.js` plays it on the page.
|
|
88
|
+
|
|
89
|
+
## The original's time
|
|
90
|
+
|
|
91
|
+
A title's code sits in the remake's memory where it came from, so the engine counts the time the original took
|
|
92
|
+
(`Engine.time`). Each line costs its instruction's cycles, read from the opcode at its address, and a branch taken
|
|
93
|
+
costs one more. A click of the speaker is kept at its cycle and played as the emulator plays it (`../audio.js`), so
|
|
94
|
+
the remake runs at the original's speed and sounds as it did.
|
|
95
|
+
|
|
96
|
+
## Bugs fixed
|
|
97
|
+
|
|
98
|
+
The remake plays the game as it was, bugs and all, because the pilots hold it against the original. A title can
|
|
99
|
+
also have a version with its bugs fixed, as a separate script beside the faithful one. Its `title.js` names each
|
|
100
|
+
script that version replaces:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
fixed: { game: 'game-fixed' }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The player's setting **Bugs fixed** (`play.js`, `fixed`) is offered only for a title that has one. With it on,
|
|
107
|
+
`loadTitle` runs `compiled/game-fixed.js` in place of `compiled/game.js`, and the page starts again when the
|
|
108
|
+
setting changes. Only compiled scripts are replaced: with `scenes`, `loadTitle` runs the faithful scenes.
|
|
109
|
+
|
|
110
|
+
Because every statement is the original's, with its address, a fix is an edit to a known line, not a guess at what
|
|
111
|
+
the game meant. Make the fixed script from the faithful one by edits that each check the line they change is the
|
|
112
|
+
one they expect. A changed line keeps its address, and a line that is new has none. That keeps the fixed version
|
|
113
|
+
the same game with only its bugs changed. When the faithful script is lifted again, the edits are made again, and
|
|
114
|
+
one whose line has changed stops instead of editing the wrong line.
|
|
115
|
+
|
|
116
|
+
Corrupted text can be fixed the same way where the text is in the code: a string a call takes inline after it, or a
|
|
117
|
+
scene's `data` line. Text the game reads from its disk at run time is the game's data, `<name>.json`, not a
|
|
118
|
+
script, and a fixed script does not change it.
|
|
119
|
+
|
|
120
|
+
## A title of its own engine
|
|
121
|
+
|
|
122
|
+
When `title.js` names a module as its `engine`, the title has no scripts. `loadTitle` imports that module as
|
|
123
|
+
`title.own`, and the player hands it the game (`../play.js`). That engine is the original program's own, read from
|
|
124
|
+
its code and written again routine for routine, playing the game's data as the original read it.
|
|
125
|
+
|
|
126
|
+
## The files
|
|
127
|
+
|
|
128
|
+
| File | What it is |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `engine.js` | runs the scripts: the statements on the registers, flags, stack and memory, the steps through the routines, every step an event |
|
|
131
|
+
| `script.js` | reads a scene script, a step to a line |
|
|
132
|
+
| `compile.js` | compiles a scene script to JavaScript (`moonbase-compile`) |
|
|
133
|
+
| `load.js` | loads a title's folder, its compiled scripts or its scenes, and makes its engine |
|
|
134
|
+
| `events.js` | what each step is as an event, in the same words for the original and the remake |
|
|
135
|
+
| `lift.js` | the lifter: a file's 6502 walked from its entries into a scene script; the card engine uses it to lift code a title's scenes miss, as it runs |
|
|
136
|
+
| `card.js`, `cards.js` | the card engine's routines, and what it knows of every title it plays |
|
|
137
|
+
| `vector.js`, `layout.js`, `raster.js`, `problems.js` | the vector engine's routines; its text laid out as the engine laid it out; the original's screen dot for dot, for the scenes that read it back; its problems' helpers |
|
|
138
|
+
| `view.js`, `card-view.js`, `c64-view.js` | the views: a vector title's as SVG, an Apple's hi-res page, a Commodore 64's screen |
|