// screens_voyage.jsx — Voyage-zone screens (responsive) window.S = window.S || {}; (() => { const { useState } = React; const { PORTS, SHIPS, FACTIONS, EQUIPMENT} = window.D; const L = window.L; const A = window.E.A; const { T, panelStyle, Bar, Pill, Btn, StatBlock, SectionTitle, LogList, EmptyState, TutorialPopup, BackButton, Tooltip, IconSailboat, IconPlay, IconAnchor, IconCompass, IconFood, IconWater, // IconSwords, IconCannon, IconTarget, IconGrapple, IconWind, IconSkull, // IconRhum, IconSugar, IconTimber, IconCloth, IconSpice, IconSilk, IconCoffee, IconCocoa, // IconSpear, IconTobacco, IconGoblet, IconPerson } = window.UI; const { FactionPill, RepPill, ShipSprite } = window.UI; const { shouldShowTutorial, markTutorialSeen } = window.L; /* // ── Goods icon mapping (same as market screen) ────────────────── 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: 6 } }); }; */ // ── MAP SCREEN ─────────────────────────────────────────────────────── function MapScreen({ state, dispatch }) { const [hov, setHov] = useState(null); const [showTutorial, setShowTutorial] = React.useState(() => shouldShowTutorial(state,"map")); const W = 760, H = 460; // ── Zoom / Pan state ─────────────────────────────────────── const mapRef = React.useRef(null); const [transform, setTransform] = useState({ x: 0, y: 0, scale: 1 }); const [isDragging, setIsDragging] = useState(false); const dragStart = React.useRef({ x: 0, y: 0, mouseX: 0, mouseY: 0 }); const lastPinchDist = React.useRef(0); // At-sea detection (unchanged) const atSea = state.route && state.route.totalDays > 0 && state.sailingDaysLeft > 0; const seaPos = atSea ? L.getSeaPosition(state.route) : null; const remainingEndurance = atSea ? state.route.enduranceBudget - state.route.enduranceSpent : 0; const playerPos = atSea ? seaPos : (state.currentPort ? PORTS[state.currentPort] : null); const getAtSeaUnreachableReason = (portKey, days) => { const port = PORTS[portKey]; if (!port) return null; if (port.hidden && !state.discoveredPorts?.includes(portKey)) return null; if (port.minHull) { const baseHull = SHIPS[state.ship?.type]?.maxHull ?? 0; if (baseHull < port.minHull) return `Requires a heavier vessel`; } if (days > remainingEndurance) return `Out of range (${days} days, only ${remainingEndurance} remaining)`; return null; }; // ── Wheel zoom (desktop) ──────────────────────────────────── const handleWheel = (e) => { e.preventDefault(); const rect = mapRef.current.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; const direction = e.deltaY > 0 ? -1 : 1; const factor = 1.15; const newScale = Math.max(1, Math.min(3, transform.scale * (direction > 0 ? factor : 1 / factor))); const scaleChange = newScale / transform.scale; setTransform(prev => ({ scale: newScale, x: mouseX - (mouseX - prev.x) * scaleChange, y: mouseY - (mouseY - prev.y) * scaleChange, })); }; // ── Pan (mouse) ───────────────────────────────────────────── const handleMouseDown = (e) => { if (e.button !== 0) return; setIsDragging(true); dragStart.current = { x: transform.x, y: transform.y, mouseX: e.clientX, mouseY: e.clientY }; e.preventDefault(); }; const handleMouseMove = (e) => { if (!isDragging) return; const dx = e.clientX - dragStart.current.mouseX; const dy = e.clientY - dragStart.current.mouseY; setTransform(prev => ({ ...prev, x: dragStart.current.x + dx, y: dragStart.current.y + dy })); }; const handleMouseUp = () => setIsDragging(false); // ── Touch pan / pinch ─────────────────────────────────────── const handleTouchStart = (e) => { if (e.touches.length === 1) { setIsDragging(true); dragStart.current = { x: transform.x, y: transform.y, mouseX: e.touches[0].clientX, mouseY: e.touches[0].clientY }; } else if (e.touches.length === 2) { lastPinchDist.current = Math.hypot( e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY ); } }; const handleTouchMove = (e) => { if (isDragging && e.touches.length === 1) { const dx = e.touches[0].clientX - dragStart.current.mouseX; const dy = e.touches[0].clientY - dragStart.current.mouseY; setTransform(prev => ({ ...prev, x: dragStart.current.x + dx, y: dragStart.current.y + dy })); } else if (e.touches.length === 2 && lastPinchDist.current > 0) { const newDist = Math.hypot( e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY ); const factor = newDist / lastPinchDist.current; const newScale = Math.max(1, Math.min(3, transform.scale * factor)); const scaleChange = newScale / transform.scale; // pinch center const cx = (e.touches[0].clientX + e.touches[1].clientX) / 2; const cy = (e.touches[0].clientY + e.touches[1].clientY) / 2; const rect = mapRef.current.getBoundingClientRect(); const mx = cx - rect.left, my = cy - rect.top; setTransform(prev => ({ scale: newScale, x: mx - (mx - prev.x) * scaleChange, y: my - (my - prev.y) * scaleChange, })); lastPinchDist.current = newDist; } }; const handleTouchEnd = () => { setIsDragging(false); lastPinchDist.current = 0; }; return (
{showTutorial && ( { markTutorialSeen("map", disableAll); setShowTutorial(false); }}>

Click any port to set sail. Hover to see:

  • How many days the voyage will take
  • Your reputation at that port

Grey ports are out of range — you'll need a bigger ship. Upgrade at a Shipyard when you can afford it.

)}
{/* ════ ALL ZOOMABLE MAP CONTENT ════ */} {state.activeMission && (() => { const fr = PORTS[state.currentPort]; const to = PORTS[state.activeMission.targetPort]; return fr && to ? : null; })()} {/* Ports */} {Object.entries(PORTS).filter(([key]) => state.discoveredPorts?.includes(key)).map(([key, p]) => { const isHov = hov === key; const isMissionTarget = state.activeMission?.targetPort === key; const fColor = FACTIONS[p.faction]?.color ?? T.textDim; const rep = state.reputation[key] ?? 20; let days, reachable; if (atSea) { days = L.travelDaysFromPosition(seaPos, key, state); reachable = L.canReachFromPosition(seaPos, key, state, remainingEndurance) && key !== state.route.destinationPort; } else { days = L.travelDays(state.currentPort, key, state); reachable = L.canReach(state, key) && key !== state.currentPort; } return ( reachable && dispatch({ type: A.SAIL_TO, port: key })} onMouseEnter={() => setHov(key)} onMouseLeave={() => setHov(null)} style={{ cursor: reachable ? "pointer" : "default" }}> {isMissionTarget && ( )} {isHov && } {p.name.toUpperCase()} {isHov && (reachable ? (<> {days} day{days !== 1 ? "s" : ""} = 40 ? T.greenBr : T.redBr} fontFamily={T.font}>{L.reputationLabel(rep)} ) : ( {atSea ? getAtSeaUnreachableReason(key, days) : (L.getUnreachableReason(state, key) || `Out of range — ${days} day${days !== 1 ? "s" : ""}`)} ))} {isHov && (() => { const alertLevel = state.factionAlerts?.[p.faction] || 0; if (alertLevel > 0) return (⚠ Heat {alertLevel}); return null; })()} ); })} {/* Ship marker */} {playerPos && ( )} {/* ════ FIXED WIND COMPASS (not zoomable) ════ */} {[["N",0,-15],["E",15,4],["S",0,18],["W",-15,4]].map(([d,dx,dy]) => {d})} {state.wind.speed}KT
{/* Zoom controls */}
setTransform(p => ({ ...p, scale: Math.max(1, p.scale / 1.2) }))}>− setTransform(p => ({ ...p, scale: Math.min(3, p.scale * 1.2) }))}>+
{/* Faction legend */} {Object.entries(FACTIONS).map(([k, f]) => (
{f.label}
))} Click a port to sail there · Hover to see distance & standing
); } // ── SAILING SCREEN (responsive single‑column on narrow) ──────── function SailingScreen({ state, dispatch }) { const from = PORTS[state.currentPort] ?? { x: 380, y: 230 }; const to = PORTS[state.destination] ?? { x: 380, y: 230 }; const progress = state.sailingDaysTotal > 0 ? 1 - (state.sailingDaysLeft / state.sailingDaysTotal) : 0; const shipX = from.x + (to.x - from.x) * progress; const shipY = from.y + (to.y - from.y) * progress; const hdgDeg = Math.atan2(to.y - from.y, to.x - from.x) * 180 / Math.PI; const arrived = state.sailingDaysLeft <= 0; const W = 760, H = 460; const consumption = L.getProvisionConsumptionPerDay(state); const daysLeft = L.getDaysOfProvisions(state.hold?.items || {}, consumption); const loadPct = L.getHoldLoadPct(state.hold?.items, L.getHoldCapacity(state)); const speedMult = L.getHoldSpeedMultiplier(loadPct); const [showTutorial, setShowTutorial] = React.useState(() => shouldShowTutorial(state,"sailing")); // Rerouting availability const reachableFromSea = L.getReachablePortsFromSea(state); const canChangeCourse = reachableFromSea.length > 0; // ── Responsive breakpoint (dynamic) ───────────────────────── const [isNarrow, setIsNarrow] = React.useState(window.innerWidth < 640); React.useEffect(() => { const handle = () => setIsNarrow(window.innerWidth < 640); window.addEventListener("resize", handle); return () => window.removeEventListener("resize", handle); }, []); return (
{/* Tutorial Popup */} {showTutorial && ( { markTutorialSeen("sailing", disableAll); setShowTutorial(false); }} >

Click Advance Day to sail toward your destination. Each day:

  • Your crew consumes food and water
  • Crew wages are deducted
  • Random events may happen — storms, encounters, opportunities

When you arrive, click Enter Port to dock.

)} {/* ── Map panel ─────────────────────────────────────────── */}
{PORTS[state.currentPort]?.name?.toUpperCase()} {PORTS[state.destination]?.name?.toUpperCase()} {[["N",0,-15],["E",15,4],["S",0,18],["W",-15,4]].map(([d,dx,dy]) => {d})} {state.wind.speed}KT
{/* ── Controls / provisions / log ──────────────────────── */}
En route to {PORTS[state.destination]?.name}
{arrived ? "Arrived — ready to dock" : `${state.sailingDaysLeft} day${state.sailingDaysLeft !== 1 ? "s" : ""} remaining`}
dispatch({ type: A.ADVANCE_DAY })} disabled={arrived}> Advance Day dispatch({ type: A.ENTER_PORT })} disabled={!arrived}> Enter Port
{!arrived && (
dispatch({ type: A.NAVIGATE, screen: "map" })} disabled={!canChangeCourse}> Change Course {!canChangeCourse && (
No alternate port is reachable from your current position under present conditions.
)}
)}
Wind {state.wind.speed}kt at {state.wind.angle}° {state.activeMission ? ` · Mission: ${state.activeMission.name}` : ""} {speedMult > 1 && ( {' '}— {speedMult >= 1.33 ? "very heavy load" : "heavy load"} )}
PROVISIONS
Food: {state.hold?.items?.food ?? 0} ({daysLeft.food} days)
Water: {state.hold?.items?.water ?? 0} ({daysLeft.water} days)
Crew consumes {consumption.food} food + {consumption.water} water / day
CAPTAIN'S LOG
); } Object.assign(window.S, { MapScreen, SailingScreen}); })();