import type { Base } from '../sim/bases'; import type { HeatLevel } from '../sim/heat'; import { MISSION_SHAPE, type MissionType, type Offer } from '../sim/quests'; /** * What each job actually asks of you, in the words the people asking would use. * The point is that the four read as different *risks*, not different nouns. */ const BRIEFING: Record = { supply: 'drop the load and come back — quick work at the far end', retrieval: 'collect and carry it home; it pays for what arrives intact', intel: 'sit still and transmit, a long minute in the open', recon: 'eyes on the area — you will come back knowing the roads', }; /** * The quest board. Deliberately words, not numbers: the player is reading a * briefing, not a stat sheet. "Barricaded, four minutes ago" is a decision; * "heat 0.62" is a spreadsheet. */ const LEVEL_WORDS: Record = { clear: 'quiet', patrol: 'patrolled', barricade: 'barricaded', turret: 'fortified', }; function ago(seconds: number): string { if (seconds < 90) return 'just now'; const minutes = Math.round(seconds / 60); return `${minutes} min ago`; } function describe(offer: Offer): string { const { worst, unknownCount, stalest } = offer.intel; const total = offer.route.segments.length; if (unknownCount === total) return 'no one has been down that way'; if (unknownCount > 0) { return `${LEVEL_WORDS[worst]} as far as we know · ${unknownCount} of ${total} roads unscouted`; } return `${LEVEL_WORDS[worst]}${stalest === null ? '' : ` · last word ${ago(stalest)}`}`; } export function createBoard() { const el = document.createElement('div'); el.id = 'board'; document.body.appendChild(el); const style = document.createElement('style'); style.textContent = ` #board { position: fixed; left: 50%; bottom: 36px; transform: translateX(-50%); width: min(680px, calc(100vw - 32px)); font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; color: #dbe2e8; background: rgba(12,15,19,.92); border: 1px solid #2f3944; border-radius: 6px; padding: 14px 16px; display: none; } #board.open { display: block; } #board h2 { font-size: 13px; font-weight: 600; margin: 0 0 10px; color: #8fa3b5; } #board ol { list-style: none; margin: 0; padding: 0; } #board li { padding: 7px 0; border-top: 1px solid #202832; } #board .key { color: #4ea3ff; } #board .novel { color: #e2b04a; } #board .meta { color: #7d8b98; } #board footer { margin-top: 10px; color: #6d7883; } `; document.head.appendChild(style); return { show(base: Base, offers: Offer[]) { el.classList.add('open'); el.innerHTML = `

${base.name} — quest board

    ${offers .map( (offer, i) => `
  1. [${i + 1}] ${MISSION_SHAPE[offer.type].label} — ${offer.novel ? 'new target' : 'known target'} · ${(offer.route.length / 1000).toFixed(1)} km · ${offer.reward.toFixed(2)} parts
    ${BRIEFING[offer.type]}
    ${describe(offer)}
  2. `, ) .join('')}
press 1–${offers.length} to accept
`; }, /** * What a base has to say to somebody who turns up still carrying a job. * * There has to be a way out of a mission. A target can end up behind * concrete you cannot get round, and until now the only exit was holding R * for five seconds — which does not drop the job, it destroys the campaign. */ showActive(base: Base, label: string) { el.classList.add('open'); el.innerHTML = `

${base.name}

  1. You are still carrying: ${label}.
    Nobody here will take it off your hands, and nobody here will hold the board open while you decide.
`; }, hide() { el.classList.remove('open'); }, get isOpen() { return el.classList.contains('open'); }, }; }