import titleBannerUrl from './assets/shipyard-ui/title_banner.png?url'; import shipSelectorUrl from './assets/shipyard-ui/ship_selector.png?url'; import shipPreviewUrl from './assets/shipyard-ui/ship_preview.png?url'; import upgradesUrl from './assets/shipyard-ui/upgrades.png?url'; import activeUrl from './assets/shipyard-ui/active.png?url'; import closeButtonUrl from './assets/shipyard-ui/close_button.png?url'; const UPGRADE_SLOTS = [ { id: 'tuned-propellers', label: 'Engine Lv 1' }, { id: 'reinforced-keel', label: 'Hull Plating Lv 1' }, { id: 'expanded-hold', label: 'Cargo Hold Lv 1' }, { id: 'reinforced-rudder', label: 'Rudder Lv 1' }, ]; const dispatchAction = (action, extra = {}) => { window.dispatchEvent(new CustomEvent('airship:action', { detail: { action, ...extra }, })); }; const overlay = document.createElement('div'); overlay.id = 'shipyard-overlay'; overlay.hidden = true; overlay.innerHTML = `
`; document.body.appendChild(overlay); const style = document.createElement('style'); style.textContent = ` #shipyard-overlay { position: fixed; inset: 0; z-index: 450; color: #f4d99a; font-family: Georgia, 'Times New Roman', serif; touch-action: pan-y; } #shipyard-overlay[hidden] { display: none; } .shipyard-backdrop { position: absolute; inset: 0; background: rgba(3, 11, 16, .9); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); } .shipyard-shell { position: relative; width: min(1180px, calc(100vw - 24px)); max-height: calc(100dvh - 20px); margin: max(10px, env(safe-area-inset-top)) auto; padding: 10px; overflow: auto; border: 1px solid rgba(211, 157, 65, .6); border-radius: 14px; background: radial-gradient(circle at 50% 5%, rgba(20, 70, 83, .34), transparent 38%), linear-gradient(180deg, #061b24 0%, #041218 100%); box-shadow: 0 18px 60px rgba(0, 0, 0, .65); overscroll-behavior: contain; } .shipyard-header { position: relative; display: flex; justify-content: center; min-height: 70px; } .shipyard-title { width: min(595px, 76vw); height: auto; object-fit: contain; } .shipyard-close { position: absolute; top: 0; right: max(0px, env(safe-area-inset-right)); width: clamp(48px, 7vw, 72px); aspect-ratio: 1; padding: 0; border: 0; background: transparent; cursor: pointer; touch-action: manipulation; } .shipyard-close img { width: 100%; height: 100%; object-fit: contain; } .shipyard-resources { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 4px auto 10px; font-size: clamp(11px, 1.6vw, 15px); } .shipyard-resources span { min-width: 120px; padding: 7px 12px; border: 1px solid rgba(198, 145, 59, .42); border-radius: 999px; background: rgba(3, 20, 27, .88); text-align: center; } .shipyard-resources strong { color: #fff0bd; } .shipyard-grid { display: grid; grid-template-columns: minmax(170px, .72fr) minmax(260px, 1fr) minmax(360px, 1.2fr); gap: 10px; align-items: start; } .shipyard-selector img, .shipyard-preview, .shipyard-upgrades { display: block; width: 100%; height: auto; user-select: none; -webkit-user-select: none; } .shipyard-preview-wrap, .shipyard-upgrade-wrap { position: relative; } .shipyard-live-stats { position: absolute; left: 5%; right: 5%; bottom: 3%; display: grid; grid-template-columns: repeat(2, 1fr); gap: 4px 10px; padding: 8px 10px; border: 1px solid rgba(213, 162, 76, .48); border-radius: 8px; background: rgba(3, 18, 24, .86); font-size: clamp(10px, 1.25vw, 13px); text-shadow: 0 1px 2px #000; } .shipyard-live-stats strong { grid-column: 1 / -1; color: #fff0bd; letter-spacing: .12em; } .shipyard-upgrade-hotspots { position: absolute; left: 3.5%; right: 3.5%; top: 18%; bottom: 10%; display: grid; grid-template-columns: repeat(4, 1fr); gap: 2.5%; } .shipyard-upgrade-button { position: relative; border: 0; border-radius: 8px; background: transparent; cursor: pointer; touch-action: manipulation; } .shipyard-upgrade-button:not(:disabled):active { background: rgba(255, 214, 120, .12); } .shipyard-upgrade-button:disabled { cursor: default; } .shipyard-upgrade-state { position: absolute; left: 50%; bottom: 3px; transform: translateX(-50%); padding: 2px 5px; border-radius: 999px; background: rgba(4, 16, 21, .88); color: #ffe4a1; font: 700 clamp(8px, 1vw, 11px)/1 system-ui, sans-serif; white-space: nowrap; } .shipyard-upgrade-button.installed .shipyard-upgrade-state { color: #aef0b8; } .shipyard-upgrade-button.locked .shipyard-upgrade-state { color: #d8a8a0; } .shipyard-active-plate { position: absolute; left: 50%; bottom: -58px; width: min(255px, 55%); transform: translateX(-50%); } .shipyard-active-plate img { display: block; width: 100%; height: auto; } .shipyard-footer { display: flex; align-items: center; justify-content: center; gap: 12px; min-height: 72px; margin-top: 56px; } .shipyard-salvage { min-width: 150px; min-height: 44px; padding: 7px 14px; border: 1px solid #b88131; border-radius: 9px; background: linear-gradient(#174e43, #0d302a); color: #e6f5d0; font: 700 12px/1.15 Georgia, serif; touch-action: manipulation; } .shipyard-salvage:disabled { opacity: .35; } .shipyard-message { min-height: 1.2em; max-width: 50%; color: #fff0bd; font-size: 12px; } body.shipyard-open #mobile-controls { visibility: hidden; } @media (max-width: 760px), (max-height: 520px) { .shipyard-shell { width: calc(100vw - 8px); max-height: calc(100dvh - 8px); margin: 4px auto; padding: 6px; border-radius: 8px; } .shipyard-header { min-height: 46px; } .shipyard-title { width: min(460px, 68vw); } .shipyard-close { width: 46px; } .shipyard-resources { gap: 4px; margin: 2px 0 6px; } .shipyard-resources span { min-width: 0; padding: 4px 7px; font-size: 10px; } .shipyard-grid { grid-template-columns: minmax(150px, .8fr) minmax(330px, 1.55fr); gap: 6px; } .shipyard-selector { display: none; } .shipyard-preview-wrap { max-height: 64dvh; overflow: hidden; } .shipyard-preview { height: min(64dvh, 430px); width: 100%; object-fit: cover; object-position: center; } .shipyard-live-stats { left: 3%; right: 3%; bottom: 2%; grid-template-columns: 1fr; gap: 2px; padding: 5px 7px; font-size: 9px; } .shipyard-upgrade-wrap { align-self: center; } .shipyard-active-plate { bottom: -46px; width: 45%; } .shipyard-footer { min-height: 54px; margin-top: 46px; gap: 7px; } .shipyard-salvage { min-width: 120px; min-height: 40px; font-size: 10px; } .shipyard-message { max-width: 56%; font-size: 10px; } } @media (orientation: portrait) and (max-width: 760px) { .shipyard-shell { min-height: calc(100dvh - 8px); display: flex; flex-direction: column; } .shipyard-title { width: min(500px, 80vw); } .shipyard-grid { grid-template-columns: 1fr; } .shipyard-preview-wrap { display: none; } .shipyard-upgrade-wrap { width: min(96vw, 520px); margin: 0 auto; } .shipyard-footer { margin-top: auto; padding: 58px 0 max(14px, env(safe-area-inset-bottom)); } } `; document.head.appendChild(style); const fields = Object.fromEntries( [...overlay.querySelectorAll('[data-shipyard]')].map((el) => [el.dataset.shipyard, el]), ); const closeButton = overlay.querySelector('.shipyard-close'); const fitButton = overlay.querySelector('.shipyard-salvage'); const upgradeButtons = [...overlay.querySelectorAll('.shipyard-upgrade-button')]; let latestState = null; const findStock = (id) => latestState?.progression?.marketStock?.find?.((module) => module.id === id) || null; const render = (state) => { if (!state?.progression) return; latestState = state; const progression = state.progression; const stats = progression.shipStats || {}; const loadout = progression.loadout || {}; fields.port.textContent = progression.nearestPort || '--'; fields.credits.textContent = progression.credits ?? 0; fields.scrap.textContent = progression.scrap ?? 0; fields.inventory.textContent = progression.inventoryCount ?? 0; fields.hull.textContent = `Hull ${stats.maxHull ?? 100}`; fields.speed.textContent = `Speed ${stats.maxSpeed ?? 180}`; fields.firepower.textContent = `Firepower ${stats.cannonDamage ?? 16}`; fields.handling.textContent = `Handling ${stats.turnRate ?? 76}`; fields.cargo.textContent = `Cargo ${stats.cargoCapacity ?? 4}`; for (const button of upgradeButtons) { const id = button.dataset.moduleId; const stock = findStock(id); const installed = Object.values(loadout).includes(id); const stateLabel = button.querySelector('.shipyard-upgrade-state'); button.classList.toggle('installed', installed); button.classList.toggle('locked', !stock && !installed); button.disabled = installed || !stock || progression.credits < (stock?.price || Infinity); stateLabel.textContent = installed ? 'INSTALLED' : !stock ? 'NOT STOCKED' : progression.credits < stock.price ? `NEED ยข${stock.price}` : 'TAP TO BUY'; } const salvage = progression.salvageModule; fitButton.disabled = !salvage; fitButton.textContent = salvage ? `FIT ${salvage.name}` : 'FIT SALVAGE'; fields.message.textContent = String(state.status || '').replace(/\n/g, ' '); }; const show = (state) => { render(state); overlay.hidden = false; document.body.classList.add('shipyard-open'); closeButton.focus({ preventScroll: true }); }; const hide = () => { overlay.hidden = true; document.body.classList.remove('shipyard-open'); }; closeButton.addEventListener('click', () => { dispatchAction('close-shipyard'); hide(); }); overlay.querySelector('.shipyard-backdrop').addEventListener('click', () => { dispatchAction('close-shipyard'); hide(); }); for (const button of upgradeButtons) { button.addEventListener('click', () => { const moduleId = button.dataset.moduleId; if (!moduleId || button.disabled) return; dispatchAction('purchase-module', { moduleId }); }); } fitButton.addEventListener('click', () => { if (fitButton.disabled) return; dispatchAction('fit-salvage'); }); window.addEventListener('airship:shipyard-open', (event) => show(event.detail)); window.addEventListener('airship:shipyard-state', (event) => render(event.detail)); window.addEventListener('airship:shipyard-close', hide); window.addEventListener('airship:hud-state', (event) => { latestState = event.detail; if (!overlay.hidden) render(event.detail); }); window.addEventListener('keydown', (event) => { if (event.key === 'Escape' && !overlay.hidden) { dispatchAction('close-shipyard'); hide(); } });