diff --git a/src/mobile-hud.js b/src/mobile-hud.js
index f642fc2..aca9b4f 100644
--- a/src/mobile-hud.js
+++ b/src/mobile-hud.js
@@ -414,6 +414,10 @@ if (!isDesktopPointer.matches) {
filter: grayscale(.7);
}
+ #mobile-controls.controller-active .mobile-control.ship-action[hidden] {
+ display: none !important;
+ }
+
#mobile-controls.controller-active .mobile-control.ship-action.active,
#mobile-controls.controller-active .mobile-control.ship-action:active {
background: rgba(117, 78, 31, .9);
@@ -799,17 +803,17 @@ if (!isDesktopPointer.matches) {
const progression = state.progression || {};
if (buyButton) {
+ buyButton.hidden = !progression.docked;
buyButton.disabled = !progression.docked;
- buyButton.textContent = progression.docked
- ? 'SHIPYARD'
- : 'SHIPYARD\nPORT';
+ buyButton.textContent = 'SHIPYARD';
}
if (fitButton) {
const module = progression.salvageModule;
+ fitButton.hidden = !module;
fitButton.disabled = !module;
fitButton.textContent = module
? `FIT\n${module.name}`
- : 'FIT\nEMPTY';
+ : 'FIT';
}
const statusText = String(state.status || '').trim();
diff --git a/src/progression/ship-loadout.js b/src/progression/ship-loadout.js
index 43c3652..33a524b 100644
--- a/src/progression/ship-loadout.js
+++ b/src/progression/ship-loadout.js
@@ -79,6 +79,7 @@ export const buyAndInstallModule = (progression, moduleId) => {
export const marketForPort = (port) => {
const stock = purchasableModules().filter((module) => {
+ if (module.slot === 'rudder') return true;
if (port?.type === 'industrial') return ['engine', 'hull', 'utility', 'cargo'].includes(module.slot);
if (port?.type === 'military') return ['weapon', 'hull', 'navigation'].includes(module.slot);
if (port?.type === 'fuel') return ['engine', 'navigation', 'cargo'].includes(module.slot);
diff --git a/src/shipyard-ui.js b/src/shipyard-ui.js
index 0505a53..e57ccd0 100644
--- a/src/shipyard-ui.js
+++ b/src/shipyard-ui.js
@@ -65,8 +65,7 @@ overlay.innerHTML = `
`).join('')}
-

-
SPARROW ACTIVE
+
@@ -253,15 +252,10 @@ style.textContent = `
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-active-plate img {
+ display: block;
+ width: 100%;
+ height: auto;
}
.shipyard-footer {
@@ -351,11 +345,21 @@ style.textContent = `
}
@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: 48px; }
+
+ .shipyard-footer {
+ margin-top: auto;
+ padding: 58px 0 max(14px, env(safe-area-inset-bottom));
+ }
}
`;
document.head.appendChild(style);
diff --git a/tests/progression.test.mjs b/tests/progression.test.mjs
index d754558..4bdfc24 100644
--- a/tests/progression.test.mjs
+++ b/tests/progression.test.mjs
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
import {
bestAffordableMarketUpgrade,
buyAndInstallModule,
+ marketForPort,
collectSalvage,
createStarterProgression,
deriveShipStats,
@@ -58,6 +59,14 @@ test('market can select an affordable upgrade for a port', () => {
});
+test('rudder is stocked at every port type so the shipyard card is actionable', () => {
+ const portTypes = ['industrial', 'military', 'fuel', 'trade'];
+ for (const type of portTypes) {
+ const stock = marketForPort({ type });
+ assert.ok(stock.some(({ id }) => id === 'reinforced-rudder'));
+ }
+});
+
test('rudder tier matches shipyard art and improves handling', () => {
const starter = { ...createStarterProgression(), credits: 650 };
const result = buyAndInstallModule(starter, 'reinforced-rudder');
diff --git a/tests/wiring.test.mjs b/tests/wiring.test.mjs
index 67d572a..379c1f4 100644
--- a/tests/wiring.test.mjs
+++ b/tests/wiring.test.mjs
@@ -280,6 +280,8 @@ test('mobile controls expose native buy and salvage-fit actions', async () => {
assert.match(game, /salvageModule: salvageModule \? \{/);
assert.match(hud, /root\.querySelector\('\.mobile-control\.buy'\)/);
assert.match(hud, /root\.querySelector\('\.mobile-control\.fit'\)/);
+ assert.match(hud, /buyButton\.hidden = !progression\.docked/);
+ assert.match(hud, /fitButton\.hidden = !module/);
assert.match(hud, /buyButton\.disabled = !progression\.docked/);
assert.match(hud, /fitButton\.disabled = !module/);
});
@@ -299,6 +301,8 @@ test('shipyard overlay uses supplied component art and explicit module purchases
assert.match(shipyard, /shipyard-ui\/upgrades\.png\?url/);
assert.match(shipyard, /airship:shipyard-open/);
assert.match(shipyard, /dispatchAction\('purchase-module', \{ moduleId \}\)/);
+ assert.doesNotMatch(shipyard, /data-shipyard="active"/);
+ assert.match(shipyard, /min-height: calc\(100dvh - 8px\)/);
assert.match(game, /marketForPort\(port\)/);
assert.match(game, /airship:shipyard-state/);
assert.match(game, /this\.shipyardOpen = true/);