bmad-visual 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/bmad-visual.js +255 -0
- package/package.json +31 -0
- package/templates/.agent/workflows/analyst.md +20 -0
- package/templates/.agent/workflows/architect.md +20 -0
- package/templates/.agent/workflows/bmad-advanced-elicitation.md +5 -0
- package/templates/.agent/workflows/bmad-brainstorming.md +5 -0
- package/templates/.agent/workflows/bmad-check-implementation-readiness.md +5 -0
- package/templates/.agent/workflows/bmad-checkpoint-preview.md +5 -0
- package/templates/.agent/workflows/bmad-code-review.md +5 -0
- package/templates/.agent/workflows/bmad-correct-course.md +5 -0
- package/templates/.agent/workflows/bmad-create-architecture.md +5 -0
- package/templates/.agent/workflows/bmad-create-epics-and-stories.md +5 -0
- package/templates/.agent/workflows/bmad-create-prd.md +5 -0
- package/templates/.agent/workflows/bmad-create-story.md +5 -0
- package/templates/.agent/workflows/bmad-create-ux-design.md +5 -0
- package/templates/.agent/workflows/bmad-dev-story.md +5 -0
- package/templates/.agent/workflows/bmad-distillator.md +5 -0
- package/templates/.agent/workflows/bmad-document-project.md +5 -0
- package/templates/.agent/workflows/bmad-domain-research.md +5 -0
- package/templates/.agent/workflows/bmad-edit-prd.md +5 -0
- package/templates/.agent/workflows/bmad-editorial-review-prose.md +5 -0
- package/templates/.agent/workflows/bmad-editorial-review-structure.md +5 -0
- package/templates/.agent/workflows/bmad-generate-project-context.md +5 -0
- package/templates/.agent/workflows/bmad-help-full.md +5 -0
- package/templates/.agent/workflows/bmad-help.md +5 -0
- package/templates/.agent/workflows/bmad-index-docs.md +5 -0
- package/templates/.agent/workflows/bmad-market-research.md +5 -0
- package/templates/.agent/workflows/bmad-party-mode.md +5 -0
- package/templates/.agent/workflows/bmad-prfaq.md +5 -0
- package/templates/.agent/workflows/bmad-product-brief.md +5 -0
- package/templates/.agent/workflows/bmad-qa-generate-e2e-tests.md +5 -0
- package/templates/.agent/workflows/bmad-quick-dev.md +5 -0
- package/templates/.agent/workflows/bmad-retrospective.md +5 -0
- package/templates/.agent/workflows/bmad-review-adversarial-general.md +5 -0
- package/templates/.agent/workflows/bmad-review-edge-case-hunter.md +5 -0
- package/templates/.agent/workflows/bmad-shard-doc.md +5 -0
- package/templates/.agent/workflows/bmad-sprint-planning.md +5 -0
- package/templates/.agent/workflows/bmad-sprint-status.md +5 -0
- package/templates/.agent/workflows/bmad-technical-research.md +5 -0
- package/templates/.agent/workflows/bmad-validate-prd.md +5 -0
- package/templates/.agent/workflows/dev.md +20 -0
- package/templates/.agent/workflows/pm.md +20 -0
- package/templates/.agent/workflows/qa.md +20 -0
- package/templates/.agent/workflows/sm.md +20 -0
- package/templates/.agent/workflows/solo-dev.md +20 -0
- package/templates/.agent/workflows/ux.md +20 -0
- package/templates/.claude/settings.local.json +50 -0
- package/templates/.claude/skills/analyst/SKILL.md +77 -0
- package/templates/.claude/skills/architect/SKILL.md +72 -0
- package/templates/.claude/skills/bmad-advanced-elicitation/SKILL.md +24 -0
- package/templates/.claude/skills/bmad-brainstorming/SKILL.md +26 -0
- package/templates/.claude/skills/bmad-check-implementation-readiness/SKILL.md +22 -0
- package/templates/.claude/skills/bmad-checkpoint-preview/SKILL.md +17 -0
- package/templates/.claude/skills/bmad-code-review/SKILL.md +22 -0
- package/templates/.claude/skills/bmad-correct-course/SKILL.md +17 -0
- package/templates/.claude/skills/bmad-create-architecture/SKILL.md +22 -0
- package/templates/.claude/skills/bmad-create-epics-and-stories/SKILL.md +22 -0
- package/templates/.claude/skills/bmad-create-prd/SKILL.md +30 -0
- package/templates/.claude/skills/bmad-create-story/SKILL.md +21 -0
- package/templates/.claude/skills/bmad-create-ux-design/SKILL.md +23 -0
- package/templates/.claude/skills/bmad-dev-story/SKILL.md +27 -0
- package/templates/.claude/skills/bmad-distillator/SKILL.md +22 -0
- package/templates/.claude/skills/bmad-document-project/SKILL.md +27 -0
- package/templates/.claude/skills/bmad-domain-research/SKILL.md +21 -0
- package/templates/.claude/skills/bmad-edit-prd/SKILL.md +18 -0
- package/templates/.claude/skills/bmad-editorial-review-prose/SKILL.md +21 -0
- package/templates/.claude/skills/bmad-editorial-review-structure/SKILL.md +25 -0
- package/templates/.claude/skills/bmad-generate-project-context/SKILL.md +20 -0
- package/templates/.claude/skills/bmad-help/SKILL.md +63 -0
- package/templates/.claude/skills/bmad-help-full/SKILL.md +23 -0
- package/templates/.claude/skills/bmad-index-docs/SKILL.md +16 -0
- package/templates/.claude/skills/bmad-market-research/SKILL.md +22 -0
- package/templates/.claude/skills/bmad-party-mode/SKILL.md +24 -0
- package/templates/.claude/skills/bmad-prfaq/SKILL.md +36 -0
- package/templates/.claude/skills/bmad-product-brief/SKILL.md +36 -0
- package/templates/.claude/skills/bmad-qa-generate-e2e-tests/SKILL.md +21 -0
- package/templates/.claude/skills/bmad-quick-dev/SKILL.md +23 -0
- package/templates/.claude/skills/bmad-retrospective/SKILL.md +17 -0
- package/templates/.claude/skills/bmad-review-adversarial-general/SKILL.md +21 -0
- package/templates/.claude/skills/bmad-review-edge-case-hunter/SKILL.md +22 -0
- package/templates/.claude/skills/bmad-shard-doc/SKILL.md +17 -0
- package/templates/.claude/skills/bmad-sprint-planning/SKILL.md +21 -0
- package/templates/.claude/skills/bmad-sprint-status/SKILL.md +17 -0
- package/templates/.claude/skills/bmad-technical-research/SKILL.md +21 -0
- package/templates/.claude/skills/bmad-validate-prd/SKILL.md +22 -0
- package/templates/.claude/skills/dev/SKILL.md +71 -0
- package/templates/.claude/skills/pm/SKILL.md +76 -0
- package/templates/.claude/skills/qa/SKILL.md +64 -0
- package/templates/.claude/skills/sm/SKILL.md +74 -0
- package/templates/.claude/skills/solo-dev/SKILL.md +64 -0
- package/templates/.claude/skills/ux/SKILL.md +71 -0
- package/templates/CLAUDE.md +60 -0
- package/templates/dashboard/index.html +15 -0
- package/templates/dashboard/package.json +30 -0
- package/templates/dashboard/public/assets/avatars/Female1_1wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female1_2wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female1_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female1_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female2_1wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female2_2wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female2_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female2_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female3_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female3_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female3_wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female4_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female4_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female4_wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female5_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female5_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female5_wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female6_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female6_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female6_wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male1_1wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male1_2wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male1_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male1_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male2_1wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male2_2wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male2_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male2_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male3_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male3_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male3_wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male4_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male4_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male4_wave.png +0 -0
- package/templates/dashboard/public/assets/desks/desktop_set_black_down.png +0 -0
- package/templates/dashboard/public/assets/desks/desktop_set_black_down_coding-1.png +0 -0
- package/templates/dashboard/public/assets/desks/desktop_set_black_down_coding.png +0 -0
- package/templates/dashboard/public/assets/desks/desktop_set_black_up.png +0 -0
- package/templates/dashboard/public/assets/desks/desktop_set_white_down.png +0 -0
- package/templates/dashboard/public/assets/desks/desktop_set_white_down_coding-1.png +0 -0
- package/templates/dashboard/public/assets/desks/desktop_set_white_down_coding.png +0 -0
- package/templates/dashboard/public/assets/desks/desktop_set_white_up.png +0 -0
- package/templates/dashboard/public/assets/furniture/armchair_tan.png +0 -0
- package/templates/dashboard/public/assets/furniture/armchair_tan_down.png +0 -0
- package/templates/dashboard/public/assets/furniture/backpack_blue.png +0 -0
- package/templates/dashboard/public/assets/furniture/backpack_red.png +0 -0
- package/templates/dashboard/public/assets/furniture/blinds.png +0 -0
- package/templates/dashboard/public/assets/furniture/blinds_large_closed_white.png +0 -0
- package/templates/dashboard/public/assets/furniture/bookshelf.png +0 -0
- package/templates/dashboard/public/assets/furniture/bookshelf_purple_tall.png +0 -0
- package/templates/dashboard/public/assets/furniture/bulletin_board.png +0 -0
- package/templates/dashboard/public/assets/furniture/clock.png +0 -0
- package/templates/dashboard/public/assets/furniture/coffee_mug.png +0 -0
- package/templates/dashboard/public/assets/furniture/coffee_mug_blue.png +0 -0
- package/templates/dashboard/public/assets/furniture/coffee_table.png +0 -0
- package/templates/dashboard/public/assets/furniture/coffeepot_right.png +0 -0
- package/templates/dashboard/public/assets/furniture/coffeetable_black_horizontal.png +0 -0
- package/templates/dashboard/public/assets/furniture/couch.png +0 -0
- package/templates/dashboard/public/assets/furniture/couch_tan_down.png +0 -0
- package/templates/dashboard/public/assets/furniture/cushion_blue.png +0 -0
- package/templates/dashboard/public/assets/furniture/cushion_tan.png +0 -0
- package/templates/dashboard/public/assets/furniture/desk_wood.png +0 -0
- package/templates/dashboard/public/assets/furniture/fancy_rug.png +0 -0
- package/templates/dashboard/public/assets/furniture/fancy_rug_wide.png +0 -0
- package/templates/dashboard/public/assets/furniture/flowers1.png +0 -0
- package/templates/dashboard/public/assets/furniture/flowers2.png +0 -0
- package/templates/dashboard/public/assets/furniture/lamp_tan.png +0 -0
- package/templates/dashboard/public/assets/furniture/lantern.png +0 -0
- package/templates/dashboard/public/assets/furniture/monstera.png +0 -0
- package/templates/dashboard/public/assets/furniture/monstera_small.png +0 -0
- package/templates/dashboard/public/assets/furniture/picture_frame.png +0 -0
- package/templates/dashboard/public/assets/furniture/plant1.png +0 -0
- package/templates/dashboard/public/assets/furniture/plant2.png +0 -0
- package/templates/dashboard/public/assets/furniture/plant3.png +0 -0
- package/templates/dashboard/public/assets/furniture/plant_poof.png +0 -0
- package/templates/dashboard/public/assets/furniture/plant_spindly.png +0 -0
- package/templates/dashboard/public/assets/furniture/poster_blue.png +0 -0
- package/templates/dashboard/public/assets/furniture/rug.png +0 -0
- package/templates/dashboard/public/assets/furniture/succulent_blue.png +0 -0
- package/templates/dashboard/public/assets/furniture/succulent_green.png +0 -0
- package/templates/dashboard/public/assets/furniture/treasurechest_closed_gold.png +0 -0
- package/templates/dashboard/public/assets/furniture/water_cooler_better.png +0 -0
- package/templates/dashboard/public/assets/furniture/whiteboard.png +0 -0
- package/templates/dashboard/public/assets/furniture/whiteboard_stand_graph.png +0 -0
- package/templates/dashboard/public/assets/furniture/window_blinds_open.png +0 -0
- package/templates/dashboard/public/assets/logo.png +0 -0
- package/templates/dashboard/src/App.tsx +119 -0
- package/templates/dashboard/src/components/SquadCard.tsx +47 -0
- package/templates/dashboard/src/components/SquadSelector.tsx +61 -0
- package/templates/dashboard/src/components/StatusBadge.tsx +32 -0
- package/templates/dashboard/src/components/StatusBar.tsx +97 -0
- package/templates/dashboard/src/hooks/useSquadSocket.ts +137 -0
- package/templates/dashboard/src/lib/formatTime.ts +16 -0
- package/templates/dashboard/src/lib/normalizeState.ts +25 -0
- package/templates/dashboard/src/main.tsx +14 -0
- package/templates/dashboard/src/office/AgentSprite.ts +249 -0
- package/templates/dashboard/src/office/OfficeScene.ts +577 -0
- package/templates/dashboard/src/office/PhaserGame.tsx +101 -0
- package/templates/dashboard/src/office/RoomBuilder.ts +190 -0
- package/templates/dashboard/src/office/assetKeys.ts +150 -0
- package/templates/dashboard/src/office/palette.ts +32 -0
- package/templates/dashboard/src/plugin/squadWatcher.ts +260 -0
- package/templates/dashboard/src/store/useSquadStore.ts +63 -0
- package/templates/dashboard/src/styles/globals.css +41 -0
- package/templates/dashboard/src/types/state.ts +69 -0
- package/templates/dashboard/src/vite-env.d.ts +1 -0
- package/templates/dashboard/tsconfig.json +24 -0
- package/templates/dashboard/tsconfig.tsbuildinfo +1 -0
- package/templates/dashboard/vite.config.ts +13 -0
- package/templates/squads/.gitkeep +0 -0
- package/templates/squads/bmad/agents/analyst.agent.md +87 -0
- package/templates/squads/bmad/agents/architect.agent.md +82 -0
- package/templates/squads/bmad/agents/developer.agent.md +91 -0
- package/templates/squads/bmad/agents/pm.agent.md +86 -0
- package/templates/squads/bmad/agents/qa-engineer.agent.md +84 -0
- package/templates/squads/bmad/agents/scrum-master.agent.md +84 -0
- package/templates/squads/bmad/agents/solo-dev.agent.md +81 -0
- package/templates/squads/bmad/agents/tech-writer.agent.md +86 -0
- package/templates/squads/bmad/agents/ux-designer.agent.md +81 -0
- package/templates/squads/bmad/squad.yaml +70 -0
- package/templates/squads/bmad/state.json +108 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import Phaser from 'phaser';
|
|
2
|
+
import {
|
|
3
|
+
CHARACTER_NAMES, MALE_CHARACTERS, FEMALE_CHARACTERS, avatarKeys, avatarPath,
|
|
4
|
+
DESK_PATHS, DESK_KEYS,
|
|
5
|
+
FURNITURE_PATHS, FURNITURE_KEYS,
|
|
6
|
+
type CharacterName,
|
|
7
|
+
} from './assetKeys';
|
|
8
|
+
import { CELL_W, CELL_H, MARGIN, WALL_H } from './palette';
|
|
9
|
+
import { RoomBuilder } from './RoomBuilder';
|
|
10
|
+
import { AgentSprite } from './AgentSprite';
|
|
11
|
+
import type { SquadState, Agent, Boss, AgentStatus } from '@/types/state';
|
|
12
|
+
|
|
13
|
+
function assignCharacters(agents: Agent[]): Map<string, CharacterName> {
|
|
14
|
+
const assignments = new Map<string, CharacterName>();
|
|
15
|
+
let maleIndex = 0;
|
|
16
|
+
let femaleIndex = 0;
|
|
17
|
+
|
|
18
|
+
for (const agent of agents) {
|
|
19
|
+
if (agent.gender === 'male') {
|
|
20
|
+
assignments.set(agent.id, MALE_CHARACTERS[maleIndex % MALE_CHARACTERS.length]);
|
|
21
|
+
maleIndex++;
|
|
22
|
+
} else {
|
|
23
|
+
assignments.set(agent.id, FEMALE_CHARACTERS[femaleIndex % FEMALE_CHARACTERS.length]);
|
|
24
|
+
femaleIndex++;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return assignments;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Layout used for the empty room (desks without agents)
|
|
32
|
+
const EMPTY_DESKS = [
|
|
33
|
+
{ col: 1, row: 1 },
|
|
34
|
+
{ col: 2, row: 1 },
|
|
35
|
+
{ col: 3, row: 1 },
|
|
36
|
+
{ col: 1, row: 2 },
|
|
37
|
+
{ col: 2, row: 2 },
|
|
38
|
+
{ col: 3, row: 2 },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export class OfficeScene extends Phaser.Scene {
|
|
42
|
+
private agentSprites: Map<string, AgentSprite> = new Map();
|
|
43
|
+
private agentPositions: Map<string, { x: number; y: number }> = new Map();
|
|
44
|
+
private roomBuilder!: RoomBuilder;
|
|
45
|
+
private isDragging = false;
|
|
46
|
+
private dragPrevX = 0;
|
|
47
|
+
private dragPrevY = 0;
|
|
48
|
+
private baseZoom = 1;
|
|
49
|
+
private roomCenterX = 0;
|
|
50
|
+
private roomCenterY = 0;
|
|
51
|
+
private focusedAgentId: string | null = null;
|
|
52
|
+
private userOverride = false;
|
|
53
|
+
|
|
54
|
+
// Boss animation state
|
|
55
|
+
private bossAvatar: Phaser.GameObjects.Image | null = null;
|
|
56
|
+
private bossCharName: CharacterName = 'Male1';
|
|
57
|
+
private bossHomeX = 0;
|
|
58
|
+
private bossHomeY = 0;
|
|
59
|
+
private prevStatuses: Map<string, AgentStatus> = new Map();
|
|
60
|
+
private animating = false;
|
|
61
|
+
private pendingState: SquadState | null = null;
|
|
62
|
+
private characterMap: Map<string, CharacterName> = new Map();
|
|
63
|
+
private currentSquadCode = '';
|
|
64
|
+
private hasIntroPlayed = false;
|
|
65
|
+
|
|
66
|
+
// Agent role descriptions in Portuguese
|
|
67
|
+
private static readonly AGENT_ROLES: Record<string, string> = {
|
|
68
|
+
'analyst': 'Pesquiso mercado e requisitos!',
|
|
69
|
+
'tech-writer': 'Documento tudo com clareza!',
|
|
70
|
+
'pm': 'Planejo o produto e priorizo!',
|
|
71
|
+
'ux-designer': 'Desenho experiências incríveis!',
|
|
72
|
+
'architect': 'Projeto a arquitetura do sistema!',
|
|
73
|
+
'developer': 'Codifico com TDD e precisão!',
|
|
74
|
+
'scrum-master': 'Organizo sprints e stories!',
|
|
75
|
+
'qa-engineer': 'Testo e garanto a qualidade!',
|
|
76
|
+
'solo-dev': 'Desenvolvo rápido de ponta a ponta!',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
constructor() {
|
|
80
|
+
super({ key: 'OfficeScene' });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
preload(): void {
|
|
84
|
+
for (const [key, path] of Object.entries(DESK_PATHS)) {
|
|
85
|
+
this.load.image(key, path);
|
|
86
|
+
}
|
|
87
|
+
for (const name of CHARACTER_NAMES) {
|
|
88
|
+
const keys = avatarKeys(name);
|
|
89
|
+
this.load.image(keys.blink, avatarPath(name, 'blink'));
|
|
90
|
+
this.load.image(keys.talk, avatarPath(name, 'talk'));
|
|
91
|
+
this.load.image(keys.wave1, avatarPath(name, 'wave1'));
|
|
92
|
+
this.load.image(keys.wave2, avatarPath(name, 'wave2'));
|
|
93
|
+
}
|
|
94
|
+
for (const [key, path] of Object.entries(FURNITURE_PATHS)) {
|
|
95
|
+
this.load.image(key, path);
|
|
96
|
+
}
|
|
97
|
+
this.load.on('loaderror', (file: Phaser.Loader.File) => {
|
|
98
|
+
console.error('Failed to load asset:', file.key, file.url);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
create(): void {
|
|
103
|
+
this.textures.list && Object.values(this.textures.list).forEach((tex) => {
|
|
104
|
+
if (tex.key !== '__DEFAULT' && tex.key !== '__MISSING') {
|
|
105
|
+
tex.setFilter(Phaser.Textures.FilterMode.NEAREST);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
this.roomBuilder = new RoomBuilder(this);
|
|
110
|
+
|
|
111
|
+
this.events.on('stateUpdate', (state: SquadState | null) => {
|
|
112
|
+
this.onStateUpdate(state);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Zoom with mouse wheel
|
|
116
|
+
this.input.on('wheel', (_pointer: Phaser.Input.Pointer, _gameObjects: unknown[], _dx: number, dy: number) => {
|
|
117
|
+
const cam = this.cameras.main;
|
|
118
|
+
const zoomDelta = dy > 0 ? -0.15 : 0.15;
|
|
119
|
+
const newZoom = Phaser.Math.Clamp(cam.zoom + zoomDelta, 0.3, 5);
|
|
120
|
+
cam.setZoom(newZoom);
|
|
121
|
+
this.userOverride = true;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Pan with left-click drag
|
|
125
|
+
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
|
126
|
+
if (pointer.leftButtonDown()) {
|
|
127
|
+
this.isDragging = true;
|
|
128
|
+
this.dragPrevX = pointer.x;
|
|
129
|
+
this.dragPrevY = pointer.y;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
this.input.on('pointermove', (pointer: Phaser.Input.Pointer) => {
|
|
134
|
+
if (!this.isDragging) return;
|
|
135
|
+
const cam = this.cameras.main;
|
|
136
|
+
const dx = (this.dragPrevX - pointer.x) / cam.zoom;
|
|
137
|
+
const dy = (this.dragPrevY - pointer.y) / cam.zoom;
|
|
138
|
+
cam.scrollX += dx;
|
|
139
|
+
cam.scrollY += dy;
|
|
140
|
+
this.dragPrevX = pointer.x;
|
|
141
|
+
this.dragPrevY = pointer.y;
|
|
142
|
+
this.userOverride = true;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
this.input.on('pointerup', () => {
|
|
146
|
+
this.isDragging = false;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Double-click to reset
|
|
150
|
+
let lastClickTime = 0;
|
|
151
|
+
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
if (now - lastClickTime < 300 && pointer.leftButtonDown()) {
|
|
154
|
+
this.userOverride = false;
|
|
155
|
+
this.focusedAgentId = null;
|
|
156
|
+
const cam = this.cameras.main;
|
|
157
|
+
cam.pan(this.roomCenterX, this.roomCenterY, 400, 'Sine.easeInOut');
|
|
158
|
+
cam.zoomTo(this.baseZoom, 400);
|
|
159
|
+
}
|
|
160
|
+
lastClickTime = now;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
this.renderEmptyRoom();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private onStateUpdate(state: SquadState | null): void {
|
|
167
|
+
if (!state) {
|
|
168
|
+
this.focusedAgentId = null;
|
|
169
|
+
this.prevStatuses.clear();
|
|
170
|
+
this.renderEmptyRoom();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// If currently animating, queue this state for after
|
|
175
|
+
if (this.animating) {
|
|
176
|
+
this.pendingState = state;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Detect transitions: who just became "working"? who just became "done"?
|
|
181
|
+
let newWorkingAgent: Agent | null = null;
|
|
182
|
+
let newDoneAgent: Agent | null = null;
|
|
183
|
+
|
|
184
|
+
for (const agent of state.agents) {
|
|
185
|
+
const prev = this.prevStatuses.get(agent.id);
|
|
186
|
+
if (agent.status === 'working' && prev !== 'working') {
|
|
187
|
+
newWorkingAgent = agent;
|
|
188
|
+
}
|
|
189
|
+
if ((agent.status === 'done' || agent.status === 'idle') && prev === 'working') {
|
|
190
|
+
newDoneAgent = agent;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.currentSquadCode = state.squad;
|
|
195
|
+
|
|
196
|
+
// Render the scene
|
|
197
|
+
this.renderScene(state.agents, state.boss);
|
|
198
|
+
|
|
199
|
+
// Save current statuses for next comparison
|
|
200
|
+
for (const agent of state.agents) {
|
|
201
|
+
this.prevStatuses.set(agent.id, agent.status);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Play delegation animation if someone just started working
|
|
205
|
+
if (newWorkingAgent && state.boss && this.bossAvatar) {
|
|
206
|
+
const isIntro = state.step.label === 'intro';
|
|
207
|
+
this.playDelegation(newWorkingAgent, isIntro);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Play delivery animation if someone just finished
|
|
212
|
+
if (newDoneAgent && state.boss && this.bossAvatar) {
|
|
213
|
+
this.playDelivery(newDoneAgent);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Auto-focus on working agent
|
|
218
|
+
this.updateAutoFocus(state);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private updateAutoFocus(state: SquadState): void {
|
|
222
|
+
const workingAgent = state.agents.find(a => a.status === 'working');
|
|
223
|
+
if (workingAgent && !this.userOverride) {
|
|
224
|
+
const pos = this.agentPositions.get(workingAgent.id);
|
|
225
|
+
if (pos && this.focusedAgentId !== workingAgent.id) {
|
|
226
|
+
this.focusedAgentId = workingAgent.id;
|
|
227
|
+
const cam = this.cameras.main;
|
|
228
|
+
const focusZoom = Math.max(this.baseZoom * 2.2, 1.8);
|
|
229
|
+
cam.pan(pos.x, pos.y - 40, 800, 'Sine.easeInOut');
|
|
230
|
+
cam.zoomTo(focusZoom, 800);
|
|
231
|
+
}
|
|
232
|
+
} else if (!workingAgent && this.focusedAgentId && !this.userOverride) {
|
|
233
|
+
this.focusedAgentId = null;
|
|
234
|
+
const cam = this.cameras.main;
|
|
235
|
+
cam.pan(this.roomCenterX, this.roomCenterY, 800, 'Sine.easeInOut');
|
|
236
|
+
cam.zoomTo(this.baseZoom, 800);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ─── Speech Bubble ───────────────────────────────────────────
|
|
241
|
+
private showBubble(x: number, y: number, text: string, color: string, duration: number): void {
|
|
242
|
+
const bubble = this.add.text(x, y, text, {
|
|
243
|
+
fontFamily: '"Segoe UI", "Helvetica Neue", Arial, sans-serif',
|
|
244
|
+
fontSize: '12px',
|
|
245
|
+
fontStyle: 'bold',
|
|
246
|
+
color: '#000000',
|
|
247
|
+
align: 'center',
|
|
248
|
+
backgroundColor: color,
|
|
249
|
+
padding: { x: 10, y: 6 },
|
|
250
|
+
resolution: 2,
|
|
251
|
+
}).setOrigin(0.5, 1).setDepth(1000);
|
|
252
|
+
|
|
253
|
+
const tail = this.add.graphics();
|
|
254
|
+
tail.fillStyle(Phaser.Display.Color.HexStringToColor(color).color, 1);
|
|
255
|
+
tail.fillTriangle(x - 4, y, x + 4, y, x, y + 7);
|
|
256
|
+
tail.setDepth(1000);
|
|
257
|
+
|
|
258
|
+
bubble.setScale(0);
|
|
259
|
+
tail.setAlpha(0);
|
|
260
|
+
this.tweens.add({ targets: bubble, scale: 1, duration: 250, ease: 'Back.easeOut' });
|
|
261
|
+
this.tweens.add({ targets: tail, alpha: 1, duration: 250, delay: 100 });
|
|
262
|
+
|
|
263
|
+
this.time.delayedCall(duration, () => {
|
|
264
|
+
this.tweens.add({
|
|
265
|
+
targets: [bubble, tail],
|
|
266
|
+
alpha: 0,
|
|
267
|
+
duration: 300,
|
|
268
|
+
onComplete: () => { bubble.destroy(); tail.destroy(); },
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ─── Boss delegates from his desk (no walking) ──────────────
|
|
274
|
+
private playDelegation(agent: Agent, isIntro = false): void {
|
|
275
|
+
const agentPos = this.agentPositions.get(agent.id);
|
|
276
|
+
if (!agentPos) return;
|
|
277
|
+
|
|
278
|
+
this.animating = true;
|
|
279
|
+
const bossY = this.bossHomeY - 70;
|
|
280
|
+
const agentY = agentPos.y - 70;
|
|
281
|
+
|
|
282
|
+
// 1. Camera pans to boss
|
|
283
|
+
const cam = this.cameras.main;
|
|
284
|
+
if (!this.userOverride) {
|
|
285
|
+
const focusZoom = Math.max(this.baseZoom * 2, 1.6);
|
|
286
|
+
cam.pan(this.bossHomeX, this.bossHomeY - 40, 600, 'Sine.easeInOut');
|
|
287
|
+
cam.zoomTo(focusZoom, 600);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 2. Boss speaks from his desk
|
|
291
|
+
this.time.delayedCall(800, () => {
|
|
292
|
+
const bossMsg = isIntro
|
|
293
|
+
? `${agent.name}, se apresente!`
|
|
294
|
+
: `${agent.name}, aqui está sua tarefa!`;
|
|
295
|
+
this.showBubble(this.bossHomeX, bossY - 35, bossMsg, '#10b981', 2500);
|
|
296
|
+
|
|
297
|
+
// 3. Camera pans to agent
|
|
298
|
+
this.time.delayedCall(1500, () => {
|
|
299
|
+
if (!this.userOverride) {
|
|
300
|
+
cam.pan(agentPos.x, agentPos.y - 40, 600, 'Sine.easeInOut');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 4. Agent responds
|
|
304
|
+
this.time.delayedCall(800, () => {
|
|
305
|
+
const agentMsg = isIntro
|
|
306
|
+
? (OfficeScene.AGENT_ROLES[agent.id] ?? 'Pronto para trabalhar!')
|
|
307
|
+
: 'Ok, Boss!';
|
|
308
|
+
this.showBubble(agentPos.x, agentY - 35, agentMsg, '#ffffff', 2000);
|
|
309
|
+
|
|
310
|
+
// 5. Stay focused on the working agent
|
|
311
|
+
this.time.delayedCall(2200, () => {
|
|
312
|
+
this.animating = false;
|
|
313
|
+
this.focusedAgentId = agent.id;
|
|
314
|
+
|
|
315
|
+
if (this.pendingState) {
|
|
316
|
+
const s = this.pendingState;
|
|
317
|
+
this.pendingState = null;
|
|
318
|
+
this.onStateUpdate(s);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ─── Agent reports done from their desk (no walking) ────────
|
|
327
|
+
private playDelivery(agent: Agent): void {
|
|
328
|
+
const agentPos = this.agentPositions.get(agent.id);
|
|
329
|
+
if (!agentPos) return;
|
|
330
|
+
|
|
331
|
+
this.animating = true;
|
|
332
|
+
const agentY = agentPos.y - 70;
|
|
333
|
+
const bossY = this.bossHomeY - 70;
|
|
334
|
+
|
|
335
|
+
// 1. Camera pans to agent
|
|
336
|
+
const cam = this.cameras.main;
|
|
337
|
+
if (!this.userOverride) {
|
|
338
|
+
const focusZoom = Math.max(this.baseZoom * 2, 1.6);
|
|
339
|
+
cam.pan(agentPos.x, agentPos.y - 40, 600, 'Sine.easeInOut');
|
|
340
|
+
cam.zoomTo(focusZoom, 600);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 2. Agent announces completion
|
|
344
|
+
this.time.delayedCall(800, () => {
|
|
345
|
+
this.showBubble(agentPos.x, agentY - 35, 'Tarefa concluída!', '#70ff90', 2500);
|
|
346
|
+
|
|
347
|
+
// 3. Camera pans to boss
|
|
348
|
+
this.time.delayedCall(1500, () => {
|
|
349
|
+
if (!this.userOverride) {
|
|
350
|
+
cam.pan(this.bossHomeX, this.bossHomeY - 40, 600, 'Sine.easeInOut');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 4. Boss thanks
|
|
354
|
+
this.time.delayedCall(800, () => {
|
|
355
|
+
this.showBubble(this.bossHomeX, bossY - 35, 'Muito obrigado!', '#10b981', 2000);
|
|
356
|
+
|
|
357
|
+
// 5. After "Muito obrigado!" + 2s → reset agent to idle, zoom out
|
|
358
|
+
this.time.delayedCall(2200, () => {
|
|
359
|
+
// Reset agent status to idle via API
|
|
360
|
+
fetch('/api/reset-agent', {
|
|
361
|
+
method: 'POST',
|
|
362
|
+
headers: { 'Content-Type': 'application/json' },
|
|
363
|
+
body: JSON.stringify({ squad: this.currentSquadCode, agentId: agent.id }),
|
|
364
|
+
}).catch(() => {});
|
|
365
|
+
|
|
366
|
+
if (!this.userOverride) {
|
|
367
|
+
cam.pan(this.roomCenterX, this.roomCenterY, 1000, 'Sine.easeInOut');
|
|
368
|
+
cam.zoomTo(this.baseZoom, 1000);
|
|
369
|
+
this.focusedAgentId = null;
|
|
370
|
+
}
|
|
371
|
+
this.animating = false;
|
|
372
|
+
|
|
373
|
+
if (this.pendingState) {
|
|
374
|
+
const s = this.pendingState;
|
|
375
|
+
this.pendingState = null;
|
|
376
|
+
this.onStateUpdate(s);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ─── Empty Room ──────────────────────────────────────────────
|
|
385
|
+
private renderEmptyRoom(): void {
|
|
386
|
+
const desks = EMPTY_DESKS;
|
|
387
|
+
let maxCol = 0, maxRow = 0;
|
|
388
|
+
for (const d of desks) {
|
|
389
|
+
maxCol = Math.max(maxCol, d.col);
|
|
390
|
+
maxRow = Math.max(maxRow, d.row);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const cellW = CELL_W + 64;
|
|
394
|
+
const cellH = CELL_H + 80;
|
|
395
|
+
const roomW = Math.max(maxCol * cellW + MARGIN * 2, 580);
|
|
396
|
+
const loungeSpace = CELL_H + 48;
|
|
397
|
+
const roomH = maxRow * cellH + MARGIN * 2 + WALL_H + loungeSpace;
|
|
398
|
+
|
|
399
|
+
this.clearScene();
|
|
400
|
+
this.roomBuilder.build(roomW, roomH);
|
|
401
|
+
|
|
402
|
+
for (let i = 0; i < desks.length; i++) {
|
|
403
|
+
const d = desks[i];
|
|
404
|
+
const x = (d.col - 1) * cellW + MARGIN + cellW / 2;
|
|
405
|
+
const y = (d.row - 1) * cellH + MARGIN + WALL_H + cellH / 2;
|
|
406
|
+
const variant = i % 2 === 0 ? 'black' : 'white';
|
|
407
|
+
|
|
408
|
+
this.add.image(x, y, FURNITURE_KEYS.deskWood)
|
|
409
|
+
.setOrigin(0.5, 0.5).setScale(1.3).setDepth(y + 1);
|
|
410
|
+
|
|
411
|
+
const deskKey = variant === 'black' ? DESK_KEYS.blackCoding : DESK_KEYS.whiteCoding;
|
|
412
|
+
this.add.image(x, y - 30, deskKey)
|
|
413
|
+
.setOrigin(0.5, 0.5).setScale(1.3).setDepth(y + 2);
|
|
414
|
+
|
|
415
|
+
this.add.image(x + 42, y + 8, 'furniture_coffee_mug')
|
|
416
|
+
.setOrigin(0.5, 1).setScale(1.4).setDepth(y + 3);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const cam = this.cameras.main;
|
|
420
|
+
const scaleX = cam.width / (roomW + 32);
|
|
421
|
+
const scaleY = cam.height / (roomH + 32);
|
|
422
|
+
const zoom = Math.min(scaleX, scaleY, 2);
|
|
423
|
+
cam.setZoom(zoom);
|
|
424
|
+
cam.centerOn(roomW / 2, roomH / 2);
|
|
425
|
+
this.baseZoom = zoom;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ─── Main Render ─────────────────────────────────────────────
|
|
429
|
+
private renderScene(agents: Agent[], boss?: Boss): void {
|
|
430
|
+
const allSameDesk = agents.length > 1 &&
|
|
431
|
+
agents.every(a => a.desk.col === agents[0].desk.col && a.desk.row === agents[0].desk.row);
|
|
432
|
+
if (allSameDesk) {
|
|
433
|
+
const cols = Math.min(agents.length, 3);
|
|
434
|
+
agents = agents.map((a, i) => ({
|
|
435
|
+
...a,
|
|
436
|
+
desk: { col: (i % cols) + 1, row: Math.floor(i / cols) + 1 },
|
|
437
|
+
}));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
let maxCol = 0, maxRow = 0;
|
|
441
|
+
for (const agent of agents) {
|
|
442
|
+
maxCol = Math.max(maxCol, agent.desk.col);
|
|
443
|
+
maxRow = Math.max(maxRow, agent.desk.row);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const cellW = CELL_W + 64;
|
|
447
|
+
const cellH = CELL_H + 80;
|
|
448
|
+
|
|
449
|
+
const roomW = Math.max(maxCol * cellW + MARGIN * 2, 580);
|
|
450
|
+
const totalRows = boss ? maxRow + 1 : maxRow;
|
|
451
|
+
const loungeSpace = CELL_H + 48;
|
|
452
|
+
const roomH = totalRows * cellH + MARGIN * 2 + WALL_H + loungeSpace;
|
|
453
|
+
|
|
454
|
+
this.clearScene();
|
|
455
|
+
this.roomBuilder.build(roomW, roomH);
|
|
456
|
+
|
|
457
|
+
this.characterMap = assignCharacters(agents);
|
|
458
|
+
this.agentPositions.clear();
|
|
459
|
+
|
|
460
|
+
const agentRowOffset = boss ? 1 : 0;
|
|
461
|
+
|
|
462
|
+
for (let i = 0; i < agents.length; i++) {
|
|
463
|
+
const agent = agents[i];
|
|
464
|
+
const x = (agent.desk.col - 1) * cellW + MARGIN + cellW / 2;
|
|
465
|
+
const y = (agent.desk.row - 1 + agentRowOffset) * cellH + MARGIN + WALL_H + cellH / 2;
|
|
466
|
+
const characterName = this.characterMap.get(agent.id)!;
|
|
467
|
+
const deskVariant = i % 2 === 0 ? 'black' : 'white';
|
|
468
|
+
const agentSprite = new AgentSprite(this, x, y, characterName, deskVariant, agent);
|
|
469
|
+
this.agentSprites.set(agent.id, agentSprite);
|
|
470
|
+
this.agentPositions.set(agent.id, { x, y });
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Render boss at the top
|
|
474
|
+
this.bossAvatar = null;
|
|
475
|
+
if (boss) {
|
|
476
|
+
const bossX = roomW / 2;
|
|
477
|
+
const bossY = MARGIN + WALL_H + cellH / 2 - 10;
|
|
478
|
+
this.bossHomeX = bossX;
|
|
479
|
+
this.bossHomeY = bossY;
|
|
480
|
+
|
|
481
|
+
const bossChar = boss.gender === 'female'
|
|
482
|
+
? FEMALE_CHARACTERS[0]
|
|
483
|
+
: MALE_CHARACTERS[0];
|
|
484
|
+
this.bossCharName = bossChar;
|
|
485
|
+
|
|
486
|
+
// Boss avatar
|
|
487
|
+
const bossAvatarKey = avatarKeys(bossChar).talk;
|
|
488
|
+
this.bossAvatar = this.add.image(bossX, bossY - 70, bossAvatarKey)
|
|
489
|
+
.setOrigin(0.5, 0.5)
|
|
490
|
+
.setScale(0.9)
|
|
491
|
+
.setDepth(bossY);
|
|
492
|
+
|
|
493
|
+
// Boss animation
|
|
494
|
+
const bossKeys = avatarKeys(bossChar);
|
|
495
|
+
let bossFrame = 0;
|
|
496
|
+
const bossRef = this.bossAvatar;
|
|
497
|
+
this.time.addEvent({
|
|
498
|
+
delay: 600,
|
|
499
|
+
loop: true,
|
|
500
|
+
callback: () => {
|
|
501
|
+
if (!bossRef.active) return;
|
|
502
|
+
bossFrame = (bossFrame + 1) % 2;
|
|
503
|
+
bossRef.setTexture(bossFrame === 0 ? bossKeys.talk : bossKeys.blink);
|
|
504
|
+
bossRef.setScale(0.9);
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// Boss desk
|
|
509
|
+
this.add.image(bossX, bossY, FURNITURE_KEYS.deskWood)
|
|
510
|
+
.setOrigin(0.5, 0.5).setScale(1.5).setDepth(bossY + 1);
|
|
511
|
+
|
|
512
|
+
// Boss monitor
|
|
513
|
+
this.add.image(bossX, bossY - 30, DESK_KEYS.blackCoding)
|
|
514
|
+
.setOrigin(0.5, 0.5).setScale(1.5).setDepth(bossY + 2);
|
|
515
|
+
|
|
516
|
+
// Boss coffee
|
|
517
|
+
this.add.image(bossX + 50, bossY + 8, 'furniture_coffee_mug')
|
|
518
|
+
.setOrigin(0.5, 1).setScale(1.5).setDepth(bossY + 3);
|
|
519
|
+
|
|
520
|
+
// Boss name badge
|
|
521
|
+
const labelY = bossY - 150;
|
|
522
|
+
const nameText = this.add.text(bossX, labelY + 5, boss.name, {
|
|
523
|
+
fontFamily: '"Segoe UI", "Helvetica Neue", Arial, sans-serif',
|
|
524
|
+
fontSize: '18px',
|
|
525
|
+
fontStyle: 'bold',
|
|
526
|
+
color: '#10b981',
|
|
527
|
+
align: 'center',
|
|
528
|
+
stroke: '#000000',
|
|
529
|
+
strokeThickness: 4,
|
|
530
|
+
resolution: 2,
|
|
531
|
+
}).setOrigin(0.5, 0).setDepth(901);
|
|
532
|
+
|
|
533
|
+
const titleText = this.add.text(bossX, labelY + 26, 'BOSS', {
|
|
534
|
+
fontFamily: '"Segoe UI", "Helvetica Neue", Arial, sans-serif',
|
|
535
|
+
fontSize: '13px',
|
|
536
|
+
fontStyle: 'bold',
|
|
537
|
+
color: '#10b981',
|
|
538
|
+
align: 'center',
|
|
539
|
+
stroke: '#000000',
|
|
540
|
+
strokeThickness: 3,
|
|
541
|
+
resolution: 2,
|
|
542
|
+
}).setOrigin(0.5, 0).setDepth(901);
|
|
543
|
+
|
|
544
|
+
const badgeW = Math.max(nameText.width, titleText.width) + 24;
|
|
545
|
+
const badgeBg = this.add.graphics();
|
|
546
|
+
badgeBg.fillStyle(0x061a12, 0.95);
|
|
547
|
+
badgeBg.fillRoundedRect(bossX - badgeW / 2, labelY, badgeW, 46, 5);
|
|
548
|
+
badgeBg.lineStyle(1.5, 0x10b981, 0.5);
|
|
549
|
+
badgeBg.strokeRoundedRect(bossX - badgeW / 2, labelY, badgeW, 46, 4);
|
|
550
|
+
badgeBg.setDepth(900);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Camera
|
|
554
|
+
const cam = this.cameras.main;
|
|
555
|
+
const scaleX = cam.width / (roomW + 32);
|
|
556
|
+
const scaleY = cam.height / (roomH + 32);
|
|
557
|
+
const zoom = Math.min(scaleX, scaleY, 2);
|
|
558
|
+
this.baseZoom = zoom;
|
|
559
|
+
this.roomCenterX = roomW / 2;
|
|
560
|
+
this.roomCenterY = roomH / 2;
|
|
561
|
+
|
|
562
|
+
if (!this.focusedAgentId || this.userOverride) {
|
|
563
|
+
cam.setZoom(zoom);
|
|
564
|
+
cam.centerOn(roomW / 2, roomH / 2);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private clearScene(): void {
|
|
570
|
+
for (const sprite of this.agentSprites.values()) {
|
|
571
|
+
sprite.destroy();
|
|
572
|
+
}
|
|
573
|
+
this.agentSprites.clear();
|
|
574
|
+
this.bossAvatar = null;
|
|
575
|
+
this.children.removeAll(true);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import Phaser from 'phaser';
|
|
3
|
+
import { OfficeScene } from './OfficeScene';
|
|
4
|
+
import { useSquadStore } from '@/store/useSquadStore';
|
|
5
|
+
|
|
6
|
+
function getSquadState() {
|
|
7
|
+
const state = useSquadStore.getState();
|
|
8
|
+
const selectedSquad = state.selectedSquad;
|
|
9
|
+
return selectedSquad
|
|
10
|
+
? state.activeStates.get(selectedSquad) ?? null
|
|
11
|
+
: null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function PhaserGame() {
|
|
15
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
16
|
+
const gameRef = useRef<Phaser.Game | null>(null);
|
|
17
|
+
|
|
18
|
+
// Create Phaser game on mount
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!containerRef.current || gameRef.current) return;
|
|
21
|
+
|
|
22
|
+
const container = containerRef.current;
|
|
23
|
+
const w = container.clientWidth || 800;
|
|
24
|
+
const h = container.clientHeight || 600;
|
|
25
|
+
|
|
26
|
+
const game = new Phaser.Game({
|
|
27
|
+
type: Phaser.AUTO,
|
|
28
|
+
parent: container,
|
|
29
|
+
width: w,
|
|
30
|
+
height: h,
|
|
31
|
+
pixelArt: false,
|
|
32
|
+
antialias: false,
|
|
33
|
+
roundPixels: true,
|
|
34
|
+
backgroundColor: '#0a0e1a',
|
|
35
|
+
scene: [OfficeScene],
|
|
36
|
+
scale: {
|
|
37
|
+
mode: Phaser.Scale.NONE,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
gameRef.current = game;
|
|
42
|
+
|
|
43
|
+
// When the scene finishes creating, push the current store state to it
|
|
44
|
+
game.events.on('ready', () => {
|
|
45
|
+
const scene = game.scene.getScene('OfficeScene') as OfficeScene | null;
|
|
46
|
+
if (!scene) return;
|
|
47
|
+
|
|
48
|
+
// Wait for scene to be fully created
|
|
49
|
+
scene.events.on('create', () => {
|
|
50
|
+
const squadState = getSquadState();
|
|
51
|
+
scene.events.emit('stateUpdate', squadState);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Resize canvas when container resizes
|
|
56
|
+
const ro = new ResizeObserver((entries) => {
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const { width, height } = entry.contentRect;
|
|
59
|
+
if (width > 0 && height > 0) {
|
|
60
|
+
game.scale.resize(width, height);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
ro.observe(container);
|
|
65
|
+
|
|
66
|
+
return () => {
|
|
67
|
+
ro.disconnect();
|
|
68
|
+
game.destroy(true);
|
|
69
|
+
gameRef.current = null;
|
|
70
|
+
};
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
// Bridge React state → Phaser scene (for ongoing updates)
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
return useSquadStore.subscribe((state) => {
|
|
76
|
+
const game = gameRef.current;
|
|
77
|
+
if (!game) return;
|
|
78
|
+
const scene = game.scene.getScene('OfficeScene') as OfficeScene | null;
|
|
79
|
+
if (!scene || !scene.scene.isActive()) return;
|
|
80
|
+
|
|
81
|
+
const selectedSquad = state.selectedSquad;
|
|
82
|
+
const squadState = selectedSquad
|
|
83
|
+
? state.activeStates.get(selectedSquad) ?? null
|
|
84
|
+
: null;
|
|
85
|
+
|
|
86
|
+
scene.events.emit('stateUpdate', squadState);
|
|
87
|
+
});
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
ref={containerRef}
|
|
93
|
+
style={{
|
|
94
|
+
flex: 1,
|
|
95
|
+
overflow: 'hidden',
|
|
96
|
+
imageRendering: 'auto',
|
|
97
|
+
background: '#0a0e1a',
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
}
|