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.
Files changed (215) hide show
  1. package/bin/bmad-visual.js +255 -0
  2. package/package.json +31 -0
  3. package/templates/.agent/workflows/analyst.md +20 -0
  4. package/templates/.agent/workflows/architect.md +20 -0
  5. package/templates/.agent/workflows/bmad-advanced-elicitation.md +5 -0
  6. package/templates/.agent/workflows/bmad-brainstorming.md +5 -0
  7. package/templates/.agent/workflows/bmad-check-implementation-readiness.md +5 -0
  8. package/templates/.agent/workflows/bmad-checkpoint-preview.md +5 -0
  9. package/templates/.agent/workflows/bmad-code-review.md +5 -0
  10. package/templates/.agent/workflows/bmad-correct-course.md +5 -0
  11. package/templates/.agent/workflows/bmad-create-architecture.md +5 -0
  12. package/templates/.agent/workflows/bmad-create-epics-and-stories.md +5 -0
  13. package/templates/.agent/workflows/bmad-create-prd.md +5 -0
  14. package/templates/.agent/workflows/bmad-create-story.md +5 -0
  15. package/templates/.agent/workflows/bmad-create-ux-design.md +5 -0
  16. package/templates/.agent/workflows/bmad-dev-story.md +5 -0
  17. package/templates/.agent/workflows/bmad-distillator.md +5 -0
  18. package/templates/.agent/workflows/bmad-document-project.md +5 -0
  19. package/templates/.agent/workflows/bmad-domain-research.md +5 -0
  20. package/templates/.agent/workflows/bmad-edit-prd.md +5 -0
  21. package/templates/.agent/workflows/bmad-editorial-review-prose.md +5 -0
  22. package/templates/.agent/workflows/bmad-editorial-review-structure.md +5 -0
  23. package/templates/.agent/workflows/bmad-generate-project-context.md +5 -0
  24. package/templates/.agent/workflows/bmad-help-full.md +5 -0
  25. package/templates/.agent/workflows/bmad-help.md +5 -0
  26. package/templates/.agent/workflows/bmad-index-docs.md +5 -0
  27. package/templates/.agent/workflows/bmad-market-research.md +5 -0
  28. package/templates/.agent/workflows/bmad-party-mode.md +5 -0
  29. package/templates/.agent/workflows/bmad-prfaq.md +5 -0
  30. package/templates/.agent/workflows/bmad-product-brief.md +5 -0
  31. package/templates/.agent/workflows/bmad-qa-generate-e2e-tests.md +5 -0
  32. package/templates/.agent/workflows/bmad-quick-dev.md +5 -0
  33. package/templates/.agent/workflows/bmad-retrospective.md +5 -0
  34. package/templates/.agent/workflows/bmad-review-adversarial-general.md +5 -0
  35. package/templates/.agent/workflows/bmad-review-edge-case-hunter.md +5 -0
  36. package/templates/.agent/workflows/bmad-shard-doc.md +5 -0
  37. package/templates/.agent/workflows/bmad-sprint-planning.md +5 -0
  38. package/templates/.agent/workflows/bmad-sprint-status.md +5 -0
  39. package/templates/.agent/workflows/bmad-technical-research.md +5 -0
  40. package/templates/.agent/workflows/bmad-validate-prd.md +5 -0
  41. package/templates/.agent/workflows/dev.md +20 -0
  42. package/templates/.agent/workflows/pm.md +20 -0
  43. package/templates/.agent/workflows/qa.md +20 -0
  44. package/templates/.agent/workflows/sm.md +20 -0
  45. package/templates/.agent/workflows/solo-dev.md +20 -0
  46. package/templates/.agent/workflows/ux.md +20 -0
  47. package/templates/.claude/settings.local.json +50 -0
  48. package/templates/.claude/skills/analyst/SKILL.md +77 -0
  49. package/templates/.claude/skills/architect/SKILL.md +72 -0
  50. package/templates/.claude/skills/bmad-advanced-elicitation/SKILL.md +24 -0
  51. package/templates/.claude/skills/bmad-brainstorming/SKILL.md +26 -0
  52. package/templates/.claude/skills/bmad-check-implementation-readiness/SKILL.md +22 -0
  53. package/templates/.claude/skills/bmad-checkpoint-preview/SKILL.md +17 -0
  54. package/templates/.claude/skills/bmad-code-review/SKILL.md +22 -0
  55. package/templates/.claude/skills/bmad-correct-course/SKILL.md +17 -0
  56. package/templates/.claude/skills/bmad-create-architecture/SKILL.md +22 -0
  57. package/templates/.claude/skills/bmad-create-epics-and-stories/SKILL.md +22 -0
  58. package/templates/.claude/skills/bmad-create-prd/SKILL.md +30 -0
  59. package/templates/.claude/skills/bmad-create-story/SKILL.md +21 -0
  60. package/templates/.claude/skills/bmad-create-ux-design/SKILL.md +23 -0
  61. package/templates/.claude/skills/bmad-dev-story/SKILL.md +27 -0
  62. package/templates/.claude/skills/bmad-distillator/SKILL.md +22 -0
  63. package/templates/.claude/skills/bmad-document-project/SKILL.md +27 -0
  64. package/templates/.claude/skills/bmad-domain-research/SKILL.md +21 -0
  65. package/templates/.claude/skills/bmad-edit-prd/SKILL.md +18 -0
  66. package/templates/.claude/skills/bmad-editorial-review-prose/SKILL.md +21 -0
  67. package/templates/.claude/skills/bmad-editorial-review-structure/SKILL.md +25 -0
  68. package/templates/.claude/skills/bmad-generate-project-context/SKILL.md +20 -0
  69. package/templates/.claude/skills/bmad-help/SKILL.md +63 -0
  70. package/templates/.claude/skills/bmad-help-full/SKILL.md +23 -0
  71. package/templates/.claude/skills/bmad-index-docs/SKILL.md +16 -0
  72. package/templates/.claude/skills/bmad-market-research/SKILL.md +22 -0
  73. package/templates/.claude/skills/bmad-party-mode/SKILL.md +24 -0
  74. package/templates/.claude/skills/bmad-prfaq/SKILL.md +36 -0
  75. package/templates/.claude/skills/bmad-product-brief/SKILL.md +36 -0
  76. package/templates/.claude/skills/bmad-qa-generate-e2e-tests/SKILL.md +21 -0
  77. package/templates/.claude/skills/bmad-quick-dev/SKILL.md +23 -0
  78. package/templates/.claude/skills/bmad-retrospective/SKILL.md +17 -0
  79. package/templates/.claude/skills/bmad-review-adversarial-general/SKILL.md +21 -0
  80. package/templates/.claude/skills/bmad-review-edge-case-hunter/SKILL.md +22 -0
  81. package/templates/.claude/skills/bmad-shard-doc/SKILL.md +17 -0
  82. package/templates/.claude/skills/bmad-sprint-planning/SKILL.md +21 -0
  83. package/templates/.claude/skills/bmad-sprint-status/SKILL.md +17 -0
  84. package/templates/.claude/skills/bmad-technical-research/SKILL.md +21 -0
  85. package/templates/.claude/skills/bmad-validate-prd/SKILL.md +22 -0
  86. package/templates/.claude/skills/dev/SKILL.md +71 -0
  87. package/templates/.claude/skills/pm/SKILL.md +76 -0
  88. package/templates/.claude/skills/qa/SKILL.md +64 -0
  89. package/templates/.claude/skills/sm/SKILL.md +74 -0
  90. package/templates/.claude/skills/solo-dev/SKILL.md +64 -0
  91. package/templates/.claude/skills/ux/SKILL.md +71 -0
  92. package/templates/CLAUDE.md +60 -0
  93. package/templates/dashboard/index.html +15 -0
  94. package/templates/dashboard/package.json +30 -0
  95. package/templates/dashboard/public/assets/avatars/Female1_1wave.png +0 -0
  96. package/templates/dashboard/public/assets/avatars/Female1_2wave.png +0 -0
  97. package/templates/dashboard/public/assets/avatars/Female1_blink.png +0 -0
  98. package/templates/dashboard/public/assets/avatars/Female1_talk.png +0 -0
  99. package/templates/dashboard/public/assets/avatars/Female2_1wave.png +0 -0
  100. package/templates/dashboard/public/assets/avatars/Female2_2wave.png +0 -0
  101. package/templates/dashboard/public/assets/avatars/Female2_blink.png +0 -0
  102. package/templates/dashboard/public/assets/avatars/Female2_talk.png +0 -0
  103. package/templates/dashboard/public/assets/avatars/Female3_blink.png +0 -0
  104. package/templates/dashboard/public/assets/avatars/Female3_talk.png +0 -0
  105. package/templates/dashboard/public/assets/avatars/Female3_wave.png +0 -0
  106. package/templates/dashboard/public/assets/avatars/Female4_blink.png +0 -0
  107. package/templates/dashboard/public/assets/avatars/Female4_talk.png +0 -0
  108. package/templates/dashboard/public/assets/avatars/Female4_wave.png +0 -0
  109. package/templates/dashboard/public/assets/avatars/Female5_blink.png +0 -0
  110. package/templates/dashboard/public/assets/avatars/Female5_talk.png +0 -0
  111. package/templates/dashboard/public/assets/avatars/Female5_wave.png +0 -0
  112. package/templates/dashboard/public/assets/avatars/Female6_blink.png +0 -0
  113. package/templates/dashboard/public/assets/avatars/Female6_talk.png +0 -0
  114. package/templates/dashboard/public/assets/avatars/Female6_wave.png +0 -0
  115. package/templates/dashboard/public/assets/avatars/Male1_1wave.png +0 -0
  116. package/templates/dashboard/public/assets/avatars/Male1_2wave.png +0 -0
  117. package/templates/dashboard/public/assets/avatars/Male1_blink.png +0 -0
  118. package/templates/dashboard/public/assets/avatars/Male1_talk.png +0 -0
  119. package/templates/dashboard/public/assets/avatars/Male2_1wave.png +0 -0
  120. package/templates/dashboard/public/assets/avatars/Male2_2wave.png +0 -0
  121. package/templates/dashboard/public/assets/avatars/Male2_blink.png +0 -0
  122. package/templates/dashboard/public/assets/avatars/Male2_talk.png +0 -0
  123. package/templates/dashboard/public/assets/avatars/Male3_blink.png +0 -0
  124. package/templates/dashboard/public/assets/avatars/Male3_talk.png +0 -0
  125. package/templates/dashboard/public/assets/avatars/Male3_wave.png +0 -0
  126. package/templates/dashboard/public/assets/avatars/Male4_blink.png +0 -0
  127. package/templates/dashboard/public/assets/avatars/Male4_talk.png +0 -0
  128. package/templates/dashboard/public/assets/avatars/Male4_wave.png +0 -0
  129. package/templates/dashboard/public/assets/desks/desktop_set_black_down.png +0 -0
  130. package/templates/dashboard/public/assets/desks/desktop_set_black_down_coding-1.png +0 -0
  131. package/templates/dashboard/public/assets/desks/desktop_set_black_down_coding.png +0 -0
  132. package/templates/dashboard/public/assets/desks/desktop_set_black_up.png +0 -0
  133. package/templates/dashboard/public/assets/desks/desktop_set_white_down.png +0 -0
  134. package/templates/dashboard/public/assets/desks/desktop_set_white_down_coding-1.png +0 -0
  135. package/templates/dashboard/public/assets/desks/desktop_set_white_down_coding.png +0 -0
  136. package/templates/dashboard/public/assets/desks/desktop_set_white_up.png +0 -0
  137. package/templates/dashboard/public/assets/furniture/armchair_tan.png +0 -0
  138. package/templates/dashboard/public/assets/furniture/armchair_tan_down.png +0 -0
  139. package/templates/dashboard/public/assets/furniture/backpack_blue.png +0 -0
  140. package/templates/dashboard/public/assets/furniture/backpack_red.png +0 -0
  141. package/templates/dashboard/public/assets/furniture/blinds.png +0 -0
  142. package/templates/dashboard/public/assets/furniture/blinds_large_closed_white.png +0 -0
  143. package/templates/dashboard/public/assets/furniture/bookshelf.png +0 -0
  144. package/templates/dashboard/public/assets/furniture/bookshelf_purple_tall.png +0 -0
  145. package/templates/dashboard/public/assets/furniture/bulletin_board.png +0 -0
  146. package/templates/dashboard/public/assets/furniture/clock.png +0 -0
  147. package/templates/dashboard/public/assets/furniture/coffee_mug.png +0 -0
  148. package/templates/dashboard/public/assets/furniture/coffee_mug_blue.png +0 -0
  149. package/templates/dashboard/public/assets/furniture/coffee_table.png +0 -0
  150. package/templates/dashboard/public/assets/furniture/coffeepot_right.png +0 -0
  151. package/templates/dashboard/public/assets/furniture/coffeetable_black_horizontal.png +0 -0
  152. package/templates/dashboard/public/assets/furniture/couch.png +0 -0
  153. package/templates/dashboard/public/assets/furniture/couch_tan_down.png +0 -0
  154. package/templates/dashboard/public/assets/furniture/cushion_blue.png +0 -0
  155. package/templates/dashboard/public/assets/furniture/cushion_tan.png +0 -0
  156. package/templates/dashboard/public/assets/furniture/desk_wood.png +0 -0
  157. package/templates/dashboard/public/assets/furniture/fancy_rug.png +0 -0
  158. package/templates/dashboard/public/assets/furniture/fancy_rug_wide.png +0 -0
  159. package/templates/dashboard/public/assets/furniture/flowers1.png +0 -0
  160. package/templates/dashboard/public/assets/furniture/flowers2.png +0 -0
  161. package/templates/dashboard/public/assets/furniture/lamp_tan.png +0 -0
  162. package/templates/dashboard/public/assets/furniture/lantern.png +0 -0
  163. package/templates/dashboard/public/assets/furniture/monstera.png +0 -0
  164. package/templates/dashboard/public/assets/furniture/monstera_small.png +0 -0
  165. package/templates/dashboard/public/assets/furniture/picture_frame.png +0 -0
  166. package/templates/dashboard/public/assets/furniture/plant1.png +0 -0
  167. package/templates/dashboard/public/assets/furniture/plant2.png +0 -0
  168. package/templates/dashboard/public/assets/furniture/plant3.png +0 -0
  169. package/templates/dashboard/public/assets/furniture/plant_poof.png +0 -0
  170. package/templates/dashboard/public/assets/furniture/plant_spindly.png +0 -0
  171. package/templates/dashboard/public/assets/furniture/poster_blue.png +0 -0
  172. package/templates/dashboard/public/assets/furniture/rug.png +0 -0
  173. package/templates/dashboard/public/assets/furniture/succulent_blue.png +0 -0
  174. package/templates/dashboard/public/assets/furniture/succulent_green.png +0 -0
  175. package/templates/dashboard/public/assets/furniture/treasurechest_closed_gold.png +0 -0
  176. package/templates/dashboard/public/assets/furniture/water_cooler_better.png +0 -0
  177. package/templates/dashboard/public/assets/furniture/whiteboard.png +0 -0
  178. package/templates/dashboard/public/assets/furniture/whiteboard_stand_graph.png +0 -0
  179. package/templates/dashboard/public/assets/furniture/window_blinds_open.png +0 -0
  180. package/templates/dashboard/public/assets/logo.png +0 -0
  181. package/templates/dashboard/src/App.tsx +119 -0
  182. package/templates/dashboard/src/components/SquadCard.tsx +47 -0
  183. package/templates/dashboard/src/components/SquadSelector.tsx +61 -0
  184. package/templates/dashboard/src/components/StatusBadge.tsx +32 -0
  185. package/templates/dashboard/src/components/StatusBar.tsx +97 -0
  186. package/templates/dashboard/src/hooks/useSquadSocket.ts +137 -0
  187. package/templates/dashboard/src/lib/formatTime.ts +16 -0
  188. package/templates/dashboard/src/lib/normalizeState.ts +25 -0
  189. package/templates/dashboard/src/main.tsx +14 -0
  190. package/templates/dashboard/src/office/AgentSprite.ts +249 -0
  191. package/templates/dashboard/src/office/OfficeScene.ts +577 -0
  192. package/templates/dashboard/src/office/PhaserGame.tsx +101 -0
  193. package/templates/dashboard/src/office/RoomBuilder.ts +190 -0
  194. package/templates/dashboard/src/office/assetKeys.ts +150 -0
  195. package/templates/dashboard/src/office/palette.ts +32 -0
  196. package/templates/dashboard/src/plugin/squadWatcher.ts +260 -0
  197. package/templates/dashboard/src/store/useSquadStore.ts +63 -0
  198. package/templates/dashboard/src/styles/globals.css +41 -0
  199. package/templates/dashboard/src/types/state.ts +69 -0
  200. package/templates/dashboard/src/vite-env.d.ts +1 -0
  201. package/templates/dashboard/tsconfig.json +24 -0
  202. package/templates/dashboard/tsconfig.tsbuildinfo +1 -0
  203. package/templates/dashboard/vite.config.ts +13 -0
  204. package/templates/squads/.gitkeep +0 -0
  205. package/templates/squads/bmad/agents/analyst.agent.md +87 -0
  206. package/templates/squads/bmad/agents/architect.agent.md +82 -0
  207. package/templates/squads/bmad/agents/developer.agent.md +91 -0
  208. package/templates/squads/bmad/agents/pm.agent.md +86 -0
  209. package/templates/squads/bmad/agents/qa-engineer.agent.md +84 -0
  210. package/templates/squads/bmad/agents/scrum-master.agent.md +84 -0
  211. package/templates/squads/bmad/agents/solo-dev.agent.md +81 -0
  212. package/templates/squads/bmad/agents/tech-writer.agent.md +86 -0
  213. package/templates/squads/bmad/agents/ux-designer.agent.md +81 -0
  214. package/templates/squads/bmad/squad.yaml +70 -0
  215. 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
+ }