refactor(agent-office): rewrite AgentSprite with BFS movement and idle wandering
This commit is contained in:
@@ -1,89 +1,261 @@
|
|||||||
import { drawAgent, getAnimSpeed } from './SpriteSheet';
|
// src/pages/agent-office/canvas/AgentSprite.js
|
||||||
|
|
||||||
|
import { ProceduralSprite } from './ProceduralSprite.js';
|
||||||
|
|
||||||
|
const WALK_SPEED = 3; // tiles per second
|
||||||
|
const WANDER_DELAY_MIN = 3;
|
||||||
|
const WANDER_DELAY_MAX = 8;
|
||||||
|
const WANDER_LIMIT_MIN = 3;
|
||||||
|
const WANDER_LIMIT_MAX = 6;
|
||||||
|
const REST_DELAY_MIN = 2;
|
||||||
|
const REST_DELAY_MAX = 20;
|
||||||
|
|
||||||
export class AgentSprite {
|
export class AgentSprite {
|
||||||
constructor(agentId, waypoints) {
|
constructor(id, meta, col, row, pathfinder) {
|
||||||
this.agentId = agentId;
|
this.id = id;
|
||||||
this.waypoints = waypoints;
|
this.meta = meta;
|
||||||
this.state = 'idle';
|
this.pathfinder = pathfinder;
|
||||||
|
|
||||||
|
// 위치 (타일 좌표, 실수)
|
||||||
|
this.x = col;
|
||||||
|
this.y = row;
|
||||||
|
this.deskCol = col;
|
||||||
|
this.deskRow = row;
|
||||||
|
|
||||||
|
// 상태
|
||||||
|
this.state = 'idle'; // FSM 상태 (from backend)
|
||||||
this.detail = '';
|
this.detail = '';
|
||||||
this.notificationCount = 0;
|
this.notificationCount = 0;
|
||||||
|
|
||||||
const deskKey = `${agentId}_desk`;
|
// 애니메이션
|
||||||
const desk = waypoints[deskKey] || { x: 5, y: 3 };
|
this.animState = 'idle'; // 렌더링용 상태
|
||||||
this.x = desk.x;
|
this.direction = 'down';
|
||||||
this.y = desk.y;
|
this.animFrame = 0;
|
||||||
this.targetX = desk.x;
|
this.animTimer = 0;
|
||||||
this.targetY = desk.y;
|
|
||||||
this.deskPos = { x: desk.x, y: desk.y };
|
|
||||||
|
|
||||||
this.frameIndex = 0;
|
// 이동
|
||||||
this._lastFrameTime = 0;
|
this.path = []; // BFS 경로 [{col, row}, ...]
|
||||||
this._moveSpeed = 0.05;
|
this.moveProgress = 0; // 0~1 현재 타일 → 다음 타일
|
||||||
|
this.moveFrom = { col, row };
|
||||||
|
this.moveTo_target = null;
|
||||||
|
|
||||||
|
// 배회
|
||||||
|
this._wandering = false;
|
||||||
|
this._wanderTimer = 0;
|
||||||
|
this._wanderCount = 0;
|
||||||
|
this._wanderLimit = 0;
|
||||||
|
this._restTimer = 0;
|
||||||
|
this._isResting = false;
|
||||||
|
this._isAtDesk = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setNotification(count) {
|
/** 매 프레임 호출 */
|
||||||
this.notificationCount = count;
|
update(dt) {
|
||||||
}
|
// 이동 처리
|
||||||
|
if (this.path.length > 0) {
|
||||||
setState(newState, detail = '') {
|
this._updateMovement(dt);
|
||||||
this.state = newState;
|
} else if (this._wandering) {
|
||||||
this.detail = detail;
|
this._updateWander(dt);
|
||||||
this.frameIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
moveTo(target) {
|
|
||||||
const wp = this.waypoints[target];
|
|
||||||
if (wp) {
|
|
||||||
this.targetX = wp.x;
|
|
||||||
this.targetY = wp.y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
moveToDesk() {
|
|
||||||
this.targetX = this.deskPos.x;
|
|
||||||
this.targetY = this.deskPos.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
update(now) {
|
|
||||||
const speed = getAnimSpeed(this.state);
|
|
||||||
if (now - this._lastFrameTime > speed) {
|
|
||||||
this.frameIndex++;
|
|
||||||
this._lastFrameTime = now;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dx = this.targetX - this.x;
|
// 애니메이션 프레임 업데이트
|
||||||
const dy = this.targetY - this.y;
|
this._updateAnimation(dt);
|
||||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
}
|
||||||
|
|
||||||
if (dist > 0.1) {
|
_updateMovement(dt) {
|
||||||
const step = Math.min(this._moveSpeed, dist);
|
this.animState = 'walk';
|
||||||
this.x += (dx / dist) * step;
|
this.moveProgress += WALK_SPEED * dt;
|
||||||
this.y += (dy / dist) * step;
|
|
||||||
|
if (this.moveProgress >= 1) {
|
||||||
|
// 현재 구간 완료
|
||||||
|
const arrived = this.path.shift();
|
||||||
|
this.x = arrived.col;
|
||||||
|
this.y = arrived.row;
|
||||||
|
this.moveFrom = { col: arrived.col, row: arrived.row };
|
||||||
|
this.moveProgress = 0;
|
||||||
|
|
||||||
|
if (this.path.length === 0) {
|
||||||
|
// 최종 목적지 도착
|
||||||
|
this._onArrival();
|
||||||
|
} else {
|
||||||
|
// 다음 구간의 방향 설정
|
||||||
|
this._updateDirection(this.path[0]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.x = this.targetX;
|
// 보간
|
||||||
this.y = this.targetY;
|
const next = this.path[0];
|
||||||
|
this.x = this.moveFrom.col + (next.col - this.moveFrom.col) * this.moveProgress;
|
||||||
|
this.y = this.moveFrom.row + (next.row - this.moveFrom.row) * this.moveProgress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(ctx, renderInfo) {
|
_onArrival() {
|
||||||
const { scale, offsetX, offsetY, tileSize } = renderInfo;
|
const atDesk = Math.abs(this.x - this.deskCol) < 0.5 && Math.abs(this.y - this.deskRow) < 0.5;
|
||||||
const canvasX = offsetX + this.x * tileSize * scale + (tileSize * scale) / 2;
|
this._isAtDesk = atDesk;
|
||||||
const canvasY = offsetY + this.y * tileSize * scale + (tileSize * scale) / 2;
|
|
||||||
|
|
||||||
const isMoving = Math.abs(this.targetX - this.x) > 0.1 || Math.abs(this.targetY - this.y) > 0.1;
|
if (this.state === 'working' || this.state === 'reporting') {
|
||||||
const drawState = isMoving ? 'walk' : this.state;
|
this.animState = 'type';
|
||||||
|
this.direction = 'up'; // 모니터를 바라봄
|
||||||
drawAgent(ctx, this.agentId, canvasX, canvasY, drawState, this.frameIndex, scale * 1.5);
|
} else if (this.state === 'waiting') {
|
||||||
|
this.animState = 'wait';
|
||||||
|
} else if (this.state === 'break') {
|
||||||
|
this.animState = 'break_anim';
|
||||||
|
} else {
|
||||||
|
// idle 도착 — 배회 계속 또는 자리에서 쉬기
|
||||||
|
if (this._wandering && this._wanderCount < this._wanderLimit) {
|
||||||
|
// 다음 배회 타이머 설정
|
||||||
|
this._wanderTimer = WANDER_DELAY_MIN + Math.random() * (WANDER_DELAY_MAX - WANDER_DELAY_MIN);
|
||||||
|
} else if (this._wandering) {
|
||||||
|
// 배회 끝, 휴식
|
||||||
|
this._wandering = false;
|
||||||
|
this._isResting = true;
|
||||||
|
this._restTimer = REST_DELAY_MIN + Math.random() * (REST_DELAY_MAX - REST_DELAY_MIN);
|
||||||
|
}
|
||||||
|
this.animState = 'idle';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTest(canvasX, canvasY, renderInfo) {
|
_updateWander(dt) {
|
||||||
const { scale, offsetX, offsetY, tileSize } = renderInfo;
|
if (this._isResting) {
|
||||||
const cx = offsetX + this.x * tileSize * scale + (tileSize * scale) / 2;
|
this._restTimer -= dt;
|
||||||
const cy = offsetY + this.y * tileSize * scale + (tileSize * scale) / 2;
|
if (this._restTimer <= 0) {
|
||||||
const hitW = 20 * scale;
|
this._isResting = false;
|
||||||
const hitH = 30 * scale;
|
this._startWandering();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return canvasX >= cx - hitW && canvasX <= cx + hitW &&
|
this._wanderTimer -= dt;
|
||||||
canvasY >= cy - hitH && canvasY <= cy + hitH;
|
if (this._wanderTimer <= 0) {
|
||||||
|
// 랜덤 인접 타일로 이동
|
||||||
|
const target = this.pathfinder.getRandomNearbyFloor(
|
||||||
|
Math.round(this.x), Math.round(this.y), 4
|
||||||
|
);
|
||||||
|
if (target) {
|
||||||
|
const path = this.pathfinder.findPath(
|
||||||
|
Math.round(this.x), Math.round(this.y), target.col, target.row
|
||||||
|
);
|
||||||
|
if (path.length > 0 && path.length <= 6) {
|
||||||
|
this.path = path;
|
||||||
|
this.moveFrom = { col: Math.round(this.x), row: Math.round(this.y) };
|
||||||
|
this.moveProgress = 0;
|
||||||
|
this._updateDirection(path[0]);
|
||||||
|
this._wanderCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 실패해도 타이머 리셋
|
||||||
|
this._wanderTimer = WANDER_DELAY_MIN + Math.random() * (WANDER_DELAY_MAX - WANDER_DELAY_MIN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateDirection(nextTile) {
|
||||||
|
const dx = nextTile.col - Math.round(this.x);
|
||||||
|
const dy = nextTile.row - Math.round(this.y);
|
||||||
|
if (Math.abs(dx) > Math.abs(dy)) {
|
||||||
|
this.direction = dx > 0 ? 'right' : 'left';
|
||||||
|
} else {
|
||||||
|
this.direction = dy > 0 ? 'down' : 'up';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateAnimation(dt) {
|
||||||
|
const config = ProceduralSprite.getAnimConfig(
|
||||||
|
this.animState === 'walk' ? 'walk' : this.state
|
||||||
|
);
|
||||||
|
this.animTimer += dt;
|
||||||
|
if (this.animTimer >= config.speed) {
|
||||||
|
this.animTimer = 0;
|
||||||
|
this.animFrame = (this.animFrame + 1) % config.frames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 상태 변경 시 호출 */
|
||||||
|
onStateChange(newState, detail, waypoints) {
|
||||||
|
const prevState = this.state;
|
||||||
|
this.state = newState;
|
||||||
|
this.detail = detail || '';
|
||||||
|
|
||||||
|
// 배회 중단
|
||||||
|
this._wandering = false;
|
||||||
|
this._isResting = false;
|
||||||
|
|
||||||
|
switch (newState) {
|
||||||
|
case 'working':
|
||||||
|
case 'reporting':
|
||||||
|
case 'waiting':
|
||||||
|
// 자리에 없으면 자리로 이동
|
||||||
|
if (!this._isAtDesk) {
|
||||||
|
this._moveToDesk();
|
||||||
|
} else {
|
||||||
|
this.animState = newState === 'waiting' ? 'wait' : 'type';
|
||||||
|
this.direction = 'up';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'break': {
|
||||||
|
// 휴게실로 이동
|
||||||
|
const breakWp = waypoints.break_room || waypoints.coffee;
|
||||||
|
if (breakWp) {
|
||||||
|
this._navigateTo(breakWp.col, breakWp.row);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'idle':
|
||||||
|
if (prevState === 'break') {
|
||||||
|
// 휴게실에서 자리로 복귀
|
||||||
|
this._moveToDesk();
|
||||||
|
}
|
||||||
|
// 복귀 후 배회 시작 (도착 콜백에서 처리)
|
||||||
|
this._startWanderingAfterDelay(3);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_moveToDesk() {
|
||||||
|
this._navigateTo(this.deskCol, this.deskRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
_navigateTo(goalCol, goalRow) {
|
||||||
|
const startCol = Math.round(this.x);
|
||||||
|
const startRow = Math.round(this.y);
|
||||||
|
const path = this.pathfinder.findPath(startCol, startRow, goalCol, goalRow);
|
||||||
|
if (path.length > 0) {
|
||||||
|
this.path = path;
|
||||||
|
this.moveFrom = { col: startCol, row: startRow };
|
||||||
|
this.moveProgress = 0;
|
||||||
|
this._updateDirection(path[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_startWanderingAfterDelay(delay) {
|
||||||
|
this._wandering = true;
|
||||||
|
this._wanderCount = 0;
|
||||||
|
this._wanderLimit = WANDER_LIMIT_MIN + Math.floor(Math.random() * (WANDER_LIMIT_MAX - WANDER_LIMIT_MIN));
|
||||||
|
this._wanderTimer = delay;
|
||||||
|
this._isResting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_startWandering() {
|
||||||
|
this._startWanderingAfterDelay(WANDER_DELAY_MIN + Math.random() * (WANDER_DELAY_MAX - WANDER_DELAY_MIN));
|
||||||
|
}
|
||||||
|
|
||||||
|
isAtDesk() {
|
||||||
|
return this._isAtDesk;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 렌더링 */
|
||||||
|
draw(ctx, zoom, panX, panY, tileSize) {
|
||||||
|
const ts = tileSize * zoom;
|
||||||
|
const screenX = this.x * ts + panX + ts / 2;
|
||||||
|
const screenY = this.y * ts + panY + ts;
|
||||||
|
const spriteScale = zoom * 1.5; // 캐릭터 약간 크게
|
||||||
|
|
||||||
|
ProceduralSprite.draw(
|
||||||
|
ctx, this.id,
|
||||||
|
this.animState === 'walk' ? 'walk' : this.state,
|
||||||
|
this.direction, this.animFrame,
|
||||||
|
screenX, screenY, spriteScale
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user