// screens_port.jsx — Port-zone screens (responsive) window.S = window.S || {}; (() => { const { useState } = React; const { PORTS, SHIPS, FACTIONS, EQUIPMENT, STARTS, RESOURCES } = window.D; const L = window.L; const A = window.E.A; const { T, panelStyle, Bar, Pill, Btn, PulseBtn, StatBlock, SectionTitle, ScreenHeader, LogList, Divider, EmptyState, NarrativePanel, NarrativeLine, TutorialPopup, BackButton, Tooltip, QMPopup, IconMap, IconBarChart, IconMarket, IconJournal, IconAnchor, IconCrew, IconFloppy, IconFileTransfer, IconTalking, IconGold, IconSkull, IconHandshake, IconSearch, PortSilhouette } = window.UI; const { FactionPill, RepPill, ShipSprite } = window.UI; const { shouldShowTutorial, markTutorialSeen } = window.L; // ── PORT SCREEN ────────────────────────────────────────────────────── function PortScreen({ state, dispatch }) { const port = PORTS[state.currentPort]; const rep = state.reputation[state.currentPort] ?? 0; const perk = L.getRepPerk(rep); const repCost = Math.floor(L.shipRepairCost(state) * (perk.repairMult || 1)); const canFinish = state.activeMission && (!state.activeMission.targetPort || state.currentPort === state.activeMission.targetPort); const importRef = React.useRef(null); const handleImport = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => dispatch({ type: A.IMPORT_SAVE, fileContent: reader.result }); reader.readAsText(file); e.target.value = ""; }; const [showTutorial, setShowTutorial] = React.useState(() => shouldShowTutorial(state,"port")); const [isNarrow, setIsNarrow] = React.useState(window.innerWidth < 700); React.useEffect(() => { const handleResize = () => setIsNarrow(window.innerWidth < 700); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); // ── Feature unlocking gates ────────────────────────────────────── const canContracts = true; // always available const canMarket = L.isFeatureUnlocked(state, 'market'); const canNavigation = L.isFeatureUnlocked(state, 'navigation'); const canCrew = L.isFeatureUnlocked(state, 'crew'); const canShipyard = L.isFeatureUnlocked(state, 'shipyard'); const canJournal = L.isFeatureUnlocked(state, 'journal'); return (
{showTutorial && ( { markTutorialSeen("port", disableAll); setShowTutorial(false); }} >

This is where you'll plan your next move. From here you can:

Your first mission is already accepted. Open the Map to set sail.

)} {/* ── Column 1: Atmosphere, Actions & Missions ─────────── */}
{/* Port header + description + gossip */}
{port.name}
{FACTIONS[port.faction]?.label.toUpperCase()} PORT

{port.desc}

{state.portGossip?.length > 0 && ( WORD ON THE DOCKS} variant="gossip"> {state.portGossip.map((line, i) => ( {line} ))} )} {perk.servicesBlocked && ( )}
{/* Action buttons */}
ACTIONS
{canNavigation && ( dispatch({ type: A.NAVIGATE, screen: "map" })}> World Map )} dispatch({ type: A.NAVIGATE, screen: "status" })}> Status {canMarket && ( dispatch({ type: A.NAVIGATE, screen: "market" })}> Market )} {canJournal && ( dispatch({ type: A.NAVIGATE, screen: "journal" })}> Journal )}
{!perk.servicesBlocked && ( <>
{port.services.includes("shipyard") && ( dispatch({ type: A.NAVIGATE, screen: "shipyard" })}> Shipyard )} {port.services.includes("crew") && ( dispatch({ type: A.NAVIGATE, screen: "crew" })}> Crew )}
{port.services.includes("shipyard") && state.ship.hull < L.getShipStats(state).maxHull && ( dispatch({ type: A.REPAIR })} disabled={state.gold < repCost}> Quick Repair ({repCost}g) )} )} {/* Save/Load/Export/Import — not gated, normal Btn */}
dispatch({ type: A.SAVE_GAME })}> Save Game
{L.hasSave() && ( dispatch({ type: A.LOAD_GAME })} style={{ marginTop: 4 }}> Load Game )}
dispatch({ type: A.EXPORT_SAVE })}> Export Save importRef.current?.click()}> Import Save
{/* Mission board */}
dispatch({ type: A.REFRESH_MISSIONS })}>Refresh }> MISSION BOARD {perk.tier !== "neutral" && (
1 ? T.greenBr : T.gold, fontSize: T.captionFontSize, marginBottom: 8 }}> {perk.missionMult > 1 ? `★ ${perk.tier} standing: +${Math.round((perk.missionMult - 1) * 100)}% mission rewards` : `⚠ Hostile standing: −${Math.round((1 - perk.missionMult) * 100)}% mission rewards`}
)} {state.activeMission && (
ACTIVE: {state.activeMission.name}
{state.activeMission.description}
Destination: {PORTS[state.activeMission.targetPort]?.name || "At sea"}
💰 {state.activeMission.gold} ★ {state.activeMission.fame}
{state.activeMission.requiredGood && state.activeMission.requiredQty && (() => { const res = window.D.RESOURCES[state.activeMission.requiredGood]; const inHold = state.hold?.items?.[state.activeMission.requiredGood] || 0; const hasGoods = inHold >= state.activeMission.requiredQty; const goodName = res?.name || state.activeMission.requiredGood; return (
{hasGoods ? `✓ ${inHold} ${goodName} in hold — ready` : `✗ ${inHold}/${state.activeMission.requiredQty} ${goodName} — visit market`}
); })()}
{canFinish && ( dispatch({ type: A.COMPLETE_MISSION })} disabled={state.activeMission.requiredGood && (state.hold?.items?.[state.activeMission.requiredGood] || 0) < state.activeMission.requiredQty}> Complete Mission )} dispatch({ type: A.ABANDON_MISSION })}>Abandon
{!canFinish && (
Sail to {PORTS[state.activeMission.targetPort]?.name} to complete.
)}
)} {!port.services.includes("missions") ? ( ) : state.missions.length === 0 ? ( ) : (
{state.missions.map((m, i) => (
{m.name}

{m.description || m.desc}

{m.enemy && (
Enemy: {m.enemy.name} — {m.enemy.cannons} cannons, hull {m.enemy.hull}, crew {m.enemy.crew}
)} {(m.requiredGood && m.requiredQty) && (() => { const res = window.D.RESOURCES[m.requiredGood]; const inHold = state.hold?.items?.[m.requiredGood] || 0; const alreadyHave = inHold >= m.requiredQty; const partialHave = inHold > 0 && inHold < m.requiredQty; const isIllegal = res?.illegal; const holdFree = (L.getHoldCapacity(state) || 0) - L.getHoldUsed(state.hold?.items || {}); const canFit = holdFree >= (m.requiredQty - inHold); return (
{m.type === "smuggle" ? "⚠ Contraband required" : "Cargo required"}
{m.requiredQty} × {res?.name || m.requiredGood} {isIllegal && (Illegal)}
{alreadyHave ? ✓ In hold ({inHold} — ready to deliver) : partialHave ? {inHold}/{m.requiredQty} in hold — need {m.requiredQty - inHold} more : Not yet sourced — check market or source elsewhere }
{!alreadyHave && !canFit && (
⚠ Only {holdFree} hold space free — sell cargo first
)} {m.type === "smuggle" && res?.sourceHint && (
{res.sourceHint}
)} {m.type === "trade" && (
Est. cost: ~{res?.basePrice * m.requiredQty}g · Payment on delivery: {m.gold}g · Est. profit: ~{m.gold - res?.basePrice * m.requiredQty}g
)} {m.type === "smuggle" && (
+{m.infamyGain} infamy on completion {m.requiredGood === "slaves" ? " · +1 infamy on purchase" : ""}
)}
); })()}
💰 {m.gold} ★ {m.fame} → {PORTS[m.targetPort]?.name} dispatch({ type: A.TAKE_MISSION, mission: m })}>Accept
))}
)}
{/* ── Column 2: Captain's Log ──────────────────────────── */}
CAPTAIN'S LOG
); } // ── STATUS SCREEN ──────────────────────────────────────────────────── // ── STATUS SCREEN ──────────────────────────────────────────────────── function StatusScreen({ state, dispatch }) { const [showTutorial, setShowTutorial] = React.useState(() => shouldShowTutorial(state, "status")); const [showFullLedger, setShowFullLedger] = React.useState(false); const [isNarrowStatus, setIsNarrowStatus] = React.useState(window.innerWidth < 700); React.useEffect(() => { const handle = () => setIsNarrowStatus(window.innerWidth < 700); window.addEventListener("resize", handle); return () => window.removeEventListener("resize", handle); }, []); // ── Derived values ───────────────────────────────────────────── const career = state.career || {}; const daysSurvived = state.day; const portsTotal = Object.keys(PORTS).length; const portsVisitedCount = (career.portsVisited || []).length; const totalBattles = (career.battles?.won || 0) + (career.battles?.lost || 0) + (career.battles?.fled || 0); const totalCrewLost = (career.crewLost?.inBattle || 0) + (career.crewLost?.inStorm || 0) + (career.crewLost?.deserted || 0) + (career.crewLost?.other || 0); // Headline identity tag const getCaptainTag = () => { const fame = state.fame || 0; const infamy = state.infamy || 0; if (infamy >= 100) return { text: "Legendary Outlaw of the Caribbean", color: T.redBr }; if (infamy >= 50) return { text: "Notorious Across the Caribbean", color: T.redBr }; if (fame >= 200) return { text: "A Legend of the Caribbean", color: T.gold }; if (fame >= 100) return { text: "A Notorious Captain", color: T.gold }; if (fame >= 50) return { text: "A Recognised Captain", color: T.gold }; if (infamy >= 25) return { text: "Wanted by the Law", color: T.redBr }; if (infamy >= 10) return { text: "A Suspect in Several Ports", color: T.gold }; return { text: "An Unknown Captain", color: T.textDim }; }; const captainTag = getCaptainTag(); // ── Career narrative highlights ──────────────────────────────── const getHighlights = () => { const lines = []; // Sea time lines.push(`You have sailed for ${daysSurvived} day${daysSurvived === 1 ? "" : "s"}.`); // Combat summary if (totalBattles > 0) { const won = career.battles?.won || 0; const lost = career.battles?.lost || 0; const fled = career.battles?.fled || 0; const parts = []; if (won > 0) parts.push(`won ${won}`); if (lost > 0) parts.push(`lost ${lost}`); if (fled > 0) parts.push(`fled ${fled}`); lines.push( `Across ${totalBattles} battle${totalBattles === 1 ? "" : "s"}, you have ${parts.join(", ")}.` ); const sunk = career.shipsSunk || 0; const plundered = career.shipsPlundered || 0; if (sunk > 0 || plundered > 0) { const detailParts = []; if (sunk > 0) detailParts.push(`sunk ${sunk}`); if (plundered > 0) detailParts.push(`boarded and plundered ${plundered}`); lines.push(`Of those, you ${detailParts.join(" and ")}.`); } } // Crew losses (the human cost) if (totalCrewLost > 0) { const inBattle = career.crewLost?.inBattle || 0; const inStorm = career.crewLost?.inStorm || 0; const deserted = career.crewLost?.deserted || 0; const parts = []; if (inBattle > 0) parts.push(`${inBattle} to combat`); if (inStorm > 0) parts.push(`${inStorm} to the storms`); if (deserted > 0) parts.push(`${deserted} who walked away`); if (parts.length > 0) { lines.push(`You have lost ${totalCrewLost} crew: ${parts.join(", ")}.`); } } // Longest tenure if (career.longestCrewTenure && career.longestCrewTenure >= 50) { lines.push(`Your longest-serving crew member sailed with you for ${career.longestCrewTenure} days.`); } // Exploration if (portsVisitedCount > 0) { lines.push(`You have made landfall at ${portsVisitedCount} of ${portsTotal} ports across the Caribbean.`); } // Economic const earned = career.goldEarned || 0; const spent = career.goldSpent || 0; if (earned > 0 || spent > 0) { lines.push(`You have earned ${earned.toLocaleString()}g and spent ${spent.toLocaleString()}g.`); } // Storms if (career.stormsSurvived > 0) { lines.push(`You have weathered ${career.stormsSurvived} storm${career.stormsSurvived === 1 ? "" : "s"}.`); } // Ships const ships = (career.shipsOwned || []).length; if (ships > 1) { lines.push(`You have commanded ${ships} ship${ships === 1 ? "" : "s"} over your career.`); } // Contraband caught if (career.contrabandSeized > 0) { lines.push(`You have been caught smuggling contraband ${career.contrabandSeized} time${career.contrabandSeized === 1 ? "" : "s"}.`); } return lines; }; const highlights = getHighlights(); // ── Per-faction summary ──────────────────────────────────────── const getFactionSummary = (factionKey) => { const ports = Object.entries(PORTS).filter(([_, p]) => p.faction === factionKey); if (ports.length === 0) return null; const avgRep = Math.round( ports.reduce((sum, [k]) => sum + (state.reputation[k] ?? 50), 0) / ports.length ); const repLabel = L.reputationLabel(avgRep); const heat = state.factionAlerts?.[factionKey] || 0; const heatLabel = L.getHeatLabel(heat); // Crew alignment const crewOfFaction = (state.crew?.roster || []).filter(m => m.faction === factionKey).length; const totalCrew = (state.crew?.roster || []).length; const crewPct = totalCrew > 0 ? Math.round((crewOfFaction / totalCrew) * 100) : 0; return { avgRep, repLabel, heat, heatLabel, crewOfFaction, totalCrew, crewPct }; }; // Service description from rep const getServiceNote = (rep) => { if (rep >= 80) return "−20% repair · +20% missions"; if (rep >= 50) return "−10% repair · +10% missions"; if (rep >= 30) return "Standard prices"; if (rep >= 10) return "−25% missions"; return "No services available"; }; // ── Render ───────────────────────────────────────────────────── return (
{showTutorial && ( { markTutorialSeen("status", disableAll); setShowTutorial(false); }} >

This is where your career is tracked — your identity, your deeds, and your standing with the powers of the Caribbean.

The Caribbean keeps a ledger. Your name is written in it.

)} {/* ═══════════════════════════════════════════════════════════════ */} {/* SECTION 1: CAPTAIN IDENTITY (Hero panel) */} {/* ═══════════════════════════════════════════════════════════════ */}
{/* Left: Captain name + faction + tag */}
Captain
{state.captainName || "Unknown"}
{FACTIONS[state.faction]?.label || "No faction"}
{captainTag.text}
{/* Right: Fame / Infamy / Days */}
★ {state.fame}
{L.getFameInfo(state.fame).label}
Fame
0 ? T.red : T.textFaint, fontSize: 22, fontWeight: "bold" }}> 0 ? T.red : T.textFaint} /> {state.infamy ?? 0}
{L.getInfamyLabel(state.infamy ?? 0)}
Infamy
{daysSurvived}
days at sea
Tenure
{/* ═══════════════════════════════════════════════════════════════ */} {/* SECTION 2: CAREER HIGHLIGHTS (Narrative) */} {/* ═══════════════════════════════════════════════════════════════ */}
CAREER
{highlights.map((line, i) => (
{line}
))}
{/* Toggle for full ledger */}
setShowFullLedger(v => !v)} style={{ color: T.textFaint, fontSize: T.captionFontSize, cursor: "pointer", marginTop: 4, padding: 4, borderTop: `1px solid ${T.borderFaint}`, }} > {showFullLedger ? "▾ Hide full ledger" : "▸ Show full ledger"}
{showFullLedger && (
{/* Economic */}
Economic
{/* Combat */}
Combat
{/* Crew */}
Crew
{/* World */}
World
)}
{/* ═══════════════════════════════════════════════════════════════ */} {/* SECTION 3: THE WORLD'S VIEW (Per-faction unified) */} {/* ═══════════════════════════════════════════════════════════════ */}
THE WORLD'S VIEW

How each faction sees you, and how your crew aligns with them.

{Object.entries(FACTIONS).map(([factionKey, fac]) => { const summary = getFactionSummary(factionKey); if (!summary) return null; const { avgRep, repLabel, heat, heatLabel, crewOfFaction, totalCrew, crewPct } = summary; const repColor = avgRep >= 60 ? T.greenBr : avgRep >= 30 ? T.gold : T.redBr; return (
{/* Header: faction name + rivals */}
{fac.label}
{fac.rivalFactions?.length ? `Rivals: ${fac.rivalFactions.map(r => FACTIONS[r]?.label ?? r).join(", ")}` : "No known rivals"}
{/* Reputation bar */}
Standing {repLabel} ({avgRep})
{getServiceNote(avgRep)}
{/* Heat (only if non-zero) */} {heat > 0 && (
Heat {heatLabel} ({heat}/10)
)} {/* Crew alignment */} {totalCrew > 0 && (
{crewOfFaction === 0 ? `None of your crew are ${fac.label}.` : crewOfFaction === totalCrew ? `Your entire crew is ${fac.label}.` : `${crewOfFaction} of ${totalCrew} crew (${crewPct}%) are ${fac.label}.` }
)}
); })}

Reputation decays slowly toward neutral (50) over time. Complete missions, aid distressed ships, or parley with faction vessels to improve standing. Attacking their ships will anger all ports of that faction. Heat decays naturally as you stay clear of trouble.

); } // ── JOURNAL SCREEN ────────────────────────────────────────────────── function JournalScreen({ state, dispatch }) { const [filterTab, setFilterTab] = useState("all"); const [search, setSearch] = useState(""); const [showTutorial, setShowTutorial] = React.useState(() => shouldShowTutorial(state,"journal")); // Parse log entries const parsed = state.log.map(entry => { const match = entry.match(/^\[(\d+)\]\s*(.*)/); const day = match ? parseInt(match[1], 10) : null; const text = match ? match[2] : entry; return { day, text, raw: entry, tab: L.getLogTabCategory(text) }; }); // Apply category filter let filtered = parsed; if (filterTab !== "all") { filtered = filtered.filter(e => e.tab === filterTab); } // Apply search filter if (search.trim()) { const query = search.toLowerCase(); filtered = filtered.filter(e => e.text.toLowerCase().includes(query)); } // Reverse chronological order (most recent first) filtered = [...filtered].reverse(); // Group by day for rendering let lastDay = null; const tabs = [ { key: "all", label: "All" }, { key: "crew", label: "Crew" }, { key: "combat", label: "Combat" }, { key: "ports", label: "Ports" }, { key: "missions", label: "Missions" }, { key: "trade", label: "Trade" }, ]; return (
{showTutorial && ( { markTutorialSeen("journal", disableAll); setShowTutorial(false); }} >

Everything that has happened on this voyage is recorded here — battles, arrivals, crew events, trades, and discoveries.

The journal is the story of your career. The longer you sail, the richer it becomes.

)} CAPTAIN'S JOURNAL

Every storm, every battle, every whispered secret—recorded here for posterity.

{/* Filter tabs */}
{tabs.map(tab => ( setFilterTab(tab.key)} > {tab.label} ))}
{/* Search bar */}
setSearch(e.target.value)} style={{ width: "100%", padding: "6px 10px", background: T.panel, border: `1px solid ${T.border}`, color: T.text, borderRadius: 3, fontSize: T.metadataFontSize, fontFamily: T.font, outline: "none", }} /> {search && (
{filtered.length} entr{filtered.length === 1 ? "y" : "ies"} found
)}
{/* Entries */}
{filtered.length === 0 ? ( ) : ( filtered.map((entry, i) => { const showDay = entry.day !== null && entry.day !== lastDay; lastDay = entry.day; return ( {showDay && (
Day {entry.day}
)}
{(() => { const categoryKey = L.classifyLogLine(entry.text); const LOG_ICONS = window.UI.LOG_ICONS || {}; const IconComponent = categoryKey ? LOG_ICONS[categoryKey] : null; return IconComponent ? ( ) : null; })()} {entry.text}
); }) )}
); } // ── EXPORT ALL SCREENS ────────────────────────────────────────────── Object.assign(window.S, { PortScreen, StatusScreen, JournalScreen, }); })();