- AGENT_META/IDS에 blog/realestate 추가 (4 컬럼 대시보드) - SpriteSheet: 블로그(노트북 액센트)/청약(서류가방 액센트) 픽셀 캐릭터 - office-map: 사무실 책상 4개로 확장, blog_desk/realestate_desk waypoint 추가 - AgentColumn/ChatPanel: 에이전트별 퀵 명령 버튼 (키워드 리서치, 매칭 리포트 등) - CommandColumn: 타겟 선택지 4명, 빠른 명령 6개, 아이콘 맵핑 - DocumentPanel: 에이전트별 탭 4개 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
212 lines
5.8 KiB
JavaScript
212 lines
5.8 KiB
JavaScript
import { drawTileMap } from './TileMap';
|
|
import { AgentSprite } from './AgentSprite';
|
|
import { getCharLabel, drawNotificationBadge } from './SpriteSheet';
|
|
|
|
const STATUS_ICONS = {
|
|
idle: null,
|
|
working: null,
|
|
waiting: '❗',
|
|
reporting: '📋',
|
|
break: '☕',
|
|
};
|
|
|
|
export class OfficeRenderer {
|
|
constructor(canvas, mapData) {
|
|
this.canvas = canvas;
|
|
this.ctx = canvas.getContext('2d');
|
|
this.mapData = mapData;
|
|
this.renderInfo = null;
|
|
this.agents = {};
|
|
this._animId = null;
|
|
this._onClick = null;
|
|
this._onCeoClick = null;
|
|
this._ceoDocBadge = 0;
|
|
|
|
const agentIds = ['stock', 'music', 'blog', 'realestate'];
|
|
for (const id of agentIds) {
|
|
this.agents[id] = new AgentSprite(id, mapData.waypoints);
|
|
}
|
|
}
|
|
|
|
start() {
|
|
this._loop = this._loop.bind(this);
|
|
this._animId = requestAnimationFrame(this._loop);
|
|
}
|
|
|
|
stop() {
|
|
if (this._animId) {
|
|
cancelAnimationFrame(this._animId);
|
|
this._animId = null;
|
|
}
|
|
}
|
|
|
|
resize(width, height) {
|
|
this.canvas.width = width;
|
|
this.canvas.height = height;
|
|
}
|
|
|
|
setOnClick(handler) {
|
|
this._onClick = handler;
|
|
}
|
|
|
|
handleClick(canvasX, canvasY) {
|
|
if (!this.renderInfo) return null;
|
|
|
|
for (const [id, sprite] of Object.entries(this.agents)) {
|
|
if (sprite.hitTest(canvasX, canvasY, this.renderInfo)) {
|
|
if (this._onClick) this._onClick(id);
|
|
return id;
|
|
}
|
|
}
|
|
// CEO desk click detection
|
|
const ceo = this.mapData.waypoints.ceo_desk;
|
|
if (ceo) {
|
|
const { scale, offsetX, offsetY, tileSize } = this.renderInfo;
|
|
const cx = offsetX + ceo.x * tileSize * scale;
|
|
const cy = offsetY + ceo.y * tileSize * scale;
|
|
const hitW = 5 * tileSize * scale;
|
|
const hitH = 2 * tileSize * scale;
|
|
if (canvasX >= cx - tileSize * scale && canvasY >= cy - tileSize * scale &&
|
|
canvasX <= cx + hitW && canvasY <= cy + hitH) {
|
|
if (this._onCeoClick) this._onCeoClick();
|
|
return 'ceo_desk';
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
updateAgentState(agentId, state, detail) {
|
|
const sprite = this.agents[agentId];
|
|
if (sprite) {
|
|
sprite.setState(state, detail);
|
|
if (state === 'idle' || state === 'working' || state === 'waiting') {
|
|
sprite.moveToDesk();
|
|
}
|
|
}
|
|
}
|
|
|
|
moveAgent(agentId, target) {
|
|
const sprite = this.agents[agentId];
|
|
if (sprite) {
|
|
sprite.moveTo(target);
|
|
}
|
|
}
|
|
|
|
setOnCeoClick(handler) {
|
|
this._onCeoClick = handler;
|
|
}
|
|
|
|
setCeoDocBadge(count) {
|
|
this._ceoDocBadge = count;
|
|
}
|
|
|
|
setAgentNotification(agentId, count) {
|
|
const sprite = this.agents[agentId];
|
|
if (sprite) sprite.setNotification(count);
|
|
}
|
|
|
|
_loop(timestamp) {
|
|
const { ctx, canvas, mapData } = this;
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
ctx.fillStyle = '#1a1a2e';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
this.renderInfo = drawTileMap(ctx, mapData, canvas.width, canvas.height);
|
|
|
|
const now = Date.now();
|
|
for (const sprite of Object.values(this.agents)) {
|
|
sprite.update(now);
|
|
sprite.draw(ctx, this.renderInfo);
|
|
}
|
|
|
|
for (const [id, sprite] of Object.entries(this.agents)) {
|
|
this._drawOverlay(ctx, sprite, id);
|
|
}
|
|
|
|
// CEO desk document icon
|
|
this._drawCeoDoc(ctx);
|
|
|
|
this._animId = requestAnimationFrame(this._loop);
|
|
}
|
|
|
|
_drawOverlay(ctx, sprite, agentId) {
|
|
if (!this.renderInfo) return;
|
|
const { scale, offsetX, offsetY, tileSize } = this.renderInfo;
|
|
const cx = offsetX + sprite.x * tileSize * scale + (tileSize * scale) / 2;
|
|
const cy = offsetY + sprite.y * tileSize * scale - 10 * scale;
|
|
|
|
const icon = STATUS_ICONS[sprite.state];
|
|
if (icon) {
|
|
ctx.font = `${14 * scale}px serif`;
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(icon, cx, cy - 15 * scale);
|
|
}
|
|
|
|
// Notification badge (separate from status icon)
|
|
if (sprite.notificationCount > 0) {
|
|
drawNotificationBadge(ctx, cx, cy - 15 * scale, sprite.notificationCount, scale * 1.5);
|
|
}
|
|
|
|
ctx.fillStyle = 'rgba(255,255,255,0.7)';
|
|
ctx.font = `${8 * scale}px monospace`;
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(getCharLabel(agentId), cx, cy + 30 * scale + 30);
|
|
|
|
if (sprite.detail && (sprite.state === 'working' || sprite.state === 'waiting')) {
|
|
const bubbleY = cy - 25 * scale;
|
|
ctx.fillStyle = 'rgba(0,0,0,0.7)';
|
|
const textW = ctx.measureText(sprite.detail).width;
|
|
ctx.fillRect(cx - textW / 2 - 6, bubbleY - 10, textW + 12, 16);
|
|
ctx.fillStyle = '#fff';
|
|
ctx.font = `${7 * scale}px monospace`;
|
|
ctx.fillText(sprite.detail, cx, bubbleY);
|
|
}
|
|
}
|
|
|
|
_drawCeoDoc(ctx) {
|
|
if (!this.renderInfo) return;
|
|
const ceo = this.mapData.waypoints.ceo_desk;
|
|
if (!ceo) return;
|
|
|
|
const { scale, offsetX, offsetY, tileSize } = this.renderInfo;
|
|
const dx = offsetX + (ceo.x - 1) * tileSize * scale;
|
|
const dy = offsetY + (ceo.y - 1) * tileSize * scale;
|
|
const docW = 12 * scale;
|
|
const docH = 16 * scale;
|
|
|
|
// Paper
|
|
ctx.fillStyle = '#e8e0d0';
|
|
ctx.fillRect(dx, dy, docW, docH);
|
|
// Lines on paper
|
|
ctx.fillStyle = '#bbb';
|
|
for (let i = 0; i < 4; i++) {
|
|
ctx.fillRect(dx + 2 * scale, dy + (3 + i * 3) * scale, 8 * scale, 1);
|
|
}
|
|
// Folded corner
|
|
ctx.fillStyle = '#d0c8b8';
|
|
ctx.beginPath();
|
|
ctx.moveTo(dx + docW - 3 * scale, dy);
|
|
ctx.lineTo(dx + docW, dy + 3 * scale);
|
|
ctx.lineTo(dx + docW - 3 * scale, dy + 3 * scale);
|
|
ctx.fill();
|
|
|
|
// Badge on document
|
|
if (this._ceoDocBadge > 0) {
|
|
const bx = dx + docW;
|
|
const by = dy;
|
|
const r = 4 * scale;
|
|
ctx.beginPath();
|
|
ctx.arc(bx, by, r, 0, Math.PI * 2);
|
|
ctx.fillStyle = '#f43f5e';
|
|
ctx.fill();
|
|
ctx.fillStyle = '#fff';
|
|
ctx.font = `bold ${5 * scale}px monospace`;
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(this._ceoDocBadge > 9 ? '9+' : String(this._ceoDocBadge), bx, by);
|
|
}
|
|
}
|
|
}
|