126 lines
3.5 KiB
JavaScript
126 lines
3.5 KiB
JavaScript
import React, { useState } from 'react';
|
||
|
||
// ── Grid → pixel position helper ─────────────────────────────────
|
||
// Simple top-down mapping: each tile is 32px × 32px
|
||
export const TILE_SIZE = 32;
|
||
|
||
export function gridToPixel(row, col) {
|
||
return {
|
||
left: col * TILE_SIZE + TILE_SIZE / 2,
|
||
top: row * TILE_SIZE + TILE_SIZE / 2,
|
||
};
|
||
}
|
||
|
||
// ── Thought bubble content map ───────────────────────────────────
|
||
const THOUGHT_BUBBLES = {
|
||
thinking: '...',
|
||
blocked: '!',
|
||
};
|
||
|
||
// ── Department accessory emoji ───────────────────────────────────
|
||
const DEPARTMENT_ACCESSORIES = {
|
||
'cos': '📋',
|
||
'engineering': '⚙️',
|
||
'senior-eng': '🔮',
|
||
'trust': '🛡️',
|
||
'market': '🔍',
|
||
'health': '🌿',
|
||
'content': '📝',
|
||
'presentations': '✨',
|
||
'vault': '🗝️',
|
||
};
|
||
|
||
export default function AgentSprite({ agent, tileSize = TILE_SIZE }) {
|
||
const [hovered, setHovered] = useState(false);
|
||
|
||
if (!agent || !agent.position) return null;
|
||
|
||
const { row, col } = agent.targetPosition || agent.position;
|
||
const { left, top } = gridToPixel(row, col);
|
||
|
||
const deptKey = agent.department || agent.id;
|
||
const departmentClass = `agent--${deptKey}`;
|
||
const activity = agent.activity || 'idle';
|
||
const task = agent.currentTask || null;
|
||
const thought = THOUGHT_BUBBLES[activity] || null;
|
||
const accessory = DEPARTMENT_ACCESSORIES[deptKey] || '';
|
||
|
||
// Status glow colors
|
||
const glowColors = {
|
||
idle: '#44cc44',
|
||
thinking: '#ffcc00',
|
||
working: '#4488ff',
|
||
blocked: '#ff4444',
|
||
offline: '#666666',
|
||
};
|
||
|
||
return (
|
||
<div
|
||
className={`agent-sprite ${departmentClass}`}
|
||
data-activity={activity}
|
||
style={{
|
||
left: `${left}px`,
|
||
top: `${top}px`,
|
||
transform: 'translate(-50%, -50%)',
|
||
transition: 'left 0.8s ease-in-out, top 0.8s ease-in-out',
|
||
}}
|
||
onMouseEnter={() => setHovered(true)}
|
||
onMouseLeave={() => setHovered(false)}
|
||
>
|
||
{/* Thought bubble */}
|
||
{thought && (
|
||
<div className="agent-thought">{thought}</div>
|
||
)}
|
||
|
||
{/* Name label */}
|
||
<div className="agent-name" style={{ top: -22 }}>
|
||
{agent.name}
|
||
</div>
|
||
|
||
{/* Sparkle particles for working */}
|
||
<div className="agent-sparkles">
|
||
<div className="sparkle" />
|
||
<div className="sparkle" />
|
||
<div className="sparkle" />
|
||
</div>
|
||
|
||
{/* Accessory icon above head */}
|
||
{accessory && (
|
||
<div style={{
|
||
position: 'absolute',
|
||
top: -18,
|
||
left: '50%',
|
||
transform: 'translateX(-50%)',
|
||
fontSize: '8px',
|
||
lineHeight: 1,
|
||
imageRendering: 'pixelated',
|
||
pointerEvents: 'none',
|
||
filter: 'drop-shadow(1px 1px 0 rgba(0,0,0,0.5))',
|
||
}}>
|
||
{accessory}
|
||
</div>
|
||
)}
|
||
|
||
{/* Pixel-art body */}
|
||
<div className="agent-body" />
|
||
|
||
{/* Status glow ring */}
|
||
<div
|
||
className="agent-glow"
|
||
style={{
|
||
background: glowColors[activity] || '#4c4',
|
||
boxShadow: `0 0 4px ${glowColors[activity] || '#4c4'}`,
|
||
}}
|
||
/>
|
||
|
||
{/* Hover tooltip */}
|
||
{hovered && (
|
||
<div className="agent-tooltip">
|
||
<strong>{agent.name}</strong>
|
||
<div>Status: {activity}</div>
|
||
{task && <div className="task">{task}</div>}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|