146 lines
6.9 KiB
JavaScript
146 lines
6.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');
|
|
}
|
|
}
|
|
|
|
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)')),
|
|
),
|
|
);
|
|
}
|