Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/play/EventLog.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
border-radius: 3px;
animation: eventSlideIn 0.2s ease;
min-height: 22px;
flex-shrink: 0;
}

.event:hover {
Expand Down
111 changes: 111 additions & 0 deletions src/components/play/panels/GalaxyPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,11 @@ export const GalaxyPanel = forwardRef<GalaxyPanelHandle, GalaxyPanelProps>(funct
const autoTravelProgressRef = useRef<AutoTravelProgress | null>(null)
autoTravelProgressRef.current = autoTravelProgress ?? null

const currentSystemIdRef = useRef<string | null>(null)
currentSystemIdRef.current = gameState.system?.id ?? null

const hasCenteredRef = useRef(false)

// ── Imperative API for sidebar ─────────────────────────────────────
useImperativeHandle(ref, () => ({
panToSystem: (systemId: string) => {
Expand Down Expand Up @@ -767,6 +772,27 @@ export const GalaxyPanel = forwardRef<GalaxyPanelHandle, GalaxyPanelProps>(funct
)
ctx.fill()

// Current location indicator — persistent bright ring with glow
if (system.id === currentSystemIdRef.current) {
ctx.save()
const glowRadius = nr * 5
const glow = ctx.createRadialGradient(pos.x, pos.y, nodeRadius * hoverScale, pos.x, pos.y, glowRadius)
glow.addColorStop(0, 'rgba(0, 212, 255, 0.3)')
glow.addColorStop(0.5, 'rgba(0, 212, 255, 0.08)')
glow.addColorStop(1, 'rgba(0, 212, 255, 0)')
ctx.fillStyle = glow
ctx.beginPath()
ctx.arc(pos.x, pos.y, glowRadius, 0, Math.PI * 2)
ctx.fill()

ctx.strokeStyle = 'rgba(0, 212, 255, 0.9)'
ctx.lineWidth = 2.5
ctx.beginPath()
ctx.arc(pos.x, pos.y, nodeRadius * hoverScale + 6 * nodeScale, 0, Math.PI * 2)
ctx.stroke()
ctx.restore()
}

// System name label — hide when tooltip is showing (system has extra info)
const hasExtraInfo = !!(system.empire || system.is_home || system.has_station || system.is_stronghold)
const tooltipShowing = isHovered && hasExtraInfo
Expand Down Expand Up @@ -881,6 +907,81 @@ export const GalaxyPanel = forwardRef<GalaxyPanelHandle, GalaxyPanelProps>(funct
}
}, [gameState.system, panToSystemWithHighlight])

// Snap the view to a system without animation (used for initial centering)
const snapToSystem = useCallback((system: MapSystemData) => {
const s = stateRef.current
s.viewX = -system.x
s.viewY = -system.y
s.targetViewX = -system.x
s.targetViewY = -system.y
s.zoom = 0.5
s.targetZoom = 0.5
}, [])

// Center on current location if game state arrives after map data loaded
useEffect(() => {
if (hasCenteredRef.current) return
const s = stateRef.current
if (!s.mapData || !gameState.system) return
const currentSystem = s.mapData.systems.find((sys) => sys.id === gameState.system!.id)
if (currentSystem) {
snapToSystem(currentSystem)
hasCenteredRef.current = true
}
}, [gameState.system, snapToSystem])

// Fit view to show full route when one is first plotted
const hasFitRouteRef = useRef(false)
useEffect(() => {
if (!plannedRoute) {
hasFitRouteRef.current = false
return
}
if (hasFitRouteRef.current) return

if (!plannedRoute.route || plannedRoute.route.length < 2) return
const s = stateRef.current
if (!s.mapData) return

hasFitRouteRef.current = true

const systemMap = new Map(s.mapData.systems.map((sys) => [sys.id, sys]))

// Compute bounding box of all route systems
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity
for (const step of plannedRoute.route) {
const sys = systemMap.get(step.system_id)
if (!sys) continue
minX = Math.min(minX, sys.x)
maxX = Math.max(maxX, sys.x)
minY = Math.min(minY, sys.y)
maxY = Math.max(maxY, sys.y)
}
if (!isFinite(minX)) return

const canvas = canvasRef.current
if (!canvas) return

const centerX = (minX + maxX) / 2
const centerY = (minY + maxY) / 2
const routeWidth = maxX - minX
const routeHeight = maxY - minY
const padding = 1.4

let fitZoom = DEFAULT_ZOOM
if (routeWidth > 0 || routeHeight > 0) {
const zoomX = routeWidth > 0 ? canvas.clientWidth / (routeWidth * padding) : MAX_ZOOM
const zoomY = routeHeight > 0 ? canvas.clientHeight / (routeHeight * padding) : MAX_ZOOM
fitZoom = Math.min(zoomX, zoomY)
}
fitZoom = Math.max(MIN_ZOOM, Math.min(1.0, fitZoom))

s.targetViewX = -centerX
s.targetViewY = -centerY
s.targetZoom = fitZoom
s.isAnimating = true
}, [plannedRoute])

const resetView = useCallback(() => {
const s = stateRef.current
s.viewX = 0
Expand Down Expand Up @@ -1000,6 +1101,16 @@ export const GalaxyPanel = forwardRef<GalaxyPanelHandle, GalaxyPanelProps>(funct

s.mapData = data

// Center on player's current location on first load
const currentId = currentSystemIdRef.current
if (currentId && !hasCenteredRef.current) {
const currentSystem = data.systems.find((sys) => sys.id === currentId)
if (currentSystem) {
snapToSystem(currentSystem)
hasCenteredRef.current = true
}
}

if (loadingRef.current)
loadingRef.current.style.display = 'none'

Expand Down
9 changes: 4 additions & 5 deletions src/components/play/panels/trading/MarketView.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -301,12 +301,11 @@
opacity: 0.8;
}

.insightDot {
width: 6px;
height: 6px;
border-radius: 50%;
.insightBadge {
flex-shrink: 0;
opacity: 0.9;
opacity: 0.85;
display: flex;
align-items: center;
}

.analysisStatus {
Expand Down
18 changes: 13 additions & 5 deletions src/components/play/panels/trading/MarketView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
AlertTriangle,
Bookmark,
X,
Lightbulb,
} from 'lucide-react'
import { useGame } from '../../GameProvider'
import { useItemCatalog } from '../../ItemCatalogContext'
Expand Down Expand Up @@ -457,12 +458,17 @@ export function MarketView() {
)}

{/* Inline analysis status */}
{analysisLoading && (
{analysisLoading ? (
<div className={styles.analysisStatus}>
<Loader2 size={10} className={shared.spinner} />
<span>Analyzing market...</span>
</div>
)}
) : analysisData && analysisData.insights.length > 0 ? (
<div className={styles.analysisStatus}>
<Lightbulb size={10} aria-hidden="true" />
<span>{analysisData.insights.length} insight{analysisData.insights.length !== 1 ? 's' : ''} found</span>
</div>
) : null}

{!marketData ? (
<Loading message="Loading market data..." />
Expand Down Expand Up @@ -539,12 +545,14 @@ export function MarketView() {
)}
{insightStyle && (
<span
className={styles.insightDot}
style={{ background: insightStyle.color }}
className={styles.insightBadge}
style={{ color: insightStyle.color }}
role="img"
aria-label={itemInsights?.map(i => `${formatCategory(i.category)}: ${i.message}`).join('. ') ?? ''}
title={itemInsights?.map(i => `[${formatCategory(i.category)}] ${i.message}`).join('\n') ?? ''}
/>
>
<Lightbulb size={10} aria-hidden="true" />
</span>
)}
</span>
<span className={`${styles.colHave} ${haveQty > 0 ? styles.haveQty : styles.noPrice}`}>
Expand Down
Loading