Skip to content
Closed
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
45 changes: 44 additions & 1 deletion src/components/realm/RealmAccessibilityControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ export type RealmNavigatorCastle = Readonly<{
r: number;
}>;

export type RealmNavigatorWaterBody = Readonly<{
bodyId: string;
label: string;
sourceCellKey: string;
mouthCellKey: string;
sourceCoord: HexCoord;
mouthCoord: HexCoord;
}>;

export type RealmNavigatorCloseReason = 'escape' | 'close-button' | 'camera-preset';

export type RealmNavigatorCoordinateJump = Readonly<{
Expand All @@ -43,11 +52,13 @@ export type RealmAccessibilityControlsProps = Readonly<{
id: string;
open: boolean;
castles: readonly RealmNavigatorCastle[];
waterBodies?: readonly RealmNavigatorWaterBody[];
ownCastleId?: number;
selectedCastleId?: number;
onRequestOpen: () => void;
onRequestClose: (reason: RealmNavigatorCloseReason) => void;
onActivateCastle: (castle: RealmNavigatorCastle) => void;
onActivateWaterCell?: (cellKey: string) => void;
coordinateJump?: RealmNavigatorCoordinateJump;
cameraPresets?: readonly RealmNavigatorCameraPreset[];
/** Player chrome may provide its own PFP launcher while reusing this dialog. */
Expand Down Expand Up @@ -75,11 +86,13 @@ export function RealmAccessibilityControls({
id,
open,
castles,
waterBodies = [],
ownCastleId,
selectedCastleId,
onRequestOpen,
onRequestClose,
onActivateCastle,
onActivateWaterCell,
coordinateJump,
cameraPresets = [],
triggerVisible = true,
Expand Down Expand Up @@ -126,6 +139,16 @@ export function RealmAccessibilityControls({
? castles.filter((castle) => searchCopy(castle).includes(query))
: castles;
}, [castles, search]);
const visibleWaterBodies = useMemo(() => {
const query = search.trim().toLocaleLowerCase();
return query
? waterBodies.filter((body) => (
`${body.label} ${body.sourceCoord.q},${body.sourceCoord.r} ${body.mouthCoord.q},${body.mouthCoord.r}`
.toLocaleLowerCase()
.includes(query)
))
: waterBodies;
}, [search, waterBodies]);

const handleDialogKeyDown = (event: KeyboardEvent<HTMLElement>) => {
if (event.key !== 'Escape') return;
Expand Down Expand Up @@ -164,7 +187,7 @@ export function RealmAccessibilityControls({
<button
ref={setTriggerRef}
type="button"
aria-label={`Explore realm, ${castles.length} founded ${castles.length === 1 ? 'castle' : 'castles'}`}
aria-label={`Explore realm, ${castles.length} founded ${castles.length === 1 ? 'castle' : 'castles'}${waterBodies.length > 0 ? ` and ${waterBodies.length} public rivers` : ''}`}
aria-controls={id}
aria-expanded={open}
aria-haspopup="dialog"
Expand Down Expand Up @@ -291,6 +314,26 @@ export function RealmAccessibilityControls({
</p>
)}

{visibleWaterBodies.length > 0 && onActivateWaterCell ? (
<section className="realm-cell-navigator__water" aria-label="Public rivers">
<span>PUBLIC WATER</span>
<ul className="realm-cell-navigator__castles">
{visibleWaterBodies.map((body) => (
<li key={body.bodyId}>
<div className="realm-cell-navigator__water-row">
<strong>{body.label}</strong>
<small>source {body.sourceCoord.q},{body.sourceCoord.r} · mouth {body.mouthCoord.q},{body.mouthCoord.r}</small>
<div>
<button type="button" onClick={() => onActivateWaterCell(body.sourceCellKey)}>SOURCE</button>
<button type="button" onClick={() => onActivateWaterCell(body.mouthCellKey)}>MOUTH</button>
</div>
</div>
</li>
))}
</ul>
</section>
) : null}

{coordinateJump ? (
<form className="realm-cell-navigator__jump" onSubmit={handleJump}>
<fieldset>
Expand Down
33 changes: 33 additions & 0 deletions src/components/realm/RealmMapScreen.css
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,39 @@
font-weight: 800;
}

.realm-cell-navigator__water {
display: grid;
gap: 0.35rem;
margin-top: 0.8rem;
}

.realm-cell-navigator__water > span {
color: var(--realm-muted);
font-size: 0.6875rem;
font-weight: 800;
letter-spacing: 0.12em;
}

.realm-cell-navigator__water-row {
display: grid;
gap: 0.28rem;
padding: 0.58rem 0.65rem;
border: 1px solid rgb(145 217 216 / 22%);
border-radius: 0.35rem;
background: rgb(19 59 70 / 24%);
}

.realm-cell-navigator__water-row > div {
display: flex;
gap: 0.35rem;
}

.realm-cell-navigator__water-row > div button {
width: auto;
padding: 0.28rem 0.45rem;
font-size: 0.625rem;
}

.realm-cell-navigator__community {
display: grid;
gap: 0.22rem;
Expand Down
95 changes: 93 additions & 2 deletions src/components/realm/RealmMapScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { FoodFarmInspectionPanel } from './FoodFarmInspectionPanel';
import { GoldMineInspectionPanel } from './GoldMineInspectionPanel';
import { LoggingCampInspectionPanel } from './LoggingCampInspectionPanel';
import { StoneQuarryInspectionPanel } from './StoneQuarryInspectionPanel';
import { WaterInspectionPanel } from './WaterInspectionPanel';
import { RealmAccessibilityControls } from './RealmAccessibilityControls';
import {
RealmCastleLabels,
Expand All @@ -54,6 +55,11 @@ import {
} from './createRealmScene';
import { resolveCanonicalWaterProjection } from './realmWaterProjection';
import { projectRealmWaterRevisionTerrainMetadata } from './realmWaterTerrainProjection';
import {
realmWaterNavigatorBodies,
resolveRealmWaterInspectionRecords,
type RealmWaterInspectionRecord
} from './realmWaterInspectionPresentation';
import {
resolveRealmGoldNodePresentations,
type RealmGoldNodePresentation
Expand Down Expand Up @@ -342,6 +348,20 @@ function CanonicalRealmMapScreen({
() => projectRealmWaterRevisionTerrainMetadata(sharedTileMetadata, waterCells),
[sharedTileMetadata, waterCells]
);
const waterRecords = useMemo(
() => resolveRealmWaterInspectionRecords(waterCells, projectedTileMetadata),
[projectedTileMetadata, waterCells]
);
const waterRecordsByKey = useMemo(
() => new Map(waterRecords.map((record) => [record.cellKey, record] as const)),
[waterRecords]
);
const waterRecordsByKeyRef = useRef<ReadonlyMap<string, RealmWaterInspectionRecord>>(waterRecordsByKey);
waterRecordsByKeyRef.current = waterRecordsByKey;
const navigatorWaterBodies = useMemo(
() => realmWaterNavigatorBodies(waterRecords),
[waterRecords]
);
const otherCastles = snapshot.castles;
const surface = useMemo(
() => createAuthoritativeRealmTerrainSurface(
Expand Down Expand Up @@ -694,10 +714,15 @@ function CanonicalRealmMapScreen({
&& 'stoneSiteId' in selectedInspectorTarget
? stoneNodesBySiteId.get(selectedInspectorTarget.stoneSiteId)
: undefined;
const inspectorWater = selectedInspectorTarget !== null
&& 'cellKey' in selectedInspectorTarget
? waterRecordsByKey.get(selectedInspectorTarget.cellKey)
: undefined;
const goldNodeAtSelectedCell = goldNodes.find((node) => sameCoord(node.coord, selectedCoord));
const foodNodeAtSelectedCell = foodNodes.find((node) => sameCoord(node.coord, selectedCoord));
const woodNodeAtSelectedCell = woodNodes.find((node) => sameCoord(node.coord, selectedCoord));
const stoneNodeAtSelectedCell = stoneNodes.find((node) => sameCoord(node.coord, selectedCoord));
const waterAtSelectedCell = waterRecords.find((record) => sameCoord(record.coord, selectedCoord));
const ownProfile = profileRecords.get(ownCastle.castleId)?.profile;
const focusedCastleId = interaction.cameraTarget.kind === 'castle'
? interaction.cameraTarget.castleId
Expand Down Expand Up @@ -768,6 +793,8 @@ function CanonicalRealmMapScreen({
inspectorFocusRef.current?.focus({ preventScroll: true });
} else if (target.kind === 'stone-quarry-inspector') {
inspectorFocusRef.current?.focus({ preventScroll: true });
} else if (target.kind === 'water-inspector') {
inspectorFocusRef.current?.focus({ preventScroll: true });
} else if (target.kind === 'castle-label') {
const label = rootRef.current
?.querySelector<HTMLButtonElement>(`.realm-castle-label[data-castle-id="${target.castleId}"]`)
Expand Down Expand Up @@ -870,6 +897,19 @@ function CanonicalRealmMapScreen({
if (focusSite) sceneRef.current?.focusCell(node.coord);
}, []);

const selectWaterCell = useCallback((record: RealmWaterInspectionRecord, focusWater = true) => {
selectedCoordRef.current = { ...record.coord };
dispatchInteraction({
type: 'activate-water-cell',
cellKey: record.cellKey,
bodyId: record.bodyId,
regime: record.regime,
coord: record.coord,
cameraIntent: focusWater ? 'focus-water' : 'preserve'
});
if (focusWater) sceneRef.current?.focusWaterCell?.(record.cellKey);
}, []);

const openActiveWagon = useCallback((wagon: RealmActiveWagonMenuItem) => {
if (!activeWagons.some((candidate) => (
candidate.resource === wagon.resource && candidate.siteId === wagon.siteId
Expand Down Expand Up @@ -985,6 +1025,14 @@ function CanonicalRealmMapScreen({
// The scene reserves its restrained ground outline for unoccupied terrain;
// castle identity and raycasting provide the occupied-cell cue without a
// depth-tested line cutting through the wider authored landscape base.
if (target?.kind === 'water-cell') {
updateHoveredCastleId(undefined);
hoveredCoordRef.current = null;
sceneRef.current?.setHoveredWaterCellKey?.(target.cellKey);
sceneRef.current?.setHovered(null);
return;
}
sceneRef.current?.setHoveredWaterCellKey?.(null);
updateHoveredCoord(target?.coord ?? null);
updateHoveredCastleId(target?.kind === 'castle' ? target.castleId : undefined);
}, [updateHoveredCastleId, updateHoveredCoord]);
Expand Down Expand Up @@ -1016,8 +1064,13 @@ function CanonicalRealmMapScreen({
if (node) selectStoneNode(node, target.source !== 'wagon');
return;
}
if (target.kind === 'water-cell') {
const record = waterRecordsByKeyRef.current.get(target.cellKey);
if (record) selectWaterCell(record);
return;
}
selectCoord(target.coord);
}, [selectCastle, selectCoord, selectFoodNode, selectGoldNode, selectStoneNode, selectWoodNode]);
}, [selectCastle, selectCoord, selectFoodNode, selectGoldNode, selectStoneNode, selectWaterCell, selectWoodNode]);

const updateCastleProjection = useCallback((frame: RealmCastleProjectionFrame) => {
latestProjectionRef.current = frame;
Expand Down Expand Up @@ -1226,7 +1279,7 @@ function CanonicalRealmMapScreen({
root.querySelectorAll<HTMLElement>(
'.realm-hud, .realm-hud__actions, .realm-profile-trigger, .realm-resource-rail, '
+ '.castle-inspection, .gold-mine-inspection, .food-farm-inspection, .logging-camp-inspection, '
+ '.stone-quarry-inspection, .realm-cell-navigator'
+ '.stone-quarry-inspection, .realm-cell-navigator, .water-inspection'
).forEach((element) => observer?.observe(element));
}
window.addEventListener('resize', updateSceneComposition, { passive: true });
Expand Down Expand Up @@ -1458,10 +1511,18 @@ function CanonicalRealmMapScreen({
? interactionRef.current.inspectorTarget.stoneSiteId
: null
);
scene.setSelectedWaterCellKey?.(
interactionRef.current.inspectorOpen
&& interactionRef.current.inspectorTarget !== null
&& 'cellKey' in interactionRef.current.inspectorTarget
? interactionRef.current.inspectorTarget.cellKey
: null
);
scene.setHovered(hoveredCoordRef.current);
const cameraTarget: RealmCameraTarget = interactionRef.current.cameraTarget;
if (cameraTarget.kind === 'castle') scene.focusCastle(cameraTarget.castleId);
else if (cameraTarget.kind === 'cell') scene.focusCell(cameraTarget.coord);
else if (cameraTarget.kind === 'water') scene.focusWaterCell?.(cameraTarget.cellKey);
else if (cameraTarget.kind === 'keep') scene.recenterKeep();
else if (cameraTarget.kind === 'founding-district') scene.frameFoundingDistrict();
else scene.showRealm();
Expand Down Expand Up @@ -1518,6 +1579,10 @@ function CanonicalRealmMapScreen({
sceneRef.current?.setSelectedStoneSiteId?.(inspectorStoneNode?.siteId ?? null);
}, [inspectorStoneNode?.siteId]);

useEffect(() => {
sceneRef.current?.setSelectedWaterCellKey?.(inspectorWater?.cellKey ?? null);
}, [inspectorWater?.cellKey]);

useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
// Treat one physical Escape press as one hierarchy step. Ignoring the
Expand Down Expand Up @@ -1600,6 +1665,8 @@ function CanonicalRealmMapScreen({
selectWoodNode(woodNodeAtSelectedCell);
} else if (stoneNodeAtSelectedCell) {
selectStoneNode(stoneNodeAtSelectedCell);
} else if (waterAtSelectedCell) {
selectWaterCell(waterAtSelectedCell);
} else {
sceneRef.current?.focusCell(selectedCoord);
dispatchInteraction({
Expand Down Expand Up @@ -2047,10 +2114,27 @@ function CanonicalRealmMapScreen({
/>
) : null}

{inspectorWater ? (
<WaterInspectionPanel
id={`${inspectorId}-water-${inspectorWater.cellKey}`}
record={inspectorWater}
focusTargetRef={inspectorFocusRef}
onRequestClose={() => dispatchInteraction({ type: 'close-inspector' })}
onFocusCell={(cellKey) => {
const record = waterRecordsByKeyRef.current.get(cellKey);
if (record) selectWaterCell(record);
}}
onViewUnderlyingCell={inspectorWater.underlyingTileKey
? () => selectCoord(inspectorWater.coord)
: undefined}
/>
) : null}

<RealmAccessibilityControls
id={navigatorId}
open={interaction.navigatorOpen}
castles={navigatorCastles}
waterBodies={navigatorWaterBodies}
ownCastleId={observerMode ? undefined : ownCastle.castleId}
selectedCastleId={selectedCastle?.castleId}
triggerRef={navigatorTriggerRef}
Expand Down Expand Up @@ -2081,6 +2165,13 @@ function CanonicalRealmMapScreen({
const castle = allCastles.find((candidate) => candidate.castleId === entry.castleId);
if (castle) selectCastle(castle);
}}
onActivateWaterCell={(cellKey) => {
const record = waterRecordsByKeyRef.current.get(cellKey);
if (record) {
selectWaterCell(record);
dispatchInteraction({ type: 'close-navigator' });
}
}}
coordinateJump={{
validate: (coord) => (
isPlayableRealmCoord(surface, coord)
Expand Down
1 change: 1 addition & 0 deletions src/components/realm/WaterInspectionPanel.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading