// 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 (
Click any port to set sail. Hover to see:
Grey ports are out of range — you'll need a bigger ship. Upgrade at a Shipyard when you can afford it.
Click Advance Day to sail toward your destination. Each day:
When you arrive, click Enter Port to dock.