diff --git a/docs/assets/caosmos-ui_screenshot_1.png b/docs/assets/caosmos-ui_screenshot_1.png index bc17a75..f28ea81 100644 Binary files a/docs/assets/caosmos-ui_screenshot_1.png and b/docs/assets/caosmos-ui_screenshot_1.png differ diff --git a/src/core/entities/index.ts b/src/core/entities/index.ts index ac91253..2a9cdf7 100644 --- a/src/core/entities/index.ts +++ b/src/core/entities/index.ts @@ -104,6 +104,51 @@ export interface ActiveTask { completed: boolean; } +export interface CognitiveAnchor { + name: string; + distance: number; + range: string; + direction: string; +} + +export interface ExplorationStatus { + percentage: number; + fullyExplored: boolean; +} + +export interface RememberedPOI { + id: string; + name: string; + category: string; + tags: string[]; + relativeDirection: string; +} + +export interface ZoneMemory { + zoneId: string; + name: string; + zoneType: string; + category: string; + exploration: ExplorationStatus; + rememberedPOIs: RememberedPOI[]; +} + +export interface ZoneMemorySummary { + zoneId: string; + name: string; + zoneType: string; + category: string; + explorationPercentage: number; + fullyExplored: boolean; +} + +export interface MentalMap { + home?: CognitiveAnchor; + nearestCity?: CognitiveAnchor; + currentZoneMemory?: ZoneMemory; + knownZones: ZoneMemorySummary[]; +} + export interface CitizenPerception { identity: Identity; status: CitizenStatus; @@ -113,6 +158,9 @@ export interface CitizenPerception { lastAction: LastAction | null; activeTask: ActiveTask | null; position: Vector3; + mentalMap?: MentalMap; + recentMessages: SpeechMessage[]; + coins: number; } @@ -144,9 +192,9 @@ export interface CitizenDetail { } export interface CognitionEntry { - entityId: string; + citizenId: string; tick: number; - thoughtProcess: string; + reasoning: string; actionTarget: string; } @@ -157,6 +205,9 @@ export interface WorldObject { y: number; z: number; displayName: string; + category?: string; + owned?: string | null; + tags: string[]; properties: Record; } @@ -178,6 +229,11 @@ export interface Zone { id: string; name: string; type: string; + category?: string; + owned?: string | null; + tags: string[]; + isEntryRestricted: boolean; + parentZoneId?: string | null; center: Vector3; width: number; length: number; diff --git a/src/data/mappers/citizenMapper.ts b/src/data/mappers/citizenMapper.ts index 87b785a..cc0018a 100644 --- a/src/data/mappers/citizenMapper.ts +++ b/src/data/mappers/citizenMapper.ts @@ -10,6 +10,11 @@ import type { InventoryItem, LastAction, ActiveTask, + MentalMap, + CognitiveAnchor, + ZoneMemory, + ZoneMemorySummary, + RememberedPOI, CognitionEntry, Vector3, SpeechMessage, @@ -112,6 +117,61 @@ function mapActiveTask(raw: Raw): ActiveTask | null { }; } +function mapCognitiveAnchor(raw: Raw): CognitiveAnchor | undefined { + if (!raw) return undefined; + return { + name: str(raw.name), + distance: num(raw.distance), + range: str(raw.range, 'UNKNOWN'), + direction: str(raw.direction, 'CENTER'), + }; +} + +function mapRememberedPOI(raw: Raw): RememberedPOI { + return { + id: str(raw?.id, ''), + name: str(raw?.name), + category: str(raw?.category, 'UNKNOWN'), + tags: arr(raw?.tags), + relativeDirection: str(raw?.relativeDirection, 'UNKNOWN'), + }; +} + +function mapZoneMemory(raw: Raw): ZoneMemory | undefined { + if (!raw) return undefined; + return { + zoneId: str(raw.zoneId, ''), + name: str(raw.name), + zoneType: str(raw.zoneType, 'UNKNOWN'), + category: str(raw.category, 'UNKNOWN'), + exploration: { + percentage: num(raw.exploration?.percentage), + fullyExplored: bool(raw.exploration?.fullyExplored), + }, + rememberedPOIs: arr(raw.rememberedPOIs).map(mapRememberedPOI), + }; +} + +function mapZoneMemorySummary(raw: Raw): ZoneMemorySummary { + return { + zoneId: str(raw?.zoneId, ''), + name: str(raw?.name), + zoneType: str(raw?.zoneType, 'UNKNOWN'), + category: str(raw?.category, 'UNKNOWN'), + explorationPercentage: num(raw?.explorationPercentage), + fullyExplored: bool(raw?.fullyExplored), + }; +} + +function mapMentalMap(raw: Raw): MentalMap { + return { + home: mapCognitiveAnchor(raw?.home), + nearestCity: mapCognitiveAnchor(raw?.nearestCity), + currentZoneMemory: mapZoneMemory(raw?.currentZoneMemory), + knownZones: arr(raw?.knownZones).map(mapZoneMemorySummary), + }; +} + function mapPerception(raw: Raw): CitizenPerception { return { identity: mapIdentity(raw?.identity), @@ -122,6 +182,9 @@ function mapPerception(raw: Raw): CitizenPerception { lastAction: mapLastAction(raw?.lastAction), activeTask: mapActiveTask(raw?.activeTask), position: mapVector3(raw?.position), + mentalMap: mapMentalMap(raw?.mentalMap), + recentMessages: arr(raw?.recentMessages).map(mapSpeechMessage), + coins: num(raw?.coins), }; } @@ -167,9 +230,9 @@ export function mapCitizenDetail(raw: Raw): CitizenDetail { export function mapCognitionEntry(raw: Raw): CognitionEntry { return { - entityId: str(raw?.entityId, ''), + citizenId: str(raw?.citizenId || raw?.entityId, ''), tick: num(raw?.tick), - thoughtProcess: str(raw?.thoughtProcess, ''), + reasoning: str(raw?.reasoning || raw?.thoughtProcess, ''), actionTarget: str(raw?.actionTarget, ''), }; } diff --git a/src/data/mappers/worldMapper.ts b/src/data/mappers/worldMapper.ts index 20bc9d5..8de5e2a 100644 --- a/src/data/mappers/worldMapper.ts +++ b/src/data/mappers/worldMapper.ts @@ -27,6 +27,9 @@ export function mapWorldObject(raw: any): WorldObject { y: Number(raw?.y || 0), z: Number(raw?.z || 0), displayName: String(raw?.displayName || raw?.id || 'Unknown Object'), + category: raw?.category ? String(raw.category) : undefined, + owned: raw?.owned ? String(raw.owned) : null, + tags: Array.isArray(raw?.tags) ? raw.tags.map(String) : [], properties: raw?.properties || {}, }; } @@ -54,6 +57,11 @@ export function mapZone(raw: Raw): Zone { id: str(raw?.id), name: str(raw?.name, 'Unnamed Zone'), type: str(raw?.type, 'UNKNOWN'), + category: raw?.category ? str(raw.category) : undefined, + owned: raw?.owned ? str(raw.owned) : null, + tags: Array.isArray(raw?.tags) ? raw.tags.map((t: string) => str(t)) : [], + isEntryRestricted: !!raw?.isEntryRestricted, + parentZoneId: raw?.parentZoneId ? str(raw.parentZoneId) : null, center: mapVector3(raw?.center), width: num(raw?.width), length: num(raw?.length), diff --git a/src/presentation/layout/RightSidebar.tsx b/src/presentation/layout/RightSidebar.tsx index 0715fa2..c45e6a7 100644 --- a/src/presentation/layout/RightSidebar.tsx +++ b/src/presentation/layout/RightSidebar.tsx @@ -4,7 +4,16 @@ import { useWorldStore } from '@store/useWorldStore'; import { useUIStore } from '@store/useUIStore'; import { useCitizenDetail, useCognitionPolling } from '@shared/hooks/usePolling'; import { vitalityColor, stateBadgeClass, truncate } from '@shared/utils/formatters'; -import type { CitizenDetail, CognitionEntry, CitizenSummary, SpeechMessage, ExplorationProgress } from '@core/entities'; +import type { + CitizenDetail, + CognitionEntry, + CitizenSummary, + SpeechMessage, + ExplorationProgress, + MentalMap, + CognitiveAnchor, + ZoneMemorySummary +} from '@core/entities'; // ─── Sub-components ────────────────────────────── @@ -105,6 +114,94 @@ function RecentMessagesList({ messages }: { messages: SpeechMessage[] }) { ); } +function CognitiveAnchorCard({ anchor, label }: { anchor: CognitiveAnchor; label: string }) { + return ( +
+
+ {label} + {anchor.range} +
+
{anchor.name}
+
+ {anchor.distance.toFixed(1)}m + {anchor.direction} +
+
+ ); +} + +function MentalMapSection({ mentalMap }: { mentalMap?: MentalMap }) { + const [isOpen, setIsOpen] = React.useState(false); + + if (!mentalMap) return null; + + const hasAnchors = mentalMap.home || mentalMap.nearestCity; + const hasZones = mentalMap.knownZones.length > 0; + + if (!hasAnchors && !hasZones) return null; + + return ( +
+ + + {isOpen && ( +
+ {/* Anchors */} + {hasAnchors && ( +
+ {mentalMap.home && } + {mentalMap.nearestCity && } +
+ )} + + {/* Known Zones */} + {hasZones && ( +
+
Known Regions
+ {mentalMap.knownZones.map((zone: ZoneMemorySummary) => ( +
+
+ {zone.name} + + {zone.fullyExplored ? '✓' : `${zone.explorationPercentage}%`} + +
+ {!zone.fullyExplored && ( +
+
+
+ )} +
+ ))} +
+ )} +
+ )} +
+ ); +} + function ThoughtHistoryList({ history }: { history: CognitionEntry[] }) { const [isOpen, setIsOpen] = React.useState(false); @@ -228,6 +325,9 @@ function CitizenInspector({ detail, uuid }: { detail: CitizenDetail; uuid: strin {/* Exploration (Collapsible) */} + {/* Mental Map (New) */} + + {/* Active Task */} {activeTask && (
@@ -343,7 +443,7 @@ function CognitionEntryCard({ entry }: { entry: CognitionEntry }) { {entry.actionTarget}

- {expanded ? entry.thoughtProcess : truncate(entry.thoughtProcess, 80)} + {expanded ? entry.reasoning : truncate(entry.reasoning, 80)}

); diff --git a/src/presentation/map/MapViewport.tsx b/src/presentation/map/MapViewport.tsx index fdb125a..25bcb46 100644 --- a/src/presentation/map/MapViewport.tsx +++ b/src/presentation/map/MapViewport.tsx @@ -186,7 +186,7 @@ export function MapViewport() { if (containerRef.current) ro.observe(containerRef.current); return () => { - app.ticker.remove(tick); + if (app.ticker) app.ticker.remove(tick); ro.disconnect(); }; }, [isReady, appRef, layers, updateViewportBounds, cameraRef, zoomRef, visibleLayers.trails]); diff --git a/src/presentation/map/renderers.ts b/src/presentation/map/renderers.ts index d6e621e..a1fe847 100644 --- a/src/presentation/map/renderers.ts +++ b/src/presentation/map/renderers.ts @@ -66,7 +66,7 @@ export function drawCitizenGlyph( // State indicator dot (stress/hunger) if (c.vitality < 35) { g.circle(CITIZEN_RADIUS - 2, -(CITIZEN_RADIUS - 2), 3) - .fill({ color: 0xef4444 }); + .fill({ color: 0xd97706 }); } } @@ -136,16 +136,18 @@ export function renderZones( const { g, label } = sprite; const color = isInterior ? 0x6366f1 : 0x8b5cf6; const fillColor = isInterior ? 0x1e1b4b : color; + const strokeColor = zone.isEntryRestricted ? 0xd97706 : color; + const strokeWidth = zone.isEntryRestricted ? 1.5 : (isInterior ? 1.5 : 1); g.clear(); if (isInterior) { g.rect(sx - hw, sy - hh, hw * 2, hh * 2) .fill({ color: fillColor, alpha: 0.4 }) - .stroke({ color, width: 1.5, alpha: 0.6, alignment: 1 }); + .stroke({ color: strokeColor, width: strokeWidth, alpha: 0.6, alignment: 1 }); } else { g.rect(sx - hw, sy - hh, hw * 2, hh * 2) .fill({ color, alpha: 0.02 }) - .stroke({ color, width: 1, alpha: 0.2 }); + .stroke({ color: strokeColor, width: strokeWidth, alpha: zone.isEntryRestricted ? 0.8 : 0.2 }); } label.x = sx - label.width / 2; diff --git a/src/presentation/views/AnalyticsView.tsx b/src/presentation/views/AnalyticsView.tsx index b092f32..13fa245 100644 --- a/src/presentation/views/AnalyticsView.tsx +++ b/src/presentation/views/AnalyticsView.tsx @@ -90,7 +90,9 @@ export function AnalyticsView() { const [sortKey, setSortKey] = React.useState('vitality'); const [sortAsc, setSortAsc] = React.useState(true); - const allCitizens = Array.from(citizens.values()).map(t => t.current); + const allCitizens = Array.from(citizens.values()) + .map(t => t.current) + .filter((c): c is CitizenSummary => !!c && !!c.uuid); const stateDistribution = countByField(allCitizens, 'state'); const scatterData = allCitizens.map(c => ({ x: c.vitality, y: c.x, z: c.z, name: c.name })); @@ -98,7 +100,7 @@ export function AnalyticsView() { const treemapData = { name: 'Population', - children: goalDistribution.map(g => ({ name: g.name, size: g.value })) + children: goalDistribution.map(g => ({ name: g.name, size: g.value || 1 })) }; const filtered = allCitizens @@ -166,7 +168,7 @@ export function AnalyticsView() { tick={{ fill: '#94a3b8', fontSize: 10 }} label={{ value: 'Vitality', position: 'insideBottom', fill: '#475569', fontSize: 10 }} /> [(val as number).toFixed(1), '']} /> + formatter={(val: unknown) => [typeof val === 'number' ? val.toFixed(1) : '—', '']} /> {scatterData.map((entry, i) => ( @@ -186,22 +188,16 @@ export function AnalyticsView() { dataKey="size" nameKey="name" animationDuration={400} - content={(props: { - x: number; - y: number; - width: number; - height: number; - index: number; - name?: string; - }) => { + content={(props: any) => { + const { x = 0, y = 0, width = 0, height = 0, index = 0, name = '' } = props; const colors = ['#06b6d4','#8b5cf6','#10b981','#f59e0b','#3b82f6','#ef4444']; return ( - - {props.width > 40 && props.height > 20 && props.name && ( - {props.name.slice(0, 18)} + {width > 40 && height > 20 && name && ( + {name.slice(0, 18)} )} );