Files
Halcyon/web/static/view-fleet.js
T
2026-09-20 14:16:41 +02:00

125 lines
5.9 KiB
JavaScript

// 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');
}
}
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.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 delivered to the station earns credits.'),
h('p', { class: 'pad' }, h('a', { class: 'btn small', href: '/manual.pdf', target: '_blank', rel: 'noopener' }, 'OPEN THE HC-33 MANUAL (PDF)')),
),
);
}