Harden room navigation layer for safe furniture rendering
- Add deriveNavigableRooms() that filters house roomOrder against known IDs and enabled state, with safe fallback to defaults - Replace unsafe BlobbiRoomId cast with deriveNavigableRooms in dashboard - Add effect in BlobbiRoomShell to reset current room when roomOrder changes and current room is no longer in the list - Thread houseLoading into dashboard and gate BlobbiRoomShell behind it to prevent flicker/temporary-default rendering during bootstrap - Export isKnownRoomId and deriveNavigableRooms from house barrel
This commit is contained in:
@@ -30,6 +30,8 @@ export {
|
||||
DEFAULT_ROOM_ORDER,
|
||||
buildDefaultHouseContent,
|
||||
getDefaultRoomScene,
|
||||
isKnownRoomId,
|
||||
deriveNavigableRooms,
|
||||
} from './lib/house-defaults';
|
||||
|
||||
// ── Content Helpers ──
|
||||
|
||||
@@ -134,3 +134,51 @@ export function buildDefaultHouseContent(): BlobbiHouseContent {
|
||||
export function getDefaultRoomScene(roomId: string): HouseRoomScene | undefined {
|
||||
return DEFAULT_ROOMS[roomId]?.scene;
|
||||
}
|
||||
|
||||
// ─── Navigable Room Derivation ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* The set of room IDs that have both a registered component and metadata.
|
||||
* Any ID from house data that is NOT in this set is silently ignored.
|
||||
*
|
||||
* Kept in sync with `ROOM_META` / `ROOM_COMPONENTS` in the rooms layer.
|
||||
* We intentionally duplicate the set here (as plain strings) to avoid
|
||||
* importing from the rooms layer and creating a circular dependency.
|
||||
*/
|
||||
const KNOWN_ROOM_IDS = new Set<string>([
|
||||
'care', 'kitchen', 'home', 'hatchery', 'rest', 'closet',
|
||||
]);
|
||||
|
||||
/** Type-guard: is `id` a known room ID string? */
|
||||
export function isKnownRoomId(id: string): boolean {
|
||||
return KNOWN_ROOM_IDS.has(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the final navigable room list from house content.
|
||||
*
|
||||
* Rules applied (in order):
|
||||
* 1. Start from `house.layout.roomOrder`.
|
||||
* 2. Keep only IDs that exist in `KNOWN_ROOM_IDS` (drop future/unknown).
|
||||
* 3. Keep only IDs whose room entry has `enabled !== false`.
|
||||
* 4. If the result is empty, fall back to `DEFAULT_ROOM_ORDER`.
|
||||
*
|
||||
* The returned array is safe to use directly for navigation, dots, and
|
||||
* prev/next helpers — no further filtering needed downstream.
|
||||
*/
|
||||
export function deriveNavigableRooms(
|
||||
house: { layout: { roomOrder: string[]; rooms: Record<string, { enabled: boolean }> } } | null,
|
||||
): string[] {
|
||||
if (!house) return [...DEFAULT_ROOM_ORDER];
|
||||
|
||||
const { roomOrder, rooms } = house.layout;
|
||||
|
||||
const navigable = roomOrder.filter((id) => {
|
||||
if (!KNOWN_ROOM_IDS.has(id)) return false;
|
||||
// A room not present in the rooms map is treated as enabled (default true).
|
||||
const room = rooms[id];
|
||||
return !room || room.enabled !== false;
|
||||
});
|
||||
|
||||
return navigable.length > 0 ? navigable : [...DEFAULT_ROOM_ORDER];
|
||||
}
|
||||
|
||||
@@ -75,6 +75,16 @@ export function BlobbiRoomShell({
|
||||
direction: null,
|
||||
});
|
||||
|
||||
// ── Keep current room valid when roomOrder changes ──
|
||||
// If the current room was removed or disabled, jump to the first available room.
|
||||
// Without this, a stale `nav.current` produces a broken header / missing component.
|
||||
useEffect(() => {
|
||||
setNav(prev => {
|
||||
if (roomOrder.includes(prev.current)) return prev; // still valid
|
||||
return { current: roomOrder[0], direction: null };
|
||||
});
|
||||
}, [roomOrder]);
|
||||
|
||||
const goRight = useCallback(() => {
|
||||
setNav(prev => ({
|
||||
current: getNextRoom(prev.current, roomOrder),
|
||||
|
||||
+18
-13
@@ -84,8 +84,7 @@ import { getActionEmotion, type ActionType } from '@/blobbi/ui/lib/status-reacti
|
||||
import type { BlobbiEmotion } from '@/blobbi/ui/lib/emotions';
|
||||
import { BlobbiRoomShell } from '@/blobbi/rooms/components/BlobbiRoomShell';
|
||||
import type { BlobbiRoomContext } from '@/blobbi/rooms/lib/room-types';
|
||||
import { useBlobbiHouse } from '@/blobbi/house';
|
||||
import { DEFAULT_ROOM_ORDER as HOUSE_DEFAULT_ROOM_ORDER } from '@/blobbi/house/lib/house-defaults';
|
||||
import { useBlobbiHouse, deriveNavigableRooms } from '@/blobbi/house';
|
||||
import type { BlobbiRoomId } from '@/blobbi/rooms/lib/room-config';
|
||||
|
||||
|
||||
@@ -160,6 +159,7 @@ function BlobbiContent() {
|
||||
const {
|
||||
house,
|
||||
houseEvent,
|
||||
isLoading: houseLoading,
|
||||
updateHouseEvent,
|
||||
} = useBlobbiHouse(profile?.event);
|
||||
|
||||
@@ -762,6 +762,7 @@ function BlobbiContent() {
|
||||
profile={profile}
|
||||
house={house}
|
||||
houseEvent={houseEvent}
|
||||
houseLoading={houseLoading}
|
||||
updateHouseEvent={updateHouseEvent}
|
||||
onEvolve={handleEvolve}
|
||||
isHatching={isHatching}
|
||||
@@ -822,6 +823,7 @@ interface BlobbiDashboardProps {
|
||||
// House (kind 11127)
|
||||
house: import('@/blobbi/house/lib/house-types').BlobbiHouseContent | null;
|
||||
houseEvent: import('@nostrify/nostrify').NostrEvent | null;
|
||||
houseLoading: boolean;
|
||||
updateHouseEvent: (event: import('@nostrify/nostrify').NostrEvent) => void;
|
||||
// Stage transition handlers
|
||||
onEvolve: () => Promise<void>;
|
||||
@@ -865,6 +867,7 @@ function BlobbiDashboard({
|
||||
profile,
|
||||
house,
|
||||
houseEvent,
|
||||
houseLoading,
|
||||
updateHouseEvent,
|
||||
onEvolve,
|
||||
isHatching,
|
||||
@@ -1363,16 +1366,12 @@ function BlobbiDashboard({
|
||||
});
|
||||
}, [isUsingItem, onUseItem]);
|
||||
|
||||
// ── Derive room order from house layout, falling back to defaults ──
|
||||
// Cast is safe: house.layout.roomOrder only contains known room IDs from
|
||||
// DEFAULT_ROOM_ORDER or user customisation. Unknown IDs are filtered out.
|
||||
const roomOrder = useMemo((): BlobbiRoomId[] => {
|
||||
const houseOrder = house?.layout.roomOrder;
|
||||
if (houseOrder && houseOrder.length > 0) {
|
||||
return houseOrder as BlobbiRoomId[];
|
||||
}
|
||||
return HOUSE_DEFAULT_ROOM_ORDER as BlobbiRoomId[];
|
||||
}, [house]);
|
||||
// ── Derive navigable room order from house layout ──
|
||||
// Filters against known room IDs, respects enabled state, falls back safely.
|
||||
const roomOrder = useMemo(
|
||||
() => deriveNavigableRooms(house) as BlobbiRoomId[],
|
||||
[house],
|
||||
);
|
||||
|
||||
// ─── Build Room Context ───
|
||||
// This is the single object that flows into BlobbiRoomShell → individual rooms.
|
||||
@@ -1552,7 +1551,13 @@ function BlobbiDashboard({
|
||||
)}
|
||||
|
||||
{/* ─── Room-based Layout ─── */}
|
||||
<BlobbiRoomShell ctx={roomCtx} roomOrder={roomOrder} />
|
||||
{houseLoading ? (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<Skeleton className="size-60 sm:size-72 rounded-full" />
|
||||
</div>
|
||||
) : (
|
||||
<BlobbiRoomShell ctx={roomCtx} roomOrder={roomOrder} />
|
||||
)}
|
||||
|
||||
{/* ─── Global Dialogs (shared across all rooms) ─── */}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user