@moonbase-machines/player 0.10.0 → 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.
@@ -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, control, $ }, { canvas, ctx, layer, speaker }) {
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
- control('<label><input type="checkbox" id="vectors"> Draw the pictures as vectors</label>');
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: vectorToggle.checked && layer.innerHTML ? m.owner : null });
78
+ screen(m, rgba, { cols, owner: vectors.value && layer.innerHTML ? m.owner : null });
77
79
  ctx.putImageData(image, 0, 0);
78
- layer.style.visibility = vectorToggle.checked && !text ? 'visible' : 'hidden';
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; }, soundOn: true,
91
+ sound(on) { sound = on; },
90
92
  engine: m,
91
93
  };
92
94
  }
@@ -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; }, soundOn: true,
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); its own
4
- // control on the options line, the old style text; the mini view where the page gives one and the title has a place
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
- g.control('<label><input type="checkbox" id="old-style"> Old style text</label>');
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 old style text is turned on or off. The choice is kept in this browser.
40
- const oldStyleBox = $('old-style'), history = [];
41
- 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 {} } };
42
- oldStyleBox.checked = remember.get() || new URLSearchParams(location.search).has('old-style'); // ?old-style, for a screenshot
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 }) : title.engine === '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 = oldStyleBox.checked; onView?.(v); return v; };
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, the choice kept in this browser; shown in the band at the top of the screen while the band is
49
- // there. A picture only: the game's screen otherwise as it is
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
- miniBox.parentElement.hidden = false;
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 = !!miniBox.value; const prompts = v.prompts.bind(v); v.prompts = () => { prompts(); at.hidden = !(miniBox.value && v.banded); }; };
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 = !!miniBox.value; mini.mode(miniBox.value); view.prompts(); };
63
- miniBox.addEventListener('change', () => { try { localStorage.setItem('remake-mini', miniBox.value); } catch {} show(); });
63
+ const show = () => { view.insetOn = !!choice.value; mini.mode(choice.value); view.prompts(); };
64
+ choice.on(show);
64
65
  show();
65
66
  }
66
- oldStyleBox.addEventListener('change', () => {
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 are off until the teacher turns them on, at the engine's sound flag.
74
- // A title that keeps the original's time (a card title's, Engine.time) clicks the speaker as
75
- // the original did, and the clicks are played as the emulator plays them (audio.js), the
76
- // box the sound's on and off.
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 = !!speaker;
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
- // the Sound box: the speaker's clicks, where the title keeps the original's time; else the game's own sound flag
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.10.0",
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.10.0",
18
- "@moonbase-machines/c64": "0.10.0",
19
- "@moonbase-machines/charset": "0.10.0",
20
- "@moonbase-machines/mos6502": "0.10.0",
21
- "@moonbase-machines/vector": "0.10.0"
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",
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 player'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.
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, backend, preview, words, screen, watch, claim, pace, route, mini,
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 player's disk (disk/disk.js); else the player opens it, `title/<slug>`, on `backend`
17
- // where the page gives one (the browser's own, unless it does)
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 makes what it draws on the stage and returns
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)?, soundOn?, quick(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), ?keys= at the start; route, the
42
- // page's route; start(), at the player's first gesture (sound may start only then); sound(on) and soundOn, the
43
- // Sound box; quick(on) the game at full speed; engine, what `watch` gives a page; diskImage(), the disk to download.
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'), soundBox = $('sound');
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
- // ?fixed: the title with its bugs fixed, for one that has such a version (its title.js `fixed`)
70
- const fixed = new URLSearchParams(location.search).has('fixed');
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 control on the options line, after the sound
100
- control(html) { $('printer').parentElement.insertAdjacentHTML('beforebegin', 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 box = $('printer'), sheet = $('paper'), shown = $('paper-box');
107
- try { box.checked = localStorage.getItem('printer') !== '0'; } catch {}
108
- box.parentElement.hidden = false;
109
- printer = g.printer = new Printer({ on: box.checked });
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
- // the choice of the fixed version, where there is one: the page again, with ?fixed or without
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 box as the engine starts it, and the engine told of each change
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 ?keys=space,space,right,return, to go straight to a
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 start = new URLSearchParams(location.search).get('keys');
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/settings.js ADDED
@@ -0,0 +1,82 @@
1
+ // A person's settings: what the player, its engines and a site's pages let a person choose (the sound, the printer,
2
+ // the old style text, a page's own choices), each declared where it acts, in one shape, and kept through the provider
3
+ // (provider.js) on a disk of the owner's, settings/<owner>, where the person's files go. The store is typed against
4
+ // the declarations: only a declared setting can be read or kept, every value checked against its type. Nothing here
5
+ // knows of an address: a site that lets its address set a setting for a visit reads it itself and gives the value.
6
+ //
7
+ // A declaration: { name, type, default, owner, words, labels?, title? }
8
+ // name its name, in the store's file (and for a site, in whatever sets it for a visit)
9
+ // type 'boolean', or an array of its fixed values, an enum
10
+ // default its value where nothing gives one
11
+ // owner whose it is: 'person' (the only owner yet; a parent's or teacher's later is another layer here, with
12
+ // nothing changed where the setting is declared)
13
+ // words what its control calls it; labels, an enum's values' words in order; title, a note on the control
14
+ //
15
+ // check(type, value) whether a value is of the type
16
+ // describe(type) the type in words, for saying what was refused
17
+ //
18
+ // const settings = settingsStore({ disks, visit, fixed, hidden })
19
+ // disks { owner: disk }, the owner's settings disk (settings/<owner>), opened by the provider; its one file,
20
+ // `settings`, the values by name
21
+ // visit def => a value for this visit, or undefined: the site's, from wherever it lets a visit set one (its
22
+ // address, say); checked here, a value not of the type refused and said once; never kept
23
+ // fixed { name: value }, what the page fixes, over everything; hidden, [name], what it does not offer
24
+ // settings.get(def) its value: the page's fixed one, else the visit's, else the kept one where it is of
25
+ // the type (one that is not falls back, said), else its default
26
+ // settings.set(def, value) kept, for the next visit; a promise, kept when stored (a value not of the type throws)
27
+ // settings.offered(def) whether its control is shown: not where the page hides it or fixes it
28
+ import { textBytes, bytesText } from './disk/disk.js';
29
+
30
+ const FILE = 'settings';
31
+
32
+ export function check(type, value) {
33
+ if (Array.isArray(type)) return type.includes(value);
34
+ if (type === 'boolean') return typeof value === 'boolean';
35
+ throw new Error('settings: no such type: ' + JSON.stringify(type));
36
+ }
37
+
38
+ export const describe = type => Array.isArray(type) ? 'one of ' + type.map(v => v === '' ? 'nothing' : v).join(', ') : 'on or off';
39
+
40
+ export function settingsStore({ disks, visit = () => undefined, fixed = {}, hidden = [] }) {
41
+ const said = new Set();
42
+ const say = (key, what) => { if (!said.has(key)) { said.add(key); console.warn(what); } };
43
+ const layers = Object.fromEntries(Object.entries(disks).map(([owner, disk]) => {
44
+ let kept = null;
45
+ try { kept = JSON.parse(bytesText(disk.read(FILE)) ?? '{}'); } catch {}
46
+ return [owner, { disk, kept: kept && typeof kept === 'object' && !Array.isArray(kept) ? kept : {} }];
47
+ }));
48
+ // (the declaration itself checked as it is first met: its owner has a store here, its default is of its type)
49
+ const seen = new Set();
50
+ const layer = def => {
51
+ const l = layers[def.owner];
52
+ if (!l) throw new Error(`settings: no store for the owner ${def.owner} (${def.name})`);
53
+ if (!seen.has(def.name)) { seen.add(def.name); if (!check(def.type, def.default)) throw new Error(`settings: ${def.name}'s default ${JSON.stringify(def.default)} is not ${describe(def.type)}`); }
54
+ return l;
55
+ };
56
+ return {
57
+ get(def) {
58
+ const l = layer(def);
59
+ if (def.name in fixed) {
60
+ if (!check(def.type, fixed[def.name])) throw new Error(`settings: ${def.name} fixed at ${JSON.stringify(fixed[def.name])}, not ${describe(def.type)}`);
61
+ return fixed[def.name];
62
+ }
63
+ const v = visit(def);
64
+ if (v !== undefined) {
65
+ if (check(def.type, v)) return v;
66
+ say('visit ' + def.name, `${def.name}: ${JSON.stringify(v)} for this visit refused: not ${describe(def.type)}`);
67
+ }
68
+ if (def.name in l.kept) {
69
+ if (check(def.type, l.kept[def.name])) return l.kept[def.name];
70
+ say('kept ' + def.name, `${def.name}: the kept ${JSON.stringify(l.kept[def.name])} is not ${describe(def.type)}; its default used`);
71
+ }
72
+ return def.default;
73
+ },
74
+ set(def, value) {
75
+ const l = layer(def);
76
+ if (!check(def.type, value)) throw new Error(`settings: ${def.name} cannot be ${JSON.stringify(value)}: not ${describe(def.type)}`);
77
+ l.kept[def.name] = value;
78
+ return l.disk.write(FILE, textBytes(JSON.stringify(l.kept)));
79
+ },
80
+ offered: def => !hidden.includes(def.name) && !(def.name in fixed),
81
+ };
82
+ }