const dispatchAction = (action, extra = {}) => { window.dispatchEvent(new CustomEvent('airship:action', { detail: { action, ...extra }, })); }; const overlay = document.createElement('div'); overlay.id = 'dock-overlay'; overlay.hidden = true; overlay.innerHTML = `
`; document.body.appendChild(overlay); const style = document.createElement('style'); style.textContent = ` #dock-overlay { position: fixed; inset: 0; z-index: 430; color: #f0d9a0; font-family: Georgia, 'Times New Roman', serif; } #dock-overlay[hidden] { display: none; } .dock-backdrop { position: absolute; inset: 0; background: rgba(2, 10, 15, .9); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); } .dock-shell { position: relative; width: min(980px, calc(100vw - 18px)); max-height: calc(100dvh - 18px); min-height: min(720px, calc(100dvh - 18px)); margin: max(9px, env(safe-area-inset-top)) auto; padding: 14px; overflow: auto; border: 1px solid rgba(196, 140, 54, .64); border-radius: 14px; background: radial-gradient(circle at 50% 0%, rgba(20, 72, 84, .33), transparent 36%), #06161e; box-shadow: 0 18px 60px rgba(0,0,0,.62); overscroll-behavior: contain; } .dock-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 10px 13px; border-bottom: 1px solid rgba(196, 140, 54, .34); } .dock-kicker { color: #adcfca; font: 700 10px/1 system-ui, sans-serif; letter-spacing: .18em; } .dock-header h1 { margin: 3px 0 2px; color: #fff0bd; font-size: clamp(26px, 5vw, 46px); line-height: .95; } .dock-header p { margin: 0; color: #9eb2b9; font: 600 11px/1.2 system-ui, sans-serif; } .dock-leave, .dock-nav button, .dock-section-head button { min-height: 44px; border: 1px solid #9a6a2b; border-radius: 9px; background: linear-gradient(#174a50, #0a2930); color: #ffe5a7; font: 700 11px/1 Georgia, serif; touch-action: manipulation; } .dock-leave { min-width: 112px; padding: 8px 14px; } .dock-resources { display: flex; flex-wrap: wrap; justify-content: center; gap: 7px; padding: 10px 0; } .dock-resources span { padding: 6px 11px; border: 1px solid rgba(196, 140, 54, .38); border-radius: 999px; background: rgba(3, 20, 27, .82); font-size: 11px; } .dock-resources strong { color: #fff0bd; } .dock-nav { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 2px 0 12px; } .dock-nav button { min-height: 52px; font-size: 13px; } .dock-content { min-height: 420px; } .dock-home { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; } .dock-service { min-height: 250px; padding: 18px; border: 1px solid rgba(194, 142, 61, .56); border-radius: 12px; background: linear-gradient(180deg, rgba(13, 63, 72, .86), rgba(5, 25, 32, .96)); color: #f0d9a0; text-align: left; touch-action: manipulation; } .dock-service strong { display:block; margin-bottom:8px; color:#fff1bc; font-size:22px; } .dock-service span { color:#aac0c6; font:600 12px/1.45 system-ui,sans-serif; } .dock-panel { display: grid; gap: 10px; } .dock-section-head { display:flex; align-items:center; justify-content:space-between; gap:10px; } .dock-section-head > div { display:grid; gap:2px; } .dock-section-head strong { color:#fff0bd; font-size:18px; } .dock-section-head span { color:#9fb3b9; font:600 10px/1.2 system-ui,sans-serif; } .dock-section-head button { min-width:76px; padding:7px 10px; } .dock-list { display:grid; gap:8px; } .dock-card, .dock-active-mission { display:grid; gap:5px; padding:11px 12px; border:1px solid rgba(190, 139, 59, .42); border-radius:10px; background:rgba(3, 21, 28, .88); } .dock-active-mission:empty { display:none; } .dock-card-head { display:flex; align-items:flex-start; justify-content:space-between; gap:10px; } .dock-card strong { color:#fff0bd; } .dock-card small, .dock-card p { margin:0; color:#9fb5bb; font:600 10px/1.35 system-ui,sans-serif; } .dock-card-actions { display:flex; flex-wrap:wrap; gap:7px; margin-top:3px; } .dock-card button, .dock-active-mission button { min-height:38px; padding:6px 12px; border:1px solid #9a6a2b; border-radius:8px; background:#103b3d; color:#f5dda0; font:700 10px/1 system-ui,sans-serif; touch-action:manipulation; } .dock-card button:disabled, .dock-active-mission button:disabled { opacity:.36; } .dock-market-row { display:grid; grid-template-columns:minmax(120px,1.2fr) .8fr .8fr auto; gap:8px; align-items:center; } .dock-price { color:#fff0bd; font:700 11px/1 system-ui,sans-serif; } .dock-tag { color:#98d3ab; font:700 9px/1 system-ui,sans-serif; } .dock-message { min-height:1.3em; padding-top:9px; color:#fff0bd; text-align:center; font:700 11px/1.2 system-ui,sans-serif; } body.dock-open #mobile-controls { visibility:hidden; } @media (max-width: 720px) { .dock-shell { width: calc(100vw - 8px); max-height: calc(100dvh - 8px); min-height: calc(100dvh - 8px); margin: 4px auto; padding: 8px; border-radius: 9px; } .dock-header h1 { font-size: clamp(24px, 8vw, 34px); } .dock-home { grid-template-columns:1fr; } .dock-service { min-height:105px; padding:13px; } .dock-service strong { font-size:18px; } .dock-content { min-height:0; } .dock-nav { position:sticky; top:-8px; z-index:2; padding:6px 0; background:#06161e; } .dock-market-row { grid-template-columns:1fr 1fr; } .dock-market-row .dock-card-actions { grid-column:1 / -1; } } `; document.head.appendChild(style); const fields = Object.fromEntries( [...overlay.querySelectorAll('[data-dock]')].map((el) => [el.dataset.dock, el]), ); const panels = [...overlay.querySelectorAll('[data-panel]')]; let latestState = null; const setPanel = (name = 'home') => { panels.forEach((panel) => { panel.hidden = panel.dataset.panel !== name; }); }; const actionButton = (label, action, extra = {}, disabled = false) => { const button = document.createElement('button'); button.type = 'button'; button.textContent = label; button.disabled = disabled; button.addEventListener('click', () => dispatchAction(action, extra)); return button; }; const renderMission = (state) => { const dock = state?.dock || {}; const active = dock.activeMission; fields['active-mission'].replaceChildren(); if (active) { const title = document.createElement('strong'); title.textContent = `ACTIVE: ${active.title}`; const progress = document.createElement('small'); progress.textContent = `${active.progressText} • Reward ¢${active.reward}`; fields['active-mission'].append(title, progress); if (active.ready) { fields['active-mission'].append(actionButton('CLAIM REWARD', 'complete-mission')); } } fields['mission-list'].replaceChildren(); for (const mission of dock.missionOffers || []) { const card = document.createElement('article'); card.className = 'dock-card'; const head = document.createElement('div'); head.className = 'dock-card-head'; const title = document.createElement('strong'); title.textContent = mission.title; const reward = document.createElement('span'); reward.className = 'dock-price'; reward.textContent = `¢${mission.reward}`; head.append(title, reward); const desc = document.createElement('p'); desc.textContent = mission.description; const actions = document.createElement('div'); actions.className = 'dock-card-actions'; actions.append(actionButton( active ? 'MISSION ACTIVE' : 'ACCEPT', 'accept-mission', { missionId: mission.id }, Boolean(active), )); card.append(head, desc, actions); fields['mission-list'].append(card); } }; const renderMarket = (state) => { const dock = state?.dock || {}; fields['market-list'].replaceChildren(); for (const item of dock.market || []) { const card = document.createElement('article'); card.className = 'dock-card dock-market-row'; const name = document.createElement('div'); const strong = document.createElement('strong'); strong.textContent = item.name; const tag = document.createElement('small'); tag.className = 'dock-tag'; tag.textContent = item.exported ? 'LOCAL EXPORT' : item.imported ? 'HIGH DEMAND' : 'MARKET'; name.append(strong, tag); const buy = document.createElement('span'); buy.className = 'dock-price'; buy.textContent = `BUY ¢${item.buyPrice}`; const sell = document.createElement('span'); sell.className = 'dock-price'; sell.textContent = `SELL ¢${item.sellPrice} • HOLD ${item.held || 0}`; const actions = document.createElement('div'); actions.className = 'dock-card-actions'; actions.append( actionButton('BUY 1', 'buy-trade-good', { goodId: item.goodId }, !item.canBuy), actionButton('SELL 1', 'sell-trade-good', { goodId: item.goodId }, !item.canSell), ); card.append(name, buy, sell, actions); fields['market-list'].append(card); } }; const render = (state) => { if (!state?.dock) return; latestState = state; const dock = state.dock; fields.port.textContent = dock.portName || 'DOCK'; fields['port-meta'].textContent = `${String(dock.portType || 'trade').toUpperCase()} • ${dock.portExports?.join(', ') || 'mixed goods'}`; fields.credits.textContent = state.progression?.credits ?? 0; fields.cargo.textContent = `${dock.cargoUsed || 0}/${dock.cargoCapacity || 0}`; fields.salvage.textContent = state.progression?.inventoryCount || 0; fields.mission.textContent = dock.activeMission ? dock.activeMission.type.toUpperCase() : 'NONE'; fields.message.textContent = String(state.status || '').replace(/\n/g, ' '); renderMission(state); renderMarket(state); }; const show = (state) => { render(state); setPanel('home'); overlay.hidden = false; document.body.classList.add('dock-open'); }; const hide = () => { overlay.hidden = true; document.body.classList.remove('dock-open'); }; for (const button of overlay.querySelectorAll('[data-dock-tab]')) { button.addEventListener('click', () => setPanel(button.dataset.dockTab)); } for (const button of overlay.querySelectorAll('[data-action="open-shipyard"]')) { button.addEventListener('click', () => dispatchAction('open-shipyard')); } overlay.querySelector('.dock-leave').addEventListener('click', () => dispatchAction('close-dock')); overlay.querySelector('.dock-backdrop').addEventListener('click', () => dispatchAction('close-dock')); window.addEventListener('airship:dock-open', (event) => show(event.detail)); window.addEventListener('airship:dock-state', (event) => render(event.detail)); window.addEventListener('airship:dock-hide', () => hide()); window.addEventListener('airship:dock-close', () => hide()); window.addEventListener('airship:dock-return', (event) => show(event.detail || latestState)); window.addEventListener('airship:hud-state', (event) => { // Keep the latest snapshot for fallback/return, but do not rebuild interactive // dock lists every frame. Replacing buttons between pointerdown and click // breaks touch interaction on mobile. latestState = event.detail; }); window.addEventListener('keydown', (event) => { if (event.key === 'Escape' && !overlay.hidden) dispatchAction('close-dock'); });