// Fleet manifest with launch controls. import { api } from './api.js'; import { h, toast, customDialog, pad } from './dom.js'; import { statusOf } from './status.js'; export function fleetView(ctx) { const { ships, selected } = ctx.state; async function launch(ship) { let slots; try { slots = await api.slots(); } catch (e) { toast(`COULD NOT LOAD YOUR CODE SLOTS: ${e.message}`, 'err'); return; } const slotLabel = (s) => `${pad(s.slot, 2)} ${s.builtin ? `${s.name} (EXAMPLE)` : s.empty ? '(EMPTY)' : s.name}`; let remembered = 0; try { remembered = Number(localStorage.getItem('halcyon.launchSlot')); } catch { /* ignore */ } const usable = slots.filter((s) => !s.empty); const first = usable.find((s) => s.slot === remembered) ?? usable.find((s) => !s.builtin) ?? usable[0]; const sel = h('select', { id: 'launch-slot' }, slots.map((s) => h('option', { value: s.slot, disabled: s.empty, selected: s.slot === first.slot }, slotLabel(s)))); const status = h('p', { class: 'launch-status', role: 'status', 'aria-live': 'polite' }, 'CHECKING...'); const dlg = customDialog({ title: `LAUNCH SHIP #${ship.id}`, content: [ h('p', null, 'CHOOSE THE CODE SLOT THIS SHIP WILL FLY. IT IS ASSEMBLED NOW AND SEALED INTO THE SHIP.'), h('label', { for: 'launch-slot' }, 'CODE SLOT'), sel, status, h('p', { class: 'warn' }, 'AT LAUNCH THE PROGRAM IS FIXED. IT CANNOT BE CHANGED, RECALLED OR RESTARTED.'), ], confirmText: 'LAUNCH', danger: true, }); // Assemble the chosen slot as soon as it is chosen, so problems show before launch. let seq = 0; const check = async () => { const mine = ++seq; const n = Number(sel.value); dlg.confirm.disabled = true; status.className = 'launch-status'; status.textContent = 'ASSEMBLING...'; let text; let good = false; try { const src = await api.slot(n); const r = await api.assemble(src.source); if (r.size === 0) text = 'THIS SLOT HAS NO CODE.'; else if (!r.fits) text = `TOO LARGE: ${r.size} BYTES, THE LIMIT IS ${r.limit}.`; else { good = true; text = `${r.size} BYTES / ${r.instructions} INSTRUCTIONS: READY TO LAUNCH.`; } } catch (e) { text = `DOES NOT ASSEMBLE: ${e.message}`; } if (mine !== seq) return; // a newer choice superseded this one status.className = `launch-status ${good ? 'ok' : 'bad'}`; status.textContent = text; dlg.confirm.disabled = !good; }; sel.addEventListener('change', check); check(); if (!(await dlg.result)) return; const n = Number(sel.value); try { const r = await api.launch(ship.id, n); try { localStorage.setItem('halcyon.launchSlot', String(n)); } catch { /* ignore */ } toast(`SHIP #${ship.id} CLEARED FOR LAUNCH WITH SLOT ${n} (${r?.program_bytes ?? '?'} BYTES). IT FLIES ON THE NEXT DAILY RUN.`); await ctx.refresh(); } catch (e) { toast(`LAUNCH FAILED: ${e.message}`, 'err'); } } async function scrap(ship) { const dlg = customDialog({ title: `SCRAP SHIP #${ship.id}`, content: [ h('p', null, 'THE SHIP IS BROKEN UP ON THE NEXT DAILY RUN AND LEAVES THE BELT. A NEW SHIP WITH NO PROGRAM APPEARS ON YOUR DOCK.'), h('p', { class: 'warn' }, 'ITS FUEL AND PROGRAM ARE LOST, AND CONTAINERS IT TOWS ARE LEFT ADRIFT. THIS CANNOT BE UNDONE.'), ], confirmText: 'SCRAP', danger: true, }); if (!(await dlg.result)) return; try { await api.scrap(ship.id); toast(`SHIP #${ship.id} WILL BE SCRAPPED ON THE NEXT DAILY RUN. A NEW SHIP JOINS YOUR DOCK THEN.`); await ctx.refresh(); } catch (e) { toast(`SCRAP FAILED: ${e.message}`, 'err'); } } const btn = (label, onclick, opts = {}) => h('button', { class: `btn small ${opts.cls ?? ''}`, type: 'button', onclick, disabled: opts.disabled, title: opts.title }, label); function actions(s) { const out = []; if (s.status === 'inventory') { out.push(btn('CODE', () => ctx.selectShip(s.id, 'code'))); out.push(btn('LAUNCH', () => launch(s), { cls: 'primary', title: 'Choose a code slot and launch on the next daily run' })); } if (s.status === 'launching' || s.status === 'active') out.push(btn('UPLINK', () => ctx.selectShip(s.id, 'uplink'))); if (s.status === 'active') out.push(btn('SCRAP', () => scrap(s), { cls: 'danger', title: 'Break the ship up: a new ship replaces it on your dock' })); if (s.downlink_day >= 0) out.push(btn('DOWNLINK', () => ctx.selectShip(s.id, 'downlink'))); return out; } const rows = ships.map((s) => { const st = statusOf(s.status); return h('tr', { class: s.id === selected ? 'sel' : '' }, h('td', { 'data-label': 'SHIP' }, h('button', { class: 'linklike', type: 'button', onclick: () => ctx.selectShip(s.id) }, `#${s.id}`)), h('td', { 'data-label': 'STATUS' }, h('span', { class: `badge ${st.cls}`, title: st.hint }, st.label)), h('td', { 'data-label': 'PROGRAM' }, s.program_bytes ? `${s.program_bytes} BYTES` : h('span', { class: 'dim' }, 'NONE')), h('td', { 'data-label': 'LAST DOWNLINK' }, s.downlink_day >= 0 ? `DAY ${s.downlink_day}` : h('span', { class: 'dim' }, '--')), h('td', { class: 'actions', 'data-label': '' }, actions(s)), ); }); const step = (n, title, text) => h('li', null, h('span', { class: 'n' }, n), h('div', null, h('strong', null, title), ' ', text)); return h('div', { class: 'grid-2 fleet-grid' }, h('section', { class: 'panel' }, h('div', { class: 'panel-title' }, 'FLEET MANIFEST'), h('div', { class: 'panel-body flush' }, ships.length ? h('table', { class: 'manifest' }, h('thead', null, h('tr', null, ['SHIP', 'STATUS', 'PROGRAM', 'LAST DOWNLINK', ''].map((t) => h('th', { scope: 'col' }, t)))), h('tbody', null, rows)) : h('p', { class: 'empty' }, 'NO SHIPS ON RECORD.'), ), ), h('aside', { class: 'panel brief' }, h('div', { class: 'panel-title' }, 'MISSION BRIEF'), h('ol', { class: 'steps' }, step('1', 'WRITE', 'a flight program in the CODE editor and save it in one of your 16 code slots. Your ship cannot be flown by hand.'), step('2', 'TEST', 'it: assemble it in the editor, and improve it in as many slots as you like.'), step('3', 'LAUNCH', 'the ship and choose which slot it flies. It enters the belt on the next daily run and the program is fixed for good.'), step('4', 'TALK', 'to it once a day: 1 KB up, 1 KB down, over the wormhole link.'), ), h('p', { class: 'hint pad' }, 'The belt runs once a day. Ore is towed to the station in containers. A container left there is taken, and paid for.'), h('p', { class: 'pad' }, h('a', { class: 'btn small', href: '/manual.pdf', target: '_blank', rel: 'noopener' }, 'OPEN THE HC-33 MANUAL (PDF)')), ), ); }