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 = `
PORT --
COIN 0
SCRAP 0
SALVAGE 0
SPARROW
Hull 100
Speed 180
Firepower 16
Handling 76
Cargo 4
${UPGRADE_SLOTS.map(({ id, label }) => `
`).join('')}
`;
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();
}
});