// screens_crew.jsx : Crew Screen window.S = window.S || {}; (() => { const { useState, useMemo, useRef, useEffect } = React; const { SHIPS, FACTIONS, PORTS } = window.D; const L = window.L; const A = window.E.A; const { T, panelStyle, Bar, Pill, Btn, StatBlock, SectionTitle, EmptyState, TutorialPopup, BackButton, Tooltip, Panel, IconCheers,IconAnchor, IconCannon, IconHammer, IconChefHat, IconCompass, IconShield } = window.UI; const G = window.G; const { shouldShowTutorial, markTutorialSeen } = window.L; // Helper: returns an SVG icon element for the given crew role const getRoleIcon = (role) => { const iconProps = { size: 14, color: T.text }; switch (role) { case "deckhand": return ; case "gunner": return ; case "cook": return ; case "carpenter": return ; case "navigator": return ; case "quartermaster":return ; default: return ๐Ÿ‘ค; } }; function CrewScreen({ state, dispatch }) { const perk = L.getRepPerk(state.reputation[state.currentPort] ?? 50); if (perk.servicesBlocked) { return (
); } const open = SHIPS[state.ship.type].maxCrew - state.crew.roster.length; const [selectedMember, setSelectedMember] = useState(null); const [showTutorial, setShowTutorial] = useState(() => shouldShowTutorial(state, "crew")); // โ”€โ”€ Filter & Sort state โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const [filterFaction, setFilterFaction] = useState("all"); const [sortBy, setSortBy] = useState("name"); // "name" | "days" const [sortAsc, setSortAsc] = useState(true); const bioCache = useRef({}); useEffect(() => { bioCache.current = {}; }, [state.currentPort]); const getCrewBio = (member) => { if (!member) return ""; if (member.bio) return member.bio; if (bioCache.current[member.id]) return bioCache.current[member.id]; if (typeof G.generateCrewBio === 'function') { const newBio = G.generateCrewBio(member, state); bioCache.current[member.id] = newBio; return newBio; } return `${member.firstName} is a crew member.`; }; // โ”€โ”€ Filter & sort roster โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const filteredRoster = useMemo(() => { let result = [...state.crew.roster]; // Filter by faction if (filterFaction !== "all") { result = result.filter(m => m.faction === filterFaction); } // Sort if (sortBy === "name") { result.sort((a, b) => { const nameA = `${a.firstName} ${a.lastName}`; const nameB = `${b.firstName} ${b.lastName}`; return sortAsc ? nameA.localeCompare(nameB) : nameB.localeCompare(nameA); }); } else if (sortBy === "days") { result.sort((a, b) => { const daysA = a.daysAboard || 0; const daysB = b.daysAboard || 0; return sortAsc ? daysA - daysB : daysB - daysA; }); } return result; }, [state.crew.roster, filterFaction, sortBy, sortAsc]); // โ”€โ”€ Build faction options for dropdown โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const factionOptions = [ { key: "all", label: "Show All" }, ...Object.entries(FACTIONS).map(([key, f]) => ({ key, label: f.label, color: f.color })), ]; const toggleSort = (field) => { if (sortBy === field) { setSortAsc(!sortAsc); } else { setSortBy(field); setSortAsc(true); } }; return (
{showTutorial && ( { markTutorialSeen("crew", disableAll); setShowTutorial(false); }} >

Click any crew member to see their story. Over time, they earn:

  • Scars from battles and storms they survived
  • Traits that reveal hidden personalities
  • Loyalty status : veterans who've served 200+ days may pledge loyalty

Watch crew faction alignment. Attacking a crew member's home faction can make them upset and eventually desert.

)}
{/* โ”€โ”€ ROSTER PANEL โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} ROSTER
60 ? T.greenBr : state.crew.morale > 30 ? T.gold : T.redBr} />
MORALE
60 ? T.greenBr : state.crew.morale > 30 ? T.gold : T.redBr} h={10} /> {state.crew.morale < 50 &&
โš  Low morale weakens combat effectiveness
} {(() => { const counts = {}; state.crew.roster.forEach(m => { counts[m.faction] = (counts[m.faction] || 0) + 1; }); return (
{Object.entries(counts).map(([faction, count]) => { const fac = FACTIONS[faction]; return ( {fac?.label || faction}: {count} ); })}
); })()}
dispatch({ type: A.RAISE_MORALE })} disabled={state.crew.roster.length === 0 || state.gold < state.crew.roster.length * 5 || state.crew.morale >= 100}> Buy Drinks ({state.crew.roster.length * 5}g) +5 Morale
{/* โ”€โ”€ HIRE PANEL โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} HIRE

50g per sailor. Your {SHIPS[state.ship.type].name} holds {state.crew.max}.

{[1, 5, 10].map(n => dispatch({ type: A.HIRE_CREW, count: n })} disabled={open < n || state.gold < n * 50}>+{n} ({n * 50}g))}
{open === 0 && }
{/* โ”€โ”€ CREW DETAIL / LEGEND PANEL โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} CREW DETAILS {selectedMember ? ( (() => { const visibleTags = (selectedMember.tags || []).filter(t => !t.startsWith("hidden_")); return (
{selectedMember.firstName} {selectedMember.lastName} setSelectedMember(null)}>โœ•
Faction: {FACTIONS[selectedMember.faction]?.label || selectedMember.faction}
Role: {selectedMember.role}
Days aboard: {selectedMember.daysAboard ?? 0}
{visibleTags.length > 0 && (
{visibleTags.map(tag => { const positiveTraits = ["loyal", "seasoned", "veteran"]; const isPositive = positiveTraits.includes(tag); const color = isPositive ? T.greenBr : T.redBr; const displayLabels = { loyal: "Loyal Officer", scar_battle: "Battle Scarred", scar_storm: "Storm Survivor", scar_shipwreck: "Shipwrecked", seasoned: "Seasoned", veteran: "Veteran", mutineer: "Mutineer", upset: "Upset", revealed_drunkard: "Drunkard", revealed_coward: "Coward", revealed_greedy: "Greedy", }; const label = displayLabels[tag] || tag .replace("revealed_", "") .replace(/_/g, " ") .replace(/\b\w/g, c => c.toUpperCase()); return ; })}
)}
{getCrewBio(selectedMember)}
{!L.hasTag(selectedMember, "protected") ? ( { dispatch({ type: A.DISMISS_CREW, memberId: selectedMember.id }); setSelectedMember(null); }}> โœ• Dismiss ) : null}
); })() ) : (
Click on any crew member icon for details.
)}
{/* โ”€โ”€ MANIFEST with Filter & Sort โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */}
MANIFEST
{/* Faction Filter Dropdown */} {/* Sort buttons */}
toggleSort("name")} style={{ fontSize: T.captionFontSize, minHeight: 28, padding: "2px 10px" }} > Name {sortBy === "name" && (sortAsc ? "โ†‘" : "โ†“")} toggleSort("days")} style={{ fontSize: T.captionFontSize, minHeight: 28, padding: "2px 10px" }} > Days {sortBy === "days" && (sortAsc ? "โ†‘" : "โ†“")}
{filteredRoster.map(member => { const visibleTags = (member.tags || []).filter(t => !t.startsWith("hidden_")); const factionColor = FACTIONS[member.faction]?.color || T.textDim; const isMutineer = L.hasTag(member, "mutineer"); const tagLabels = visibleTags.map(t => { if (t === "upset") return "Upset"; if (t === "mutineer") return "Mutineer"; if (t === "scar_shipwreck") return "Shipwreck Survivor"; if (t === "loyal") return "Loyal Officer"; if (t === "seasoned") return "Seasoned"; if (t === "veteran") return "Veteran"; if (t.startsWith("revealed_")) return t.replace("revealed_", "").replace(/_/g, " "); return t; }); const tooltipText = `${member.firstName} ${member.lastName} ยท ${member.role} ยท ${member.faction} ยท ${member.daysAboard}d aboard` + (tagLabels.length > 0 ? ` ยท ${tagLabels.join(", ")}` : ""); return (
setSelectedMember(selectedMember?.id === member.id ? null : member)} style={{ width: 34, height: 34, borderRadius: 3, display: "flex", alignItems: "center", justifyContent: "center", fontSize: T.heading2FontSize, cursor: "pointer", background: selectedMember?.id === member.id ? T.panelAlt : T.panel, border: `2px solid ${selectedMember?.id === member.id ? T.gold : T.border}`, position: "relative", }} > {getRoleIcon(member.role)}
{isMutineer && ( โš  )}
); })}
{filteredRoster.length === 0 && (
No crew members match the selected filter.
)}
); } Object.assign(window.S, { CrewScreen }); })();