import { useMemo, useState } from 'react' import { Link, useParams } from 'react-router-dom' import PublicLayout from '../../components/PublicLayout.jsx' import PageHeader from '../../components/PageHeader.jsx' import { Loading, ErrorState, EmptyState } from '../../components/PageState.jsx' import { useAsync } from '../../lib/useAsync.js' import { api } from '../../api/client.js' // One creature: where it spawns, and what spawns alongside it. // // `places` is the point of the page — the aggregate that turns 62 raw // coordinates into "Shrines, Isamu-Jima, Yew". The individual spawners are // available underneath for the reader who actually wants a coordinate, but they // are secondary and collapsed by default. const num = (v) => (Number.isFinite(v) ? v.toLocaleString() : '—') // Spawn delays are stored in seconds. A raw "1200" tells the reader nothing. function delay(min, max) { const fmt = (s) => (s >= 60 ? `${Math.round(s / 60)}m` : `${s}s`) if (!Number.isFinite(min) || !Number.isFinite(max)) return null if (min === max) return fmt(min) return `${fmt(min)}–${fmt(max)}` } function Panel({ title, right, children }) { return (

{title}

{right}
{children}
) } function Places({ places }) { if (places.length === 0) { return

No placed spawners.

} return (
{places.map((place) => (
{place.label} {place.facet} · {num(place.spawners)} spawner{place.spawners === 1 ? '' : 's'} · up to{' '} {num(place.maxAlive)} at once
))}
) } function Spawners({ spawners, truncated }) { const [open, setOpen] = useState(false) if (spawners.length === 0) return null return ( setOpen((v) => !v)} style={{ background: 'none', border: 'none', color: 'var(--accent)', cursor: 'pointer', fontSize: '0.78rem' }} > {open ? 'Hide' : `Show ${num(spawners.length)}`} } > {open && (
{spawners.map((s) => ( ))}
Place Facet Coords Max Respawn
{s.label} {s.facet} {s.x}, {s.y} {num(s.maxCount)} {delay(s.minDelay, s.maxDelay) || '—'}
{truncated && (

Only the largest spawners are listed.

)}
)}
) } export default function AtlasCreature() { const { slug } = useParams() const { loading, error, data } = useAsync(() => api.atlas.creature(slug), [slug]) // A 404 here means "no such creature in this atlas", which is a real answer // and not a failure — a visitor following a stale link deserves to be told // that plainly rather than shown a generic error box. const missing = error?.status === 404 || error?.message === 'Not Found' const facets = useMemo( () => Object.entries(data?.facets || {}).sort((a, b) => b[1] - a[1]), [data], ) return (

← Spawn atlas

{loading && } {error && !missing && } {missing && Nothing by that name spawns on this shard.} {!loading && !error && data && ( <>
{facets.map(([facet, n]) => `${facet} (${n})`).join(' · ')} } > {data.alsoHere?.length > 0 && (
{data.alsoHere.map((other) => ( {other.name} ×{num(other.shared)} ))}
)}
)}
) }