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'
import type { Captain, Patrol } from '../game/types'
import { isWin } from '../game/types'
import { COMMANDS } from './BridgeScreen'
import { Btn, Line } from './Frame'
/**
* Everything that is not the patrol itself. These are small enough that
* keeping them in one file makes the shape of the game easier to see than
* eight files would, and none of them holds a rule.
*/
// ------------------------------------------------------------------- boot
/**
* Dialling in.
*
* 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 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.
*
* 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'],
[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'],
]
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(() => {
stop.current?.()
onLoaded()
}, DIAL_UP.total * 1000)
return () => {
for (const t of timers) window.clearTimeout(t)
window.clearTimeout(done)
stop.current?.()
}
}, [dialling, onLoaded])
if (!dialling) {
return (
LONG RANGE SCANTHE PROGRAM IS NOT ON THIS MACHINE. IT IS ON SOMEBODY ELSE'S.PUT THE HANDSET IN THE COUPLER AND CALL IT. {
synth.ensure()
setDialling(true)
}}
>
DIAL
YOUR ORDERSFIND EVERY RAIDER IN THE GALAXY AND DESTROY IT BEFORE YOUR TIME RUNS OUT.THE GALAXYEIGHT BY EIGHT QUADRANTS. EACH QUADRANT IS EIGHT BY EIGHT SECTORS.A LONG RANGE SCAN READS THREE DIGITS PER QUADRANT: RAIDERS, STARBASES, STARS.SO 205 IS TWO RAIDERS, NO BASE AND FIVE STARS. READING THOSE IS THE GAME.THE SHIP
{START_ENERGY} UNITS OF ENERGY AND {START_TORPEDOES} TORPEDOES. ENERGY MOVES YOU, FIRES
THE BEAMS AND FILLS THE SHIELDS -- IT IS ALL THE SAME POOL.
EIGHT DEVICES CAN BE BROKEN BY A HIT. DAMAGE CONTROL WORKS ONLY WHILE YOU FLY.MOOR NEXT TO A STARBASE AND YOU ARE FULL AGAIN. THERE ARE NOT MANY OF THEM.STEERINGCOURSES RUN 1 TO 9 ANTICLOCKWISE. 1 IS ALONG THE ROW, 3 IS UP, 5 IS BACK, 7 IS
DOWN, AND 9 IS 1 AGAIN. WARP 1 IS EIGHT SECTORS.THE COMMANDS
{COMMANDS.map(([id, what]) => (
{id} {' '} {what}
))}
UNDERSTOOD
WHO IS FLYING?EACH CAPTAIN GETS THEIR OWN GALAXY. YOU TAKE IT IN TURNS.
{names.map((name, i) => (
set(i, e.target.value)}
/>
))}
{names.length < MAX_CAPTAINS && (
{
onBlip()
setNames((n) => [...n, ''])
}}
>
ONE MORE
)}
onStart(names)}>
CAST OFF
)
}
// ----------------------------------------------------------------- report
/** The end of one captain's patrol, before the next one sits down. */
export function ReportScreen({
patrol,
captain,
moreToCome,
onNext,
}: {
patrol: Patrol
captain: Captain
moreToCome: boolean
onNext: () => void
}) {
const won = isWin(patrol.outcome)
return (
{patrol.outcome ? outcomeText(patrol.outcome) : ''}
{captain.name} — {captain.killed} DESTROYED, {captain.daysLeft.toFixed(1)} DAYS LEFT,{' '}
{captain.torpedoes} TORPEDO{captain.torpedoes === 1 ? '' : 'ES'}
{patrol.raidersLeft} RAIDER{patrol.raidersLeft === 1 ? '' : 'S'} STILL OUT THERE.
{moreToCome ? 'NEXT CAPTAIN' : 'THAT IS THAT'}
{anyWon ? 'THE FLEET IS SATISFIED.' : 'THE RAIDERS ARE STILL OUT THERE.'}
{ranked.map((c, i) => (
{String(i + 1).padStart(2)}. {c.name.padEnd(MAX_NAME)}{' '}
{String(c.killed).padStart(2)} KILLED {c.daysLeft.toFixed(1).padStart(5)} DAYS{' '}
{String(c.torpedoes).padStart(2)} TORP
))}
THE BOARDRANKED ON RAIDERS DESTROYED, THEN DAYS LEFT, THEN TORPEDOES.
{state === 'loading' && ASKING THE MACHINE...}
{state === 'error' && (
<>
{/* The game is entirely playable without the board, and says so
rather than pretending the patrol did not happen. */}
THE BOARD IS NOT ANSWERING. YOUR PATROL STILL COUNTED.TRY AGAIN
>
)}
{state === 'ready' && scores.length === 0 && NOBODY HAS REPORTED IN YET.}
{state === 'ready' &&
scores.map((s, i) => (
{String(i + 1).padStart(2)}. {s.name.padEnd(MAX_NAME)}{' '}
{String(s.killed).padStart(2)} {s.daysLeft.toFixed(1).padStart(5)}{' '}
{String(s.torpedoes).padStart(2)}
))}
BACK