// screens_core.jsx — Title, New Game & Centralised Onboarding QM Popup
window.S = window.S || {};
(() => {
const { useState, useEffect, useMemo } = React;
const { PORTS, FACTIONS, STARTS, CREW_FIRST_NAMES, CREW_LAST_NAMES, QM_DIALOGUE } = window.D;
const L = window.L;
const A = window.E.A;
const { T, panelStyle, Bar, Pill, Btn, StatBlock, SectionTitle, ScreenHeader, LogList, Divider, EmptyState, NarrativePanel, NarrativeLine, TutorialPopup, BackButton, Tooltip,
IconAnchor,IconPlay,IconContinue,IconFileTransfer,IconDice,IconSailboat } = window.UI;
// ── TITLE SCREEN (moved from screens_port.jsx) ───────────────────
function TitleScreen({ dispatch }) {
const hasSave = L.hasSave();
const importRef = React.useRef(null);
const localStorageWarning = React.useMemo(() => !L.checkLocalStorageAvailable(), []);
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 = "";
};
return (
Broadside
CARIBBEAN · 1695
dispatch({ type: A.NAVIGATE, screen: "newgame" })}> New Game
{hasSave && (
dispatch({ type: A.LOAD_GAME })}> Continue
)}
importRef.current?.click()}> Import Save
{localStorageWarning && (
Browser storage is blocked. Use Import/Export Save to keep your progress.
)}
);
}
// ── NEW GAME SCREEN (moved from screens_port.jsx) ────────────────
function NewGameScreen({ dispatch }) {
const { FACTIONS, STARTS, CREW_FIRST_NAMES, CREW_LAST_NAMES } = window.D;
const L = window.L;
const A = window.E.A;
const { T, panelStyle, Btn } = window.UI;
const [captainName, setCaptainName] = useState(() => {
const first = CREW_FIRST_NAMES?.english || ["William"];
const last = CREW_LAST_NAMES?.english || ["Hartley"];
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
return `${pick(first)} ${pick(last)}`;
});
const [selectedFaction, setSelectedFaction] = useState(null);
const [tutorialMode, setTutorialMode] = useState("full");
const handleRandomName = () => {
const faction = selectedFaction || "english";
const first = CREW_FIRST_NAMES?.[faction] || ["Captain"];
const last = CREW_LAST_NAMES?.[faction] || ["Unknown"];
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
setCaptainName(`${pick(first)} ${pick(last)}`);
};
const handleSetSail = () => {
if (!captainName.trim()) return;
if (!selectedFaction) return;
dispatch({
type: A.START_GAME,
captainName: captainName.trim(),
faction: selectedFaction,
tutorialMode,
});
};
const backstory = selectedFaction ? STARTS.factionBackstory?.[selectedFaction] : null;
const startPort = selectedFaction ? (window.D.PORTS[STARTS.factionPorts?.[selectedFaction]]?.name || "") : "";
return (
Broadside
CARIBBEAN · 1695
{/* Captain Name */}
{/* Faction Selection */}
Choose your allegiance
{Object.entries(FACTIONS).map(([key, fac]) => (
setSelectedFaction(key)}
style={{ flex: 1, padding: "10px 6px", textAlign: "center", cursor: "pointer",
background: selectedFaction === key ? (fac.color + "20") : T.panel,
border: `2px solid ${selectedFaction === key ? fac.color : T.border}`,
borderRadius: 2, transition: "border-color 0.15s" }}>
{fac.label.substring(0,3).toUpperCase()}
{fac.label}
))}
{/* Backstory */}
{backstory && (
You arrived in {startPort} with {backstory.hook}.
{backstory.flavour}
Your adventure begins in {startPort} .
)}
{/* Onboarding toggle */}
{/* Tutorial choice */}
{/* Set Sail */}
Set Sail
);
}
// ── ONBOARDING POPUP (global QM dialogue) ────────────────────────
const QMPopup = ({ qmName, message, onDismiss, onSkip }) => {
if (!message) return null;
return React.createElement('div', {
style: {
position: "fixed", bottom: 20, left: "50%", transform: "translateX(-50%)",
maxWidth: 560, width: "90%", zIndex: 500,
background: T.panel, border: `1px solid ${T.gold}`, borderRadius: 2,
padding: 12, display: "flex", alignItems: "flex-start", gap: 10,
boxShadow: "0 4px 20px rgba(0,0,0,0.5)",
animation: "qmSlideIn 0.3s ease-out",
}
},
React.createElement('div', { style: { flex: 1 } },
React.createElement('div', { style: { color: T.gold, fontSize: 11, fontWeight: "bold", marginBottom: 4 } }, qmName),
React.createElement('div', { style: { color: T.textDim, fontSize: T.narrativeFontSize, lineHeight: T.narrativeLineHeight } }, message),
React.createElement('div', { style: { marginTop: 8, display: "flex", gap: 12 } },
React.createElement(Btn, { sm: true, v: "gold", onClick: onDismiss }, "Got it"),
React.createElement('div', {
onClick: onSkip,
style: { color: T.textFaint, fontSize: T.captionFontSize, cursor: "pointer", textDecoration: "underline", alignSelf: "center" }
}, "I'll take it from here"),
),
),
);
};
function OnboardingPopup({ state, dispatch }) {
const onboarding = state.onboarding;
if (!onboarding?.enabled || onboarding?.completed) return null;
const qm = state.crew?.roster?.find(m => (m.tags || []).includes('quartermaster'));
const qmName = qm ? `${qm.firstName} ${qm.lastName}` : 'Quartermaster';
// ── Helper that picks the next unseen message ──────────────────
const getMessage = useMemo(() => {
const seen = onboarding.qmMessagesSeen || {};
const steps = onboarding.stepsCompleted || {};
const screen = state.screen;
// Ordered list of conditions → dialogue keys → message text
const msgSteps = [
// Step 0: Welcome
{ key: 'welcome', condition: !steps.firstArrival, screen: 'port',
text: () => QM_DIALOGUE.step0_welcome(qmName, PORTS[state.currentPort]?.name) },
// Step 1: Contract accepted
{ key: 'contractAccepted', condition: steps.firstContractAccepted && !steps.marketOpened, screen: null,
text: () => QM_DIALOGUE.step1_contractAccepted(qmName) },
// Step 2: Market open
{ key: 'marketOpen', condition: steps.marketOpened && !steps.provisionsAndGoodsBought, screen: 'market',
text: () => QM_DIALOGUE.step2_marketOpen(qmName) },
// Step 3: Goods bought
{ key: 'stocked', condition: steps.provisionsAndGoodsBought && !steps.mapOpened, screen: null,
text: () => QM_DIALOGUE.step2_stocked(qmName) },
// Step 4: Map open
{ key: 'mapOpen', condition: steps.mapOpened && !steps.firstVoyageStarted, screen: 'map',
text: () => QM_DIALOGUE.step3_mapOpen(qmName) },
// Step 5: Sailing
{ key: 'sailing', condition: steps.firstVoyageStarted && !steps.firstArrival, screen: 'sailing',
text: () => QM_DIALOGUE.step4_sailing(qmName) },
// Step 6: Arrival
{ key: 'arrival', condition: steps.firstArrival && !steps.firstContractDelivered, screen: 'port',
text: () => QM_DIALOGUE.step5_arrival(qmName) },
// Step 7: Contract delivered
{ key: 'delivered', condition: steps.firstContractDelivered && !steps.crewOpened, screen: 'port',
text: () => QM_DIALOGUE.step5_delivered(qmName) },
// Step 8: Crew open
{ key: 'crewOpen', condition: steps.crewOpened && !steps.firstCrewHired, screen: 'crew',
text: () => QM_DIALOGUE.step6_crewOpen(qmName) },
// Step 9: Crew hired
{ key: 'crewHired', condition: steps.firstCrewHired && !steps.tutorialHuntAccepted, screen: 'port',
text: () => QM_DIALOGUE.step6_hired(qmName) },
// Step 10: Tutorial hunt accepted
{ key: 'huntAccepted', condition: steps.tutorialHuntAccepted && !steps.tutorialHuntCompleted, screen: 'port',
text: () => QM_DIALOGUE.step6b_huntAccepted(qmName) },
// Step 11: Tutorial hunt victory
{ key: 'huntVictory', condition: steps.tutorialHuntCompleted && !steps.shipyardOpened, screen: 'port',
text: () => QM_DIALOGUE.step6b_victory(qmName) },
// Step 12: Shipyard open
{ key: 'shipyardOpen', condition: steps.shipyardOpened && !steps.shipRepaired, screen: 'shipyard',
text: () => QM_DIALOGUE.step7_shipyardOpen(qmName) },
// Step 13: Ship repaired
{ key: 'repaired', condition: steps.shipRepaired && !steps.journalOpened, screen: 'port',
text: () => QM_DIALOGUE.step7_repaired(qmName) },
// Step 14: Journal open
{ key: 'journalOpen', condition: steps.journalOpened, screen: 'journal',
text: () => QM_DIALOGUE.step8_journalOpen(qmName) },
// Step 15: Back to port after journal : farewell & onboarding completion
{ key: 'departure',
condition: steps.journalOpened && !onboarding.completed,
screen: 'port',
text: () => QM_DIALOGUE.step9_departure(qmName) },
];
for (const step of msgSteps) {
if (!step.condition) continue;
if (step.screen && screen !== step.screen) continue; // wait for the right screen
if (seen[step.key]) continue; // already shown
return { key: step.key, text: step.text() };
}
return null;
}, [state]); // react to any state change
const handleDismiss = () => {
if (getMessage?.key) {
dispatch({ type: A.ONBOARDING_QM_SEEN, messageKey: getMessage.key });
if (getMessage.key === 'departure') {
dispatch({ type: A.ONBOARDING_COMPLETE });
}
}
};
const handleSkip = () => {
dispatch({ type: A.ONBOARDING_SKIP });
};
return (
);
}
// ── EXPORTS ─────────────────────────────────────────────────────
Object.assign(window.S, {
TitleScreen,
NewGameScreen,
OnboardingPopup,
});
})();