// screens_shipyard.jsx — Shipyard Screen (Option A: Split Dashboard)
window.S = window.S || {};
(() => {
const { useState, useEffect, useMemo } = React;
const { SHIPS, EQUIPMENT, PORTS } = window.D;
const L = window.L;
const A = window.E.A;
const { T, panelStyle, Bar, Pill, Btn, StatBlock, SectionTitle, EmptyState, TutorialPopup, BackButton, Panel,
IconShield, IconCannon, IconSailboat, IconSparkles, IconChest, IconHammer, IconCog, IconShip, ShipSideSprite,
IconLock, // <-- Import the new IconLock here
} = window.UI;
const { shouldShowTutorial, markTutorialSeen } = window.L;
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// CONSTANTS
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
const VISUAL_EQUIPMENT = ["war_pennants", "extra_sails", "lateen_rig"];
const getVisualEquipment = (state) => {
const allEquipped = [
...(state.ship.equipment?.hull || []),
...(state.ship.equipment?.armament || []),
...(state.ship.equipment?.rigging || []),
...(state.ship.equipment?.special || []),
];
return allEquipped.filter(key => VISUAL_EQUIPMENT.includes(key));
};
const SLOT_LABELS = {
hull: { label: "Hull", Icon: IconShield },
armament: { label: "Armament", Icon: IconCannon },
rigging: { label: "Rigging", Icon: IconSailboat },
special: { label: "Special", Icon: IconSparkles },
};
const TABS = { EQUIP: "equip", SHIPS: "ships", LOCKER: "locker" };
function StatDelta({ label, before, after }) {
const diff = after - before;
const arrow = diff > 0 ? " ↑" : diff < 0 ? " ↓" : " =";
const color = diff > 0 ? T.greenBr : diff < 0 ? T.redBr : T.textDim;
return (
{label}
{before} → {after}{arrow}
);
}
function ShipyardScreen({ state, dispatch }) {
const perk = L.getRepPerk(state.reputation[state.currentPort] ?? 50);
if (perk.servicesBlocked) {
return (
);
}
const repCost = Math.floor(L.shipRepairCost(state) * (perk.repairMult || 1));
const currentShip = SHIPS[state.ship.type];
const effectiveStats = L.getShipStats(state);
const [activeTab, setActiveTab] = useState(TABS.SHIPS);
const [slotFilter, setSlotFilter] = useState("all");
const [selectedEquip, setSelectedEquip] = useState(null);
const [selectedShip, setSelectedShip] = useState(null);
const [selectedShipName, setSelectedShipName] = useState("");
const [showTutorial, setShowTutorial] = useState(() => shouldShowTutorial(state,"shipyard"));
const [isNarrow, setIsNarrow] = useState(window.innerWidth < 700);
useEffect(() => {
const h = () => setIsNarrow(window.innerWidth < 700);
window.addEventListener("resize", h);
return () => window.removeEventListener("resize", h);
}, []);
const [equippedOpen, setEquippedOpen] = useState(!isNarrow);
const switchTab = (tab) => {
setActiveTab(tab);
setSelectedEquip(null);
setSelectedShip(null);
setSlotFilter("all");
};
const lockerItems = useMemo(() => (state.equipmentInventory || []).map(key => {
const item = EQUIPMENT[key];
if (!item) return null;
const validation = L.canInstallEquipment(state, key);
return { key, ...item, validation, canAfford: state.gold >= item.installFee };
}).filter(Boolean), [state]);
const hasLocker = lockerItems.length > 0;
const previewEquipStats = (equipmentKey) => {
const newEquip = { ...state.ship.equipment };
const item = EQUIPMENT[equipmentKey];
if (!item) return effectiveStats;
newEquip[item.slot] = [...(newEquip[item.slot] || []), equipmentKey];
const tempState = { ...state, ship: { ...state.ship, equipment: newEquip } };
return L.getShipStats(tempState);
};
const renderPreviewPanel = () => {
if (selectedEquip) {
const item = EQUIPMENT[selectedEquip];
if (!item) return null;
const after = previewEquipStats(selectedEquip);
const validation = L.canInstallEquipment(state, selectedEquip);
const isFromLocker = (state.equipmentInventory || []).includes(selectedEquip);
const totalCost = isFromLocker ? item.installFee : item.cost + item.installFee;
const canAfford = state.gold >= totalCost;
return (
{isFromLocker
? React.createElement(React.Fragment, null,
React.createElement(IconChest, { size: 12, color: T.gold }),
" Install from Locker"
)
: React.createElement(React.Fragment, null,
React.createElement(IconHammer, { size: 12, color: T.gold }),
" Preview: Buy & Install"
)
}
setSelectedEquip(null)} style={{ flexShrink: 0 }}>✕
{item.name}
{item.desc}{item.downsideDesc ? ` ${item.downsideDesc}` : ""}
{!validation.ok ? (
{validation.reason}
) : !canAfford ? (
Need {totalCost - state.gold}g more
) : (
{
if (isFromLocker) {
dispatch({ type: A.INSTALL_EQUIPMENT, equipmentKey: selectedEquip });
} else {
dispatch({ type: A.BUY_EQUIPMENT, equipmentKey: selectedEquip });
}
setSelectedEquip(null);
}}>
{isFromLocker ? `Install (${item.installFee}g)` : `Buy & Install (${totalCost}g)`}
)}
);
}
if (selectedShip && selectedShip !== state.ship.type) {
const s = SHIPS[selectedShip];
const cur = currentShip;
const shipReq = L.meetsRequirement(state, s);
const canBuy = shipReq.allowed && state.gold >= s.cost;
const lack = shipReq.allowed && state.gold < s.cost ? s.cost - state.gold : 0;
return (
Compare: {s.name} vs {cur.name}
setSelectedShip(null)} style={{ flexShrink: 0 }}>✕
{/* Ship Name Input */}
⚠ Buying a new ship clears all installed equipment. Remove items to your locker first.
{!shipReq.allowed ? (
{shipReq.reason}
) : lack > 0 ? (
Need {lack.toLocaleString()}g more
) : (
{
dispatch({
type: A.BUY_SHIP,
shipType: selectedShip,
shipName: selectedShipName.trim() || SHIPS[selectedShip].name,
});
setSelectedShip(null);
}}>
Purchase ({s.cost.toLocaleString()}g)
)}
);
}
return null;
};
const renderLeftPanel = () => (
CURRENT VESSEL — {state.ship.name}
Hull: {state.ship.hull} / {effectiveStats.maxHull}
= 0.6 ? T.greenBr :
state.ship.hull / effectiveStats.maxHull >= 0.3 ? T.gold :
T.redBr
}
h={8} />
dispatch({ type: A.REPAIR })}
disabled={state.ship.hull >= effectiveStats.maxHull || state.gold < repCost}>
Full Repair ({repCost}g)
isNarrow && setEquippedOpen(v => !v)}
>
EQUIPPED
{isNarrow && {equippedOpen ? "▾" : "▸"}}
{(isNarrow ? equippedOpen : true) && (
{Object.entries(SLOT_LABELS).map(([slotKey, slotInfo]) => {
const SlotIcon = slotInfo.Icon;
const slotMax = currentShip.slots?.[slotKey] || 0;
if (slotMax === 0) return null;
const installed = state.ship.equipment?.[slotKey] || [];
return (
{slotInfo.label} ({installed.length}/{slotMax})
{installed.length === 0 ? (
empty
) : installed.map(key => {
const item = EQUIPMENT[key];
if (!item) return null;
return (
{item.name}
{!item.removable && (Struct.)}
{item.removable && (
dispatch({ type: A.REMOVE_EQUIPMENT, equipmentKey: key })}
disabled={state.gold < item.installFee}
style={{ fontSize: 9, padding: "2px 5px", minHeight: 24 }}>
−{item.installFee}g
)}
);
})}
);
})}
)}
);
const renderEquipmentTab = () => {
const filterButtons = [
{ key: "all", label: "All" },
...Object.entries(SLOT_LABELS).map(([k, v]) => ({
key: k,
label: React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center' } },
React.createElement(v.Icon, { size: 10, color: T.textDim, style: { marginRight: 3 } }),
v.label
)
})),
];
const shopItems = Object.entries(EQUIPMENT).map(([key, item]) => {
const validation = L.canInstallEquipment(state, key);
const totalCost = item.cost + item.installFee;
const canAfford = state.gold >= totalCost;
return { key, ...item, validation, canAfford, totalCost };
}).filter(item => slotFilter === "all" || item.slot === slotFilter);
return (
{filterButtons.map(f => (
{ setSlotFilter(f.key); setSelectedEquip(null); }}>
{f.label}
))}
{shopItems.map(item => {
const isSelected = selectedEquip === item.key;
const SlotIcon = SLOT_LABELS[item.slot] ? SLOT_LABELS[item.slot].Icon : null;
const priceColor = item.validation.ok && !item.canAfford ? T.redBr : T.gold;
return (
setSelectedEquip(isSelected ? null : item.key)}
>
{item.name}
{item.totalCost}g
{item.desc}{item.downsideDesc ? ` ${item.downsideDesc}` : ""}
{SlotIcon && (
)}
{SLOT_LABELS[item.slot]?.label || item.slot}
{item.requiredFame > 0 || item.requiredHull > 0 ? (
· Fame {item.requiredFame} · Hull {item.requiredHull}+
) : null}
{!item.validation.ok && (
{item.validation.reason}
)}
);
})}
);
};
const renderShipsTab = () => {
return (
{Object.entries(SHIPS).map(([key, s]) => {
const isCur = key === state.ship.type;
const shipReq = L.meetsRequirement(state, s);
const isAffordable = state.gold >= s.cost;
const priceColor = shipReq.allowed && !isAffordable ? T.redBr : T.gold;
const isSelected = selectedShip === key;
return (
{
if (!isCur) {
setSelectedShip(isSelected ? null : key);
setSelectedShipName(SHIPS[key].name);
}
}}
>
{s.name}
{isCur
?
:
{s.cost.toLocaleString()}g
}
{s.desc}
{[["Crew", s.maxCrew], ["Guns", s.cannons], ["Spd", s.speed], ["Hull", s.maxHull]].map(([l, v]) =>
)}
{!shipReq.allowed && (
{shipReq.reason}
)}
);
})}
);
};
const renderLockerTab = () => {
if (lockerItems.length === 0) {
return ;
}
return (
{lockerItems.map(item => {
const isSelected = selectedEquip === item.key;
const SlotIcon = SLOT_LABELS[item.slot] ? SLOT_LABELS[item.slot].Icon : null;
const priceColor = item.validation.ok && !item.canAfford ? T.redBr : T.gold;
return (
setSelectedEquip(isSelected ? null : item.key)}
>
{item.name}
Install: {item.installFee}g
{item.desc}{item.downsideDesc ? ` ${item.downsideDesc}` : ""}
{SlotIcon && (
)}
{SLOT_LABELS[item.slot]?.label || item.slot}
{!item.validation.ok && (
{item.validation.reason}
)}
);
})}
);
};
const renderRightPanel = () => (
switchTab(TABS.EQUIP)}> Equipment
switchTab(TABS.SHIPS)}> Ships
{hasLocker && (
switchTab(TABS.LOCKER)}> Locker ({lockerItems.length})
)}
{renderPreviewPanel()}
{activeTab === TABS.EQUIP && renderEquipmentTab()}
{activeTab === TABS.SHIPS && renderShipsTab()}
{activeTab === TABS.LOCKER && renderLockerTab()}
);
return (
{showTutorial && (
{
markTutorialSeen("shipyard", disableAll);
setShowTutorial(false);
}}
>
This is where you upgrade your ship — or buy a new one entirely.
- Left panel shows your current ship stats and equipped items
- Use the tabs to browse Equipment, Ships, or your Locker
- Click any item to see a stat preview before buying
- Ships are locked behind fame requirements
- Buying a new ship clears all equipment — remove items first
A bigger ship means more crew, more cargo, more firepower — but also higher wages.
)}
{renderLeftPanel()}
{renderRightPanel()}
);
}
Object.assign(window.S, { ShipyardScreen });
})();