conectese 0.1.14
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/README.md +265 -0
- package/_conectese/.conectese-version +1 -0
- package/_conectese/config/playwright.config.json +11 -0
- package/_conectese/core/architect.agent.yaml +110 -0
- package/_conectese/core/best-practices/_catalog.yaml +116 -0
- package/_conectese/core/best-practices/blog-post.md +132 -0
- package/_conectese/core/best-practices/blog-seo.md +127 -0
- package/_conectese/core/best-practices/copywriting.md +426 -0
- package/_conectese/core/best-practices/data-analysis.md +401 -0
- package/_conectese/core/best-practices/email-newsletter.md +118 -0
- package/_conectese/core/best-practices/email-sales.md +110 -0
- package/_conectese/core/best-practices/image-design.md +348 -0
- package/_conectese/core/best-practices/instagram-feed.md +235 -0
- package/_conectese/core/best-practices/instagram-reels.md +112 -0
- package/_conectese/core/best-practices/instagram-stories.md +107 -0
- package/_conectese/core/best-practices/linkedin-article.md +116 -0
- package/_conectese/core/best-practices/linkedin-post.md +121 -0
- package/_conectese/core/best-practices/researching.md +349 -0
- package/_conectese/core/best-practices/review.md +269 -0
- package/_conectese/core/best-practices/social-networks-publishing.md +294 -0
- package/_conectese/core/best-practices/strategist.md +344 -0
- package/_conectese/core/best-practices/technical-writing.md +365 -0
- package/_conectese/core/best-practices/twitter-post.md +105 -0
- package/_conectese/core/best-practices/twitter-thread.md +122 -0
- package/_conectese/core/best-practices/whatsapp-broadcast.md +107 -0
- package/_conectese/core/best-practices/youtube-script.md +122 -0
- package/_conectese/core/best-practices/youtube-shorts.md +112 -0
- package/_conectese/core/prompts/build.prompt.md +547 -0
- package/_conectese/core/prompts/design.prompt.md +469 -0
- package/_conectese/core/prompts/discovery.prompt.md +269 -0
- package/_conectese/core/prompts/sherlock-instagram.md +123 -0
- package/_conectese/core/prompts/sherlock-linkedin.md +73 -0
- package/_conectese/core/prompts/sherlock-shared.md +684 -0
- package/_conectese/core/prompts/sherlock-twitter.md +78 -0
- package/_conectese/core/prompts/sherlock-youtube.md +85 -0
- package/_conectese/core/runner.pipeline.md +535 -0
- package/_conectese/core/skills.engine.md +381 -0
- package/agents/data-extractor/AGENT.md +13 -0
- package/agents/direito-adaneiro/AGENT.md +18 -0
- package/agents/direito-administrativo/AGENT.md +18 -0
- package/agents/direito-aeroporta-rio/AGENT.md +18 -0
- package/agents/direito-agra-rio/AGENT.md +18 -0
- package/agents/direito-ambiental/AGENT.md +18 -0
- package/agents/direito-banca-rio/AGENT.md +18 -0
- package/agents/direito-civil/AGENT.md +18 -0
- package/agents/direito-constitcional/AGENT.md +18 -0
- package/agents/direito-da-crianc-a-e-do-adolescente-eca/AGENT.md +18 -0
- package/agents/direito-da-propriedade-intelectal/AGENT.md +18 -0
- package/agents/direito-de-ami-lia/AGENT.md +18 -0
- package/agents/direito-de-tra-nsito/AGENT.md +18 -0
- package/agents/direito-desportivo/AGENT.md +18 -0
- package/agents/direito-digital/AGENT.md +18 -0
- package/agents/direito-do-consmidor/AGENT.md +18 -0
- package/agents/direito-do-trabalho/AGENT.md +18 -0
- package/agents/direito-econo-mico/AGENT.md +18 -0
- package/agents/direito-eleitoral/AGENT.md +18 -0
- package/agents/direito-empresarial/AGENT.md +18 -0
- package/agents/direito-imobilia-rio/AGENT.md +18 -0
- package/agents/direito-inanceiro/AGENT.md +18 -0
- package/agents/direito-internacional/AGENT.md +18 -0
- package/agents/direito-mari-timo/AGENT.md +18 -0
- package/agents/direito-me-dico-e-da-sa-de/AGENT.md +18 -0
- package/agents/direito-militar/AGENT.md +18 -0
- package/agents/direito-ndia-rio/AGENT.md +18 -0
- package/agents/direito-notarial-e-registral/AGENT.md +18 -0
- package/agents/direito-penal/AGENT.md +18 -0
- package/agents/direito-previdencia-rio/AGENT.md +18 -0
- package/agents/direito-processal-civil/AGENT.md +18 -0
- package/agents/direito-processal-do-trabalho/AGENT.md +18 -0
- package/agents/direito-processal-militar/AGENT.md +18 -0
- package/agents/direito-processal-penal/AGENT.md +18 -0
- package/agents/direito-rbani-stico/AGENT.md +18 -0
- package/agents/direito-secrita-rio/AGENT.md +18 -0
- package/agents/direito-sindical/AGENT.md +18 -0
- package/agents/direito-societa-rio/AGENT.md +18 -0
- package/agents/direito-tribta-rio/AGENT.md +18 -0
- package/agents/direitos-hmanos/AGENT.md +18 -0
- package/agents/legal-analyst/AGENT.md +16 -0
- package/agents/legal-synthesizer/AGENT.md +13 -0
- package/agents/lgpd-anonymizer/AGENT.md +14 -0
- package/agents/lgpd-restorer/AGENT.md +14 -0
- package/agents/task-router/AGENT.md +13 -0
- package/bin/conectese.js +73 -0
- package/dashboard/index.html +12 -0
- package/dashboard/package-lock.json +1971 -0
- package/dashboard/package.json +28 -0
- package/dashboard/public/assets/avatars/Female1_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Female1_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Female1_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female1_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female2_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Female2_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Female2_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female2_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female3_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female3_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female3_wave.png +0 -0
- package/dashboard/public/assets/avatars/Female4_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female4_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female4_wave.png +0 -0
- package/dashboard/public/assets/avatars/Female5_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female5_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female5_wave.png +0 -0
- package/dashboard/public/assets/avatars/Female6_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female6_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female6_wave.png +0 -0
- package/dashboard/public/assets/avatars/Male1_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Male1_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Male1_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male1_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male2_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Male2_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Male2_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male2_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male3_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male3_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male3_wave.png +0 -0
- package/dashboard/public/assets/avatars/Male4_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male4_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male4_wave.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_down.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_down_coding-1.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_down_coding.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_up.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_down.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_down_coding-1.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_down_coding.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_up.png +0 -0
- package/dashboard/public/assets/furniture/armchair_tan.png +0 -0
- package/dashboard/public/assets/furniture/armchair_tan_down.png +0 -0
- package/dashboard/public/assets/furniture/backpack_blue.png +0 -0
- package/dashboard/public/assets/furniture/backpack_red.png +0 -0
- package/dashboard/public/assets/furniture/blinds.png +0 -0
- package/dashboard/public/assets/furniture/blinds_large_closed_white.png +0 -0
- package/dashboard/public/assets/furniture/bookshelf.png +0 -0
- package/dashboard/public/assets/furniture/bookshelf_purple_tall.png +0 -0
- package/dashboard/public/assets/furniture/bulletin_board.png +0 -0
- package/dashboard/public/assets/furniture/clock.png +0 -0
- package/dashboard/public/assets/furniture/coffee_mug.png +0 -0
- package/dashboard/public/assets/furniture/coffee_mug_blue.png +0 -0
- package/dashboard/public/assets/furniture/coffee_table.png +0 -0
- package/dashboard/public/assets/furniture/coffeepot_right.png +0 -0
- package/dashboard/public/assets/furniture/coffeetable_black_horizontal.png +0 -0
- package/dashboard/public/assets/furniture/couch.png +0 -0
- package/dashboard/public/assets/furniture/couch_tan_down.png +0 -0
- package/dashboard/public/assets/furniture/cushion_blue.png +0 -0
- package/dashboard/public/assets/furniture/cushion_tan.png +0 -0
- package/dashboard/public/assets/furniture/desk_wood.png +0 -0
- package/dashboard/public/assets/furniture/fancy_rug.png +0 -0
- package/dashboard/public/assets/furniture/fancy_rug_wide.png +0 -0
- package/dashboard/public/assets/furniture/flowers1.png +0 -0
- package/dashboard/public/assets/furniture/flowers2.png +0 -0
- package/dashboard/public/assets/furniture/lamp_tan.png +0 -0
- package/dashboard/public/assets/furniture/lantern.png +0 -0
- package/dashboard/public/assets/furniture/monstera.png +0 -0
- package/dashboard/public/assets/furniture/monstera_small.png +0 -0
- package/dashboard/public/assets/furniture/picture_frame.png +0 -0
- package/dashboard/public/assets/furniture/plant1.png +0 -0
- package/dashboard/public/assets/furniture/plant2.png +0 -0
- package/dashboard/public/assets/furniture/plant3.png +0 -0
- package/dashboard/public/assets/furniture/plant_poof.png +0 -0
- package/dashboard/public/assets/furniture/plant_spindly.png +0 -0
- package/dashboard/public/assets/furniture/poster_blue.png +0 -0
- package/dashboard/public/assets/furniture/rug.png +0 -0
- package/dashboard/public/assets/furniture/succulent_blue.png +0 -0
- package/dashboard/public/assets/furniture/succulent_green.png +0 -0
- package/dashboard/public/assets/furniture/treasurechest_closed_gold.png +0 -0
- package/dashboard/public/assets/furniture/water_cooler_better.png +0 -0
- package/dashboard/public/assets/furniture/whiteboard.png +0 -0
- package/dashboard/public/assets/furniture/whiteboard_stand_graph.png +0 -0
- package/dashboard/public/assets/furniture/window_blinds_open.png +0 -0
- package/dashboard/src/App.tsx +46 -0
- package/dashboard/src/components/SquadCard.tsx +47 -0
- package/dashboard/src/components/SquadSelector.tsx +61 -0
- package/dashboard/src/components/StatusBadge.tsx +32 -0
- package/dashboard/src/components/StatusBar.tsx +97 -0
- package/dashboard/src/hooks/useSquadSocket.ts +135 -0
- package/dashboard/src/lib/formatTime.ts +16 -0
- package/dashboard/src/lib/normalizeState.ts +25 -0
- package/dashboard/src/main.tsx +10 -0
- package/dashboard/src/office/AgentSprite.ts +241 -0
- package/dashboard/src/office/OfficeScene.ts +153 -0
- package/dashboard/src/office/PhaserGame.tsx +80 -0
- package/dashboard/src/office/RoomBuilder.ts +190 -0
- package/dashboard/src/office/assetKeys.ts +150 -0
- package/dashboard/src/office/palette.ts +32 -0
- package/dashboard/src/plugin/squadWatcher.ts +233 -0
- package/dashboard/src/store/useSquadStore.ts +56 -0
- package/dashboard/src/styles/globals.css +36 -0
- package/dashboard/src/types/state.ts +63 -0
- package/dashboard/src/vite-env.d.ts +1 -0
- package/dashboard/test-results/.last-run.json +4 -0
- package/dashboard/tsconfig.json +24 -0
- package/dashboard/tsconfig.tsbuildinfo +1 -0
- package/dashboard/vite.config.ts +13 -0
- package/package.json +53 -0
- package/skills/README.md +63 -0
- package/skills/apify/SKILL.md +55 -0
- package/skills/blotato/SKILL.md +63 -0
- package/skills/canva/SKILL.md +60 -0
- package/skills/conectese-agent-creator/SKILL.md +192 -0
- package/skills/conectese-skill-creator/SKILL.md +407 -0
- package/skills/conectese-skill-creator/agents/analyzer.md +274 -0
- package/skills/conectese-skill-creator/agents/comparator.md +202 -0
- package/skills/conectese-skill-creator/agents/grader.md +223 -0
- package/skills/conectese-skill-creator/assets/eval_review.html +146 -0
- package/skills/conectese-skill-creator/eval-viewer/generate_review.py +471 -0
- package/skills/conectese-skill-creator/eval-viewer/viewer.html +1325 -0
- package/skills/conectese-skill-creator/references/schemas.md +430 -0
- package/skills/conectese-skill-creator/references/skill-format.md +235 -0
- package/skills/conectese-skill-creator/scripts/__init__.py +0 -0
- package/skills/conectese-skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/skills/conectese-skill-creator/scripts/quick_validate.py +103 -0
- package/skills/conectese-skill-creator/scripts/run_eval.py +310 -0
- package/skills/conectese-skill-creator/scripts/utils.py +47 -0
- package/skills/image-ai-generator/SKILL.md +124 -0
- package/skills/image-ai-generator/scripts/generate.py +175 -0
- package/skills/image-creator/SKILL.md +155 -0
- package/skills/image-fetcher/SKILL.md +91 -0
- package/skills/instagram-publisher/SKILL.md +119 -0
- package/skills/instagram-publisher/scripts/publish.js +165 -0
- package/skills/resend/SKILL.md +80 -0
- package/skills/template-designer/SKILL.md +201 -0
- package/skills/template-designer/base-templates/model-a.html +27 -0
- package/skills/template-designer/base-templates/model-b.html +31 -0
- package/skills/template-designer/base-templates/model-c.html +42 -0
- package/src/agents-cli.js +158 -0
- package/src/agents.js +134 -0
- package/src/i18n.js +48 -0
- package/src/init.js +341 -0
- package/src/locales/en.json +73 -0
- package/src/locales/es.json +72 -0
- package/src/locales/pt-BR.json +72 -0
- package/src/logger.js +38 -0
- package/src/prompt.js +46 -0
- package/src/readme/README.md +119 -0
- package/src/runs.js +90 -0
- package/src/skills-cli.js +157 -0
- package/src/skills.js +146 -0
- package/src/update.js +169 -0
- package/templates/_conectese/.conectese-version +1 -0
- package/templates/_conectese/_investigations/.gitkeep +0 -0
- package/templates/ide-templates/antigravity/.agent/rules/conectese.md +55 -0
- package/templates/ide-templates/antigravity/.agent/workflows/conectese.md +102 -0
- package/templates/ide-templates/claude-code/.claude/skills/conectese/SKILL.md +182 -0
- package/templates/ide-templates/claude-code/.mcp.json +8 -0
- package/templates/ide-templates/claude-code/CLAUDE.md +43 -0
- package/templates/ide-templates/codex/.agents/skills/conectese/SKILL.md +6 -0
- package/templates/ide-templates/codex/AGENTS.md +105 -0
- package/templates/ide-templates/cursor/.cursor/commands/conectese.md +9 -0
- package/templates/ide-templates/cursor/.cursor/mcp.json +8 -0
- package/templates/ide-templates/cursor/.cursor/rules/conectese.mdc +48 -0
- package/templates/ide-templates/cursor/.cursorignore +3 -0
- package/templates/ide-templates/opencode/.opencode/commands/conectese.md +9 -0
- package/templates/ide-templates/opencode/AGENTS.md +105 -0
- package/templates/ide-templates/vscode-copilot/.github/prompts/conectese.prompt.md +201 -0
- package/templates/ide-templates/vscode-copilot/.vscode/mcp.json +8 -0
- package/templates/ide-templates/vscode-copilot/.vscode/settings.json +3 -0
- package/templates/package.json +8 -0
- package/templates/squads/.gitkeep +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useSquadStore } from "@/store/useSquadStore";
|
|
3
|
+
import type { WsMessage } from "@/types/state";
|
|
4
|
+
|
|
5
|
+
const RECONNECT_BASE_MS = 1000;
|
|
6
|
+
const RECONNECT_MAX_MS = 30000;
|
|
7
|
+
const WS_FAIL_THRESHOLD = 3;
|
|
8
|
+
const POLL_INTERVAL_MS = 3000;
|
|
9
|
+
|
|
10
|
+
export function useSquadSocket() {
|
|
11
|
+
const wsRef = useRef<WebSocket | null>(null);
|
|
12
|
+
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
13
|
+
|
|
14
|
+
const setConnected = useSquadStore((s) => s.setConnected);
|
|
15
|
+
const setSnapshot = useSquadStore((s) => s.setSnapshot);
|
|
16
|
+
const updateSquadState = useSquadStore((s) => s.updateSquadState);
|
|
17
|
+
const setSquadInactive = useSquadStore((s) => s.setSquadInactive);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
let disposed = false;
|
|
21
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | undefined;
|
|
22
|
+
let reconnectDelay = RECONNECT_BASE_MS;
|
|
23
|
+
let wsFailCount = 0;
|
|
24
|
+
|
|
25
|
+
function dispatch(msg: WsMessage) {
|
|
26
|
+
if (disposed) return;
|
|
27
|
+
switch (msg.type) {
|
|
28
|
+
case "SNAPSHOT":
|
|
29
|
+
setSnapshot(msg.squads, msg.activeStates);
|
|
30
|
+
break;
|
|
31
|
+
case "SQUAD_UPDATE":
|
|
32
|
+
updateSquadState(msg.squad, msg.state);
|
|
33
|
+
break;
|
|
34
|
+
case "SQUAD_INACTIVE":
|
|
35
|
+
setSquadInactive(msg.squad);
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── HTTP polling fallback ───────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function stopPolling() {
|
|
43
|
+
if (pollTimerRef.current) {
|
|
44
|
+
clearInterval(pollTimerRef.current);
|
|
45
|
+
pollTimerRef.current = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function startPolling() {
|
|
50
|
+
stopPolling();
|
|
51
|
+
|
|
52
|
+
const poll = async () => {
|
|
53
|
+
if (disposed) return;
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch("/api/snapshot", { cache: "no-store" });
|
|
56
|
+
if (!res.ok || disposed) return;
|
|
57
|
+
const msg: WsMessage = await res.json();
|
|
58
|
+
dispatch(msg);
|
|
59
|
+
setConnected(true);
|
|
60
|
+
} catch {
|
|
61
|
+
// Endpoint not available — will retry on next interval
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
poll();
|
|
66
|
+
pollTimerRef.current = setInterval(poll, POLL_INTERVAL_MS);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── WebSocket connection ────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function connect() {
|
|
72
|
+
if (disposed) return;
|
|
73
|
+
|
|
74
|
+
if (reconnectTimer !== undefined) {
|
|
75
|
+
clearTimeout(reconnectTimer);
|
|
76
|
+
reconnectTimer = undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
80
|
+
const ws = new WebSocket(
|
|
81
|
+
`${protocol}//${window.location.host}/__squads_ws`,
|
|
82
|
+
);
|
|
83
|
+
wsRef.current = ws;
|
|
84
|
+
|
|
85
|
+
ws.onopen = () => {
|
|
86
|
+
if (disposed) { ws.close(); return; }
|
|
87
|
+
setConnected(true);
|
|
88
|
+
reconnectDelay = RECONNECT_BASE_MS;
|
|
89
|
+
wsFailCount = 0;
|
|
90
|
+
stopPolling();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
ws.onmessage = (event) => {
|
|
94
|
+
if (disposed) return;
|
|
95
|
+
try {
|
|
96
|
+
const msg: WsMessage = JSON.parse(event.data);
|
|
97
|
+
dispatch(msg);
|
|
98
|
+
} catch {
|
|
99
|
+
// Ignore malformed messages
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
ws.onclose = () => {
|
|
104
|
+
if (disposed) return;
|
|
105
|
+
|
|
106
|
+
setConnected(false);
|
|
107
|
+
wsFailCount++;
|
|
108
|
+
|
|
109
|
+
if (wsFailCount >= WS_FAIL_THRESHOLD) {
|
|
110
|
+
startPolling();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
reconnectTimer = setTimeout(() => {
|
|
114
|
+
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
|
|
115
|
+
connect();
|
|
116
|
+
}, reconnectDelay);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
ws.onerror = () => {
|
|
120
|
+
// onerror is always followed by onclose — just let onclose handle it.
|
|
121
|
+
// Don't call ws.close() here to avoid "closed before established" in StrictMode.
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
connect();
|
|
126
|
+
|
|
127
|
+
return () => {
|
|
128
|
+
disposed = true;
|
|
129
|
+
if (reconnectTimer !== undefined) clearTimeout(reconnectTimer);
|
|
130
|
+
stopPolling();
|
|
131
|
+
wsRef.current?.close();
|
|
132
|
+
wsRef.current = null;
|
|
133
|
+
};
|
|
134
|
+
}, [setConnected, setSnapshot, updateSquadState, setSquadInactive]);
|
|
135
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats elapsed milliseconds as "MM:SS" or "HH:MM:SS" if over an hour.
|
|
3
|
+
*/
|
|
4
|
+
export function formatElapsed(ms: number): string {
|
|
5
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
6
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
7
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
8
|
+
const seconds = totalSeconds % 60;
|
|
9
|
+
|
|
10
|
+
const pad = (n: number) => n.toString().padStart(2, "0");
|
|
11
|
+
|
|
12
|
+
if (hours > 0) {
|
|
13
|
+
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
|
|
14
|
+
}
|
|
15
|
+
return `${pad(minutes)}:${pad(seconds)}`;
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SquadState, Agent } from "@/types/state";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns agents sorted by desk position (row first, then col).
|
|
5
|
+
*/
|
|
6
|
+
export function sortAgentsByDesk(agents: Agent[]): Agent[] {
|
|
7
|
+
return [...agents].sort((a, b) => {
|
|
8
|
+
if (a.desk.row !== b.desk.row) return a.desk.row - b.desk.row;
|
|
9
|
+
return a.desk.col - b.desk.col;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find agent by id.
|
|
15
|
+
*/
|
|
16
|
+
export function findAgent(state: SquadState, agentId: string): Agent | undefined {
|
|
17
|
+
return state.agents.find((a) => a.id === agentId);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns the currently working agent, if any.
|
|
22
|
+
*/
|
|
23
|
+
export function getWorkingAgent(state: SquadState): Agent | undefined {
|
|
24
|
+
return state.agents.find((a) => a.status === "working");
|
|
25
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import Phaser from 'phaser';
|
|
2
|
+
import { avatarKeys, DESK_KEYS, FURNITURE_KEYS, type CharacterName } from './assetKeys';
|
|
3
|
+
import { COLORS } from './palette';
|
|
4
|
+
import type { Agent, AgentStatus } from '@/types/state';
|
|
5
|
+
|
|
6
|
+
// Avatar display scale — characters should be prominent at desk
|
|
7
|
+
const AVATAR_SCALE = 0.8;
|
|
8
|
+
|
|
9
|
+
// Status → badge color mapping
|
|
10
|
+
const STATUS_COLORS: Record<AgentStatus, number> = {
|
|
11
|
+
idle: COLORS.statusIdle,
|
|
12
|
+
working: COLORS.statusWorking,
|
|
13
|
+
done: COLORS.statusDone,
|
|
14
|
+
checkpoint: COLORS.statusCheckpoint,
|
|
15
|
+
delivering: COLORS.statusWorking,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Status → display label
|
|
19
|
+
const STATUS_LABELS: Record<AgentStatus, string> = {
|
|
20
|
+
idle: 'idle',
|
|
21
|
+
working: 'working',
|
|
22
|
+
done: 'done',
|
|
23
|
+
checkpoint: 'checkpoint',
|
|
24
|
+
delivering: 'delivering',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class AgentSprite {
|
|
28
|
+
private scene: Phaser.Scene;
|
|
29
|
+
private deskTable: Phaser.GameObjects.Image;
|
|
30
|
+
private deskShadow: Phaser.GameObjects.Graphics;
|
|
31
|
+
private desk: Phaser.GameObjects.Image;
|
|
32
|
+
private coffeeMug: Phaser.GameObjects.Image;
|
|
33
|
+
private avatar: Phaser.GameObjects.Image;
|
|
34
|
+
private nameText: Phaser.GameObjects.Text;
|
|
35
|
+
private badgeBg: Phaser.GameObjects.Graphics;
|
|
36
|
+
private statusDot: Phaser.GameObjects.Graphics;
|
|
37
|
+
private statusText: Phaser.GameObjects.Text;
|
|
38
|
+
private animTimer?: Phaser.Time.TimerEvent;
|
|
39
|
+
private agent: Agent;
|
|
40
|
+
private characterName: CharacterName;
|
|
41
|
+
private deskVariant: 'black' | 'white';
|
|
42
|
+
private avatarDisplayH: number = 0;
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
scene: Phaser.Scene,
|
|
46
|
+
x: number,
|
|
47
|
+
y: number,
|
|
48
|
+
characterName: CharacterName,
|
|
49
|
+
deskVariant: 'black' | 'white',
|
|
50
|
+
agent: Agent,
|
|
51
|
+
) {
|
|
52
|
+
this.scene = scene;
|
|
53
|
+
this.agent = agent;
|
|
54
|
+
this.characterName = characterName;
|
|
55
|
+
this.deskVariant = deskVariant;
|
|
56
|
+
|
|
57
|
+
// === VERTICAL LAYOUT (sprites, top to bottom on screen) ===
|
|
58
|
+
// desk_wood: 96x64 @ 1.3x = 125x83px → y-42 to y+42
|
|
59
|
+
// desktop_set: ~48x40 @ 1.3x = 62x52px → y-56 to y-4 (center at y-30)
|
|
60
|
+
// avatar: 48x51 @ 0.8x = 38x41px → y-91 to y-50 (center at y-70)
|
|
61
|
+
//
|
|
62
|
+
// Depth order (low = behind, high = front):
|
|
63
|
+
// avatar → y (seated character, lowest — desk will cover lower body)
|
|
64
|
+
// desk_wood → y+1 (desk surface IN FRONT of avatar → covers avatar's lower half)
|
|
65
|
+
// monitor → y+2 (on desk surface, screen faces viewer)
|
|
66
|
+
// coffee mug → y+3 (foreground item on front desk edge)
|
|
67
|
+
// label → 900/901 (always on top)
|
|
68
|
+
//
|
|
69
|
+
// Result: avatar fully visible above monitor, lower body hidden by desk → seated look
|
|
70
|
+
// =========================================
|
|
71
|
+
|
|
72
|
+
// Avatar — positioned further behind the desk so head/torso is clearly visible
|
|
73
|
+
const avatarKey = this.getAvatarKey(agent.status);
|
|
74
|
+
this.avatar = scene.add.image(x, y - 70, avatarKey)
|
|
75
|
+
.setOrigin(0.5, 0.5)
|
|
76
|
+
.setScale(AVATAR_SCALE)
|
|
77
|
+
.setDepth(y); // LOWEST depth — desk and monitor render in front
|
|
78
|
+
// Lock display height so texture swaps between frames of different pixel dimensions
|
|
79
|
+
// don't cause a visible scale jump (e.g. Male1 blink=56px tall vs talk=51px tall).
|
|
80
|
+
// Width is NOT locked — each frame scales proportionally from this height reference.
|
|
81
|
+
this.avatarDisplayH = this.avatar.displayHeight;
|
|
82
|
+
|
|
83
|
+
// Desk table surface — renders IN FRONT of avatar (covers lower body)
|
|
84
|
+
this.deskTable = scene.add.image(x, y, FURNITURE_KEYS.deskWood)
|
|
85
|
+
.setOrigin(0.5, 0.5)
|
|
86
|
+
.setScale(1.3)
|
|
87
|
+
.setDepth(y + 1);
|
|
88
|
+
|
|
89
|
+
// Monitor — screen-facing (_down orientation), sits on desk surface
|
|
90
|
+
const deskKey = this.getDeskKey(agent.status);
|
|
91
|
+
this.desk = scene.add.image(x, y - 30, deskKey)
|
|
92
|
+
.setOrigin(0.5, 0.5)
|
|
93
|
+
.setScale(1.3)
|
|
94
|
+
.setDepth(y + 2); // On top of desk surface, screen visible to viewer
|
|
95
|
+
|
|
96
|
+
// Coffee mug — right side of desk, away from monitor
|
|
97
|
+
this.coffeeMug = scene.add.image(x + 42, y + 8, 'furniture_coffee_mug')
|
|
98
|
+
.setOrigin(0.5, 1).setScale(1.4).setDepth(y + 3);
|
|
99
|
+
|
|
100
|
+
// Shadow (unused graphics object kept for destroy() compatibility)
|
|
101
|
+
this.deskShadow = scene.add.graphics();
|
|
102
|
+
this.deskShadow.setDepth(y - 1);
|
|
103
|
+
|
|
104
|
+
// Name badge — above avatar head (avatar center y-70, head top ≈ y-91, badge at y-140)
|
|
105
|
+
// badge height = 44px → badge bottom at y-96, leaving 5px gap above avatar top
|
|
106
|
+
const labelY = y - 140;
|
|
107
|
+
|
|
108
|
+
// Background pill behind name + status
|
|
109
|
+
this.badgeBg = scene.add.graphics();
|
|
110
|
+
|
|
111
|
+
// Name text — bold, clean, high contrast
|
|
112
|
+
this.nameText = scene.add.text(x, labelY + 5, agent.name, {
|
|
113
|
+
fontFamily: '"Segoe UI", "Helvetica Neue", Arial, sans-serif',
|
|
114
|
+
fontSize: '16px',
|
|
115
|
+
fontStyle: 'bold',
|
|
116
|
+
color: '#ffffff',
|
|
117
|
+
align: 'center',
|
|
118
|
+
stroke: '#000000',
|
|
119
|
+
strokeThickness: 4,
|
|
120
|
+
resolution: 2,
|
|
121
|
+
}).setOrigin(0.5, 0);
|
|
122
|
+
this.nameText.setDepth(901);
|
|
123
|
+
|
|
124
|
+
// Status dot
|
|
125
|
+
this.statusDot = scene.add.graphics();
|
|
126
|
+
|
|
127
|
+
// Status text — colored with outline
|
|
128
|
+
const statusColor = this.getStatusHexColor(agent.status);
|
|
129
|
+
this.statusText = scene.add.text(x, labelY + 24, STATUS_LABELS[agent.status], {
|
|
130
|
+
fontFamily: '"Segoe UI", "Helvetica Neue", Arial, sans-serif',
|
|
131
|
+
fontSize: '13px',
|
|
132
|
+
fontStyle: 'bold',
|
|
133
|
+
color: statusColor,
|
|
134
|
+
align: 'center',
|
|
135
|
+
stroke: '#000000',
|
|
136
|
+
strokeThickness: 3,
|
|
137
|
+
resolution: 2,
|
|
138
|
+
}).setOrigin(0.5, 0);
|
|
139
|
+
this.statusText.setDepth(901);
|
|
140
|
+
|
|
141
|
+
// Draw background and status dot
|
|
142
|
+
this.drawLabelBackground(x, labelY);
|
|
143
|
+
this.drawStatusDot(x, labelY + 22, agent.status);
|
|
144
|
+
|
|
145
|
+
this.startAnimation(agent.status);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private getStatusHexColor(status: AgentStatus): string {
|
|
149
|
+
const num = STATUS_COLORS[status] ?? COLORS.statusIdle;
|
|
150
|
+
return '#' + num.toString(16).padStart(6, '0');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private getDeskKey(_status: AgentStatus): string {
|
|
154
|
+
// Always show coding desk — all agents are always working
|
|
155
|
+
return this.deskVariant === 'black' ? DESK_KEYS.blackCoding : DESK_KEYS.whiteCoding;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private getAvatarKey(_status: AgentStatus): string {
|
|
159
|
+
// Always start in talk frame — animation will cycle from there
|
|
160
|
+
return avatarKeys(this.characterName).talk;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private drawLabelBackground(x: number, labelY: number): void {
|
|
164
|
+
const nameW = Math.max(this.nameText.width, this.statusText.width + 18);
|
|
165
|
+
const bgW = nameW + 20;
|
|
166
|
+
const bgH = 44;
|
|
167
|
+
// Solid dark background with rounded corners
|
|
168
|
+
this.badgeBg.fillStyle(0x1a1225, 0.95);
|
|
169
|
+
this.badgeBg.fillRoundedRect(x - bgW / 2, labelY, bgW, bgH, 5);
|
|
170
|
+
// Subtle border
|
|
171
|
+
this.badgeBg.lineStyle(1, 0x6a5a80, 0.4);
|
|
172
|
+
this.badgeBg.strokeRoundedRect(x - bgW / 2, labelY, bgW, bgH, 4);
|
|
173
|
+
this.badgeBg.setDepth(900);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private drawStatusDot(x: number, _statusY: number, status: AgentStatus): void {
|
|
177
|
+
const dotColor = STATUS_COLORS[status] ?? COLORS.statusIdle;
|
|
178
|
+
const textW = Math.max(this.statusText.width, 24);
|
|
179
|
+
this.statusDot.fillStyle(dotColor, 1);
|
|
180
|
+
this.statusDot.fillCircle(x - textW / 2 - 5, this.statusText.y + this.statusText.height / 2, 3);
|
|
181
|
+
this.statusDot.setDepth(901);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private setAvatarFrame(key: string): void {
|
|
185
|
+
this.avatar.setTexture(key);
|
|
186
|
+
// Scale uniformly so height matches the reference (talk frame) — preserves aspect ratio
|
|
187
|
+
this.avatar.setScale(this.avatarDisplayH / this.avatar.height);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private startAnimation(_status: AgentStatus): void {
|
|
191
|
+
// Always run the working animation regardless of status
|
|
192
|
+
const keys = avatarKeys(this.characterName);
|
|
193
|
+
let frame = 0;
|
|
194
|
+
this.animTimer = this.scene.time.addEvent({
|
|
195
|
+
delay: 500,
|
|
196
|
+
loop: true,
|
|
197
|
+
callback: () => {
|
|
198
|
+
frame = (frame + 1) % 2;
|
|
199
|
+
this.setAvatarFrame(frame === 0 ? keys.talk : keys.blink);
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
updateStatus(agent: Agent): void {
|
|
205
|
+
if (this.agent.status === agent.status) return;
|
|
206
|
+
this.agent = agent;
|
|
207
|
+
|
|
208
|
+
this.desk.setTexture(this.getDeskKey(agent.status));
|
|
209
|
+
this.setAvatarFrame(this.getAvatarKey(agent.status));
|
|
210
|
+
|
|
211
|
+
this.animTimer?.destroy();
|
|
212
|
+
this.startAnimation(agent.status);
|
|
213
|
+
|
|
214
|
+
// Update status text and dot
|
|
215
|
+
this.statusText.setText(STATUS_LABELS[agent.status]);
|
|
216
|
+
this.statusText.setColor(this.getStatusHexColor(agent.status));
|
|
217
|
+
|
|
218
|
+
this.statusDot.clear();
|
|
219
|
+
const dotColor = STATUS_COLORS[agent.status] ?? COLORS.statusIdle;
|
|
220
|
+
this.statusDot.fillStyle(dotColor, 1);
|
|
221
|
+
const textW = Math.max(this.statusText.width, 24);
|
|
222
|
+
this.statusDot.fillCircle(
|
|
223
|
+
this.statusText.x - textW / 2 - 5,
|
|
224
|
+
this.statusText.y + this.statusText.height / 2,
|
|
225
|
+
3,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
destroy(): void {
|
|
230
|
+
this.animTimer?.destroy();
|
|
231
|
+
this.deskTable.destroy();
|
|
232
|
+
this.deskShadow.destroy();
|
|
233
|
+
this.desk.destroy();
|
|
234
|
+
this.coffeeMug.destroy();
|
|
235
|
+
this.avatar.destroy();
|
|
236
|
+
this.nameText.destroy();
|
|
237
|
+
this.badgeBg.destroy();
|
|
238
|
+
this.statusDot.destroy();
|
|
239
|
+
this.statusText.destroy();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import Phaser from 'phaser';
|
|
2
|
+
import {
|
|
3
|
+
CHARACTER_NAMES, MALE_CHARACTERS, FEMALE_CHARACTERS, avatarKeys, avatarPath,
|
|
4
|
+
DESK_PATHS,
|
|
5
|
+
FURNITURE_PATHS,
|
|
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 } 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
|
+
const DEMO_AGENTS: Agent[] = [
|
|
32
|
+
{ id: '1', name: 'Researcher', icon: '', status: 'working', gender: 'female', desk: { col: 1, row: 1 } },
|
|
33
|
+
{ id: '2', name: 'Writer', icon: '', status: 'idle', gender: 'male', desk: { col: 2, row: 1 } },
|
|
34
|
+
{ id: '3', name: 'Editor', icon: '', status: 'done', gender: 'female', desk: { col: 3, row: 1 } },
|
|
35
|
+
{ id: '4', name: 'Designer', icon: '', status: 'working', gender: 'female', desk: { col: 1, row: 2 } },
|
|
36
|
+
{ id: '5', name: 'Reviewer', icon: '', status: 'checkpoint', gender: 'male', desk: { col: 2, row: 2 } },
|
|
37
|
+
{ id: '6', name: 'Publisher', icon: '', status: 'idle', gender: 'male', desk: { col: 3, row: 2 } },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export class OfficeScene extends Phaser.Scene {
|
|
41
|
+
private agentSprites: Map<string, AgentSprite> = new Map();
|
|
42
|
+
private roomBuilder!: RoomBuilder;
|
|
43
|
+
|
|
44
|
+
constructor() {
|
|
45
|
+
super({ key: 'OfficeScene' });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
preload(): void {
|
|
49
|
+
// Load desk sprites
|
|
50
|
+
for (const [key, path] of Object.entries(DESK_PATHS)) {
|
|
51
|
+
this.load.image(key, path);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Load avatar sprites
|
|
55
|
+
for (const name of CHARACTER_NAMES) {
|
|
56
|
+
const keys = avatarKeys(name);
|
|
57
|
+
this.load.image(keys.blink, avatarPath(name, 'blink'));
|
|
58
|
+
this.load.image(keys.talk, avatarPath(name, 'talk'));
|
|
59
|
+
this.load.image(keys.wave1, avatarPath(name, 'wave1'));
|
|
60
|
+
this.load.image(keys.wave2, avatarPath(name, 'wave2'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Load furniture sprites
|
|
64
|
+
for (const [key, path] of Object.entries(FURNITURE_PATHS)) {
|
|
65
|
+
this.load.image(key, path);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.load.on('loaderror', (file: Phaser.Loader.File) => {
|
|
69
|
+
console.error('Failed to load asset:', file.key, file.url);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
create(): void {
|
|
74
|
+
// Set all loaded textures to NEAREST filter for crisp pixel art
|
|
75
|
+
this.textures.list && Object.values(this.textures.list).forEach((tex) => {
|
|
76
|
+
if (tex.key !== '__DEFAULT' && tex.key !== '__MISSING') {
|
|
77
|
+
tex.setFilter(Phaser.Textures.FilterMode.NEAREST);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
this.roomBuilder = new RoomBuilder(this);
|
|
82
|
+
|
|
83
|
+
this.events.on('stateUpdate', (state: SquadState | null) => {
|
|
84
|
+
this.onStateUpdate(state);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
this.renderScene(DEMO_AGENTS);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private onStateUpdate(state: SquadState | null): void {
|
|
91
|
+
const agents = state?.agents ?? DEMO_AGENTS;
|
|
92
|
+
this.renderScene(agents);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private renderScene(agents: Agent[]): void {
|
|
96
|
+
// Auto-assign desk positions if all agents are at the same spot (default 1,1)
|
|
97
|
+
const allSameDesk = agents.length > 1 &&
|
|
98
|
+
agents.every(a => a.desk.col === agents[0].desk.col && a.desk.row === agents[0].desk.row);
|
|
99
|
+
if (allSameDesk) {
|
|
100
|
+
const cols = Math.min(agents.length, 3); // max 3 columns
|
|
101
|
+
agents = agents.map((a, i) => ({
|
|
102
|
+
...a,
|
|
103
|
+
desk: { col: (i % cols) + 1, row: Math.floor(i / cols) + 1 },
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let maxCol = 0, maxRow = 0;
|
|
108
|
+
for (const agent of agents) {
|
|
109
|
+
maxCol = Math.max(maxCol, agent.desk.col);
|
|
110
|
+
maxRow = Math.max(maxRow, agent.desk.row);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Wider cells for comfortable spacing between agents + labels
|
|
114
|
+
const cellW = CELL_W + 64; // 160px per cell (wider for desk tables + decorations)
|
|
115
|
+
const cellH = CELL_H + 80; // 176px per cell (label + monitor + desk + avatar)
|
|
116
|
+
|
|
117
|
+
const roomW = Math.max(maxCol * cellW + MARGIN * 2, 580);
|
|
118
|
+
// Extra space below desk grid for lounge area
|
|
119
|
+
const loungeSpace = CELL_H + 48;
|
|
120
|
+
const roomH = maxRow * cellH + MARGIN * 2 + WALL_H + loungeSpace;
|
|
121
|
+
|
|
122
|
+
this.clearScene();
|
|
123
|
+
this.roomBuilder.build(roomW, roomH);
|
|
124
|
+
|
|
125
|
+
const characterMap = assignCharacters(agents);
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < agents.length; i++) {
|
|
128
|
+
const agent = agents[i];
|
|
129
|
+
const x = (agent.desk.col - 1) * cellW + MARGIN + cellW / 2;
|
|
130
|
+
const y = (agent.desk.row - 1) * cellH + MARGIN + WALL_H + cellH / 2;
|
|
131
|
+
const characterName = characterMap.get(agent.id)!;
|
|
132
|
+
const deskVariant = i % 2 === 0 ? 'black' : 'white';
|
|
133
|
+
const agentSprite = new AgentSprite(this, x, y, characterName, deskVariant, agent);
|
|
134
|
+
this.agentSprites.set(agent.id, agentSprite);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Fit room in viewport with slight padding
|
|
138
|
+
const cam = this.cameras.main;
|
|
139
|
+
const scaleX = cam.width / (roomW + 32);
|
|
140
|
+
const scaleY = cam.height / (roomH + 32);
|
|
141
|
+
const zoom = Math.min(scaleX, scaleY, 2);
|
|
142
|
+
cam.setZoom(zoom);
|
|
143
|
+
cam.centerOn(roomW / 2, roomH / 2);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private clearScene(): void {
|
|
147
|
+
for (const sprite of this.agentSprites.values()) {
|
|
148
|
+
sprite.destroy();
|
|
149
|
+
}
|
|
150
|
+
this.agentSprites.clear();
|
|
151
|
+
this.children.removeAll(true);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import Phaser from 'phaser';
|
|
3
|
+
import { OfficeScene } from './OfficeScene';
|
|
4
|
+
import { useSquadStore } from '@/store/useSquadStore';
|
|
5
|
+
|
|
6
|
+
export function PhaserGame() {
|
|
7
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
8
|
+
const gameRef = useRef<Phaser.Game | null>(null);
|
|
9
|
+
|
|
10
|
+
// Create Phaser game on mount
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!containerRef.current || gameRef.current) return;
|
|
13
|
+
|
|
14
|
+
const container = containerRef.current;
|
|
15
|
+
const w = container.clientWidth || 800;
|
|
16
|
+
const h = container.clientHeight || 600;
|
|
17
|
+
|
|
18
|
+
const game = new Phaser.Game({
|
|
19
|
+
type: Phaser.AUTO,
|
|
20
|
+
parent: container,
|
|
21
|
+
width: w,
|
|
22
|
+
height: h,
|
|
23
|
+
pixelArt: false, // disabled globally so text renders smooth
|
|
24
|
+
antialias: false, // keep pixel art look for sprites
|
|
25
|
+
roundPixels: true, // snap sprites to whole pixels
|
|
26
|
+
backgroundColor: '#1a1420',
|
|
27
|
+
scene: [OfficeScene],
|
|
28
|
+
scale: {
|
|
29
|
+
mode: Phaser.Scale.NONE,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
gameRef.current = game;
|
|
34
|
+
|
|
35
|
+
// Resize canvas when container resizes
|
|
36
|
+
const ro = new ResizeObserver((entries) => {
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const { width, height } = entry.contentRect;
|
|
39
|
+
if (width > 0 && height > 0) {
|
|
40
|
+
game.scale.resize(width, height);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
ro.observe(container);
|
|
45
|
+
|
|
46
|
+
return () => {
|
|
47
|
+
ro.disconnect();
|
|
48
|
+
game.destroy(true);
|
|
49
|
+
gameRef.current = null;
|
|
50
|
+
};
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
// Bridge React state → Phaser scene
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
return useSquadStore.subscribe((state) => {
|
|
56
|
+
const game = gameRef.current;
|
|
57
|
+
if (!game) return;
|
|
58
|
+
const scene = game.scene.getScene('OfficeScene') as OfficeScene | null;
|
|
59
|
+
if (!scene || !scene.scene.isActive()) return;
|
|
60
|
+
|
|
61
|
+
const selectedSquad = state.selectedSquad;
|
|
62
|
+
const squadState = selectedSquad
|
|
63
|
+
? state.activeStates.get(selectedSquad) ?? null
|
|
64
|
+
: null;
|
|
65
|
+
|
|
66
|
+
scene.events.emit('stateUpdate', squadState);
|
|
67
|
+
});
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
ref={containerRef}
|
|
73
|
+
style={{
|
|
74
|
+
flex: 1,
|
|
75
|
+
overflow: 'hidden',
|
|
76
|
+
imageRendering: 'auto',
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}
|