@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.
package/original.js DELETED
@@ -1,200 +0,0 @@
1
- // The original: a game's disk in the machine it was made for, its screen on a canvas, the keyboard passed
2
- // on, and each picture the engine draws also given as SVG over the canvas. Each game's machine is loaded when its
3
- // game asks for it, the Commodore 64 (@moonbase-machines/c64) or the Apple IIe (@moonbase-machines/apple2e), and not
4
- // the other; on moonbase's own ROMs, which need nothing of the makers'.
5
- //
6
- // await play({ data, root, disk, words, downloadDisk, roms, studies })
7
- // data the game's data, an absolute URL (<name>.json: its disks' files, its kind, its saves)
8
- // root the page's section the player is made in
9
- // disk the title's player's disk (disk/disk.js), opened before the player starts
10
- // words the page's words the player shows: { printer: { line, print, tear } }
11
- // downloadDisk a C64 game's disk offered to download, with what is saved on it
12
- // and what only a workshop has, given by its pages, never looked for here:
13
- // roms the makers' ROMs this machine has: an Apple game's line 1, Apple's own booting its disks,
14
- // { apple: { system, disk2 }, image: async name => its disk image's bytes }; a C64 game's
15
- // Commodore's, { choice ('commodore' or 'moonbase-petscii', @moonbase-machines/c64/start.js), load:
16
- // async name => a ROM's bytes }
17
- // studies files of its own for a C64 game's disk, a name to its bytes in base64, in the drive, not kept
18
- import { Speaker } from './audio.js';
19
- import { paperBox, phoneKeys, KEYS_MARKUP } from './parts.js';
20
-
21
- export async function play({ data, root, disk, words, downloadDisk = false, roms = null, studies = null }) {
22
- root.classList.add('game');
23
- root.innerHTML = `<div class="stage"><svg id="vector" viewBox="0 0 140 192" preserveAspectRatio="none"></svg><canvas id="screen" width="560" height="384" tabindex="0"></canvas></div>
24
- <p class="options"><span id="status">Loading.</span>${KEYS_MARKUP}<label><input type="checkbox" id="vectors"> Draw the pictures as vectors</label><label hidden><input type="checkbox" id="printer" checked> Printer</label><button id="disk" type="button" hidden>Download the disk</button></p>`;
25
- root.append(paperBox(words.printer));
26
- const $ = id => root.querySelector('#' + id);
27
- const canvas = $('screen'), ctx = canvas.getContext('2d');
28
- phoneKeys(root, canvas.parentElement);
29
- // (the game's name, for a disk downloaded: its data's file's)
30
- const name = new URL(data).pathname.split('/').pop().replace(/\.json$/, '');
31
- const parts = { name, roms, studies, disk, downloadDisk, $, canvas, ctx, layer: $('vector'), status: $('status'), vectorToggle: $('vectors'), speaker: new Speaker() };
32
- const game = await (await fetch(data)).json();
33
- if (game.kind === 'c64') await c64(game, parts); else await apple(game, parts);
34
- }
35
-
36
- // the screen stretched evenly: a whole number of the device's pixels to each of the machine's dots
37
- // (the 64's 384 by 272, the Apple's 280 by 192), so every dot is drawn the same size and a line one
38
- // dot thick is as thick as any other; the next whole number up from the column's width, a little
39
- // wider than it and centred on it, unless that is wider than the window; kept so as the window, or
40
- // its zoom, changes
41
- function evenly(canvas, dots, lines) {
42
- const stage = canvas.parentElement, column = stage.parentElement;
43
- const fit = () => {
44
- const dpr = window.devicePixelRatio || 1, pad = parseFloat(getComputedStyle(column).paddingLeft) * 2;
45
- const room = column.clientWidth - pad, most = document.documentElement.clientWidth - 16;
46
- let k = Math.ceil(room * dpr / dots - 0.01);
47
- if (dots * k / dpr > most) k = Math.max(1, Math.floor(most * dpr / dots));
48
- const w = dots * k / dpr;
49
- Object.assign(stage.style, { width: w + 'px', height: lines * k / dpr + 'px', aspectRatio: 'auto', marginLeft: (room - w) / 2 + 'px' });
50
- };
51
- fit();
52
- addEventListener('resize', fit);
53
- }
54
-
55
- // An Apple II game: the machine, the canvas, the keys, and the hooks for the vectors. With Apple's
56
- // ROMs and the game's disk images, where the page gives them (`roms`), it is line 1, the Apple itself booting the
57
- // disks; without them, moonbase's (line 2), the game started at once.
58
- async function apple(game, { roms: makers, disk, canvas, ctx, layer, status, vectorToggle, speaker }) {
59
- const [{ Machine }, { placed }, { withSaves, keepSaves, onDisks, driveFiles }, { screen }, { shapes }, { elements }] = await Promise.all([
60
- import('@moonbase-machines/apple2e/machine.js'), import('@moonbase-machines/apple2e/prodos.js'), import('@moonbase-machines/apple2e/saves.js'),
61
- import('@moonbase-machines/apple2e/screen.js'), import('@moonbase-machines/vector/pictures.js'), import('@moonbase-machines/vector/svg.js')]);
62
- evenly(canvas, 280, 192);
63
- // the title's saves on its own volume, the player's disk's (@moonbase-machines/apple2e/saves.js): on line 2 the
64
- // machine's files; on line 1 written onto that volume's disk before it boots, and kept as the title's DOS writes it
65
- const saving = withSaves(game, disk);
66
- let roms = null, disks = null;
67
- if (makers?.apple) try {
68
- roms = makers.apple;
69
- disks = onDisks(game, placed(saving, new Map(await Promise.all(game.images.map(async n => [n, await makers.image(n)])))), disk);
70
- } catch { roms = null; }
71
- const m = new Machine(saving, { log: (...a) => console.log(...a), ...roms ? { roms, disks } : {} });
72
- console.log(roms ? "line 1: Apple's ROMs, the disks booted" : "line 2: moonbase's machine");
73
- keepSaves(game, roms ? driveFiles(m, disks, game) : m.disk, disk);
74
- m.diskTime = true; // reads from the disk take the time a disk took, as the games' loading screens expect
75
-
76
- // Where the engine is hooked, found in PART0 once it is loaded, in whichever bank the game runs
77
- // in. The picture routine's loop draws the shapes the loader has put at $E000 one by one, so on
78
- // its first shape the whole picture is read for the SVG, and from there to the routine's exit,
79
- // which sets X to 0 and Y to $E0, the machine marks every screen byte written as the picture's.
80
- // The box routine, which clears the picture window, starts by keeping its three colors, and
81
- // there the SVG and the marks are cleared, and the window's color, in A, is kept to go under
82
- // the next pictures' polygons.
83
- let hooked = false, window_ = 0;
84
- function hook() {
85
- 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; };
86
- for (const code of [m.aux, m.main]) {
87
- const shape = find([0x20, null, null, 0x20, null, null, 0xee, null, null, 0x4c], code); // JSR fill; JSR outline; INC count; JMP loop
88
- const exit = find([0xa2, 0x00, 0xa0, 0xe0, 0x18, 0x60], code); // LDX #0; LDY #$E0; CLC; RTS
89
- const box = find([0x8d, null, null, 0x8e, null, null, 0x8c, null, null, 0xad, null, null, 0xc9, 0x80, 0xf0], code);
90
- if (shape < 0 || exit < 0 || box < 0) continue;
91
- 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; });
92
- m.cpu.hooks.set(exit, () => { m.marking = 0; return false; });
93
- m.cpu.hooks.set(box, c => { if (c.a !== 0x80) window_ = c.a & 15; layer.innerHTML = ''; m.owner.fill(0); return false; });
94
- hooked = true;
95
- return;
96
- }
97
- }
98
-
99
- // the keys, as the Apple II's keyboard gives them
100
- const KEYS = { Escape: 27, Enter: 13, Backspace: 127, Delete: 127, ArrowLeft: 8, ArrowRight: 21, ArrowUp: 11, ArrowDown: 10, Tab: 9 };
101
- window.addEventListener('keydown', e => {
102
- speaker.start();
103
- if (e.metaKey || e.altKey || e.repeat) return; // a held key is one key
104
- // control with a letter is the Apple's control character: control-E opens Animal Rescue's main menu
105
- const k = e.ctrlKey ? (/^[a-z]$/i.test(e.key) ? e.key.toUpperCase().charCodeAt(0) & 0x1f : undefined) : KEYS[e.key] ?? (e.key.length === 1 ? e.key.charCodeAt(0) : undefined);
106
- if (k === undefined) return;
107
- console.log('key', JSON.stringify(e.key), '->', k, 'at pc $' + m.cpu.pc.toString(16));
108
- m.press(k); e.preventDefault();
109
- });
110
-
111
- const rgba = new Uint8ClampedArray(560 * 384 * 4);
112
- const image = new ImageData(rgba, 560, 384);
113
- const cols = new Uint8Array(140 * 192);
114
- let mode = '';
115
- function frame() {
116
- if (!m.halted) m.runCycles(17030); // a frame's worth of the Apple's clock, at 60 a second
117
- const now = `text ${m.sw.text} page2 ${m.sw.page2} hires ${m.sw.hires} dhires ${m.sw.dhires}`;
118
- if (now !== mode) { console.log('screen:', now, 'at pc $' + m.cpu.pc.toString(16), 'cycles', m.cpu.cycles); mode = now; }
119
- speaker.feed(m.clicks, m.cpu.cycles); m.clicks.length = 0;
120
- if (!hooked) hook();
121
- const text = m.sw.text;
122
- screen(m, rgba, { cols, owner: vectorToggle.checked && layer.innerHTML ? m.owner : null });
123
- ctx.putImageData(image, 0, 0);
124
- layer.style.visibility = vectorToggle.checked && !text ? 'visible' : 'hidden';
125
- status.textContent = m.halted ? 'The game has stopped: ' + m.halted + '.' : (m.idle > 2000 ? 'Waiting for a key.' : 'Running.');
126
- requestAnimationFrame(frame);
127
- }
128
- canvas.addEventListener('pointerdown', () => { speaker.start(); canvas.focus(); });
129
- canvas.focus();
130
- frame();
131
- }
132
-
133
- // A C64 game: on moonbase's own ROMs (@moonbase-machines/c64/rom.js), as the site has it everywhere, or on
134
- // Commodore's where the page gives them (`roms`) and asks for them; the machine autostarted
135
- // with LOAD"name",8,1, and RUN for a BASIC program, typed into the KERNAL's buffer, the frame on a canvas of
136
- // 384 by 272 doubled, the keys as the matrix and the arrows as the joystick.
137
- async function c64(game, { name, roms: makers, studies, disk, downloadDisk, $, canvas, layer, status, vectorToggle, speaker }) {
138
- const [{ C64 }, { Printer, paper }, { WIDTH, HEIGHT }, { keyboard, painter }, { SID }, { c64Roms, typed: startTyped }, { add }, { keepDrive }] = await Promise.all([
139
- import('@moonbase-machines/c64/machine.js'), import('@moonbase-machines/c64/printer.js'), import('@moonbase-machines/c64/vic.js'), import('@moonbase-machines/c64/browser.js'),
140
- import('@moonbase-machines/c64/sid.js'), import('@moonbase-machines/c64/start.js'), import('@moonbase-machines/c64/d64.js'), import('@moonbase-machines/c64/saves.js')]);
141
- evenly(canvas, WIDTH, HEIGHT);
142
- const roms = await c64Roms(makers?.choice ?? null, makers?.load);
143
- if (!roms) { status.textContent = 'Commodore\'s ROMs, asked for, are not here.'; return; }
144
- // a printer on device 4 (@moonbase-machines/c64/printer.js): on unless its box is off, what it prints drawn
145
- // below the screen as a sheet of paper, until it is torn off
146
- const box = $('printer'), sheet = $('paper'), shown = $('paper-box');
147
- try { box.checked = localStorage.getItem('printer') !== '0'; } catch {}
148
- box.parentElement.hidden = false;
149
- const printer = new Printer({ on: box.checked });
150
- box.addEventListener('change', () => { printer.on = box.checked; try { localStorage.setItem('printer', box.checked ? '1' : '0'); } catch {} });
151
- printer.changed = () => { shown.hidden = !printer.lines.length; if (printer.lines.length) paper(sheet, printer, roms.chargen); };
152
- $('tear').addEventListener('click', () => printer.tear());
153
- $('print-paper').addEventListener('click', () => window.print()); // (the sheet alone: its print style)
154
- const m = new C64(game, roms, { log: (...a) => console.log(...a), printer });
155
- // What the game SAVEs to its disk, kept on the player's disk and given back to the drive the next time
156
- // (@moonbase-machines/c64/saves.js)
157
- keepDrive(m.drive, disk);
158
- // (and, where the page gives them, `studies`, files of its own for the disk, a name to a file: in the drive, not
159
- // kept)
160
- const raw = t => Uint8Array.from(atob(t), c => c.charCodeAt(0));
161
- for (const [file, data] of Object.entries(studies ?? {})) m.drive.saved.set(file, raw(data));
162
- // the disk as it now stands, its own files and those saved, to download where the title offers it
163
- // (Crayon's): a .d64 that VICE or a real 64 can use (@moonbase-machines/c64/d64.js `add`)
164
- if (downloadDisk) {
165
- const download = $('disk');
166
- download.hidden = false;
167
- download.addEventListener('click', () => {
168
- const image = add(m.drive.image, [...m.drive.saved].map(([name, data]) => ({ name, data })));
169
- const a = Object.assign(document.createElement('a'), { href: URL.createObjectURL(new Blob([image], { type: 'application/octet-stream' })), download: name + '.d64' });
170
- a.click(); URL.revokeObjectURL(a.href);
171
- });
172
- }
173
- layer.style.display = 'none'; vectorToggle.parentElement.style.display = 'none';
174
- const draw = painter(canvas);
175
- let typing = null, typed = 0;
176
- const keys = keyboard(m, { onKey: () => speaker.start(), log: true });
177
- // once BASIC is ready, the start typed in; the KERNAL's buffer takes ten characters at a time.
178
- // The C64 runs 50 frames a second, so frames are run to keep up with the clock, two at most.
179
- let frames = 0, sid = null, started = performance.now();
180
- function frame() {
181
- const due = Math.min(2, Math.floor((performance.now() - started) / 20) - frames);
182
- for (let i = 0; i < due; i++) {
183
- m.runFrame(); frames++; keys.frame();
184
- if (speaker.node) {
185
- if (!sid) sid = new SID(speaker.rate);
186
- const n = Math.round(19656 / 985248 * speaker.rate), first = m.cpu.cycles - 19656;
187
- const wave = sid.render(n, m.sidWrites.map(([c, r, v]) => [Math.floor((c - first) / 19656 * n), r, v]));
188
- speaker.node.port.postMessage(wave, [wave.buffer]);
189
- }
190
- m.sidWrites.length = 0;
191
- }
192
- // (LOAD, and RUN for a BASIC program: @moonbase-machines/c64/start.js)
193
- if (frames >= 120 && typing === null && !typed) { typing = startTyped(m, game); typed = 1; }
194
- if (typing !== null && !m.ram[0xc6]) { typing = m.type(typing); if (!typing.length) typing = null; }
195
- draw(m.vic.frame);
196
- status.textContent = m.halted ? 'The game has stopped: ' + m.halted + '.' : 'Running.';
197
- requestAnimationFrame(frame);
198
- }
199
- canvas.focus(); frame();
200
- }
package/remake/main.js DELETED
@@ -1,51 +0,0 @@
1
- // The remake in the browser: one player for every title. The page gives it the title's folder and its game's data;
2
- // the player makes its part of the page, the stage the game is drawn on and the options line under it with the
3
- // game's status, loads the title (load.js), puts its stylesheet in the page where it has one, and hands the game to
4
- // its engine: the remake's own for a title of scenes, 'card' or 'vector' (scenes.js), or the title's own, a module in
5
- // its folder (its title.js `engine`), given the same. A failure is said in the status line.
6
- //
7
- // await play({ title, data, root, disk, preview, words, watch, claim, pace, route, mini, roms })
8
- // title the title's folder, an absolute URL
9
- // data its game's data, an absolute URL (for a title of its own engine, the folder its data is in)
10
- // root the page's section the player is made in
11
- // disk the title's player's disk (disk/disk.js), opened before the player starts
12
- // preview the preview's build: what only it has may be used
13
- // words the page's words the player shows: { printer: { line, print, tear } }
14
- // watch called with the game, { engine, press, quick }, for a page that follows it
15
- // claim k => true to take a key of the player's before the game does
16
- // pace how much of the original's waits: 1 all (the default), 0 none, between a part of each; read at
17
- // each wait
18
- // and what only a workshop has, given by its pages, never looked for here:
19
- // route a route to play first, { route, keys }: its keys, and the pilots' key handling (expand, give,
20
- // petscii, pollsAt)
21
- // mini a mini view for a title with an `inset`, (at, { memory }) => { mode(m) }
22
- // roms the makers' ROMs this machine has: { chargen } for a Commodore 64 title's characters
23
- //
24
- // What the engine is handed, its own engine's or the remake's, `game`: { title, scripts, disk, folder, stage,
25
- // options, status, fail, kept, preview, words, ...the page's options }; `stage` is its to draw in, `options` the
26
- // line its own controls go on, before the status.
27
- import { loadTitle } from './load.js';
28
- import { play as scenes } from './scenes.js';
29
-
30
- export async function play(options) {
31
- const { title: folder, data, root, disk: kept, preview = false, words } = options;
32
- root.classList.add('remake');
33
- root.innerHTML = '<div class="stage"></div><p class="options"><span id="status">Loading.</span></p>';
34
- const stage = root.querySelector('.stage'), line = root.querySelector('.options'), status = root.querySelector('#status');
35
- const fail = err => { status.textContent = 'The remake stopped: ' + err.message; status.className = 'error'; console.error(err); };
36
- try {
37
- // ?fixed: the title with its bugs fixed, for one that has such a version (its title.js `fixed`)
38
- const fixed = new URLSearchParams(location.search).has('fixed');
39
- const { title, scripts, disk } = await loadTitle(folder, { data, fixed });
40
- title.disk = disk;
41
- // the title's own styles, where it has them (its title.js `style`, a file in its folder), in place before it
42
- // draws by them
43
- if (title.style && !document.querySelector(`link[data-title="${title.slug}"]`)) {
44
- const link = Object.assign(document.createElement('link'), { rel: 'stylesheet', href: new URL(title.style, folder).href });
45
- link.dataset.title = title.slug; document.head.append(link);
46
- await new Promise(done => { link.onload = link.onerror = done; });
47
- }
48
- const game = { ...options, title, scripts, disk, folder, data, stage, options: line, status, fail, kept, preview, words, fixed };
49
- await (title.own ?? { play: scenes }).play(game);
50
- } catch (err) { fail(err); }
51
- }
package/remake/scenes.js DELETED
@@ -1,243 +0,0 @@
1
- // A title of scenes played, the remake's own engine (main.js hands it the game): its scripts on the engine, its events
2
- // drawn by the view, on the stage; its controls on the options line, the sound, the old style text, a C64 title's
3
- // printer, the bugs fixed where the title has a version so, the mini view where the page gives one. A run's events
4
- // are drawn one after another, with a moment for each sound and flash, so that what the original took time over takes
5
- // time here too.
6
- import { Printer, paper } from '@moonbase-machines/c64/printer.js';
7
- import { makeEngine } from './load.js';
8
- import { Speaker } from '../audio.js';
9
- import { SID } from '@moonbase-machines/c64/sid.js';
10
- import { matrixOf } from '@moonbase-machines/c64/kernal.js';
11
- import { standIn } from '@moonbase-machines/c64/rom.js';
12
- import { withSaves, keepSaves } from '@moonbase-machines/apple2e/saves.js';
13
- import { Drive } from '@moonbase-machines/c64/drive.js';
14
- import { keepDrive } from '@moonbase-machines/c64/saves.js';
15
- import { paperBox, phoneKeys, KEYS_MARKUP } from '../parts.js';
16
- import { View } from './view.js';
17
- import { CardView } from './card-view.js';
18
- import { C64View } from './c64-view.js';
19
-
20
- export async function play(game) {
21
- const { title, scripts, disk, root, stage, status, fail, kept, words, fixed } = game;
22
- root.classList.add('game'); // (the screen of a machine: the stage the game's own shape, player.css)
23
- stage.innerHTML = `<svg id="screen" preserveAspectRatio="xMidYMid meet" tabindex="0" aria-label="The game's screen"></svg>`;
24
- status.insertAdjacentHTML('afterend', `${KEYS_MARKUP}<label><input type="checkbox" id="sound"> Sound</label><label><input type="checkbox" id="old-style"> Old style text</label><label hidden><input type="checkbox" id="printer" checked> Printer</label><label hidden title="The game with its bugs fixed: it starts again"><input type="checkbox" id="fixed"> Bugs fixed</label><label hidden>Mini view <select id="mini"><option value="">Off</option><option value="live">Live</option><option value="pictures">Pictures</option></select></label>`);
25
- root.append(paperBox(words.printer));
26
- const $ = id => root.querySelector('#' + id);
27
- const svg = $('screen'), soundBox = $('sound');
28
- phoneKeys(root, stage);
29
-
30
- try {
31
- // the choice of the fixed version, where there is one: the page again, with ?fixed or without
32
- const fixedBox = $('fixed');
33
- if (fixedBox && title.fixed) {
34
- fixedBox.checked = fixed; fixedBox.parentElement.hidden = false;
35
- fixedBox.addEventListener('change', () => { const u = new URL(location.href); if (fixedBox.checked) u.searchParams.set('fixed', ''); else u.searchParams.delete('fixed'); location.href = u.href.replace('fixed=&', 'fixed&').replace(/fixed=$/, 'fixed'); });
36
- }
37
- // its view's own drawing, for a title that has one (its title.js `view`, load.js)
38
- const extra = title.viewSteps ?? {};
39
- // a Commodore 64's character ROM, for its view: Commodore's where the page gives them (`roms`), else moonbase's own
40
- const chargen = disk.kind === 'c64' ? game.roms?.chargen ?? standIn().chargen : null;
41
-
42
- // A printer on device 4, for a Commodore 64 title (@moonbase-machines/c64/printer.js): on unless its box is off,
43
- // what it prints drawn below the screen as a sheet of paper, until it is torn off
44
- if (disk.kind === 'c64') {
45
- const box = $('printer'), sheet = $('paper'), shown = $('paper-box');
46
- try { box.checked = localStorage.getItem('remake-printer') !== '0'; } catch {}
47
- box.parentElement.hidden = false;
48
- const printer = title.printer = new Printer({ on: box.checked });
49
- box.addEventListener('change', () => { printer.on = box.checked; try { localStorage.setItem('remake-printer', box.checked ? '1' : '0'); } catch {} });
50
- printer.changed = () => { shown.hidden = !printer.lines.length; if (printer.lines.length) paper(sheet, printer, chargen); };
51
- $('tear').addEventListener('click', () => printer.tear());
52
- $('print-paper').addEventListener('click', () => window.print()); // (the sheet alone: its print style)
53
- }
54
- // what the game saves, on the player's disk: an Apple title's data disk (@moonbase-machines/apple2e/saves.js), a
55
- // Commodore 64 title's drive (@moonbase-machines/c64/saves.js, the drive card.js puts on the bus)
56
- title.disk = withSaves(disk, kept);
57
- if (disk.kind === 'c64') keepDrive(title.drive = new Drive(Uint8Array.from(atob(disk.image), ch => ch.charCodeAt(0))), kept);
58
- const engine = makeEngine(title, scripts);
59
- if (title.files) keepSaves(disk, title.files, kept);
60
- const queue = [];
61
- // The view, and every event it has drawn, so that it can be drawn again from the start when
62
- // the old style text is turned on or off. The choice is kept in this browser.
63
- const oldStyleBox = $('old-style'), history = [];
64
- const remember = { get() { try { return localStorage.getItem('remake-old-style') === '1'; } catch { return false; } }, set(v) { try { localStorage.setItem('remake-old-style', v ? '1' : '0'); } catch {} } };
65
- oldStyleBox.checked = remember.get() || new URLSearchParams(location.search).has('old-style'); // ?old-style, for a screenshot
66
- let onView = null; // (the mini view's, given each view made)
67
- const makeView = () => { svg.replaceChildren(); const v = disk.kind === 'c64' ? new C64View(svg, { engine, chargen }) : title.engine === 'card' ? new CardView(svg, { engine, inset: game.mini ? title.inset : null }) : new View(svg, { disk, volume: Object.keys(disk.volumes)[0], engine, extra }); v.oldStyle = oldStyleBox.checked; onView?.(v); return v; };
68
- let view = makeView();
69
- engine.on(e => queue.push(view.snapshot({ ...e })));
70
- // The mini view, where the page gives one (`mini`) and the title has a place for it (its title.js `inset`): Off,
71
- // Live or Pictures, the choice kept in this browser; shown in the band at the top of the screen while the band is
72
- // there. A picture only: the game's screen otherwise as it is
73
- const miniBox = $('mini');
74
- if (title.inset && game.mini) {
75
- miniBox.parentElement.hidden = false;
76
- try { miniBox.value = localStorage.getItem('remake-mini') ?? ''; } catch {}
77
- const { top, bottom, view: [left, right] } = title.inset, at = document.createElement('div');
78
- at.className = 'mini'; at.hidden = true;
79
- Object.assign(at.style, { left: `${left / 5.6}%`, top: `${top / 3.84}%`, width: `${(right - left) / 5.6}%`, height: `${(bottom - top + 1) / 3.84}%` });
80
- svg.parentElement.append(at);
81
- const mini = await game.mini(at, { memory: () => engine.mem });
82
- // (each view's painting followed by the mini view shown or not, as the band is there or not)
83
- onView = v => { v.insetOn = !!miniBox.value; const prompts = v.prompts.bind(v); v.prompts = () => { prompts(); at.hidden = !(miniBox.value && v.banded); }; };
84
- onView(view);
85
- const show = () => { view.insetOn = !!miniBox.value; mini.mode(miniBox.value); view.prompts(); };
86
- miniBox.addEventListener('change', () => { try { localStorage.setItem('remake-mini', miniBox.value); } catch {} show(); });
87
- show();
88
- }
89
- oldStyleBox.addEventListener('change', () => {
90
- remember.set(oldStyleBox.checked);
91
- view = makeView();
92
- for (const e of history) view.apply(e);
93
- view.prompts();
94
- });
95
-
96
- // the sound: the original's are off until the teacher turns them on, at the engine's sound flag.
97
- // A title that keeps the original's time (a card title's, Engine.time) clicks the speaker as
98
- // the original did, and the clicks are played as the emulator plays them (audio.js), the
99
- // box the sound's on and off.
100
- const speaker = engine.timed ? new Speaker() : null;
101
- if (speaker) soundBox.checked = true;
102
- const hear = () => { if (disk.kind !== 'c64') speaker?.feed(soundBox.checked ? engine.clicks : [], engine.cycles); engine.clicks.length = 0; };
103
- const V = title.map?.vars ?? { sound: 0, soundOn: 0 };
104
- let audio = null;
105
- const tone = (from, to, ms) => {
106
- if (!(engine.mem[V.sound] & V.soundOn)) return;
107
- audio ??= new AudioContext();
108
- const o = audio.createOscillator(), g = audio.createGain(), t = audio.currentTime;
109
- o.type = 'square'; o.frequency.setValueAtTime(from, t); o.frequency.linearRampToValueAtTime(to, t + ms / 1000);
110
- g.gain.setValueAtTime(0.06, t); g.gain.linearRampToValueAtTime(0, t + ms / 1000);
111
- o.connect(g).connect(audio.destination); o.start(t); o.stop(t + ms / 1000);
112
- };
113
- const pitch = p => 16000 / (p + 8);
114
- soundBox.addEventListener('change', () => { if (!speaker) engine.mem[V.sound] = soundBox.checked ? V.soundOn : 0; });
115
-
116
- // how long each step takes to be seen or heard, in milliseconds
117
- const PACE = { sound: 220, beep: 60, 'flash-box': 60, flash: 250, object: 30, 'object-keep': 30 };
118
- const pace = () => game.pace ?? 1; // (the original's waits, a part of each: 1 all, 0 none; read at each wait)
119
- let drawing = false;
120
- const sleep = ms => new Promise(r => setTimeout(r, ms));
121
- async function runs() {
122
- if (drawing) return;
123
- drawing = true;
124
- try {
125
- for (;;) {
126
- const r = engine.run();
127
- hear();
128
- // the original took time here, a disk's read or a wait it counted out: the screen as it
129
- // is, for that long (card.js)
130
- if (r === 'pace') {
131
- for (const e of queue.splice(0)) { history.push(e); view.apply(e); }
132
- view.prompts(); if (!quick && pace()) await sleep(engine.delay * pace()); engine.delay = 0;
133
- continue;
134
- }
135
- engine.delay = 0;
136
- while (queue.length) {
137
- const e = queue.shift();
138
- history.push(e);
139
- view.apply(e);
140
- if (quick) continue; // (at full speed: no sound, no wait)
141
- if (e.step === 'sound') { const [end, start] = e.words.split(' ').map(h => parseInt(h, 16)); tone(pitch(start), pitch(end), 180); }
142
- if (e.step === 'beep') tone(pitch(e.a & 31) * 2, pitch(e.a & 31) * 2, 50);
143
- if (!pace()) continue; // (none of the original's waits: its sounds still)
144
- if (PACE[e.step]) await sleep(PACE[e.step] * pace());
145
- if (e.step === 'pause') await sleep(e.ms * pace()); // a delay the original counted out
146
- }
147
- view.prompts();
148
- status.textContent = r === 'end' ? 'The end.' : 'Waiting for a key.';
149
- if (!engine.keys.length || r === 'end') break;
150
- }
151
- } catch (err) { fail(err); }
152
- drawing = false;
153
- }
154
-
155
- // keys as the Apple's keyboard gave them
156
- // (a Commodore 64's in PETSCII: its cursor keys, DEL and RUN/STOP, and its letters capitals)
157
- const KEYS = disk.kind === 'c64'
158
- ? { ArrowLeft: 157, ArrowRight: 29, ArrowDown: 17, ArrowUp: 145, Enter: 13, Backspace: 20, Delete: 20, Escape: 3, ' ': 32 }
159
- : { ArrowLeft: 8, ArrowRight: 21, ArrowDown: 10, ArrowUp: 11, Enter: 13, Backspace: 127, Delete: 127, Escape: 27, ' ': 32 };
160
- let quick = false;
161
- const press = k => { engine.press(k); audio?.resume(); speaker?.start(); if (disk.kind !== 'c64') runs(); };
162
- // (a page that follows the game, the island's, given it: the engine, its memory, a key pressed as the
163
- // player presses one, and `quick(on)`, the game at full speed, none of its waits, to jump ahead in it)
164
- game.watch?.({ engine, press, quick: on => { quick = on; } });
165
- // A game that goes on without waiting for keys, as Nature Park's truck: each turn of its loop is
166
- // a step `tick`, which takes a key or none. A turn passes every 100 milliseconds, as the
167
- // original's took, with the key pressed since the last or still held, as the Apple's keyboard
168
- // told the game a key was down.
169
- const ticking = () => engine.waitingOn?.words?.[0] === 'tick';
170
- let held = null, tapped = null;
171
- setInterval(() => {
172
- if (!ticking() || drawing || engine.keys.length) return;
173
- const k = tapped ?? held ?? -1; tapped = null;
174
- press(k);
175
- }, 100);
176
- window.addEventListener('keydown', e => {
177
- if (e.metaKey || e.altKey) return;
178
- const k = e.ctrlKey ? (/^[a-z]$/i.test(e.key) ? e.key.toUpperCase().charCodeAt(0) & 0x1f : undefined) : KEYS[e.key] ?? (e.key.length === 1 ? (disk.kind === 'c64' ? e.key.toUpperCase() : e.key).charCodeAt(0) : undefined);
179
- if (k === undefined) return;
180
- e.preventDefault();
181
- if (ticking()) { tapped = held = k; return; }
182
- // a Commodore 64's key held down in its keyboard's matrix, which its KERNAL scans and repeats
183
- // as long as it is held, as the original's did (@moonbase-machines/c64/kernal.js); a character typed only
184
- // with shift into the buffer
185
- const m = disk.kind === 'c64' && !e.ctrlKey && matrixOf(k);
186
- if (m) { if (!e.repeat) { engine.clock.held = { ...m, key: e.key }; audio?.resume(); speaker?.start(); } return; }
187
- if (!e.repeat && !game.claim?.(k)) press(k); // (the player's key offered to the page first: `claim`, main.js)
188
- });
189
- window.addEventListener('keyup', e => {
190
- held = null;
191
- const h = engine.clock?.held;
192
- if (h?.key === e.key) { if (h.seen) engine.clock.held = null; else h.up = true; }
193
- });
194
- svg.addEventListener('click', e => { const key = e.target.closest('.key')?.dataset.key; if (key) press(+key); });
195
- svg.focus();
196
- // A Commodore 64 title runs on the clock, as the original did, waiting for a key or not: a
197
- // sixtieth of a second of the original's cycles at a time (the engine's run stops at each 16 ms of
198
- // them), its raster and its interrupt at the time they come (card.js), and the screen
199
- // painted after each. And the SID, its writes played a sixtieth of a second at a time, as the
200
- // emulator plays a frame's (original.js).
201
- if (disk.kind === 'c64') {
202
- // a route the page gives (`route`, a workshop's pilots' route) played first as the pilots play it, each key when
203
- // the game waits for one, off the clock, so that it goes where it went on both sides; then the game goes on as
204
- // usual
205
- if (game.route) {
206
- const { route, name: routeName, keys: { expand, give, petscii, pollsAt } } = game.route;
207
- title.seed = route.seed; // (RND's numbers from the route's seed, as the pilots have them)
208
- if (route.printer && title.printer) { title.printer.on = true; $('printer').checked = true; }
209
- status.textContent = 'Playing the route ' + routeName + '.';
210
- // (a key behind the one pressed goes into the KERNAL's buffer, as the pilots hold a key)
211
- 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); } };
212
- const go = () => { let r; do { engine.delay = 0; r = engine.run(); } while (r === 'pace'); queue.length = 0; };
213
- go();
214
- for (const [i, [k]] of expand(route.keys).entries()) { give(side, k, petscii); engine.polls = pollsAt(route, i + 1) ?? undefined; go(); }
215
- engine.polls = undefined; view.prompts();
216
- }
217
- engine.sid = []; engine.clock = { irq: 0, ahead: [], since: 0 };
218
- let sid = null, from = null, sent = 0, stopped = false;
219
- status.textContent = 'Playing.';
220
- setInterval(() => {
221
- engine.delay -= 1000 / 60; // a load's time, or the last run's
222
- if (!stopped && !engine.ended && engine.delay < 16) {
223
- engine.delay = Math.max(0, engine.delay);
224
- // a key of ?keys when the game is waiting for one, two seconds at least after the last, as
225
- // a player would press them (a wait the game counts out goes on by itself on the clock)
226
- 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; }
227
- try { engine.run(); queue.length = 0; view.prompts(); } catch (err) { stopped = true; fail(err); }
228
- if (engine.ended) status.textContent = 'The end.';
229
- }
230
- if (speaker?.node && soundBox.checked) {
231
- sid ??= new SID(speaker.rate); from ??= performance.now();
232
- const n = Math.floor((performance.now() - from) / 1000 * speaker.rate) - sent;
233
- 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]); }
234
- }
235
- engine.sid.length = 0;
236
- }, 1000 / 60);
237
- }
238
- // ?keys=space,space,right,return presses keys at the start, to go straight to a scene
239
- const start = new URLSearchParams(location.search).get('keys');
240
- if (start) for (const k of start.split(',')) { const a = { space: 32, return: 13, escape: 27, delete: 127, left: 8, right: 21, down: 10, up: 11, 'control-e': 5, 'control-t': 20, none: -1 }[k] ?? k.charCodeAt(0); if (disk.kind === 'c64') engine.clock.ahead.push({ 8: 157, 21: 29, 10: 17, 11: 145, 127: 20, 27: 3 }[a] ?? a); else engine.press(a); }
241
- if (disk.kind !== 'c64') runs();
242
- } catch (err) { fail(err); }
243
- }