// screens_voyage.jsx — Voyage-zone screens (responsive) window.S = window.S || {}; (() => { const { useState, useRef, useEffect } = 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, Panel, PortCard, PortModal } = window.UI; const { FactionPill, RepPill, ShipSprite } = window.UI; const { shouldShowTutorial, markTutorialSeen } = window.L; // ── MAP SCREEN ─────────────────────────────────────────────────────── function MapScreen({ state, dispatch }) { const [hov, setHov] = useState(null); const [selectedPort, setSelectedPort] = useState(null); // <-- NEW const [showTutorial, setShowTutorial] = React.useState(() => shouldShowTutorial(state, "map")); const W = 760, H = 460; // ── Zoom / Pan state ─────────────────────────────────────── const mapRef = useRef(null); const [transform, setTransform] = useState({ x: 0, y: 0, scale: 1 }); const [isDragging, setIsDragging] = useState(false); const dragStart = useRef({ x: 0, y: 0, mouseX: 0, mouseY: 0 }); const lastPinchDist = useRef(0); // ── Map size responsive to viewport ──────────────────────── const [mapSize, setMapSize] = useState({ width: 760, height: 460 }); useEffect(() => { const computeSize = () => { const hudHeight = 150; const controlsHeight = 60; const padding = 40; const maxHeight = window.innerHeight - hudHeight - controlsHeight - padding; const maxWidth = window.innerWidth - padding * 2; const aspect = 760 / 460; let w, h; if (maxWidth / maxHeight > aspect) { h = maxHeight; w = h * aspect; } else { w = maxWidth; h = w / aspect; } setMapSize({ width: Math.floor(w), height: Math.floor(h) }); }; computeSize(); window.addEventListener("resize", computeSize); return () => window.removeEventListener("resize", computeSize); }, []); // At-sea detection 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 effectiveHull = L.getShipStats(state).maxHull; if (effectiveHull < port.minHull) return `Requires a heavier vessel`; } if (days > remainingEndurance) return `Out of range (${days} days, only ${remainingEndurance} remaining)`; return null; }; // ── Stable wheel zoom listener ──────────────────────────────── useEffect(() => { const el = mapRef.current; if (!el) return; const onWheel = (e) => { e.preventDefault(); const rect = el.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; const direction = e.deltaY > 0 ? -1 : 1; const factor = 1.15; setTransform(prev => { const newScale = Math.max(1, Math.min(3, prev.scale * (direction > 0 ? factor : 1 / factor))); const scaleChange = newScale / prev.scale; return { scale: newScale, x: mouseX - (mouseX - prev.x) * scaleChange, y: mouseY - (mouseY - prev.y) * scaleChange, }; }); }; el.addEventListener('wheel', onWheel, { passive: false }); return () => el.removeEventListener('wheel', onWheel); }, []); // ── Prevent page scroll during touch pan/pinch ────────────── useEffect(() => { const el = mapRef.current; if (!el) return; const preventScroll = (e) => e.preventDefault(); el.addEventListener('touchmove', preventScroll, { passive: false }); return () => el.removeEventListener('touchmove', preventScroll); }, []); // ── 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; 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 open its details. From there you can set sail.
Grey ports are out of range — you'll need a bigger ship.
Click Advance Day to sail toward your destination. Each day:
When you arrive, click Enter Port to dock.