Expose mission trade salvage and repair progression

This commit is contained in:
Admin 2026-09-21 01:10:59 +00:00
parent acb5126389
commit 2c4cb471ab
4 changed files with 174 additions and 9 deletions

View File

@ -24,6 +24,7 @@ overlay.innerHTML = `
<span>CARGO <strong data-dock="cargo">0/0</strong></span>
<span>SALVAGE <strong data-dock="salvage">0</strong></span>
<span>MISSION <strong data-dock="mission">NONE</strong></span>
<span>WANTED <strong data-dock="wanted">0</strong></span>
</div>
<nav class="dock-nav" aria-label="Dock services">
@ -46,6 +47,13 @@ overlay.innerHTML = `
<strong>SHIPYARD</strong>
<span>Upgrade the Sparrow and manage recovered salvage.</span>
</button>
<div class="dock-repair-strip">
<div>
<strong>HULL SERVICE</strong>
<span data-dock="repair-status">Hull already at full strength.</span>
</div>
<button type="button" data-action="repair-hull">REPAIR</button>
</div>
</section>
<section class="dock-panel" data-panel="missions" hidden>
@ -166,6 +174,32 @@ style.textContent = `
}
.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-repair-strip {
grid-column: 1 / -1;
display:flex;
align-items:center;
justify-content:space-between;
gap:12px;
padding:12px 14px;
border:1px solid rgba(194, 142, 61, .46);
border-radius:10px;
background:rgba(4, 28, 35, .9);
}
.dock-repair-strip > div { display:grid; gap:3px; }
.dock-repair-strip strong { color:#fff0bd; font-size:14px; }
.dock-repair-strip span { color:#9fb5bb; font:600 10px/1.3 system-ui,sans-serif; }
.dock-repair-strip button {
min-width:110px;
min-height:42px;
padding:7px 12px;
border:1px solid #9a6a2b;
border-radius:8px;
background:#103b3d;
color:#f5dda0;
font:700 10px/1 system-ui,sans-serif;
touch-action:manipulation;
}
.dock-repair-strip button:disabled { opacity:.36; }
.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; }
@ -309,7 +343,13 @@ const renderMarket = (state) => {
strong.textContent = item.name;
const tag = document.createElement('small');
tag.className = 'dock-tag';
tag.textContent = item.exported ? 'LOCAL EXPORT' : item.imported ? 'HIGH DEMAND' : 'MARKET';
tag.textContent = item.shortage > 0.5
? `SHORTAGE +${Math.round(item.shortage * 8)}%`
: item.exported
? 'LOCAL EXPORT'
: item.imported
? 'HIGH DEMAND'
: 'MARKET';
name.append(strong, tag);
const buy = document.createElement('span');
buy.className = 'dock-price';
@ -317,13 +357,20 @@ const renderMarket = (state) => {
const sell = document.createElement('span');
sell.className = 'dock-price';
sell.textContent = `SELL ¢${item.sellPrice} • HOLD ${item.held || 0}`;
const hint = document.createElement('p');
const opportunity = item.opportunity;
hint.textContent = opportunity
? opportunity.margin > 0
? `BEST KNOWN: ${opportunity.portName} • SELL ¢${opportunity.sellPrice} • +¢${opportunity.margin}/UNIT`
: `BEST KNOWN: ${opportunity.portName} • no positive margin`
: 'No known destination price.';
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);
card.append(name, buy, sell, hint, actions);
fields['market-list'].append(card);
}
};
@ -338,6 +385,19 @@ const render = (state) => {
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.wanted.textContent = dock.wanted || 0;
const repairButton = overlay.querySelector('[data-action="repair-hull"]');
const repair = dock.repair;
if (repair) {
fields['repair-status'].textContent = repair.missing > 0
? `Repair ${repair.missing} hull for ¢${repair.cost}.`
: 'Hull already at full strength.';
repairButton.disabled = repair.missing <= 0
|| (state.progression?.credits || 0) < repair.cost;
repairButton.textContent = repair.missing > 0 ? `REPAIR ¢${repair.cost}` : 'FULL HULL';
}
fields.message.textContent = String(state.status || '').replace(/\n/g, ' ');
renderMission(state);
renderMarket(state);
@ -360,6 +420,8 @@ for (const button of overlay.querySelectorAll('[data-dock-tab]')) {
for (const button of overlay.querySelectorAll('[data-action="open-shipyard"]')) {
button.addEventListener('click', () => dispatchAction('open-shipyard'));
}
overlay.querySelector('[data-action="repair-hull"]')
.addEventListener('click', () => dispatchAction('repair-hull'));
overlay.querySelector('.dock-leave').addEventListener('click', () => dispatchAction('close-dock'));
overlay.querySelector('.dock-backdrop').addEventListener('click', () => dispatchAction('close-dock'));

View File

@ -326,6 +326,42 @@ if (!isDesktopPointer.matches) {
display: block;
}
.steam-objective {
display: none;
position: absolute;
left: 50%;
top: 20.5%;
transform: translateX(-50%);
align-items: center;
gap: 7px;
max-width: 78%;
padding: 5px 9px;
border: 1px solid rgba(201, 146, 60, .58);
border-radius: 999px;
background: rgba(5, 22, 28, .82);
color: #f7dfa1;
font: 700 clamp(9px, 2.45vw, 12px)/1.1 system-ui, sans-serif;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-shadow: 0 2px 10px rgba(0,0,0,.28);
}
.steam-objective.visible { display: flex; }
.steam-objective-arrow {
display: inline-block;
color: #ffe08c;
font-size: 18px;
line-height: 1;
transform-origin: center;
}
.steam-objective-text {
overflow: hidden;
text-overflow: ellipsis;
}
#mobile-controls.controller-active #drag-stick {
left: 1%;
bottom: max(8px, env(safe-area-inset-bottom));
@ -697,6 +733,14 @@ if (!isDesktopPointer.matches) {
status.dataset.steam = 'status';
hud.appendChild(status);
const objective = document.createElement('div');
objective.className = 'steam-objective';
objective.innerHTML = `
<span class="steam-objective-arrow" data-steam="objective-arrow">↑</span>
<span class="steam-objective-text" data-steam="objective-text"></span>
`;
hud.appendChild(objective);
const fields = Object.fromEntries(
[...hud.querySelectorAll('[data-steam]')].map((el) => [el.dataset.steam, el]),
);
@ -800,6 +844,21 @@ if (!isDesktopPointer.matches) {
fields['port-reload'].style.transform = `scaleX(${clamp01(state.reload?.port ?? 1)})`;
fields['starboard-reload'].style.transform = `scaleX(${clamp01(state.reload?.starboard ?? 1)})`;
const mission = state.objective || null;
objective.classList.toggle('visible', Boolean(mission));
if (mission?.type === 'delivery') {
fields['objective-arrow'].hidden = false;
fields['objective-arrow'].style.transform = `rotate(${mission.relativeBearing || 0}deg)`;
fields['objective-text'].textContent =
`${mission.label} • ${Math.round(mission.distance || 0)}m`;
} else if (mission?.type === 'bounty') {
fields['objective-arrow'].hidden = true;
fields['objective-text'].textContent = mission.progressText || 'PIRATE BOUNTY';
} else {
fields['objective-arrow'].hidden = true;
fields['objective-text'].textContent = '';
}
const progression = state.progression || {};
if (buyButton) {
buyButton.hidden = !progression.docked;

View File

@ -79,12 +79,14 @@ overlay.innerHTML = `
<footer class="shipyard-footer">
<div class="shipyard-salvage-panel">
<div class="shipyard-salvage-copy">
<strong>SALVAGE HOLD</strong>
<strong>SALVAGE HOLD <span data-shipyard="salvage-index">0/0</span></strong>
<span data-shipyard="salvage-name">EMPTY</span>
<span data-shipyard="salvage-meta">Pirate wrecks repair hull immediately.</span>
<span data-shipyard="salvage-effect">Recovered modules are stored here to fit or scrap.</span>
</div>
<div class="shipyard-salvage-actions">
<button class="shipyard-salvage" type="button" data-shipyard="prev">◀ PREV</button>
<button class="shipyard-salvage" type="button" data-shipyard="next">NEXT ▶</button>
<button class="shipyard-salvage" type="button" data-shipyard="fit">FIT MODULE</button>
<button class="shipyard-salvage shipyard-scrap" type="button" data-shipyard="scrap">SCRAP MODULE</button>
</div>
@ -477,9 +479,12 @@ const fields = Object.fromEntries(
const closeButton = overlay.querySelector('.shipyard-close');
const fitButton = overlay.querySelector('[data-shipyard="fit"]');
const scrapButton = overlay.querySelector('[data-shipyard="scrap"]');
const prevButton = overlay.querySelector('[data-shipyard="prev"]');
const nextButton = overlay.querySelector('[data-shipyard="next"]');
const upgradeButtons = [...overlay.querySelectorAll('.shipyard-upgrade-button')];
let latestState = null;
let selectedSalvageIndex = 0;
const render = (state) => {
if (!state?.progression) return;
@ -538,13 +543,24 @@ const render = (state) => {
);
}
const salvage = progression.salvageItem;
const salvageItems = progression.salvageItems || [];
selectedSalvageIndex = salvageItems.length
? Math.min(selectedSalvageIndex, salvageItems.length - 1)
: 0;
const salvage = salvageItems[selectedSalvageIndex] || null;
fields['salvage-index'].textContent = salvageItems.length
? `${selectedSalvageIndex + 1}/${salvageItems.length}`
: '0/0';
prevButton.disabled = salvageItems.length <= 1;
nextButton.disabled = salvageItems.length <= 1;
if (salvage) {
const effectiveness = Math.round(55 + salvage.condition * 45);
fields['salvage-name'].textContent = salvage.name;
fields['salvage-meta'].textContent =
`${String(salvage.slot || 'module').toUpperCase()} • ${salvage.conditionPercent}% CONDITION`;
`${String(salvage.slot || 'module').toUpperCase()} • ${salvage.conditionPercent}% CONDITION • ${effectiveness}% EFFECT`;
fields['salvage-effect'].textContent =
`${salvage.effectSummary || 'Recovered equipment'} • Fit it or convert it to scrap.`;
`${salvage.effectSummary || 'Recovered equipment'} • Fit this module or convert it to scrap.`;
fitButton.disabled = false;
fitButton.textContent = 'FIT MODULE';
scrapButton.disabled = false;
@ -592,22 +608,37 @@ for (const button of upgradeButtons) {
});
}
prevButton.addEventListener('click', () => {
const items = latestState?.progression?.salvageItems || [];
if (items.length <= 1) return;
selectedSalvageIndex = (selectedSalvageIndex - 1 + items.length) % items.length;
render(latestState);
});
nextButton.addEventListener('click', () => {
const items = latestState?.progression?.salvageItems || [];
if (items.length <= 1) return;
selectedSalvageIndex = (selectedSalvageIndex + 1) % items.length;
render(latestState);
});
fitButton.addEventListener('click', () => {
if (fitButton.disabled) return;
dispatchAction('fit-salvage');
dispatchAction('fit-salvage', { inventoryIndex: selectedSalvageIndex });
});
scrapButton.addEventListener('click', () => {
if (scrapButton.disabled) return;
dispatchAction('scrap-salvage');
dispatchAction('scrap-salvage', { inventoryIndex: selectedSalvageIndex });
});
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) => {
// Shipyard buttons must remain stable during touch gestures.
// Explicit shipyard-state events redraw after purchases/fits/scraps.
latestState = event.detail;
if (!overlay.hidden) render(event.detail);
});
window.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !overlay.hidden) {

View File

@ -285,6 +285,9 @@ test('mobile controls enter a dock hub before shipyard, trade, or missions', asy
assert.match(dockUi, /panel\.setAttribute\('aria-hidden'/);
assert.match(dockUi, /dispatchAction\('open-shipyard'\)/);
assert.match(hud, /buyButton\.textContent = badges\.length/);
assert.match(hud, /steam-objective/);
assert.match(hud, /objective-arrow/);
assert.match(hud, /mission\.relativeBearing/);
});
test('shipyard overlay uses supplied component art and explicit module purchases', async () => {
@ -306,6 +309,13 @@ test('shipyard overlay uses supplied component art and explicit module purchases
assert.match(shipyard, /CURRENT:/);
assert.match(shipyard, /NEXT:/);
assert.match(shipyard, /dispatchAction\('scrap-salvage'\)/);
assert.match(shipyard, /selectedSalvageIndex/);
assert.match(shipyard, /inventoryIndex: selectedSalvageIndex/);
assert.match(shipyard, /salvageItems/);
assert.doesNotMatch(
shipyard,
/airship:hud-state'[\s\S]{0,180}if \(!overlay\.hidden\) render\(event\.detail\)/,
);
assert.match(shipyard, /Pirate wrecks repair hull immediately on pickup/);
assert.doesNotMatch(shipyard, /data-shipyard="active"/);
assert.match(shipyard, /min-height: calc\(100dvh - 8px\)/);
@ -344,6 +354,9 @@ test('dock trade and missions are driven by generated port economy state', async
assert.match(dockUi, /sell-trade-good/);
assert.match(dockUi, /accept-mission/);
assert.match(dockUi, /complete-mission/);
assert.match(dockUi, /repair-hull/);
assert.match(dockUi, /BEST KNOWN:/);
assert.match(dockUi, /data-dock="wanted"/);
assert.match(dockUi, /airship:dock-state/);
assert.match(dockUi, /latestState = event\.detail/);
assert.doesNotMatch(