// screens_market.jsx : Market Screen window.S = window.S || {}; (() => { const { useState, useMemo } = React; const { PORTS, RESOURCES } = window.D; const L = window.L; const A = window.E.A; const { T, panelStyle, Bar, Pill, Btn, SectionTitle, EmptyState, TutorialPopup, BackButton, IconAnchor, IconFood, IconWater, IconRhum, IconSugar, IconTimber, IconCloth, IconSpice, IconSilk, IconCoffee, IconCocoa, IconSpear, IconTobacco, IconGoblet, IconPerson,TransferLayout, } = window.UI; const { shouldShowTutorial, markTutorialSeen } = window.L; // Map good keys to their icon components (use IconSpear for weapons, IconPerson for slaves) const GOOD_ICONS = { food: IconFood, water: IconWater, rum: IconRhum, sugar: IconSugar, timber: IconTimber, cloth: IconCloth, spices: IconSpice, silk: IconSilk, coffee: IconCoffee, cocoa: IconCocoa, tobacco: IconTobacco, weapons: IconSpear, silver: IconGoblet, slaves: IconPerson, }; const getGoodIcon = (good) => { const IconComponent = GOOD_ICONS[good]; if (!IconComponent) return null; return React.createElement(IconComponent, { size: 14, color: T.textDim, style: { marginRight: 8 } }); }; // ── Sort helper: groups legal goods first, illegal goods last. // Array.prototype.sort is stable in modern JS, so within each group // the original colOrder is preserved. const sortLegalFirst = (a, b) => { const aIllegal = RESOURCES[a]?.illegal ? 1 : 0; const bIllegal = RESOURCES[b]?.illegal ? 1 : 0; return aIllegal - bIllegal; }; const MarketScreen = ({ state, dispatch }) => { const market = state.portMarket; const portName = PORTS[state.currentPort]?.name || "Port"; const [buyPending, setBuyPending] = useState({}); const [sellPending, setSellPending] = useState({}); const [showTutorial, setShowTutorial] = React.useState(() => shouldShowTutorial(state,"market")); if (!market) return (
); const flavourLines = useMemo( () => G.generateMarketFlavour(state, state.currentPort), [state.currentPort] ); const holdItems = state.hold?.items || {}; const capacity = L.getHoldCapacity(state) || 0; const previewItems = { ...holdItems }; Object.entries(buyPending).forEach(([good, qty]) => { previewItems[good] = (previewItems[good] || 0) + (qty || 0); }); Object.entries(sellPending).forEach(([good, qty]) => { previewItems[good] = (previewItems[good] || 0) - (qty || 0); }); const used = L.getHoldUsed(previewItems); const loadPct = L.getHoldLoadPct(previewItems, capacity); const speedMult = L.getHoldSpeedMultiplier(loadPct); // ── Gold delta: iterate from pending sets (not market.goods) ── // After the generator fix, market.goods has all 14 goods with real prices, // so selling a good the port doesn't stock now contributes correctly. const goldDelta = (() => { let total = 0; Object.entries(sellPending).forEach(([good, qty]) => { if (!qty || qty <= 0) return; const pg = market.goods[good]; if (!pg) return; total += qty * pg.sellToPort; }); Object.entries(buyPending).forEach(([good, qty]) => { if (!qty || qty <= 0) return; const pg = market.goods[good]; if (!pg) return; total -= qty * pg.buyFromPort; }); return total; })(); const hasPending = Object.values(buyPending).some(v => (v || 0) > 0) || Object.values(sellPending).some(v => (v || 0) > 0); const confirmTrade = () => { const buys = {}, sells = {}; Object.entries(buyPending).forEach(([g, qty]) => { if (qty > 0) buys[g] = qty; }); Object.entries(sellPending).forEach(([g, qty]) => { if (qty > 0) sells[g] = qty; }); dispatch({ type: A.CONFIRM_TRADE, buys, sells }); setBuyPending({}); setSellPending({}); }; const adjustBuy = (good, delta) => { setBuyPending(prev => { const cur = prev[good] || 0; const newVal = Math.max(0, cur + delta); const pg = market.goods[good]; if (!pg) return prev; const freeSpace = capacity - used + (sellPending[good] || 0); const max = Math.min(pg.available, freeSpace + cur); return { ...prev, [good]: Math.min(newVal, max) }; }); }; const adjustSell = (good, delta) => { setSellPending(prev => { const cur = prev[good] || 0; const newVal = Math.max(0, cur + delta); const max = (holdItems[good] || 0) + (buyPending[good] || 0); return { ...prev, [good]: Math.min(newVal, max) }; }); }; // ── Top summary: list of goods in pending trade ─────────────── const pendingBuyGoods = Object.keys(buyPending).filter(g => (buyPending[g]||0) > 0); const pendingSellGoods = Object.keys(sellPending).filter(g => (sellPending[g]||0) > 0); return (
{showTutorial && ( { markTutorialSeen("market", disableAll); setShowTutorial(false); }} >

Buy goods to trade at other ports for profit. A few things to know:

  • Prices vary between ports. Buy where it's cheap, sell where it's rare
  • Loading your hold above 50% slows your ship
  • Goods marked illegal risk confiscation by patrol inspection at sea

Check the port gossips for hints about good deals to be made.

)} <> MARKET — {portName} {flavourLines.length > 0 && (
{flavourLines.join(" ")}
)} {/* ── Top summary panel ──────────────────────────────────── */}
Hold: {used} / {capacity} {Math.round(loadPct * 100)}% full
0.75 ? T.redBr : T.greenBr} h={10} /> {speedMult > 1 && (
⚠ Hold over 50% : voyages take {Math.round((speedMult - 1) * 100)}% longer.
)} {/* Pending trade summary */} {(pendingBuyGoods.length > 0 || pendingSellGoods.length > 0) && (
{pendingBuyGoods.map(good => ( {getGoodIcon(good, { size: 12, style: { marginRight: 2 } })} +{buyPending[good]} {window.D.RESOURCES[good]?.name || good} ))} {pendingSellGoods.map(good => ( {getGoodIcon(good, { size: 12, style: { marginRight: 2 } })} -{sellPending[good]} {window.D.RESOURCES[good]?.name || good} ))} = 0 ? T.greenBr : T.redBr, fontSize: T.narrativeFontSize, fontWeight: "bold", marginLeft: 8 }}> {goldDelta >= 0 ? "+" : ""}{goldDelta}g
)} {/* Reset / Confirm */}
{ setBuyPending({}); setSellPending({}); }} disabled={!hasPending}>Reset Confirm Trade
{/* ── Two‑column transfer layout ────────────────────────── */} (holdItems[good]||0) > 0 || (sellPending[good]||0) > 0).length === 0 ? ( ) : (
{Object.keys(holdItems) .filter(good => (holdItems[good]||0) > 0 || (sellPending[good]||0) > 0) .sort(sortLegalFirst) .map(good => { const pg = market.goods[good]; // After generator fix, pg always exists for any of the 14 goods. // The fallback is defensive only. const price = pg ? pg.sellToPort : 0; const inHold = holdItems[good] || 0; const pending = sellPending[good] || 0; const max = inHold + (buyPending[good] || 0); return (
{getGoodIcon(good, { size: 12, style: { marginRight: 4 } })} {window.D.RESOURCES[good]?.name || good} ×{inHold}
adjustSell(good, -1)} disabled={(sellPending[good]||0) <= 0}>- { const v = parseInt(e.target.value, 10) || 0; setSellPending(prev => ({ ...prev, [good]: Math.max(0, Math.min(v, max)) })); }} style={{ width:40, textAlign:"center", background:T.panel, border:`1px solid ${T.border}`, color:T.text, borderRadius:2, fontSize:11, fontFamily:T.font, minHeight: 32 }} /> adjustSell(good, 1)} disabled={(sellPending[good]||0) >= max}>+ {price}g
(Base: {window.D.RESOURCES[good]?.basePrice || 0}g)
); })}
) } rightTitle="PORT MARKET" rightContent={ Object.keys(market.goods).filter(g => market.goods[g].available > 0).length === 0 ? ( ) : ( (() => { let illegalDividerShown = false; // Filter to only goods the port actually has stock of. // After the generator fix, all 14 goods are in market.goods, // but many have available: 0 (port doesn't trade them). // Sort puts illegal goods last so the divider correctly separates them. return Object.keys(market.goods) .filter(good => market.goods[good].available > 0) .sort(sortLegalFirst) .map(good => { const pg = market.goods[good]; const price = pg.buyFromPort; const available = pg.available; const pending = buyPending[good] || 0; const freeSpace = capacity - used + (sellPending[good] || 0); const max = Math.min(available, freeSpace + pending); const isIllegal = RESOURCES[good]?.illegal; const showDivider = isIllegal && !illegalDividerShown; if (showDivider) illegalDividerShown = true; return ( {showDivider && (
)}
{getGoodIcon(good, { size: 12, style: { marginRight: 4 } })} {RESOURCES[good]?.name || good} {isIllegal && (Illegal)} ×{available} {/* buy controls unchanged */}
adjustBuy(good, -1)} disabled={(buyPending[good]||0) <= 0}>- { const v = parseInt(e.target.value, 10) || 0; setBuyPending(prev => ({ ...prev, [good]: Math.max(0, Math.min(v, max)) })); }} style={{ width:40, textAlign:"center", background:T.panel, border:`1px solid ${T.border}`, color:T.text, borderRadius:2, fontSize:11, fontFamily:T.font, minHeight: 32 }} /> adjustBuy(good, 1)} disabled={(buyPending[good]||0) >= max}>+ {price}g
(Base: {pg.basePrice}g)
); }); })() ) } /> {/* ── Bottom confirm panel ───────────────────────────────── */}
= 0 ? T.greenBr : T.redBr, fontSize:13 }}> {goldDelta >= 0 ? "+" : ""}{goldDelta}g
{ setBuyPending({}); setSellPending({}); }} disabled={!hasPending}>Reset Confirm Trade
); }; Object.assign(window.S, { MarketScreen }); })();