diff --git a/src/shipyard-ui.js b/src/shipyard-ui.js new file mode 100644 index 0000000..0505a53 --- /dev/null +++ b/src/shipyard-ui.js @@ -0,0 +1,465 @@ +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%; } + .shipyard-active-plate span { + position: absolute; + inset: 0; + display: grid; + place-items: center; + color: #b9efc0; + font-size: clamp(9px, 1.15vw, 13px); + letter-spacing: .08em; + } + + .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-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: 48px; } + } +`; +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(); + } +});