@moonbase-machines/player 0.0.0-stage → 0.9.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/LICENSE +21 -0
- package/README.md +40 -2
- package/audio.js +54 -0
- package/bin/moonbase-compile.mjs +23 -0
- package/bin/moonbase-play.mjs +81 -0
- package/data.js +39 -0
- package/disk/dialogs.js +101 -0
- package/disk/disk.js +57 -0
- package/disk/local.js +28 -0
- package/disk/memory.js +13 -0
- package/dist/moonbase-player.js +33 -0
- package/dist/moonbase-player.js.map +7 -0
- package/index.js +28 -0
- package/modal.js +68 -0
- package/original.js +200 -0
- package/package.json +25 -4
- package/parts.js +33 -0
- package/player.css +48 -0
- package/remake/c64-view.js +30 -0
- package/remake/card-view.js +53 -0
- package/remake/card.js +526 -0
- package/remake/cards.js +47 -0
- package/remake/compile.js +179 -0
- package/remake/engine.js +461 -0
- package/remake/events.js +79 -0
- package/remake/layout.js +144 -0
- package/remake/lift.js +197 -0
- package/remake/load.js +55 -0
- package/remake/main.js +51 -0
- package/remake/problems.js +43 -0
- package/remake/raster.js +72 -0
- package/remake/scenes.js +243 -0
- package/remake/script.js +60 -0
- package/remake/vector.js +363 -0
- package/remake/view.js +479 -0
- package/test/disk.test.js +65 -0
- package/test/load.test.js +56 -0
package/remake/vector.js
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
// The routines of a vector engine title, for the remake engine, as its title.js `map` names them: what each step
|
|
2
|
+
// of a scene does. Each gives the event the original's routine gives
|
|
3
|
+
// (events.js), does what the original did to the registers, the flags and the memory that the
|
|
4
|
+
// scenes read, and paints the original's screen for the scenes that read it back. The rules for
|
|
5
|
+
// the keys, the random numbers and the waits are the original's, from PART0 (docs in
|
|
6
|
+
// @moonbase-machines/vector's README).
|
|
7
|
+
import { describe, result, TAKES, nothing } from './events.js';
|
|
8
|
+
import { helpers } from './problems.js';
|
|
9
|
+
import { tables, carry } from '@moonbase-machines/vector/objects.js';
|
|
10
|
+
import { pictures } from '@moonbase-machines/vector/pictures.js';
|
|
11
|
+
import { width, useFont } from '@moonbase-machines/vector/font.js';
|
|
12
|
+
import { Raster } from './raster.js';
|
|
13
|
+
import { Files } from '@moonbase-machines/apple2e/mli.js';
|
|
14
|
+
import { layout, resolve, PANELS, textLayout, textPen, textPage } from './layout.js';
|
|
15
|
+
|
|
16
|
+
// A vector title's map (its title.js `map`): `volume`, its disk's volume; `objects`, the routine that sets the table
|
|
17
|
+
// of objects; `seed` ({ at, used }), where the random routine keeps its seed and the byte that is $FF until its first
|
|
18
|
+
// call; `menu`, the Main Menu, which the key reader jumps to on control-E; `visit` ({ at, kept, back }), a way to its
|
|
19
|
+
// menus instead, the screen kept and put back (Nature Park's); `keyPanel`, the arrows' panel; `vars`, where the
|
|
20
|
+
// engine keeps what the remake reads (`zero`, how far up the zero page its routines' inputs are; the screen's
|
|
21
|
+
// layout; the difficulty; practice; repeats; the sound flag, on with its `soundOn` bit); `loads`, where its code
|
|
22
|
+
// files load when its catalog has lost it; `askClearsCarry`, `countInX`; `modern`, characters a modern font draws
|
|
23
|
+
// otherwise; `routines`, its engine's routines by address, each the name of its step; `problems`, each problem's own
|
|
24
|
+
// routines that draw, by the problem's number.
|
|
25
|
+
|
|
26
|
+
// A game's code files, PART0, the parts and the problems, each with the address it loads at: the map's `loads`, by
|
|
27
|
+
// the file's name or its name without the digit, or else the catalog's
|
|
28
|
+
export function codeFiles(map, volume) {
|
|
29
|
+
const loads = map?.loads ?? {};
|
|
30
|
+
return Object.entries(volume).filter(([n]) => /^(PART|PROB)\d$/.test(n))
|
|
31
|
+
.map(([name, f]) => ({ name, bytes: Uint8Array.from(atob(f.data), c => c.charCodeAt(0)), origin: loads[name] ?? loads[name.slice(0, -1)] ?? f.aux }));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const KEY = { escape: 27, space: 32, return: 13, delete: 127, left: 8, right: 21, down: 10, up: 11, menu: 5 };
|
|
35
|
+
|
|
36
|
+
// the inputs of a step: numbers, a register's name, or an address for the table of objects
|
|
37
|
+
function inputs(e, name, args) {
|
|
38
|
+
const takes = TAKES[name] || '', fixed = [];
|
|
39
|
+
let k = 0;
|
|
40
|
+
if (name === 'panel') fixed.push(args[k++]);
|
|
41
|
+
const regs = { a: e.a, x: e.x, y: e.y };
|
|
42
|
+
if (name === 'objects' && args[0]?.startsWith?.('$')) { const t = parseInt(args[k++].slice(1), 16); regs.x = t & 0xff; regs.y = t >> 8; }
|
|
43
|
+
else for (const r of takes) {
|
|
44
|
+
const v = args[k++];
|
|
45
|
+
if (v === undefined || v === r) continue;
|
|
46
|
+
regs[r] = typeof v === 'object' ? v.string.charCodeAt(0) | 0x80 : v.startsWith('$') ? e.mem[parseInt(v.slice(1), 16)] : +v;
|
|
47
|
+
}
|
|
48
|
+
e.a = regs.a & 0xff; e.x = regs.x & 0xff; e.y = regs.y & 0xff;
|
|
49
|
+
return { fixed, rest: args.slice(k) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A step that takes a key: its event once, then it waits until a key comes. Control-E, at any
|
|
53
|
+
// key the original read, went to the Main Menu and dropped everything called.
|
|
54
|
+
function waiting(e, step, name, fixed, words, take) {
|
|
55
|
+
if (e.waitingOn !== step) { e.emit(describe(name, fixed, e, words)); e.waitingOn = step; e.asked = null; }
|
|
56
|
+
for (;;) {
|
|
57
|
+
const k = e.key();
|
|
58
|
+
if (k === undefined) return 'wait';
|
|
59
|
+
if (k === KEY.menu && e.title.menu) e.leave(...e.title.menu);
|
|
60
|
+
if (e.title.visit?.[k]) { e.visit(k); return 'wait'; }
|
|
61
|
+
if (take(k)) { e.waitingOn = null; return; }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// $6415 calls the problem's own routines as a choice is made and taken back ($F729 and $F74B,
|
|
66
|
+
// then $F776 and $F6CE, in PROB2): drawing that happens inside the ask. The problem gives them as
|
|
67
|
+
// generators in e.askHooks, which run to their end here, inside, so they are drawn and no step.
|
|
68
|
+
function quietly(e, run) {
|
|
69
|
+
if (!run) return;
|
|
70
|
+
e.inside++;
|
|
71
|
+
try { for (let r = run.next(); !r.done; r = run.next()) if (r.value === 'wait') throw new Error('a choice drawn inside the ask cannot wait'); }
|
|
72
|
+
finally { e.inside--; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The disk's objects, by the address of their table, for the carry the object routine leaves.
|
|
76
|
+
// `title.disk` is the title's disk from its game's data.
|
|
77
|
+
function objectsOf(title) {
|
|
78
|
+
if (title.objectTables || !title.disk) return title.objectTables;
|
|
79
|
+
const files = codeFiles(title.map, Object.values(title.disk.volumes)[0]);
|
|
80
|
+
return title.objectTables = new Map(tables(files, title.map.objects).map(t => [t.address, t.objects]));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The disk's pictures, by name, for the screen model
|
|
84
|
+
function picturesOf(title) {
|
|
85
|
+
if (title.pictureShapes || !title.disk) return title.pictureShapes;
|
|
86
|
+
const volume = Object.values(title.disk.volumes)[0], map = new Map();
|
|
87
|
+
for (const [name, f] of Object.entries(volume)) if (/^PICS\d$/.test(name)) for (const p of pictures(Uint8Array.from(atob(f.data), c => c.charCodeAt(0)))) map.set(p.name, p.shapes);
|
|
88
|
+
return title.pictureShapes = map;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// a picture's shapes by its name, as the disk has them, for a problem that works with them
|
|
92
|
+
export const pictureShapes = (title, name) => picturesOf(title)?.get(name.toUpperCase().replace(/_/g, ' '));
|
|
93
|
+
|
|
94
|
+
export function vectorRoutines(title) {
|
|
95
|
+
// where this engine keeps its variables, and its routines' inputs in the zero page
|
|
96
|
+
const V = title.map.vars, Z = z => V.zero + z;
|
|
97
|
+
useFont(title.font);
|
|
98
|
+
// what an object's routine leaves: the carry, and in the row its rows were counted in (zero
|
|
99
|
+
// page $02 from the engine's), the row past the object's bottom, which a span after it may take
|
|
100
|
+
const objectCarry = e => {
|
|
101
|
+
if (e.a < 2) return; // putting ground back ends another way; no scene is known to use its carry
|
|
102
|
+
const o = objectsOf(title)?.get(e.objectTable)?.find(o => o.number === e.a);
|
|
103
|
+
if (o) { e.c = carry(o, e.x); e.mem[Z(0x02)] = (e.y + o.height) & 0xff; }
|
|
104
|
+
};
|
|
105
|
+
// The original's screen, dot for dot (raster.js), which each step paints as the original's
|
|
106
|
+
// routine painted, for read-dot; and the text state that lasts from text to text.
|
|
107
|
+
const raster = title.raster ??= new Raster();
|
|
108
|
+
const pen = title.pen ??= { x: 0, y: 0, margin: 0, centre: false, spacing: 1, gap: 0, ink: 15, paper: 10 };
|
|
109
|
+
// The text screen, where the menus are: its characters, its pen, and whether it is showing.
|
|
110
|
+
// `show` ($5BA9) puts the graphics back.
|
|
111
|
+
title.text ??= { showing: false, pen: textPen(), page: textPage() };
|
|
112
|
+
const objectOf = (e, n) => objectsOf(title)?.get(e.objectTable)?.find(o => o.number === n);
|
|
113
|
+
const drawObject = (e, how) => { if (e.a < 2) return raster.restore(); const o = objectOf(e, e.a); if (o) raster.object(o, e.x, e.y, how); };
|
|
114
|
+
// a key cap at the pen, as $76F3 draws it: outlined in color 13 and filled with 1, four dots
|
|
115
|
+
// wider than the glyph or than four, and the glyph in 15 from a dot on, after a spacer that
|
|
116
|
+
// brings a narrow glyph to the cap's right; the pen is left two dots past the text's end
|
|
117
|
+
const cap = ch => {
|
|
118
|
+
const w = width(ch), x = pen.x, spacer = (w < 4 ? 4 - w : 0) + 1;
|
|
119
|
+
raster.rectangle(1, x, pen.y - 2, Math.max(4, w) + 4, 11); raster.outline(13, x, pen.y - 2, Math.max(4, w) + 4, 11); // ($5C83)
|
|
120
|
+
raster.glyph(ch, x + 1 + spacer, pen.y, 15, 1, 1, 8); // its text's lines are 8 high
|
|
121
|
+
pen.x = x + 1 + spacer + w + 1 + 2;
|
|
122
|
+
};
|
|
123
|
+
// the arrows' panel, all four caps (n 0) or the nth, as the ask draws it in its arrow mode
|
|
124
|
+
const keyPanel = n => {
|
|
125
|
+
const k = title.map.keyPanel;
|
|
126
|
+
if (!k) return;
|
|
127
|
+
const c = raster.get(2, 2), { x, y } = pen;
|
|
128
|
+
for (let r = k.top; r < k.top + k.rows; r++) raster.span(c, k.left, k.right, r);
|
|
129
|
+
k.caps.forEach(([cx, cy, ch], i) => { if (!n || n === i + 1) { pen.x = cx; pen.y = cy; cap(String.fromCharCode(ch & 0x7f)); } });
|
|
130
|
+
Object.assign(pen, { x, y });
|
|
131
|
+
};
|
|
132
|
+
// a text on the graphics screen, as $6CE2 paints it
|
|
133
|
+
const print = (e, words) => { for (const line of layout(resolve(words, e.mem), pen)) for (const c of line.chars) raster.glyph(c.ch, c.x, line.top, c.ink ?? line.ink, line.paper, line.tall ? 2 : 1, line.height, pen.spacing); };
|
|
134
|
+
// object n of the table set last, put as `how` ('whole', 'keep' or 'clear')
|
|
135
|
+
const put = (e, n, x, y, how) => { const o = objectOf(e, n); if (o) raster.object(o, x, y, how); };
|
|
136
|
+
// what a title's painters (titles/<title>/paint.js) paint the model with
|
|
137
|
+
title.model = { raster, pen, cap, keyPanel, print, put };
|
|
138
|
+
const simple = (name, draw) => (e, args, step) => {
|
|
139
|
+
let { fixed, rest } = inputs(e, name, args);
|
|
140
|
+
const words = rest.map(r => typeof r === 'object' ? r.string : r).join(' ');
|
|
141
|
+
e.emit(describe(name, fixed, e, rest.length ? words : undefined, z => e.mem[Z(z)]));
|
|
142
|
+
rest = rest.map(r => typeof r === 'object' ? r.string : r);
|
|
143
|
+
draw?.(e, fixed, rest);
|
|
144
|
+
};
|
|
145
|
+
// $641B: A $80 asks for an arrow, $B1 to $B9 a digit from 1, any other a letter from A. The
|
|
146
|
+
// key chosen is then confirmed with return, or taken back with delete or the left arrow.
|
|
147
|
+
// Gives X the key's place from 0, Y twice that, and A the key, its high bit set from 'A' up.
|
|
148
|
+
// Animal Rescue's leaves the carry set for a letter ($6572); Nature Park's clears it ($5A56).
|
|
149
|
+
// Nature Park's $58DA, ask-escape, also takes escape, at either key, and gives A 27 and the
|
|
150
|
+
// carry set, and X the keyboard's byte for it, $9B, as the key read left it ($5A58, a jump into
|
|
151
|
+
// the middle of a BIT that leaves only SEC and RTS).
|
|
152
|
+
const letterCarry = !title.map.askClearsCarry;
|
|
153
|
+
const asking = name => (e, args, step) => {
|
|
154
|
+
if (e.waitingOn !== step) inputs(e, name, args);
|
|
155
|
+
const mode = e.waitingOn === step ? e.askMode : e.a;
|
|
156
|
+
e.askMode = mode;
|
|
157
|
+
const arrows = [KEY.left, KEY.right, KEY.down, KEY.up];
|
|
158
|
+
if (mode === 0x80 && e.waitingOn !== step) keyPanel(0);
|
|
159
|
+
const r = waiting(e, step, name, [], undefined, k => {
|
|
160
|
+
if (name === 'ask-escape' && k === KEY.escape) { e.a = e.flags(27); e.x = 0x9b; e.c = 1; e.asked = null; return true; }
|
|
161
|
+
if (e.asked === null) {
|
|
162
|
+
let place = -1, key = k;
|
|
163
|
+
if (mode === 0x80) { place = arrows.indexOf(k); key = 0xdb + place; if (place >= 0) keyPanel(place + 1); }
|
|
164
|
+
else if (mode >= 0xb1 && mode < 0xba) { if (k >= 0x31 && k <= (mode & 0x7f)) place = k - 0x31; }
|
|
165
|
+
else { key = k & 0x5f; if (key >= 0x41 && key <= (mode & 0x5f & 0x7f)) { place = key - 0x41; key |= 0x80; } }
|
|
166
|
+
if (place >= 0) { e.asked = { place, key }; if (name === 'ask-mode') quietly(e, e.askHooks?.chose?.(place)); }
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
if (k === KEY.return) { e.x = e.asked.place; e.y = 2 * e.x; e.a = e.asked.key; e.c = letterCarry && e.a >= 0xc1 ? 1 : 0; return true; }
|
|
170
|
+
if (k === KEY.delete || k === KEY.left) { if (name === 'ask-mode') quietly(e, e.askHooks?.undo?.(e.asked.place)); if (mode === 0x80) keyPanel(0); e.asked = null; }
|
|
171
|
+
return false;
|
|
172
|
+
});
|
|
173
|
+
if (r !== 'wait') { e.mem[V.layout] = 0x22; e.emit(result(name, e)); } // $655E: panel 6, which sets $22
|
|
174
|
+
return r;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const routines = {
|
|
178
|
+
// graphics shown: $C01D reads with its high bit set, as a scene may test before it shows them
|
|
179
|
+
show: simple('show', e => { title.text.showing = false; e.mem[0xc01d] = 0x80; }),
|
|
180
|
+
// $573D: the text screen cleared ($7645) and shown ($5BCB), which the view draws and is no step
|
|
181
|
+
'text-screen': e => {
|
|
182
|
+
Object.assign(title.text, { showing: true, page: textPage() });
|
|
183
|
+
e.mem[0xc01d] = 0;
|
|
184
|
+
e.inside++; e.emit({ step: 'text-screen' }); e.inside--;
|
|
185
|
+
},
|
|
186
|
+
picture: simple('picture', (e, f, [name]) => { const shapes = picturesOf(title)?.get(name.toUpperCase().replace(/_/g, ' ')); if (shapes) raster.picture(shapes); }),
|
|
187
|
+
box: simple('box', e => raster.box(e.mem[V.layout], e.a, e.x, e.y)),
|
|
188
|
+
// a panel sets the layout it clears to, as the original's panels do ($6299 and the rest)
|
|
189
|
+
panel: simple('panel', (e, [n]) => {
|
|
190
|
+
const p = PANELS[n];
|
|
191
|
+
if (!p) return;
|
|
192
|
+
e.mem[V.layout] = p.layout;
|
|
193
|
+
raster.panel(p.rows[0], p.rows[1], p.fill);
|
|
194
|
+
for (const y of p.more ?? []) raster.whiteLines(y);
|
|
195
|
+
Object.assign(pen, { paper: p.paper, ink: p.ink, centre: true, spacing: 1, gap: p.gap, y: p.y });
|
|
196
|
+
}),
|
|
197
|
+
// A code's byte may be written {3:$63D8}, and a character {$4DA5}, for a byte the scene's code
|
|
198
|
+
// writes into its own text before printing it: it is read from memory when the text is shown.
|
|
199
|
+
// On the text screen the text goes into its characters, and the graphics stay as they are.
|
|
200
|
+
text: (e, args) => {
|
|
201
|
+
const words = args[0].string
|
|
202
|
+
.replace(/\{([0-9a-f]+):([^}]*)\}/g, (all, code, bytes) => '{' + code + ':' + bytes.split(',').map(b => b.startsWith('$') ? e.mem[parseInt(b.slice(1), 16)].toString(16) : b).join(',') + '}')
|
|
203
|
+
.replace(/\{\$([0-9A-Fa-f]{4})\}/g, (all, at) => String.fromCharCode(e.mem[parseInt(at, 16)] & 0x7f));
|
|
204
|
+
e.emit(describe('text', [], e, words));
|
|
205
|
+
if (title.text.showing) { for (const c of textLayout(resolve(words, e.mem), title.text.pen)) if (title.text.page[c.row]?.[c.col]) title.text.page[c.row][c.col] = { ch: c.ch, inverse: c.inverse }; return; }
|
|
206
|
+
print(e, words);
|
|
207
|
+
},
|
|
208
|
+
objects: simple('objects', e => { e.objectTable = e.x | e.y << 8; }),
|
|
209
|
+
// an object drawn leaves the carry the original's routine left ($6B0F), which a scene may add in
|
|
210
|
+
object: simple('object', e => { drawObject(e, 'whole'); objectCarry(e); }),
|
|
211
|
+
'object-keep': simple('object-keep', e => { drawObject(e, 'keep'); objectCarry(e); }),
|
|
212
|
+
// $6863: the clear color shows the ground, from a buffer of its own, and no ground is kept
|
|
213
|
+
'object-mode': simple('object-mode', e => drawObject(e, 'clear')),
|
|
214
|
+
// $76ED sets the pen to X and Y, and goes on into $76F3
|
|
215
|
+
'key-at': simple('key-at', e => { pen.x = e.x; pen.y = e.y; cap(String.fromCharCode(e.a & 0x7f)); }),
|
|
216
|
+
key: simple('key', e => cap(String.fromCharCode(e.a & 0x7f))),
|
|
217
|
+
'white-lines': simple('white-lines', e => raster.whiteLines(e.y)),
|
|
218
|
+
'fill-rows': simple('fill-rows', e => raster.rows(e.a, e.x, e.y, e.c)),
|
|
219
|
+
span: simple('span', e => raster.span(e.a, e.x, e.y, e.mem[Z(0x02)])),
|
|
220
|
+
// a dot, which returns with A its place in its group of seven times 16, and its color ($5B45)
|
|
221
|
+
dot: simple('dot', e => { raster.set(e.x, e.y, e.a); e.a = (e.x % 7) << 4 | e.a & 15; }),
|
|
222
|
+
line: simple('line', e => raster.line(e.mem[Z(0x11)], e.mem[Z(0x00)], e.mem[Z(0x02)], e.mem[Z(0x01)], e.mem[Z(0x03)])),
|
|
223
|
+
// $67B9 fills columns x to x + $0E, both ends, down $0F rows; $677E's four lines go from x to
|
|
224
|
+
// x + $0E and from y to y + $0F
|
|
225
|
+
rectangle: simple('rectangle', e => raster.rectangle(e.a, e.x, e.y, e.mem[Z(0x0e)] + 1, e.mem[Z(0x0f)])),
|
|
226
|
+
'rectangle-outline': simple('rectangle-outline', e => raster.outline(e.a, e.x, e.y, e.mem[Z(0x0e)] + 1, e.mem[Z(0x0f)] + 1)),
|
|
227
|
+
flash: simple('flash'),
|
|
228
|
+
// $5B4C: the color of a dot of the original's screen, from the screen model
|
|
229
|
+
'read-dot': (e, args) => { inputs(e, 'read-dot', args); e.emit(describe('read-dot', [], e)); e.a = e.flags(raster.get(e.x, e.y)); e.emit(result('read-dot', e)); },
|
|
230
|
+
'key-panel': simple('key-panel', e => keyPanel(e.a)), 'keep-span': simple('keep-span'), 'put-span': simple('put-span'), 'invert-span': simple('invert-span'),
|
|
231
|
+
button: simple('button'),
|
|
232
|
+
// a beep of 0 does nothing, and is no step (events.js)
|
|
233
|
+
beep: (e, args) => { inputs(e, 'beep', args); if (!nothing('beep', e.a)) e.emit(describe('beep', [], e)); },
|
|
234
|
+
// a sound's five bytes; a byte may be an address, for one the scene's code writes before it plays
|
|
235
|
+
sound: (e, args) => e.emit(describe('sound', [], e, args.map(b => b.startsWith('$') ? e.mem[parseInt(b.slice(1), 16)].toString(16) : b).join(' '))),
|
|
236
|
+
// PART3's own code, written at $E000, moves the picture a step to the left: drawn, and no step
|
|
237
|
+
scroll: e => { raster.scroll(); e.inside++; e.emit({ step: 'scroll' }); e.inside--; },
|
|
238
|
+
// Nature Park's PART3 at $9796: the dots from `left` to `right` of rows `top` to `bottom` copied
|
|
239
|
+
// `by` rows down, from the bottom row up, so the rows at the top stay to be copied again:
|
|
240
|
+
// drawing, and no step
|
|
241
|
+
'shift-down': (e, args) => {
|
|
242
|
+
const [left, right, top, bottom, by] = args.map(Number);
|
|
243
|
+
for (let r = bottom; r >= top; r--) for (let x = left; x <= right; x++) raster.set(x, r + by, raster.get(x, r));
|
|
244
|
+
e.inside++; e.emit({ step: 'shift-down', left, right, top, bottom, by }); e.inside--;
|
|
245
|
+
},
|
|
246
|
+
// a part's title marks where the original's loader starts a part
|
|
247
|
+
part: simple('part'),
|
|
248
|
+
|
|
249
|
+
// the waits, $5C23, $5C08 and $5C20: the space bar, or for the last escape too, which it
|
|
250
|
+
// returns with the carry set
|
|
251
|
+
wait: (e, args, step) => waiting(e, step, 'wait', [], undefined, k => k === KEY.space && (e.a = 32, e.c = 0, true)),
|
|
252
|
+
'wait-erase': (e, args, step) => waiting(e, step, 'wait-erase', [], undefined, k => k === KEY.space),
|
|
253
|
+
'wait-or-escape': (e, args, step) => {
|
|
254
|
+
const r = waiting(e, step, 'wait-or-escape', [], undefined, k => (k === KEY.space || k === KEY.escape) && (e.c = k === KEY.escape ? 1 : 0, e.a = k, true));
|
|
255
|
+
if (r !== 'wait') e.emit(result('wait-or-escape', e));
|
|
256
|
+
return r;
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
// $641B, the ask for a key, above
|
|
260
|
+
ask: asking('ask'),
|
|
261
|
+
// $6415: the same, and the problem draws the choice as it is made, which is left to the view
|
|
262
|
+
'ask-mode': asking('ask-mode'),
|
|
263
|
+
'ask-escape': asking('ask-escape'),
|
|
264
|
+
|
|
265
|
+
// $60C2: the seed times 259, divided by 65535 over n. The seed is at $6131 and $6130 says it is
|
|
266
|
+
// in use; a pilot sets it at the first call, as it does on the original.
|
|
267
|
+
random: (e, args) => {
|
|
268
|
+
const { fixed } = inputs(e, 'random', args);
|
|
269
|
+
e.emit(describe('random', fixed, e));
|
|
270
|
+
const n = e.a;
|
|
271
|
+
const { at, used } = title.map.seed;
|
|
272
|
+
if (e.mem[used] === 0xff && title.seed !== undefined) { e.mem[at] = title.seed & 0xff; e.mem[at + 1] = title.seed >> 8; }
|
|
273
|
+
let seed = e.mem[at] | e.mem[at + 1] << 8;
|
|
274
|
+
seed = (seed * 259) & 0xffff;
|
|
275
|
+
e.mem[at] = seed & 0xff; e.mem[at + 1] = seed >> 8; e.mem[used] = n;
|
|
276
|
+
const q = n ? Math.floor(0xffff / n) : 0xffff;
|
|
277
|
+
e.a = Math.floor(seed / q) & 0xff; e.c = 0;
|
|
278
|
+
e.emit(result('random', e));
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
multiply: (e, args) => { inputs(e, 'multiply', args); e.emit(describe('multiply', [], e)); const p = e.a * e.x; e.a = p & 0xff; e.x = p >> 8; e.emit(result('multiply', e)); },
|
|
282
|
+
divide: (e, args) => { inputs(e, 'divide', args); e.emit(describe('divide', [], e)); const d = e.x | e.y << 8, n = e.a || 1; e.a = Math.floor(d / n) & 0xff; e.x = d % n; e.emit(result('divide', e)); },
|
|
283
|
+
|
|
284
|
+
// a problem, written by hand for the remake (its title's problems.js). It runs
|
|
285
|
+
// as its own script or function and returns here; the carry comes back clear, as it does on
|
|
286
|
+
// the original when the problem loaded. Nature Park's ($4DC3) takes the number of times from X
|
|
287
|
+
// for a problem from the Problems menu, in place of the teacher's, until the problem is done.
|
|
288
|
+
problem: (e, args, step) => {
|
|
289
|
+
if (!e.problem) {
|
|
290
|
+
inputs(e, 'problem', args); e.emit(describe('problem', [], e)); e.mem[V.practice] = e.c ? 0x80 : 0;
|
|
291
|
+
e.repeats = e.mem[V.repeats];
|
|
292
|
+
if (title.map.countInX && e.c) e.mem[V.repeats] = e.x;
|
|
293
|
+
const run = title.problems?.[e.a];
|
|
294
|
+
if (!run) throw new Error('problem ' + e.a + ' is not written yet');
|
|
295
|
+
// the problem's file, where the loader puts it, for the tables a problem reads from it
|
|
296
|
+
const file = title.disk && codeFiles(title.map, Object.values(title.disk.volumes)[0]).find(f => f.name === 'PROB' + e.a);
|
|
297
|
+
if (file) e.mem.set(file.bytes, file.origin);
|
|
298
|
+
e.problem = run(helpers(e, routines));
|
|
299
|
+
}
|
|
300
|
+
if (!e.problem.next().done) return 'wait';
|
|
301
|
+
e.problem = null; e.c = 0; e.mem[V.repeats] = e.repeats; e.emit(result('problem', e));
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
// $5D66 and $5DA9: a key, as the title's own loops read it; A the key, the carry set for escape
|
|
305
|
+
'read-key': (e, args, step) => {
|
|
306
|
+
if (e.waitingOn !== step) { e.waitingOn = step; e.asked = null; }
|
|
307
|
+
const k = e.key();
|
|
308
|
+
if (k === undefined) return 'wait';
|
|
309
|
+
if (title.visit?.[k]) { e.visit(k); return 'wait'; }
|
|
310
|
+
e.waitingOn = null;
|
|
311
|
+
if (k === KEY.menu && title.menu) e.leave(...title.menu);
|
|
312
|
+
e.a = e.flags(k & 0x7f); e.c = k === KEY.escape ? 1 : 0;
|
|
313
|
+
e.emit(result('read-key', e));
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
// The loader's splash, before PART0 runs: TITLE, a double hi-res picture, until a key. The
|
|
317
|
+
// original's engine is not in yet, so it is no step; the view draws it.
|
|
318
|
+
splash: (e, args, step) => {
|
|
319
|
+
if (e.waitingOn !== step) { e.inside++; e.emit({ step: 'splash' }); e.inside--; e.waitingOn = step; }
|
|
320
|
+
if (e.key() === undefined) return 'wait';
|
|
321
|
+
e.waitingOn = null;
|
|
322
|
+
},
|
|
323
|
+
// Nature Park's $4C00: both halves of the graphics screen cleared to black, which is no step
|
|
324
|
+
'clear-screen': e => {
|
|
325
|
+
for (const row of raster.dots) row.fill(0);
|
|
326
|
+
e.inside++; e.emit({ step: 'clear-screen' }); e.inside--;
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
// A delay the original made by counting X and Y down, given as the milliseconds it took: no
|
|
330
|
+
// step, and nothing changes but the registers, both 0 when the count is done. The page waits.
|
|
331
|
+
pause: (e, [ms]) => { e.x = 0; e.y = 0; e.flags(0); e.inside++; e.emit({ step: 'pause', ms: +ms }); e.inside--; },
|
|
332
|
+
|
|
333
|
+
// End, on the Main Menu: ProDOS's quit, through $0200, and the title is over
|
|
334
|
+
quit: e => { e.ended = true; },
|
|
335
|
+
|
|
336
|
+
// a part of the title not written yet: the run stops here and says which
|
|
337
|
+
unwritten: (e, [what]) => { throw new Error('not written yet: ' + (what?.string ?? what)); },
|
|
338
|
+
|
|
339
|
+
// a jump or a call into PART0's own code, which the title's scripts have: the title's
|
|
340
|
+
// `entries` say where each address is
|
|
341
|
+
'goto-engine': (e, [at]) => { const to = title.entries?.[at]; if (!to) throw new Error('no script for ' + at); e.enter(...to); },
|
|
342
|
+
'call-engine': (e, [at]) => { const to = title.entries?.[at]; if (!to) throw new Error('no script for ' + at); e.stack.push({ name: e.name, pc: e.pc }); e.enter(...to); },
|
|
343
|
+
|
|
344
|
+
// the loader: part A's script, from its start, after the title has shown it
|
|
345
|
+
// (and, with the disk out of its drive, it comes back, as the loader's read does with its error)
|
|
346
|
+
'load-part': (e) => {
|
|
347
|
+
const volume = Object.keys(title.disk?.volumes ?? {})[0]?.toUpperCase();
|
|
348
|
+
if (title.files?.out?.has(volume)) { e.a = e.flags(0x45); e.c = 1; return; }
|
|
349
|
+
e.stack = []; e.enter('part' + e.a, undefined, true);
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
// Nature Park's settings written to its own disk through the loader ($08E9): ProDOS's
|
|
353
|
+
// SET_FILE_INFO, answered from the disks as @moonbase-machines/apple2e/mli.js answers the original's call: the
|
|
354
|
+
// write-protect error ($2B) with the tab on, no volume ($45) with the disk out of its drive,
|
|
355
|
+
// and the carry set for either; clear when it goes through
|
|
356
|
+
'write-settings': e => {
|
|
357
|
+
const files = title.files ??= new Files(title.disk), volume = Object.keys(title.disk.volumes)[0].toUpperCase();
|
|
358
|
+
const err = files.out?.has(volume) ? 0x45 : files.disks[volume] === 'protected' ? 0x2b : 0;
|
|
359
|
+
e.a = e.flags(err); e.c = err ? 1 : 0;
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
return routines;
|
|
363
|
+
}
|