Spaces:
Runtime error
Runtime error
| import type { | |
| BeastSnapshot, | |
| EntitySnapshot, | |
| HouseSnapshot, | |
| ResourceNodeSnapshot, | |
| } from "../types"; | |
| import { TooltipLabel } from "./TooltipLabel"; | |
| type AgentPanelProps = { | |
| entity: EntitySnapshot | null; | |
| beast?: BeastSnapshot | null; | |
| resource?: ResourceNodeSnapshot | null; | |
| house?: HouseSnapshot | null; | |
| entities?: EntitySnapshot[]; | |
| onSelectEntity?: (id: string) => void; | |
| }; | |
| export function AgentPanel({ | |
| entity, | |
| beast = null, | |
| resource = null, | |
| house = null, | |
| entities = [], | |
| onSelectEntity, | |
| }: AgentPanelProps) { | |
| if (beast) { | |
| return <BeastPanel beast={beast} />; | |
| } | |
| if (resource) { | |
| return <ResourcePanel resource={resource} />; | |
| } | |
| if (house) { | |
| return <HousePanel house={house} entities={entities} onSelectEntity={onSelectEntity} />; | |
| } | |
| const selectedName = entity?.label ?? "No NPC"; | |
| const selectedRole = entity | |
| ? [entity.country_id, entity.special_status ?? entity.role].filter(Boolean).join(" / ") | |
| : "world"; | |
| return ( | |
| <aside className="agentPanel" aria-label="Selected NPC"> | |
| <div className="panelHeader"> | |
| <TooltipLabel className="panelTitle" value={selectedName} /> | |
| <TooltipLabel className="panelRole" value={selectedRole} /> | |
| </div> | |
| {entity ? <AgentDetails entity={entity} /> : <EmptyAgentReadout />} | |
| </aside> | |
| ); | |
| } | |
| function BeastPanel({ beast }: { beast: BeastSnapshot }) { | |
| const suffix = beast.id.replace(/^beast[_-]?/, ""); | |
| const beastName = suffix ? `Beast ${suffix}` : "Beast"; | |
| const healthLabel = `${beast.health} / ${beast.max_health} HP`; | |
| const positionLabel = `x ${beast.position.x.toFixed(1)} / z ${beast.position.z.toFixed(1)}`; | |
| return ( | |
| <aside className="agentPanel" aria-label="Selected beast"> | |
| <div className="panelHeader"> | |
| <TooltipLabel className="panelTitle" value={beastName} /> | |
| <TooltipLabel className="panelRole" value="beast" /> | |
| </div> | |
| <div className="agentReadout"> | |
| <TooltipLabel className="readoutCell healthCell" value={healthLabel} /> | |
| <TooltipLabel className="readoutCell" value={beast.state} /> | |
| <TooltipLabel | |
| className="readoutCell" | |
| value={beast.target_npc_id ? `hunting ${beast.target_npc_id}` : "no target"} | |
| /> | |
| <TooltipLabel className="readoutCell" value={positionLabel} /> | |
| </div> | |
| </aside> | |
| ); | |
| } | |
| function ResourcePanel({ resource }: { resource: ResourceNodeSnapshot }) { | |
| const title = resource.type | |
| ? `${resource.type.charAt(0).toUpperCase()}${resource.type.slice(1)} node` | |
| : "Resource node"; | |
| const amountLabel = resource.max_amount | |
| ? `${resource.amount} / ${resource.max_amount}` | |
| : `${resource.amount} left`; | |
| const positionLabel = `x ${resource.position.x.toFixed(1)} / z ${resource.position.z.toFixed(1)}`; | |
| return ( | |
| <aside className="agentPanel" aria-label="Selected resource"> | |
| <div className="panelHeader"> | |
| <TooltipLabel className="panelTitle" value={title} /> | |
| <TooltipLabel className="panelRole" value="resource" /> | |
| </div> | |
| <div className="agentReadout"> | |
| <TooltipLabel className="readoutCell" value={amountLabel} /> | |
| <TooltipLabel className="readoutCell" value={resource.id} /> | |
| <TooltipLabel className="readoutCell" value={positionLabel} /> | |
| </div> | |
| </aside> | |
| ); | |
| } | |
| type HousePanelProps = { | |
| house: HouseSnapshot; | |
| entities: EntitySnapshot[]; | |
| onSelectEntity?: (id: string) => void; | |
| }; | |
| function HousePanel({ house, entities, onSelectEntity }: HousePanelProps) { | |
| const isComplete = house.state === "completed"; | |
| const suffix = house.id.replace(/^house[_-]?/, ""); | |
| const title = suffix ? `Home ${suffix}` : "Home"; | |
| const buildRatio = Math.min(1, house.build_progress / 10); | |
| const statusLabel = isComplete | |
| ? house.hp < house.max_hp | |
| ? "damaged" | |
| : "intact" | |
| : `building ${Math.round(buildRatio * 100)}%`; | |
| const positionLabel = `x ${house.position.x.toFixed(1)} / z ${house.position.z.toFixed(1)}`; | |
| return ( | |
| <aside className="agentPanel" aria-label="Selected house"> | |
| <div className="panelHeader"> | |
| <TooltipLabel className="panelTitle" value={title} /> | |
| <TooltipLabel className="panelRole" value={isComplete ? "home" : "construction"} /> | |
| </div> | |
| <div className="agentReadout"> | |
| <TooltipLabel className="readoutCell healthCell" value={`${house.hp} / ${house.max_hp} HP`} /> | |
| <TooltipLabel className="readoutCell" value={statusLabel} /> | |
| <TooltipLabel | |
| className="readoutCell" | |
| value={`inside ${house.occupant_ids.length}/${house.capacity}`} | |
| /> | |
| <TooltipLabel className="readoutCell" value={positionLabel} /> | |
| </div> | |
| <HouseResidentList | |
| label="Inside now" | |
| ids={house.occupant_ids} | |
| emptyLabel="nobody inside" | |
| entities={entities} | |
| onSelectEntity={onSelectEntity} | |
| /> | |
| <HouseResidentList | |
| label="Owners" | |
| ids={house.owner_ids} | |
| emptyLabel="unclaimed" | |
| entities={entities} | |
| onSelectEntity={onSelectEntity} | |
| /> | |
| </aside> | |
| ); | |
| } | |
| type HouseResidentListProps = { | |
| label: string; | |
| ids: string[]; | |
| emptyLabel: string; | |
| entities: EntitySnapshot[]; | |
| onSelectEntity?: (id: string) => void; | |
| }; | |
| function HouseResidentList({ label, ids, emptyLabel, entities, onSelectEntity }: HouseResidentListProps) { | |
| return ( | |
| <div className="houseResidents" aria-label={label}> | |
| <span className="houseResidentsLabel">{label}</span> | |
| <div className="relationshipStrip houseResidentStrip"> | |
| {ids.length === 0 ? ( | |
| <span className="relationshipPill">{emptyLabel}</span> | |
| ) : ( | |
| ids.map((id) => { | |
| const npc = entities.find((entity) => entity.id === id); | |
| return ( | |
| <button | |
| key={id} | |
| type="button" | |
| className="relationshipPill houseResidentPill" | |
| onClick={() => onSelectEntity?.(id)} | |
| title={`Select ${npc?.label ?? id}`} | |
| > | |
| {npc?.label ?? id} | |
| </button> | |
| ); | |
| }) | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| type AgentDetailsProps = { | |
| entity: EntitySnapshot; | |
| }; | |
| function AgentDetails({ entity }: AgentDetailsProps) { | |
| return ( | |
| <> | |
| <AgentReadout entity={entity} /> | |
| <SurvivalReadout entity={entity} /> | |
| <ThinkingBlock entity={entity} /> | |
| <RelationshipList entity={entity} /> | |
| <MemoryList entity={entity} /> | |
| </> | |
| ); | |
| } | |
| function SurvivalReadout({ entity }: { entity: EntitySnapshot }) { | |
| const { hunger, fear, safety, age, max_age, importance, goal, inventory } = entity.state; | |
| if ( | |
| hunger === undefined && | |
| fear === undefined && | |
| safety === undefined && | |
| age === undefined && | |
| importance === undefined && | |
| goal === undefined && | |
| !inventory | |
| ) { | |
| return null; | |
| } | |
| const inventoryLabel = inventory | |
| ? `food ${inventory.food} / herbs ${inventory.herbs} / wood ${inventory.wood} / coins ${inventory.coins} / wpn ${inventory.weapon}` | |
| : "-"; | |
| return ( | |
| <div className="agentReadout"> | |
| {hunger !== undefined ? ( | |
| <TooltipLabel className="readoutCell" value={`Hunger ${hunger.toFixed(0)}`} /> | |
| ) : null} | |
| {fear !== undefined ? ( | |
| <TooltipLabel className="readoutCell" value={`Fear ${fear.toFixed(0)}`} /> | |
| ) : null} | |
| {safety !== undefined ? ( | |
| <TooltipLabel className="readoutCell" value={`Safety ${safety.toFixed(0)}`} /> | |
| ) : null} | |
| {age !== undefined && max_age !== undefined ? ( | |
| <TooltipLabel className="readoutCell" value={`Age ${age}/${max_age}`} /> | |
| ) : null} | |
| {importance !== undefined ? ( | |
| <TooltipLabel className="readoutCell" value={`Importance ${importance.toFixed(1)}`} /> | |
| ) : null} | |
| {goal ? <TooltipLabel className="readoutCell" value={goal} /> : null} | |
| <TooltipLabel className="readoutCell" value={inventoryLabel} /> | |
| </div> | |
| ); | |
| } | |
| function AgentReadout({ entity }: { entity: EntitySnapshot }) { | |
| const selectedHealth = `${entity.state.health} / ${entity.state.max_health} HP`; | |
| const selectedAttackDamage = `DMG ${entity.state.attack_damage}`; | |
| const selectedPosition = `x ${entity.position.x.toFixed(1)} / z ${entity.position.z.toFixed(1)}`; | |
| return ( | |
| <div className="agentReadout"> | |
| <TooltipLabel className="readoutCell healthCell" value={selectedHealth} /> | |
| <TooltipLabel className="readoutCell" value={selectedAttackDamage} /> | |
| <TooltipLabel className="readoutCell" value={entity.state.intention} /> | |
| <TooltipLabel className="readoutCell" value={selectedPosition} /> | |
| </div> | |
| ); | |
| } | |
| function ThinkingBlock({ entity }: { entity: EntitySnapshot }) { | |
| const reasoning = entity.state.last_reasoning; | |
| if (!reasoning || !reasoning.trim()) { | |
| return null; | |
| } | |
| const tick = entity.state.last_reasoning_tick; | |
| return ( | |
| <div className="thinkingBlock" aria-label={`${entity.label} thinking`}> | |
| <TooltipLabel | |
| className="thinkingLabel" | |
| value={tick !== undefined && tick !== null ? `Thinking · tick ${tick}` : "Thinking"} | |
| /> | |
| <p className="thinkingText">{reasoning}</p> | |
| </div> | |
| ); | |
| } | |
| function RelationshipList({ entity }: { entity: EntitySnapshot }) { | |
| const relationships = Object.entries(entity.state.relationships ?? {}) | |
| .sort((left, right) => Math.abs(right[1]) - Math.abs(left[1]) || left[0].localeCompare(right[0])) | |
| .slice(0, 3); | |
| if (relationships.length === 0) { | |
| return null; | |
| } | |
| return ( | |
| <div className="relationshipStrip" aria-label={`${entity.label} relationships`}> | |
| {relationships.map(([id, trust]) => ( | |
| <TooltipLabel | |
| key={id} | |
| className="relationshipPill" | |
| value={`${id} ${trust >= 0 ? "+" : ""}${trust.toFixed(2)}`} | |
| /> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| function MemoryList({ entity }: { entity: EntitySnapshot }) { | |
| const recentEpisodes = entity.state.recent_episodes ?? []; | |
| return ( | |
| <div className="memoryList" aria-label={`${entity.label} memories`}> | |
| {entity.state.memory_summary ? ( | |
| <div className="memorySummary"> | |
| <TooltipLabel className="memoryTick" value="Summary" /> | |
| <p>{entity.state.memory_summary}</p> | |
| </div> | |
| ) : null} | |
| {recentEpisodes | |
| .slice() | |
| .reverse() | |
| .map((episode) => ( | |
| <div className="memoryItem" key={`${episode.tick}-${episode.kind}-${episode.summary}`}> | |
| <TooltipLabel | |
| className="memoryTick" | |
| value={`Tick ${episode.tick} ${episode.kind}/${episode.perspective}`} | |
| /> | |
| <p>{episode.summary}</p> | |
| </div> | |
| ))} | |
| {entity.state.memories | |
| .slice() | |
| .reverse() | |
| .map((memory) => ( | |
| <div className="memoryItem" key={`${memory.tick}-${memory.text}`}> | |
| <TooltipLabel className="memoryTick" value={`Tick ${memory.tick}`} /> | |
| <p>{memory.text}</p> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| function EmptyAgentReadout() { | |
| return ( | |
| <div className="agentReadout"> | |
| <TooltipLabel className="readoutCell" value="waiting" /> | |
| </div> | |
| ); | |
| } | |