// screens_crew.jsx : Crew Screen
window.S = window.S || {};
(() => {
const { useState } = 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,
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] = React.useState(null);
const [showTutorial, setShowTutorial] = React.useState(() => shouldShowTutorial(state,"crew"));
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
;
})}
)}
{selectedMember.bio
? selectedMember.bio
: typeof G.generateCrewBio === 'function'
? G.generateCrewBio(selectedMember, state)
: `${selectedMember.firstName} is a crew member.`}
{!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 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */}
MANIFEST
{state.crew.roster.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",
}}
>
{/* Render role icon here */}
{getRoleIcon(member.role)}
{isMutineer && (
โ
)}
);
})}
);
}
Object.assign(window.S, { CrewScreen });
})();