@moonbase-machines/player 0.9.1 → 0.11.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.
@@ -0,0 +1,155 @@
1
+ // The Original's engine (play.js hands it the game): the game's disk in the machine it was made for, its screen on a
2
+ // canvas on the stage, and each picture the engine draws also given as SVG over the canvas. Each game's machine is
3
+ // loaded when its game asks for it, the Commodore 64 (@moonbase-machines/c64) or the Apple IIe
4
+ // (@moonbase-machines/apple2e), and not the other; on moonbase's own ROMs, which need nothing of the makers'.
5
+ //
6
+ // Of the player's `game` it reads: the game's data (`game`), its saves (`saves`), the stage, the status, and what only
7
+ // a workshop has (play.js): `roms`, the makers' ROMs this machine has, an Apple game's line 1, Apple's own booting its
8
+ // disks, { apple: { system, disk2 }, image: async name => its disk image's bytes }, a Commodore 64 game's Commodore's,
9
+ // { choice ('commodore' or 'moonbase-petscii', @moonbase-machines/c64/start.js), load: async name => a ROM's bytes };
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
+ import { Speaker } from '../audio.js';
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
+
16
+ export async function play(g) {
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>';
18
+ const canvas = g.stage.querySelector('canvas'), ctx = canvas.getContext('2d'), layer = g.stage.querySelector('#vector');
19
+ return g.game.kind === 'c64' ? c64(g, { canvas, layer, speaker: new Speaker() }) : apple(g, { canvas, ctx, layer, speaker: new Speaker() });
20
+ }
21
+
22
+ // An Apple II game: the machine, the canvas, the keys, and the hooks for the vectors. With Apple's
23
+ // ROMs and the game's disk images, where the page gives them (`roms`), it is line 1, the Apple itself booting the
24
+ // disks; without them, moonbase's (line 2), the game started at once.
25
+ async function apple({ game, saves, roms: makers, status, setting }, { canvas, ctx, layer, speaker }) {
26
+ const [{ Machine }, { placed }, { screen }, { shapes }, { elements }] = await Promise.all([
27
+ import('@moonbase-machines/apple2e/machine.js'), import('@moonbase-machines/apple2e/prodos.js'),
28
+ import('@moonbase-machines/apple2e/screen.js'), import('@moonbase-machines/vector/pictures.js'), import('@moonbase-machines/vector/svg.js')]);
29
+ const vectors = setting(VECTORS);
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
31
+ // written onto that volume's disk before it boots, and kept as the title's DOS writes it
32
+ const saving = saves.data;
33
+ let roms = null, disks = null;
34
+ if (makers?.apple) try {
35
+ roms = makers.apple;
36
+ disks = saves.disks(placed(saving, new Map(await Promise.all(game.images.map(async n => [n, await makers.image(n)])))));
37
+ } catch { roms = null; }
38
+ const m = new Machine(saving, { log: (...a) => console.log(...a), ...roms ? { roms, disks } : {} });
39
+ console.log(roms ? "line 1: Apple's ROMs, the disks booted" : "line 2: moonbase's machine");
40
+ saves.keep(roms ? saves.driveFiles(m, disks) : m.disk);
41
+ m.diskTime = true; // reads from the disk take the time a disk took, as the games' loading screens expect
42
+
43
+ // Where the engine is hooked, found in PART0 once it is loaded, in whichever bank the game runs
44
+ // in. The picture routine's loop draws the shapes the loader has put at $E000 one by one, so on
45
+ // its first shape the whole picture is read for the SVG, and from there to the routine's exit,
46
+ // which sets X to 0 and Y to $E0, the machine marks every screen byte written as the picture's.
47
+ // The box routine, which clears the picture window, starts by keeping its three colors, and
48
+ // there the SVG and the marks are cleared, and the window's color, in A, is kept to go under
49
+ // the next pictures' polygons.
50
+ let hooked = false, window_ = 0;
51
+ function hook() {
52
+ const find = (bytes, code) => { outer: for (let a = 0x4000; a < 0x8000 - bytes.length; a++) { for (let i = 0; i < bytes.length; i++) if (bytes[i] !== null && code[a + i] !== bytes[i]) continue outer; return a; } return -1; };
53
+ for (const code of [m.aux, m.main]) {
54
+ const shape = find([0x20, null, null, 0x20, null, null, 0xee, null, null, 0x4c], code); // JSR fill; JSR outline; INC count; JMP loop
55
+ const exit = find([0xa2, 0x00, 0xa0, 0xe0, 0x18, 0x60], code); // LDX #0; LDY #$E0; CLC; RTS
56
+ const box = find([0x8d, null, null, 0x8e, null, null, 0x8c, null, null, 0xad, null, null, 0xc9, 0x80, 0xf0], code);
57
+ if (shape < 0 || exit < 0 || box < 0) continue;
58
+ m.cpu.hooks.set(shape, c => { if (c.read16(0x60) === 0xe000) { layer.insertAdjacentHTML('beforeend', elements(shapes(a => m.read(a), 0xe000), { background: window_ })); m.marking = 1; } return false; });
59
+ m.cpu.hooks.set(exit, () => { m.marking = 0; return false; });
60
+ m.cpu.hooks.set(box, c => { if (c.a !== 0x80) window_ = c.a & 15; layer.innerHTML = ''; m.owner.fill(0); return false; });
61
+ hooked = true;
62
+ return;
63
+ }
64
+ }
65
+
66
+
67
+ const rgba = new Uint8ClampedArray(560 * 384 * 4);
68
+ const image = new ImageData(rgba, 560, 384);
69
+ const cols = new Uint8Array(140 * 192);
70
+ let mode = '';
71
+ function frame() {
72
+ if (!m.halted) m.runCycles(17030); // a frame's worth of the Apple's clock, at 60 a second
73
+ const now = `text ${m.sw.text} page2 ${m.sw.page2} hires ${m.sw.hires} dhires ${m.sw.dhires}`;
74
+ if (now !== mode) { console.log('screen:', now, 'at pc $' + m.cpu.pc.toString(16), 'cycles', m.cpu.cycles); mode = now; }
75
+ speaker.feed(sound ? m.clicks : [], m.cpu.cycles); m.clicks.length = 0;
76
+ if (!hooked) hook();
77
+ const text = m.sw.text;
78
+ screen(m, rgba, { cols, owner: vectors.value && layer.innerHTML ? m.owner : null });
79
+ ctx.putImageData(image, 0, 0);
80
+ layer.style.visibility = vectors.value && !text ? 'visible' : 'hidden';
81
+ status.textContent = m.halted ? 'The game has stopped: ' + m.halted + '.' : (m.idle > 2000 ? 'Waiting for a key.' : 'Running.');
82
+ requestAnimationFrame(frame);
83
+ }
84
+ canvas.addEventListener('pointerdown', () => canvas.focus());
85
+ // (the keys, the Apple's codes, given by the player: play.js, keys.js)
86
+ let sound = true;
87
+ return {
88
+ run() { canvas.focus(); frame(); },
89
+ press(k) { console.log('key', k, 'at pc $' + m.cpu.pc.toString(16)); m.press(k); },
90
+ start() { speaker.start(); },
91
+ sound(on) { sound = on; },
92
+ engine: m,
93
+ };
94
+ }
95
+
96
+ // A C64 game: on moonbase's own ROMs (@moonbase-machines/c64/rom.js), as the site has it everywhere, or on
97
+ // Commodore's where the page gives them (`roms`) and asks for them; the machine autostarted
98
+ // with LOAD"name",8,1, and RUN for a BASIC program, typed into the KERNAL's buffer, the frame on a canvas of
99
+ // 384 by 272 doubled, the keys as the matrix and the arrows as the joystick.
100
+ async function c64(g, { canvas, layer, speaker }) {
101
+ const { game, saves, roms: makers, studies, status, printer } = g;
102
+ const [{ C64 }, { keyboard, painter }, { SID }, { c64Roms, typed: startTyped }, { add }] = await Promise.all([
103
+ import('@moonbase-machines/c64/machine.js'), import('@moonbase-machines/c64/browser.js'),
104
+ import('@moonbase-machines/c64/sid.js'), import('@moonbase-machines/c64/start.js'), import('@moonbase-machines/c64/d64.js')]);
105
+ const roms = await c64Roms(makers?.choice ?? null, makers?.load);
106
+ if (!roms) { status.textContent = 'Commodore\'s ROMs, asked for, are not here.'; return null; }
107
+ g.chargen = roms.chargen; // (the printer's paper in the machine's characters: play.js)
108
+ const m = new C64(game, roms, { log: (...a) => console.log(...a), printer });
109
+ // What the game SAVEs to its disk, kept on the player's disk and given back to the drive the next time (play.js
110
+ // `saves`)
111
+ saves.drive(m.drive);
112
+ // (and, where the page gives them, `studies`, files of its own for the disk, a name to a file: in the drive, not
113
+ // kept)
114
+ const raw = t => Uint8Array.from(atob(t), c => c.charCodeAt(0));
115
+ for (const [file, data] of Object.entries(studies ?? {})) m.drive.saved.set(file, raw(data));
116
+ layer.style.display = 'none';
117
+ const draw = painter(canvas);
118
+ let typing = null, typed = 0, sound = true;
119
+ // the keys as the 64's matrix (@moonbase-machines/c64/browser.js), given them by the player (play.js)
120
+ const listeners = { keydown: [], keyup: [] };
121
+ const keys = keyboard(m, { log: true, listen: (type, f) => listeners[type].push(f) });
122
+ // once BASIC is ready, the start typed in; the KERNAL's buffer takes ten characters at a time.
123
+ // The C64 runs 50 frames a second, so frames are run to keep up with the clock, two at most.
124
+ let frames = 0, sid = null, started = performance.now();
125
+ function frame() {
126
+ const due = Math.min(2, Math.floor((performance.now() - started) / 20) - frames);
127
+ for (let i = 0; i < due; i++) {
128
+ m.runFrame(); frames++; keys.frame();
129
+ if (speaker.node && sound) {
130
+ if (!sid) sid = new SID(speaker.rate);
131
+ const n = Math.round(19656 / 985248 * speaker.rate), first = m.cpu.cycles - 19656;
132
+ const wave = sid.render(n, m.sidWrites.map(([c, r, v]) => [Math.floor((c - first) / 19656 * n), r, v]));
133
+ speaker.node.port.postMessage(wave, [wave.buffer]);
134
+ }
135
+ m.sidWrites.length = 0;
136
+ }
137
+ // (LOAD, and RUN for a BASIC program: @moonbase-machines/c64/start.js)
138
+ if (frames >= 120 && typing === null && !typed) { typing = startTyped(m, game); typed = 1; }
139
+ if (typing !== null && !m.ram[0xc6]) { typing = m.type(typing); if (!typing.length) typing = null; }
140
+ draw(m.vic.frame);
141
+ status.textContent = m.halted ? 'The game has stopped: ' + m.halted + '.' : 'Running.';
142
+ requestAnimationFrame(frame);
143
+ }
144
+ return {
145
+ run() { canvas.focus(); frame(); },
146
+ press(k) { m.type(String.fromCharCode(k)); }, // (into the KERNAL's buffer, as typed)
147
+ key(e) { for (const f of listeners[e.type]) f(e); },
148
+ start() { speaker.start(); },
149
+ sound(on) { sound = on; },
150
+ engine: m,
151
+ // the disk as it now stands, its own files and those saved: a .d64 that VICE or a real 64 can use
152
+ // (@moonbase-machines/c64/d64.js `add`)
153
+ diskImage: () => add(m.drive.image, [...m.drive.saved].map(([name, data]) => ({ name, data }))),
154
+ };
155
+ }
@@ -0,0 +1,213 @@
1
+ // The Remake's engine for a title of scenes, 'card' or 'vector' (play.js hands it the game): its scripts on the
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); 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
+ // original took time over takes time here too.
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'] };
12
+ import { Speaker } from '../audio.js';
13
+ import { SID } from '@moonbase-machines/c64/sid.js';
14
+ import { matrixOf } from '@moonbase-machines/c64/kernal.js';
15
+ import { standIn } from '@moonbase-machines/c64/rom.js';
16
+ import { Drive } from '@moonbase-machines/c64/drive.js';
17
+ import { View } from '../remake/view.js';
18
+ import { CardView } from '../remake/card-view.js';
19
+ import { C64View } from '../remake/c64-view.js';
20
+
21
+ export async function play(g) {
22
+ const { title, scripts, game: disk, stage, status, fail, saves, $ } = g;
23
+ const c64 = disk.kind === 'c64';
24
+ stage.innerHTML = `<svg id="screen" preserveAspectRatio="xMidYMid meet" tabindex="0" aria-label="The game's screen"></svg>`;
25
+ const svg = $('screen'), drawn = c64 ? 'c64' : title.engine === 'card' ? 'card' : 'vector';
26
+
27
+ // its view's own drawing, for a title that has one (its title.js `view`, load.js)
28
+ const extra = title.viewSteps ?? {};
29
+ // a Commodore 64's character ROM, for its view and the printer's paper: Commodore's where the page gives them
30
+ // (`roms`), else moonbase's own
31
+ const chargen = g.chargen = c64 ? g.roms?.chargen ?? standIn().chargen : null;
32
+ // a Commodore 64 title's printer on device 4, the player's (play.js), on the bus card.js makes
33
+ if (g.printer) title.printer = g.printer;
34
+ // what the game saves, on the player's disk (play.js `saves`): an Apple title's data disk, a Commodore 64 title's
35
+ // drive (the drive card.js puts on the bus)
36
+ title.disk = saves.data;
37
+ if (c64) saves.drive(title.drive = new Drive(Uint8Array.from(atob(disk.image), ch => ch.charCodeAt(0))));
38
+ const engine = makeEngine(title, scripts);
39
+ if (title.files) saves.keep(title.files);
40
+ const queue = [];
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; } };
46
+ let onView = null; // (the mini view's, given each view made)
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; };
48
+ let view = makeView();
49
+ engine.on(e => queue.push(view.snapshot({ ...e })));
50
+ // The mini view, where the page gives one (`mini`) and the title has a place for it (its title.js `inset`): Off,
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
53
+ if (title.inset && g.mini) {
54
+ const choice = g.setting(MINI);
55
+ const { top, bottom, view: [left, right] } = title.inset, at = document.createElement('div');
56
+ at.className = 'mini'; at.hidden = true;
57
+ Object.assign(at.style, { left: `${left / 5.6}%`, top: `${top / 3.84}%`, width: `${(right - left) / 5.6}%`, height: `${(bottom - top + 1) / 3.84}%` });
58
+ svg.parentElement.append(at);
59
+ const mini = await g.mini(at, { memory: () => engine.mem });
60
+ // (each view's painting followed by the mini view shown or not, as the band is there or not)
61
+ onView = v => { v.insetOn = !!choice.value; const prompts = v.prompts.bind(v); v.prompts = () => { prompts(); at.hidden = !(choice.value && v.banded); }; };
62
+ onView(view);
63
+ const show = () => { view.insetOn = !!choice.value; mini.mode(choice.value); view.prompts(); };
64
+ choice.on(show);
65
+ show();
66
+ }
67
+ oldStyle.on(() => {
68
+ view = makeView();
69
+ for (const e of history) view.apply(e);
70
+ view.prompts();
71
+ });
72
+
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
+ const speaker = engine.timed ? new Speaker() : null;
78
+ let sounding = true;
79
+ const hear = () => { if (!c64) speaker?.feed(sounding ? engine.clicks : [], engine.cycles); engine.clicks.length = 0; };
80
+ const V = title.map?.vars ?? { sound: 0, soundOn: 0 };
81
+ let audio = null;
82
+ const tone = (from, to, ms) => {
83
+ if (!sounding || !(engine.mem[V.sound] & V.soundOn)) return;
84
+ audio ??= new AudioContext();
85
+ const o = audio.createOscillator(), gain = audio.createGain(), t = audio.currentTime;
86
+ o.type = 'square'; o.frequency.setValueAtTime(from, t); o.frequency.linearRampToValueAtTime(to, t + ms / 1000);
87
+ gain.gain.setValueAtTime(0.06, t); gain.gain.linearRampToValueAtTime(0, t + ms / 1000);
88
+ o.connect(gain).connect(audio.destination); o.start(t); o.stop(t + ms / 1000);
89
+ };
90
+ const pitch = p => 16000 / (p + 8);
91
+
92
+ // how long each step takes to be seen or heard, in milliseconds
93
+ const PACE = { sound: 220, beep: 60, 'flash-box': 60, flash: 250, object: 30, 'object-keep': 30 };
94
+ let drawing = false, quick = false;
95
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
96
+ async function runs() {
97
+ if (drawing) return;
98
+ drawing = true;
99
+ try {
100
+ for (;;) {
101
+ const r = engine.run();
102
+ hear();
103
+ // the original took time here, a disk's read or a wait it counted out: the screen as it
104
+ // is, for that long (card.js)
105
+ if (r === 'pace') {
106
+ for (const e of queue.splice(0)) { history.push(e); view.apply(e); }
107
+ view.prompts(); if (!quick && g.pace()) await sleep(engine.delay * g.pace()); engine.delay = 0;
108
+ continue;
109
+ }
110
+ engine.delay = 0;
111
+ while (queue.length) {
112
+ const e = queue.shift();
113
+ history.push(e);
114
+ view.apply(e);
115
+ if (quick) continue; // (at full speed: no sound, no wait)
116
+ if (e.step === 'sound') { const [end, start] = e.words.split(' ').map(h => parseInt(h, 16)); tone(pitch(start), pitch(end), 180); }
117
+ if (e.step === 'beep') tone(pitch(e.a & 31) * 2, pitch(e.a & 31) * 2, 50);
118
+ if (!g.pace()) continue; // (none of the original's waits: its sounds still)
119
+ if (PACE[e.step]) await sleep(PACE[e.step] * g.pace());
120
+ if (e.step === 'pause') await sleep(e.ms * g.pace()); // a delay the original counted out
121
+ }
122
+ view.prompts();
123
+ status.textContent = r === 'end' ? 'The end.' : 'Waiting for a key.';
124
+ if (!engine.keys.length || r === 'end') break;
125
+ }
126
+ } catch (err) { fail(err); }
127
+ drawing = false;
128
+ }
129
+
130
+ const press = k => { engine.press(k); audio?.resume(); speaker?.start(); if (!c64) runs(); };
131
+ // A game that goes on without waiting for keys, as Nature Park's truck: each turn of its loop is
132
+ // a step `tick`, which takes a key or none. A turn passes every 100 milliseconds, as the
133
+ // original's took, with the key pressed since the last or still held, as the Apple's keyboard
134
+ // told the game a key was down.
135
+ const ticking = () => engine.waitingOn?.words?.[0] === 'tick';
136
+ let held = null, tapped = null;
137
+ svg.addEventListener('click', e => { const key = e.target.closest('.key')?.dataset.key; if (key) press(+key); });
138
+
139
+ // A Commodore 64 title runs on the clock, as the original did, waiting for a key or not: a
140
+ // sixtieth of a second of the original's cycles at a time (the engine's run stops at each 16 ms of
141
+ // them), its raster and its interrupt at the time they come (card.js), and the screen
142
+ // painted after each. And the SID, its writes played a sixtieth of a second at a time, as the
143
+ // emulator plays a frame's (engines/original.js).
144
+ function clock() {
145
+ engine.sid = []; engine.clock = { irq: 0, ahead: [], since: 0 };
146
+ let sid = null, from = null, sent = 0, stopped = false;
147
+ status.textContent = 'Playing.';
148
+ setInterval(() => {
149
+ engine.delay -= 1000 / 60; // a load's time, or the last run's
150
+ if (!stopped && !engine.ended && engine.delay < 16) {
151
+ engine.delay = Math.max(0, engine.delay);
152
+ // a key of ?keys when the game is waiting for one, two seconds at least after the last, as
153
+ // a player would press them (a wait the game counts out goes on by itself on the clock)
154
+ if (engine.clock.ahead.length && engine.idle > (engine.routines.polls ?? 200) && !engine.keys.length && ++engine.clock.since > 120) { engine.press(engine.clock.ahead.shift()); engine.clock.since = 0; }
155
+ try { engine.run(); queue.length = 0; view.prompts(); } catch (err) { stopped = true; fail(err); }
156
+ if (engine.ended) status.textContent = 'The end.';
157
+ }
158
+ if (speaker?.node && sounding) {
159
+ sid ??= new SID(speaker.rate); from ??= performance.now();
160
+ const n = Math.floor((performance.now() - from) / 1000 * speaker.rate) - sent;
161
+ if (n > 0) { const wave = sid.render(n, engine.sid.map(([, r, v]) => [0, r, v])); sent += n; speaker.node.port.postMessage(wave, [wave.buffer]); }
162
+ }
163
+ engine.sid.length = 0;
164
+ }, 1000 / 60);
165
+ }
166
+ setInterval(() => {
167
+ if (!ticking() || drawing || engine.keys.length) return;
168
+ const k = tapped ?? held ?? -1; tapped = null;
169
+ press(k);
170
+ }, 100);
171
+
172
+ return {
173
+ engine, press,
174
+ run() { svg.focus(); if (c64) clock(); else runs(); },
175
+ // the keys the player gives (play.js): a turn's key while the game ticks; a Commodore 64's held in its keyboard's
176
+ // matrix, which its KERNAL scans and repeats as long as it is held, as the original's did
177
+ // (@moonbase-machines/c64/kernal.js), a character typed only with shift into the buffer; else pressed
178
+ key(e, k) {
179
+ if (e.type === 'keyup') {
180
+ held = null;
181
+ const h = engine.clock?.held;
182
+ if (h?.key === e.key) { if (h.seen) engine.clock.held = null; else h.up = true; }
183
+ return;
184
+ }
185
+ if (k === undefined) return;
186
+ e.preventDefault();
187
+ if (ticking()) { tapped = held = k; return; }
188
+ const m = c64 && !e.ctrlKey && matrixOf(k);
189
+ if (m) { if (!e.repeat) { engine.clock.held = { ...m, key: e.key }; audio?.resume(); speaker?.start(); } return; }
190
+ if (!e.repeat) press(k);
191
+ },
192
+ // ?keys at the start: a Commodore 64's into the KERNAL's buffer on the clock, as a player would press them; else
193
+ // pressed
194
+ keys(list) { if (c64) engine.clock.ahead.push(...list); else for (const k of list) press(k); },
195
+ start() { audio?.resume(); speaker?.start(); },
196
+ sound(on) { sounding = on; }, // (the player's speaker: play.js)
197
+ quick(on) { quick = on; },
198
+ // A route the page gives (`route`, a workshop's pilots' route), played first as the pilots play it, each key when
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
200
+ // usual
201
+ route({ route, name, keys: { expand, give, petscii, pollsAt } }) {
202
+ title.seed = route.seed; // (the random numbers from the route's seed, as the pilots have them)
203
+ if (route.printer && title.printer) { title.printer.on = true; const box = $('printer'); if (box) box.checked = true; }
204
+ status.textContent = 'Playing the route ' + name + '.';
205
+ // (on a Commodore 64 a key behind the one pressed goes into the KERNAL's buffer, as the pilots hold a key)
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); } };
207
+ const go = () => { let r; do { engine.delay = 0; r = engine.run(); } while (r === 'pace'); if (c64) queue.length = 0; else for (const e of queue.splice(0)) { history.push(e); view.apply(e); } };
208
+ go();
209
+ for (const [i, [k]] of expand(route.keys).entries()) { give(side, k, c64 ? petscii : undefined); engine.polls = pollsAt(route, i + 1) ?? undefined; go(); }
210
+ engine.polls = undefined; view.prompts();
211
+ },
212
+ };
213
+ }
package/index.js CHANGED
@@ -1,7 +1,14 @@
1
1
  // @moonbase-machines/player: every module by name, for one bundled file or to take the whole package at once:
2
- // import { remakeMain } from '@moonbase-machines/player'. Each module can be imported on its own too:
3
- // '@moonbase-machines/player/<module>.js' (remake/main.js, original.js, disk/disk.js).
4
- export * as original from './original.js';
2
+ // import { play } from '@moonbase-machines/player', its modules each a namespace named by its path (play, diskDisk,
3
+ // enginesOriginal). Each module can be imported on its own too: '@moonbase-machines/player/<module>.js' (play.js,
4
+ // disk/disk.js).
5
+ export * as play from './play.js';
6
+ export * as keys from './keys.js';
7
+ export * as saves from './saves.js';
8
+ export * as settings from './settings.js';
9
+ export * as provider from './provider.js';
10
+ export * as enginesOriginal from './engines/original.js';
11
+ export * as enginesScenes from './engines/scenes.js';
5
12
  export * as audio from './audio.js';
6
13
  export * as modal from './modal.js';
7
14
  export * as parts from './parts.js';
@@ -19,10 +26,8 @@ export * as remakeEvents from './remake/events.js';
19
26
  export * as remakeLayout from './remake/layout.js';
20
27
  export * as remakeLift from './remake/lift.js';
21
28
  export * as remakeLoad from './remake/load.js';
22
- export * as remakeMain from './remake/main.js';
23
29
  export * as remakeProblems from './remake/problems.js';
24
30
  export * as remakeRaster from './remake/raster.js';
25
- export * as remakeScenes from './remake/scenes.js';
26
31
  export * as remakeScript from './remake/script.js';
27
32
  export * as remakeVector from './remake/vector.js';
28
33
  export * as remakeView from './remake/view.js';
package/keys.js ADDED
@@ -0,0 +1,28 @@
1
+ // The keys a page's keyboard gives, as the machine a game was made for takes them: the one table of each, for every
2
+ // way to play (play.js reads them; an engine that reads the keyboard itself, a Commodore 64's matrix, Aztlan's menus'
3
+ // shortcuts, is handed the event as well).
4
+ //
5
+ // code(event, kind) the key's code for the machine of that kind ('prodos', 'dos33' or 'c64'; none for a title
6
+ // of its own engine), or undefined for a key it has none for
7
+ // named(name, kind) a key by its name in a route or ?keys= (space, return, left...), as the machine's code
8
+
9
+ // the Apple II's keyboard: the arrows as its control keys, DELETE, ESC, TAB
10
+ const APPLE = { ArrowLeft: 8, ArrowRight: 21, ArrowDown: 10, ArrowUp: 11, Enter: 13, Backspace: 127, Delete: 127, Escape: 27, Tab: 9, ' ': 32 };
11
+ // a Commodore 64's, in PETSCII: its cursor keys, DEL and RUN/STOP, and its letters capitals
12
+ const C64 = { ArrowLeft: 157, ArrowRight: 29, ArrowDown: 17, ArrowUp: 145, Enter: 13, Backspace: 20, Delete: 20, Escape: 3, ' ': 32 };
13
+ // the names a route's keys and ?keys= use, as the Apple's codes, and those a Commodore 64 has in their place
14
+ const NAMES = { space: 32, return: 13, escape: 27, delete: 127, left: 8, right: 21, down: 10, up: 11, 'control-e': 5, 'control-t': 20, none: -1 };
15
+ const PETSCII = { 8: 157, 21: 29, 10: 17, 11: 145, 127: 20, 27: 3 };
16
+
17
+ export function code(e, kind) {
18
+ if (!kind || e.metaKey || e.altKey) return undefined;
19
+ const c64 = kind === 'c64';
20
+ // control with a letter is the machine's control character (control-E opens a vector title's main menu)
21
+ if (e.ctrlKey) return /^[a-z]$/i.test(e.key) ? e.key.toUpperCase().charCodeAt(0) & 0x1f : undefined;
22
+ return (c64 ? C64 : APPLE)[e.key] ?? (e.key.length === 1 ? (c64 ? e.key.toUpperCase() : e.key).charCodeAt(0) : undefined);
23
+ }
24
+
25
+ export function named(name, kind) {
26
+ const k = NAMES[name] ?? name.charCodeAt(0);
27
+ return kind === 'c64' ? PETSCII[k] ?? k : k;
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moonbase-machines/player",
3
- "version": "0.9.1",
3
+ "version": "0.11.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.9.1",
18
- "@moonbase-machines/c64": "0.9.1",
19
- "@moonbase-machines/charset": "0.9.1",
20
- "@moonbase-machines/mos6502": "0.9.1",
21
- "@moonbase-machines/vector": "0.9.1"
17
+ "@moonbase-machines/apple2e": "0.11.0",
18
+ "@moonbase-machines/c64": "0.11.0",
19
+ "@moonbase-machines/charset": "0.11.0",
20
+ "@moonbase-machines/mos6502": "0.11.0",
21
+ "@moonbase-machines/vector": "0.11.0"
22
22
  },
23
23
  "scripts": {
24
24
  "test": "node --test test/*.test.js",