The remaster: a bridge, two bands, and a real modem
Adds the cel-shaded skin, the score and the sound, and puts the skin machinery back so the toggle under the frame has somewhere to go. The remaster is a bridge rather than a paper column: the ship down the left as meters and pips, a cel-shaded sensor grid in the middle, the log down the right. Both panels are devices -- break the short range sensors and the sector grid goes dark, break the computer and the chart does, and the chart draws what has been scanned rather than the galaxy as it is. Scans are drawn instead of printed, so the log gets the sentences and stops being eight rows of ASCII squeezed into a strip. One theme, two arrangements, so switching skin re-scores the piece rather than changing the record: brass, strings and timpani for the remaster, the same eight bars as a three-channel chiptune for 1971. Written in the idiom rather than quoted from it, for the same reason the game is not called what you expect -- NOTICE.md now argues both. Effects are the ship's: warp, beams, torpedo, hit, explode, klaxon, dock, and the print head on paper only. The boot dials in for real: Bell 103 at the real frequencies, which is what a 110-baud acoustic coupler sounded like and not the modem noise everybody remembers. The synth engine is the siblings', unforked. Tests cover the music the only way a test can: a mistyped note name does not throw, it silently removes a voice, so every step of every track is checked against the parser.
This commit is contained in:
+41
-16
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { DIAL_UP, synth } from '../audio/synth'
|
||||
import { MAX_CAPTAINS, MAX_NAME, START_ENERGY, START_TORPEDOES } from '../game/constants'
|
||||
import { comparePatrols, outcomeText } from '../game/engine'
|
||||
import type { Score } from '../game/highscores'
|
||||
@@ -21,36 +22,41 @@ import { Btn, Line } from './Frame'
|
||||
* The lemonade stand loads from cassette because that is how an Atari got its
|
||||
* programs. This game is eight years older than that machine and did not live
|
||||
* on a machine you owned at all -- it lived on a mainframe at the end of a
|
||||
* telephone line, which you called. So the boot is a call, and it is the same
|
||||
* gesture the cave game opens with for the same reason.
|
||||
* telephone line, which you called. So the boot is a real Bell 103 call, and
|
||||
* pushing the handset into the coupler is the gesture that unlocks the audio:
|
||||
* browsers want a gesture before they will make a sound, and that happens to
|
||||
* be exactly the gesture the game already wanted.
|
||||
*
|
||||
* There is no sound yet. When the remaster lands it brings the synth with it,
|
||||
* and this schedule is already shaped to be played rather than only printed.
|
||||
* The stages are printed as they happen, from the same schedule the audio is
|
||||
* built on, so what is on the paper is what is on the line. See DIAL_UP.
|
||||
*/
|
||||
const STAGES: [number, string][] = [
|
||||
[0, 'DIAL TONE'],
|
||||
[1.1, 'DIALLING'],
|
||||
[2.6, 'RINGING'],
|
||||
[4.2, 'ANSWER TONE'],
|
||||
[5.2, 'CARRIER -- 110 BAUD, FULL DUPLEX'],
|
||||
[6.0, 'LOGIN ACCEPTED'],
|
||||
[DIAL_UP.dialTone, 'DIALLING 555-1971'],
|
||||
[DIAL_UP.ringAt, 'RINGING'],
|
||||
[DIAL_UP.answerAt, 'ANSWER TONE 2225 HZ'],
|
||||
[DIAL_UP.answerAt + DIAL_UP.answer, 'CARRIER -- 110 BAUD, FULL DUPLEX'],
|
||||
]
|
||||
|
||||
const BOOT_TOTAL = 6.8
|
||||
|
||||
export function BootScreen({ onLoaded }: { onLoaded: () => void }) {
|
||||
const [dialling, setDialling] = useState(false)
|
||||
const [stage, setStage] = useState(-1)
|
||||
const stop = useRef<(() => void) | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!dialling) return
|
||||
stop.current = synth.dialUp()
|
||||
|
||||
const timers = STAGES.map(([at], i) => window.setTimeout(() => setStage(i), at * 1000))
|
||||
const done = window.setTimeout(onLoaded, BOOT_TOTAL * 1000)
|
||||
const done = window.setTimeout(() => {
|
||||
stop.current?.()
|
||||
onLoaded()
|
||||
}, DIAL_UP.total * 1000)
|
||||
|
||||
return () => {
|
||||
for (const t of timers) window.clearTimeout(t)
|
||||
window.clearTimeout(done)
|
||||
stop.current?.()
|
||||
}
|
||||
}, [dialling, onLoaded])
|
||||
|
||||
@@ -62,7 +68,13 @@ export function BootScreen({ onLoaded }: { onLoaded: () => void }) {
|
||||
<Line>THE PROGRAM IS NOT ON THIS MACHINE. IT IS ON SOMEBODY ELSE'S.</Line>
|
||||
<Line className="dim">PUT THE HANDSET IN THE COUPLER AND CALL IT.</Line>
|
||||
<Line> </Line>
|
||||
<Btn kind="primary" onClick={() => setDialling(true)}>
|
||||
<Btn
|
||||
kind="primary"
|
||||
onClick={() => {
|
||||
synth.ensure()
|
||||
setDialling(true)
|
||||
}}
|
||||
>
|
||||
DIAL
|
||||
</Btn>
|
||||
</div>
|
||||
@@ -150,7 +162,13 @@ export function InstructionsScreen({ onDone }: { onDone: () => void }) {
|
||||
|
||||
// ------------------------------------------------------------------ setup
|
||||
|
||||
export function SetupScreen({ onStart }: { onStart: (names: string[]) => void }) {
|
||||
export function SetupScreen({
|
||||
onStart,
|
||||
onBlip,
|
||||
}: {
|
||||
onStart: (names: string[]) => void
|
||||
onBlip: () => void
|
||||
}) {
|
||||
const [names, setNames] = useState<string[]>([''])
|
||||
|
||||
const set = (i: number, v: string) =>
|
||||
@@ -177,7 +195,14 @@ export function SetupScreen({ onStart }: { onStart: (names: string[]) => void })
|
||||
<Line> </Line>
|
||||
<div className="prompt-row">
|
||||
{names.length < MAX_CAPTAINS && (
|
||||
<Btn onClick={() => setNames((n) => [...n, ''])}>ONE MORE</Btn>
|
||||
<Btn
|
||||
onClick={() => {
|
||||
onBlip()
|
||||
setNames((n) => [...n, ''])
|
||||
}}
|
||||
>
|
||||
ONE MORE
|
||||
</Btn>
|
||||
)}
|
||||
<Btn kind="primary" onClick={() => onStart(names)}>
|
||||
CAST OFF
|
||||
|
||||
Reference in New Issue
Block a user