spatial-office/src/frontend/store.js

208 lines
6.4 KiB
JavaScript

import { create } from 'zustand';
// ── Stardew-inspired Zone definitions ───────────────────────────────
const ZONES = {
'pod-a': {
id: 'pod-a',
name: 'Workshop',
type: 'collaboration',
color: '#A0522D',
bounds: { row: 2, col: 8, rows: 5, cols: 5 },
},
'pod-b': {
id: 'pod-b',
name: 'Studio',
type: 'collaboration',
color: '#DEB887',
bounds: { row: 2, col: 14, rows: 5, cols: 5 },
},
'solo-1': {
id: 'solo-1',
name: 'Cabins',
type: 'solo',
color: '#8B4513',
bounds: { row: 2, col: 0, rows: 10, cols: 6 },
},
'solo-2': {
id: 'solo-2',
name: 'Library',
type: 'solo',
color: '#5B8FA8',
bounds: { row: 2, col: 22, rows: 10, cols: 6 },
},
'coaching': {
id: 'coaching',
name: 'Shrine',
type: 'self-improvement',
color: '#DAA520',
bounds: { row: 13, col: 8, rows: 4, cols: 12 },
},
'coffee': {
id: 'coffee',
name: 'Campfire',
type: 'social',
color: '#FF8C42',
bounds: { row: 0, col: 10, rows: 2, cols: 8 },
},
};
// ── Agent definitions ──────────────────────────────────────────────
const AGENT_DEFS = {
cos: { id: 'cos', name: 'COS', department: 'cos', color: '#FF6B35' },
engineering: { id: 'engineering', name: 'Engineering', department: 'engineering', color: '#4ECDC4' },
'senior-eng': { id: 'senior-eng', name: 'Senior Eng', department: 'senior-eng', color: '#7B2D8E' },
trust: { id: 'trust', name: 'Trust', department: 'trust', color: '#45B7D1' },
market: { id: 'market', name: 'Market', department: 'market', color: '#96CEB4' },
health: { id: 'health', name: 'Health', department: 'health', color: '#FFEAA7' },
content: { id: 'content', name: 'Content', department: 'content', color: '#DDA0DD' },
presentations: { id: 'presentations', name: 'Presentations', department: 'presentations', color: '#FF8A80' },
vault: { id: 'vault', name: 'Vault', department: 'vault', color: '#A5D6A7' },
};
// ── Default agent positions (row, col on the pixel grid) ───────────
const DEFAULT_POSITIONS = {
cos: { row: 4, col: 2 },
engineering: { row: 5, col: 10 },
'senior-eng': { row: 6, col: 10 },
trust: { row: 8, col: 2 },
market: { row: 5, col: 16 },
health: { row: 4, col: 24 },
content: { row: 6, col: 16 },
presentations: { row: 7, col: 16 },
vault: { row: 8, col: 24 },
};
// ── Helpers ────────────────────────────────────────────────────────
function generateTileMap(gridRows, gridCols) {
const tiles = [];
for (let row = 0; row < gridRows; row++) {
tiles[row] = [];
for (let col = 0; col < gridCols; col++) {
tiles[row][col] = { type: 'floor', zoneId: findZoneForTile(row, col) };
}
}
return { rows: gridRows, cols: gridCols, tiles };
}
function findZoneForTile(row, col) {
for (const zone of Object.values(ZONES)) {
const { row: zr, col: zc, rows, cols } = zone.bounds;
if (row >= zr && row < zr + rows && col >= zc && col < zc + cols) {
return zone.id;
}
}
return null;
}
// ── Store ──────────────────────────────────────────────────────────
const useStore = create((set, get) => ({
// Connection state
wsConnected: false,
wsReconnecting: false,
// World state
grid: generateTileMap(20, 28),
zones: ZONES,
agents: {},
// Initialize from full state snapshot
initFromSnapshot(snapshot) {
const agents = {};
for (const agentId of Object.keys(AGENT_DEFS)) {
const def = AGENT_DEFS[agentId];
const pos = DEFAULT_POSITIONS[agentId];
agents[agentId] = {
...def,
position: { row: pos.row, col: pos.col },
targetPosition: { row: pos.row, col: pos.col },
activity: 'idle',
currentTask: null,
lastHeartbeat: Date.now(),
pulse: 0,
};
}
// Apply snapshot overrides if provided
if (snapshot && snapshot.agents) {
for (const [id, data] of Object.entries(snapshot.agents)) {
if (agents[id]) {
Object.assign(agents[id], data);
if (data.position) {
agents[id].targetPosition = { ...data.position };
}
}
}
}
set({ agents, wsConnected: true, wsReconnecting: false });
},
// Apply a diff update
applyDiff(diffs) {
const state = get();
const updated = { ...state.agents };
for (const diff of diffs) {
if (diff.op === 'agent.move' || diff.op === 'agent.update') {
const agentId = diff.path.split('.')[1];
if (updated[agentId]) {
const delta = diff.value || {};
if (delta.position) {
delta.targetPosition = { ...delta.position };
}
Object.assign(updated[agentId], delta);
}
} else if (diff.op === 'agent.position') {
const agentId = diff.path.split('.')[1];
if (updated[agentId]) {
updated[agentId].targetPosition = { ...diff.value };
}
} else if (diff.op === 'agent.activity') {
const agentId = diff.path.split('.')[1];
if (updated[agentId]) {
updated[agentId] = { ...updated[agentId], ...diff.value };
}
}
}
set({ agents: updated });
},
// WS connection status
setWsConnected(connected) {
set({ wsConnected: connected, wsReconnecting: !connected });
},
setWsReconnecting(reconnecting) {
set({ wsReconnecting: reconnecting });
},
// Update agent positions directly (used by mock mode)
updateAgentPosition(agentId, position) {
set(state => {
const agents = { ...state.agents };
if (agents[agentId]) {
agents[agentId] = {
...agents[agentId],
targetPosition: { ...position },
};
}
return { agents };
});
},
updateAgentActivity(agentId, activity, task) {
set(state => {
const agents = { ...state.agents };
if (agents[agentId]) {
agents[agentId] = {
...agents[agentId],
activity,
currentTask: task || agents[agentId].currentTask,
};
}
return { agents };
});
},
}));
export { ZONES, AGENT_DEFS, DEFAULT_POSITIONS };
export default useStore;