erne-universal 0.2.0 → 0.3.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/README.md +92 -26
- package/agents/feature-builder.md +88 -0
- package/agents/senior-developer.md +77 -0
- package/bin/cli.js +4 -2
- package/dashboard/package.json +10 -0
- package/dashboard/public/agents.js +329 -0
- package/dashboard/public/canvas.js +275 -0
- package/dashboard/public/index.html +113 -0
- package/dashboard/public/sidebar.js +107 -0
- package/dashboard/public/ws-client.js +69 -0
- package/dashboard/server.js +191 -0
- package/docs/assets/dashboard-preview.png +0 -0
- package/docs/superpowers/plans/2026-03-11-agent-dashboard.md +1537 -0
- package/docs/superpowers/specs/2026-03-11-agent-dashboard-design.md +275 -0
- package/hooks/hooks.json +14 -0
- package/lib/dashboard.js +156 -0
- package/lib/init.js +294 -0
- package/lib/start.js +26 -0
- package/lib/update.js +60 -0
- package/package.json +3 -1
- package/scripts/daily-news/scan-ai-agents.js +222 -0
- package/scripts/daily-news/scan-rn-expo.js +233 -0
- package/scripts/hooks/dashboard-event.js +89 -0
- package/scripts/sync/issue-to-clickup.js +108 -0
- package/scripts/validate-all.js +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Complete AI coding agent harness for React Native and Expo development.
|
|
4
4
|
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="docs/assets/dashboard-preview.png" alt="ERNE Agent Dashboard — pixel-art office with 10 agents working in real-time" width="800">
|
|
7
|
+
</p>
|
|
8
|
+
|
|
5
9
|
## Quick Start
|
|
6
10
|
|
|
7
11
|
```bash
|
|
@@ -16,15 +20,50 @@ This will:
|
|
|
16
20
|
|
|
17
21
|
## What's Included
|
|
18
22
|
|
|
19
|
-
| Component | Count |
|
|
20
|
-
|
|
21
|
-
| Agents |
|
|
22
|
-
| Commands | 16
|
|
23
|
-
| Rule layers | 5
|
|
24
|
-
| Hook profiles | 3
|
|
25
|
-
| Skills | 8
|
|
26
|
-
| Contexts | 3
|
|
27
|
-
| MCP configs | 10 server integrations |
|
|
23
|
+
| Component | Count | Description |
|
|
24
|
+
|-----------|-------|-------------|
|
|
25
|
+
| Agents | 10 | Specialized AI agents for architecture, development, review, testing, UI, native, and more |
|
|
26
|
+
| Commands | 16 | Slash commands for every React Native workflow |
|
|
27
|
+
| Rule layers | 5 | Conditional rules: common, expo, bare-rn, native-ios, native-android |
|
|
28
|
+
| Hook profiles | 3 | Minimal, standard, strict — quality enforcement your way |
|
|
29
|
+
| Skills | 8 | Reusable knowledge modules loaded on-demand |
|
|
30
|
+
| Contexts | 3 | Behavior modes: dev, review, vibe |
|
|
31
|
+
| MCP configs | 10 | Pre-configured server integrations |
|
|
32
|
+
|
|
33
|
+
## Token Efficiency
|
|
34
|
+
|
|
35
|
+
ERNE's architecture is designed to minimize token usage through six layered mechanisms:
|
|
36
|
+
|
|
37
|
+
| Mechanism | How it works | Savings |
|
|
38
|
+
|-----------|-------------|---------|
|
|
39
|
+
| **Profile-gated hooks** | Minimal profile runs 3 hooks instead of 17 | ~31% |
|
|
40
|
+
| **Conditional rules** | Only loads rules matching your project type (Expo, bare RN, native) | ~26% |
|
|
41
|
+
| **On-demand skills** | Skills load only when their command is invoked, not always in context | ~12% |
|
|
42
|
+
| **Subagent isolation** | Fresh agent per task with only its own definition + relevant rules | ~12% |
|
|
43
|
+
| **Task-specific commands** | 16 focused prompts instead of one monolithic instruction set | ~13% |
|
|
44
|
+
| **Context-based behavior** | Modes change behavior dynamically without loading new rulesets | ~3% |
|
|
45
|
+
|
|
46
|
+
**Result:** Typical workflows use **60–67% fewer tokens** compared to a naive all-in-context approach. Vibe mode (minimal profile) reaches 67% savings, standard development 64%, and even strict mode saves 57%.
|
|
47
|
+
|
|
48
|
+
## Agent Dashboard
|
|
49
|
+
|
|
50
|
+
ERNE includes a real-time pixel-art dashboard that visualizes all 10 agents working in an animated office environment.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
erne dashboard # Start on port 3333, open browser
|
|
54
|
+
erne dashboard --port 4444 # Custom port
|
|
55
|
+
erne dashboard --no-open # Don't open browser
|
|
56
|
+
erne start # Init project + dashboard in background
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Features:**
|
|
60
|
+
- 4 office rooms (Development, Code Review, Testing, Conference)
|
|
61
|
+
- 8 unique procedural pixel-art agent sprites with animations
|
|
62
|
+
- Real-time status updates via WebSocket (connected to Claude Code hooks)
|
|
63
|
+
- Sidebar with agent status, task descriptions, and connection indicator
|
|
64
|
+
- Auto-reconnect with exponential backoff
|
|
65
|
+
|
|
66
|
+
The dashboard hooks into Claude Code's `PreToolUse` and `PostToolUse` events (pattern: `Agent`) to track which agents are actively working and what they're doing.
|
|
28
67
|
|
|
29
68
|
## IDE & Editor Support
|
|
30
69
|
|
|
@@ -43,32 +82,59 @@ All config files share the same React Native & Expo conventions: TypeScript stri
|
|
|
43
82
|
|
|
44
83
|
## Agents
|
|
45
84
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
85
|
+
| Agent | Domain | Room |
|
|
86
|
+
|-------|--------|------|
|
|
87
|
+
| **architect** | System design and project structure | Development |
|
|
88
|
+
| **senior-developer** | End-to-end feature implementation, screens, hooks, API | Development |
|
|
89
|
+
| **feature-builder** | Focused implementation units, works in parallel | Development |
|
|
90
|
+
| **native-bridge-builder** | Turbo Modules and native platform APIs | Development |
|
|
91
|
+
| **expo-config-resolver** | Expo configuration and build issues | Development |
|
|
92
|
+
| **ui-designer** | Accessible, performant UI components | Development |
|
|
93
|
+
| **code-reviewer** | Code quality and best practices | Code Review |
|
|
94
|
+
| **upgrade-assistant** | Version migration guidance | Code Review |
|
|
95
|
+
| **tdd-guide** | Test-driven development workflow | Testing |
|
|
96
|
+
| **performance-profiler** | FPS diagnostics and bundle optimization | Testing |
|
|
54
97
|
|
|
55
98
|
## Hook Profiles
|
|
56
99
|
|
|
57
|
-
| Profile | Use Case |
|
|
58
|
-
|
|
59
|
-
| minimal | Fast iteration, vibe coding |
|
|
60
|
-
| standard | Balanced quality + speed (recommended) |
|
|
61
|
-
| strict | Production-grade enforcement |
|
|
100
|
+
| Profile | Hooks | Use Case |
|
|
101
|
+
|---------|-------|----------|
|
|
102
|
+
| minimal | 3 | Fast iteration, vibe coding — maximum speed, minimum friction |
|
|
103
|
+
| standard | 11 | Balanced quality + speed (recommended) — catches real issues |
|
|
104
|
+
| strict | 17 | Production-grade enforcement — full security, accessibility, perf budgets |
|
|
62
105
|
|
|
63
|
-
Change profile:
|
|
106
|
+
Change profile: set `ERNE_PROFILE` env var, add `<!-- Hook Profile: standard -->` to CLAUDE.md, or use `/vibe` context.
|
|
64
107
|
|
|
65
108
|
## Commands
|
|
66
109
|
|
|
67
|
-
Core
|
|
110
|
+
**Core:** `/plan`, `/code-review`, `/tdd`, `/build-fix`, `/perf`, `/upgrade`, `/native-module`, `/navigate`
|
|
111
|
+
|
|
112
|
+
**Extended:** `/animate`, `/deploy`, `/component`, `/debug`, `/quality-gate`
|
|
68
113
|
|
|
69
|
-
|
|
114
|
+
**Learning:** `/learn`, `/retrospective`, `/setup-device`
|
|
115
|
+
|
|
116
|
+
## Architecture
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
Claude Code Hooks ──▶ run-with-flags.js ──▶ Profile gate ──▶ Hook scripts
|
|
120
|
+
│
|
|
121
|
+
┌──────────┴──────────┐
|
|
122
|
+
│ Only hooks for │
|
|
123
|
+
│ active profile │
|
|
124
|
+
│ are executed │
|
|
125
|
+
└─────────────────────┘
|
|
126
|
+
|
|
127
|
+
erne dashboard ──▶ HTTP + WS Server ──▶ Browser Canvas
|
|
128
|
+
▲
|
|
129
|
+
Claude Code PreToolUse ─┤ (Agent pattern)
|
|
130
|
+
Claude Code PostToolUse ┘
|
|
131
|
+
```
|
|
70
132
|
|
|
71
|
-
|
|
133
|
+
**Key design principles:**
|
|
134
|
+
- **Zero runtime dependencies** for the harness itself (ws package only for dashboard)
|
|
135
|
+
- **Conditional loading** — rules, skills, and hooks load based on project type and profile
|
|
136
|
+
- **Fresh subagent per task** — no context pollution between agent invocations
|
|
137
|
+
- **Silent failure** — hooks never block Claude Code if something goes wrong
|
|
72
138
|
|
|
73
139
|
## Available On
|
|
74
140
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: feature-builder
|
|
3
|
+
description: Focused feature implementation — individual screens, components, hooks, and API endpoints. Designed to work in parallel with senior-developer. Triggered by /code, /feature, /component.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are the ERNE Feature Builder agent — a focused React Native/Expo implementation specialist who builds individual feature units quickly and correctly.
|
|
7
|
+
|
|
8
|
+
## Your Role
|
|
9
|
+
|
|
10
|
+
Implement discrete feature units: a single screen, a custom hook, an API integration module, or a reusable component. You work best when given a clear, scoped task — often in parallel with the senior-developer agent on different parts of the same feature.
|
|
11
|
+
|
|
12
|
+
## Capabilities
|
|
13
|
+
|
|
14
|
+
- **Screen building**: Implement individual screens with proper data fetching and state handling
|
|
15
|
+
- **Hook extraction**: Build focused custom hooks with clean interfaces and error handling
|
|
16
|
+
- **API modules**: Create typed API client methods, TanStack Query wrappers, and cache invalidation
|
|
17
|
+
- **Component building**: Build reusable UI components with props API, accessibility, and platform variants
|
|
18
|
+
- **Utility modules**: Implement formatters, validators, transforms, and platform-specific helpers
|
|
19
|
+
- **Migration scripts**: Write codemods and data migration utilities
|
|
20
|
+
|
|
21
|
+
## Tech Stack
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
// Screen with data fetching
|
|
25
|
+
export const ProfileScreen = () => {
|
|
26
|
+
const { id } = useLocalSearchParams<{ id: string }>();
|
|
27
|
+
const { data: user, isLoading, error } = useUser(id);
|
|
28
|
+
|
|
29
|
+
if (isLoading) return <LoadingSkeleton />;
|
|
30
|
+
if (error) return <ErrorView error={error} onRetry={refetch} />;
|
|
31
|
+
if (!user) return <EmptyState message="User not found" />;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<ScrollView contentContainerStyle={styles.container}>
|
|
35
|
+
<ProfileHeader user={user} />
|
|
36
|
+
<ProfileStats stats={user.stats} />
|
|
37
|
+
<ProfileActions userId={user.id} />
|
|
38
|
+
</ScrollView>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Custom hook
|
|
43
|
+
export const useUser = (userId: string) => {
|
|
44
|
+
return useQuery({
|
|
45
|
+
queryKey: ['user', userId],
|
|
46
|
+
queryFn: () => apiClient.get<User>(`/users/${userId}`),
|
|
47
|
+
enabled: !!userId,
|
|
48
|
+
staleTime: 5 * 60 * 1000,
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Process
|
|
54
|
+
|
|
55
|
+
1. **Receive scoped task** — One screen, one hook, one module at a time
|
|
56
|
+
2. **Check dependencies** — Verify types, API contracts, and shared state are defined
|
|
57
|
+
3. **Implement** — Write the code with all states handled (loading, error, empty, success)
|
|
58
|
+
4. **Type everything** — Explicit return types, param interfaces, no `any`
|
|
59
|
+
5. **Handle edges** — Null checks, empty arrays, network failures, platform differences
|
|
60
|
+
6. **Deliver** — Complete file ready to integrate
|
|
61
|
+
|
|
62
|
+
## Parallel Work Pattern
|
|
63
|
+
|
|
64
|
+
When working alongside senior-developer:
|
|
65
|
+
- **Senior-developer** handles: data layer, stores, navigation skeleton, complex multi-screen flows
|
|
66
|
+
- **Feature-builder** handles: individual screens, isolated components, utility hooks, API wrappers
|
|
67
|
+
- Coordinate via shared type definitions and agreed interfaces
|
|
68
|
+
- Never modify files the other agent is actively editing
|
|
69
|
+
|
|
70
|
+
## Guidelines
|
|
71
|
+
|
|
72
|
+
- Functional components with `const` + arrow functions, named exports only
|
|
73
|
+
- Group imports: react → react-native → expo → external → internal → types
|
|
74
|
+
- Max 250 lines per file — if larger, you're doing too much
|
|
75
|
+
- `StyleSheet.create()` always, no inline styles
|
|
76
|
+
- Handle all UI states: loading, error, empty, success
|
|
77
|
+
- Every public function needs a TypeScript return type
|
|
78
|
+
- No `any` — use `unknown` and narrow, or define the type
|
|
79
|
+
- Accessibility: `accessibilityLabel`, `accessibilityRole`, `accessibilityHint` on interactive elements
|
|
80
|
+
- Test-ready: props-based, no hidden global state, injectable dependencies
|
|
81
|
+
|
|
82
|
+
## Output Format
|
|
83
|
+
|
|
84
|
+
For each unit:
|
|
85
|
+
1. File path and complete code
|
|
86
|
+
2. Props/params interface
|
|
87
|
+
3. Dependencies (what it imports from other modules)
|
|
88
|
+
4. Integration point (how the parent screen/module uses it)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: senior-developer
|
|
3
|
+
description: End-to-end feature implementation — screens, hooks, API integration, state management, navigation wiring. Triggered by /code, /feature, /plan (implementation phase).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are the ERNE Senior Developer agent — a senior React Native/Expo engineer who writes production-grade feature code.
|
|
7
|
+
|
|
8
|
+
## Your Role
|
|
9
|
+
|
|
10
|
+
Implement complete features end-to-end: screens, custom hooks, API integration, state management, navigation wiring, and error handling. You are the one who turns architect plans into working code.
|
|
11
|
+
|
|
12
|
+
## Capabilities
|
|
13
|
+
|
|
14
|
+
- **Screen implementation**: Build full screens with data fetching, loading/error states, pull-to-refresh, pagination
|
|
15
|
+
- **Custom hooks**: Extract reusable logic into typed hooks (`useAuth`, `useForm`, `useDebounce`, etc.)
|
|
16
|
+
- **API integration**: Wire TanStack Query mutations/queries, handle optimistic updates, error boundaries
|
|
17
|
+
- **State management**: Implement Zustand stores for client state, connect TanStack Query for server state
|
|
18
|
+
- **Navigation wiring**: Create Expo Router layouts, typed navigation params, deep link handlers
|
|
19
|
+
- **Form handling**: Build validated forms with proper keyboard handling, accessibility, and submission logic
|
|
20
|
+
- **Error handling**: Implement error boundaries, retry logic, user-facing error messages, offline fallbacks
|
|
21
|
+
|
|
22
|
+
## Tech Stack
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
// Data fetching — TanStack Query
|
|
26
|
+
const { data, isLoading } = useQuery({
|
|
27
|
+
queryKey: ['users', userId],
|
|
28
|
+
queryFn: () => api.getUser(userId),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Client state — Zustand
|
|
32
|
+
const useAuthStore = create<AuthState>()((set) => ({
|
|
33
|
+
user: null,
|
|
34
|
+
setUser: (user) => set({ user }),
|
|
35
|
+
logout: () => set({ user: null }),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// Navigation — Expo Router (typed)
|
|
39
|
+
import { useLocalSearchParams, router } from 'expo-router';
|
|
40
|
+
|
|
41
|
+
type Params = { id: string; mode: 'edit' | 'view' };
|
|
42
|
+
const { id, mode } = useLocalSearchParams<Params>();
|
|
43
|
+
|
|
44
|
+
// Secure storage
|
|
45
|
+
import * as SecureStore from 'expo-secure-store';
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Process
|
|
49
|
+
|
|
50
|
+
1. **Read the plan** — Understand the architect's design, component hierarchy, and data flow
|
|
51
|
+
2. **Set up the skeleton** — Create files, routes, and type definitions
|
|
52
|
+
3. **Implement data layer first** — API client, queries/mutations, stores
|
|
53
|
+
4. **Build screens** — Wire data to UI, handle all states (loading, error, empty, success)
|
|
54
|
+
5. **Add navigation** — Route params, transitions, deep links, back handling
|
|
55
|
+
6. **Handle edge cases** — Offline, token expiry, race conditions, keyboard avoidance
|
|
56
|
+
7. **Self-review** — Check for re-renders, missing error handling, accessibility, type safety
|
|
57
|
+
|
|
58
|
+
## Guidelines
|
|
59
|
+
|
|
60
|
+
- Functional components with `const` + arrow functions, named exports only
|
|
61
|
+
- Group imports: react → react-native → expo → external → internal → types
|
|
62
|
+
- Max 250 lines per component — extract hooks and subcomponents when larger
|
|
63
|
+
- `StyleSheet.create()` for styles, no inline styles
|
|
64
|
+
- `FlashList` over `FlatList` for 100+ items
|
|
65
|
+
- Memoize with `React.memo`, `useMemo`, `useCallback` where measurable
|
|
66
|
+
- No anonymous functions in JSX render paths
|
|
67
|
+
- Validate all deep link params and external input
|
|
68
|
+
- Use `expo-secure-store` for tokens, never AsyncStorage
|
|
69
|
+
- Conventional Commits: `feat:`, `fix:`, `refactor:`
|
|
70
|
+
|
|
71
|
+
## Output Format
|
|
72
|
+
|
|
73
|
+
For each implementation unit:
|
|
74
|
+
1. File path and complete code
|
|
75
|
+
2. Type definitions (interfaces, params)
|
|
76
|
+
3. Integration notes (how it connects to other modules)
|
|
77
|
+
4. Known trade-offs or TODOs for follow-up
|
package/bin/cli.js
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
|
-
const { resolve, join } = require('path');
|
|
12
|
-
|
|
13
11
|
const COMMANDS = {
|
|
14
12
|
init: () => require('../lib/init'),
|
|
15
13
|
update: () => require('../lib/update'),
|
|
14
|
+
dashboard: () => require('../lib/dashboard'),
|
|
15
|
+
start: () => require('../lib/start'),
|
|
16
16
|
version: () => {
|
|
17
17
|
const pkg = require('../package.json');
|
|
18
18
|
console.log(`erne v${pkg.version}`);
|
|
@@ -28,6 +28,8 @@ const COMMANDS = {
|
|
|
28
28
|
Commands:
|
|
29
29
|
init Set up ERNE in your project
|
|
30
30
|
update Update to the latest version
|
|
31
|
+
dashboard Launch the ERNE Agent Dashboard
|
|
32
|
+
start Init project and start dashboard
|
|
31
33
|
version Show installed version
|
|
32
34
|
help Show this help message
|
|
33
35
|
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ERNE Dashboard — Procedural 32x32 pixel-art agent sprites with animations
|
|
3
|
+
*/
|
|
4
|
+
(function () {
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const FRAME_SIZE = 32;
|
|
8
|
+
const SHEET_COLS = 4;
|
|
9
|
+
const SHEET_ROWS = 4;
|
|
10
|
+
const SHEET_W = FRAME_SIZE * SHEET_COLS; // 128
|
|
11
|
+
const SHEET_H = FRAME_SIZE * SHEET_ROWS; // 128
|
|
12
|
+
const ANIM_FPS = 12;
|
|
13
|
+
|
|
14
|
+
const SKIN = '#FDBCB4';
|
|
15
|
+
const HAIR = '#4A3728';
|
|
16
|
+
const LEGS_COLOR = '#2c3e50';
|
|
17
|
+
|
|
18
|
+
const AGENT_DEFS = {
|
|
19
|
+
architect: { bodyColor: '#3498db', traitColor: '#2980b9', trait: 'hardhat' },
|
|
20
|
+
'native-bridge-builder': { bodyColor: '#e74c3c', traitColor: '#c0392b', trait: 'wrench' },
|
|
21
|
+
'expo-config-resolver': { bodyColor: '#9b59b6', traitColor: '#8e44ad', trait: 'gear' },
|
|
22
|
+
'ui-designer': { bodyColor: '#e91e63', traitColor: '#c2185b', trait: 'paintbrush' },
|
|
23
|
+
'code-reviewer': { bodyColor: '#2ecc71', traitColor: '#27ae60', trait: 'glasses' },
|
|
24
|
+
'upgrade-assistant': { bodyColor: '#f39c12', traitColor: '#e67e22', trait: 'arrow' },
|
|
25
|
+
'tdd-guide': { bodyColor: '#1abc9c', traitColor: '#16a085', trait: 'testtube' },
|
|
26
|
+
'performance-profiler': { bodyColor: '#e67e22', traitColor: '#d35400', trait: 'stopwatch' },
|
|
27
|
+
'senior-developer': { bodyColor: '#3455db', traitColor: '#2a44b0', trait: 'laptop' },
|
|
28
|
+
'feature-builder': { bodyColor: '#00bcd4', traitColor: '#0097a7', trait: 'hammer' },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/* ---- Drawing primitives ---- */
|
|
32
|
+
|
|
33
|
+
const drawCharacter = (ctx, ox, oy, def, headBob, typing, armOffset) => {
|
|
34
|
+
const hb = headBob || 0;
|
|
35
|
+
const ao = armOffset || 0;
|
|
36
|
+
|
|
37
|
+
// Legs
|
|
38
|
+
ctx.fillStyle = LEGS_COLOR;
|
|
39
|
+
ctx.fillRect(ox + 11, oy + 24, 4, 6);
|
|
40
|
+
ctx.fillRect(ox + 17, oy + 24, 4, 6);
|
|
41
|
+
|
|
42
|
+
// Body
|
|
43
|
+
ctx.fillStyle = def.bodyColor;
|
|
44
|
+
ctx.fillRect(ox + 9, oy + 14, 14, 11);
|
|
45
|
+
// Shirt detail
|
|
46
|
+
ctx.fillStyle = def.traitColor;
|
|
47
|
+
ctx.fillRect(ox + 14, oy + 15, 4, 3);
|
|
48
|
+
|
|
49
|
+
// Arms
|
|
50
|
+
ctx.fillStyle = def.bodyColor;
|
|
51
|
+
ctx.fillRect(ox + 5, oy + 15 + ao, 4, 8);
|
|
52
|
+
ctx.fillRect(ox + 23, oy + 15 - ao, 4, 8);
|
|
53
|
+
|
|
54
|
+
// Hands
|
|
55
|
+
ctx.fillStyle = SKIN;
|
|
56
|
+
ctx.fillRect(ox + 5, oy + 23 + ao, 4, 3);
|
|
57
|
+
ctx.fillRect(ox + 23, oy + 23 - ao, 4, 3);
|
|
58
|
+
|
|
59
|
+
// Head
|
|
60
|
+
ctx.fillStyle = SKIN;
|
|
61
|
+
ctx.fillRect(ox + 10, oy + 4 + hb, 12, 11);
|
|
62
|
+
|
|
63
|
+
// Hair
|
|
64
|
+
ctx.fillStyle = HAIR;
|
|
65
|
+
ctx.fillRect(ox + 9, oy + 3 + hb, 14, 4);
|
|
66
|
+
ctx.fillRect(ox + 9, oy + 4 + hb, 2, 6);
|
|
67
|
+
|
|
68
|
+
// Eyes
|
|
69
|
+
ctx.fillStyle = '#222';
|
|
70
|
+
ctx.fillRect(ox + 13, oy + 8 + hb, 2, 2);
|
|
71
|
+
ctx.fillRect(ox + 18, oy + 8 + hb, 2, 2);
|
|
72
|
+
|
|
73
|
+
// Mouth
|
|
74
|
+
ctx.fillStyle = '#c0392b';
|
|
75
|
+
ctx.fillRect(ox + 14, oy + 12 + hb, 4, 1);
|
|
76
|
+
|
|
77
|
+
drawTrait(ctx, ox, oy + hb, def);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const drawWalkingCharacter = (ctx, ox, oy, def, legOffset) => {
|
|
81
|
+
const lo = legOffset || 0;
|
|
82
|
+
|
|
83
|
+
// Legs — walk cycle
|
|
84
|
+
ctx.fillStyle = LEGS_COLOR;
|
|
85
|
+
ctx.fillRect(ox + 11, oy + 24 + lo, 4, 6);
|
|
86
|
+
ctx.fillRect(ox + 17, oy + 24 - lo, 4, 6);
|
|
87
|
+
|
|
88
|
+
// Body
|
|
89
|
+
ctx.fillStyle = def.bodyColor;
|
|
90
|
+
ctx.fillRect(ox + 9, oy + 14, 14, 11);
|
|
91
|
+
ctx.fillStyle = def.traitColor;
|
|
92
|
+
ctx.fillRect(ox + 14, oy + 15, 4, 3);
|
|
93
|
+
|
|
94
|
+
// Arms swing
|
|
95
|
+
ctx.fillStyle = def.bodyColor;
|
|
96
|
+
ctx.fillRect(ox + 5, oy + 15 - lo, 4, 8);
|
|
97
|
+
ctx.fillRect(ox + 23, oy + 15 + lo, 4, 8);
|
|
98
|
+
ctx.fillStyle = SKIN;
|
|
99
|
+
ctx.fillRect(ox + 5, oy + 23 - lo, 4, 3);
|
|
100
|
+
ctx.fillRect(ox + 23, oy + 23 + lo, 4, 3);
|
|
101
|
+
|
|
102
|
+
// Head
|
|
103
|
+
ctx.fillStyle = SKIN;
|
|
104
|
+
ctx.fillRect(ox + 10, oy + 4, 12, 11);
|
|
105
|
+
ctx.fillStyle = HAIR;
|
|
106
|
+
ctx.fillRect(ox + 9, oy + 3, 14, 4);
|
|
107
|
+
ctx.fillRect(ox + 9, oy + 4, 2, 6);
|
|
108
|
+
ctx.fillStyle = '#222';
|
|
109
|
+
ctx.fillRect(ox + 13, oy + 8, 2, 2);
|
|
110
|
+
ctx.fillRect(ox + 18, oy + 8, 2, 2);
|
|
111
|
+
ctx.fillStyle = '#c0392b';
|
|
112
|
+
ctx.fillRect(ox + 14, oy + 12, 4, 1);
|
|
113
|
+
|
|
114
|
+
drawTrait(ctx, ox, oy, def);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const drawTrait = (ctx, ox, oy, def) => {
|
|
118
|
+
switch (def.trait) {
|
|
119
|
+
case 'hardhat':
|
|
120
|
+
ctx.fillStyle = '#f1c40f';
|
|
121
|
+
ctx.fillRect(ox + 8, oy + 1, 16, 4);
|
|
122
|
+
ctx.fillRect(ox + 10, oy - 1, 12, 3);
|
|
123
|
+
break;
|
|
124
|
+
case 'wrench':
|
|
125
|
+
ctx.fillStyle = def.traitColor;
|
|
126
|
+
ctx.fillRect(ox + 24, oy + 5, 3, 8);
|
|
127
|
+
ctx.fillRect(ox + 23, oy + 4, 5, 3);
|
|
128
|
+
break;
|
|
129
|
+
case 'gear':
|
|
130
|
+
ctx.fillStyle = def.traitColor;
|
|
131
|
+
ctx.fillRect(ox + 25, oy + 5, 5, 5);
|
|
132
|
+
ctx.fillStyle = def.bodyColor;
|
|
133
|
+
ctx.fillRect(ox + 26, oy + 6, 3, 3);
|
|
134
|
+
break;
|
|
135
|
+
case 'paintbrush':
|
|
136
|
+
ctx.fillStyle = '#8B4513';
|
|
137
|
+
ctx.fillRect(ox + 25, oy + 6, 2, 10);
|
|
138
|
+
ctx.fillStyle = def.traitColor;
|
|
139
|
+
ctx.fillRect(ox + 24, oy + 4, 4, 3);
|
|
140
|
+
break;
|
|
141
|
+
case 'glasses':
|
|
142
|
+
ctx.fillStyle = '#FFD700';
|
|
143
|
+
ctx.fillRect(ox + 11, oy + 7, 6, 4);
|
|
144
|
+
ctx.fillRect(ox + 16, oy + 7, 6, 4);
|
|
145
|
+
ctx.fillStyle = 'rgba(255,255,255,0.3)';
|
|
146
|
+
ctx.fillRect(ox + 12, oy + 8, 4, 2);
|
|
147
|
+
ctx.fillRect(ox + 17, oy + 8, 4, 2);
|
|
148
|
+
ctx.fillStyle = '#FFD700';
|
|
149
|
+
ctx.fillRect(ox + 17, oy + 8, 1, 1);
|
|
150
|
+
break;
|
|
151
|
+
case 'arrow':
|
|
152
|
+
ctx.fillStyle = def.traitColor;
|
|
153
|
+
ctx.fillRect(ox + 25, oy + 4, 3, 8);
|
|
154
|
+
ctx.fillStyle = '#fff';
|
|
155
|
+
ctx.fillRect(ox + 24, oy + 3, 5, 3);
|
|
156
|
+
ctx.fillRect(ox + 25, oy + 2, 3, 2);
|
|
157
|
+
break;
|
|
158
|
+
case 'testtube':
|
|
159
|
+
ctx.fillStyle = '#ecf0f1';
|
|
160
|
+
ctx.fillRect(ox + 25, oy + 4, 3, 10);
|
|
161
|
+
ctx.fillStyle = '#2ecc71';
|
|
162
|
+
ctx.fillRect(ox + 25, oy + 10, 3, 4);
|
|
163
|
+
ctx.fillRect(ox + 24, oy + 13, 5, 2);
|
|
164
|
+
break;
|
|
165
|
+
case 'stopwatch':
|
|
166
|
+
ctx.fillStyle = def.traitColor;
|
|
167
|
+
ctx.fillRect(ox + 24, oy + 4, 6, 6);
|
|
168
|
+
ctx.fillStyle = '#fff';
|
|
169
|
+
ctx.fillRect(ox + 25, oy + 5, 4, 4);
|
|
170
|
+
ctx.fillStyle = '#333';
|
|
171
|
+
ctx.fillRect(ox + 27, oy + 5, 1, 3);
|
|
172
|
+
ctx.fillRect(ox + 26, oy + 3, 2, 2);
|
|
173
|
+
break;
|
|
174
|
+
case 'laptop':
|
|
175
|
+
ctx.fillStyle = '#333';
|
|
176
|
+
ctx.fillRect(ox + 23, oy + 8, 7, 5);
|
|
177
|
+
ctx.fillStyle = '#5dade2';
|
|
178
|
+
ctx.fillRect(ox + 24, oy + 9, 5, 3);
|
|
179
|
+
ctx.fillStyle = '#555';
|
|
180
|
+
ctx.fillRect(ox + 22, oy + 13, 9, 2);
|
|
181
|
+
break;
|
|
182
|
+
case 'hammer':
|
|
183
|
+
ctx.fillStyle = '#8B4513';
|
|
184
|
+
ctx.fillRect(ox + 26, oy + 6, 2, 10);
|
|
185
|
+
ctx.fillStyle = def.traitColor;
|
|
186
|
+
ctx.fillRect(ox + 23, oy + 3, 8, 4);
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const drawCheckmark = (ctx, ox, oy, frame) => {
|
|
192
|
+
const bounce = Math.max(0, 3 - frame);
|
|
193
|
+
ctx.fillStyle = '#2ecc71';
|
|
194
|
+
ctx.fillRect(ox + 10, oy - 6 - bounce, 12, 10);
|
|
195
|
+
ctx.fillStyle = '#fff';
|
|
196
|
+
// Checkmark shape
|
|
197
|
+
ctx.fillRect(ox + 13, oy - 2 - bounce, 2, 2);
|
|
198
|
+
ctx.fillRect(ox + 14, oy - 1 - bounce, 2, 2);
|
|
199
|
+
ctx.fillRect(ox + 15, oy - 2 - bounce, 2, 2);
|
|
200
|
+
ctx.fillRect(ox + 16, oy - 3 - bounce, 2, 2);
|
|
201
|
+
ctx.fillRect(ox + 17, oy - 4 - bounce, 2, 2);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/* ---- Sprite sheet generation ---- */
|
|
205
|
+
|
|
206
|
+
const generateSpriteSheet = (agentName) => {
|
|
207
|
+
const def = AGENT_DEFS[agentName];
|
|
208
|
+
if (!def) return null;
|
|
209
|
+
|
|
210
|
+
const canvas = document.createElement('canvas');
|
|
211
|
+
canvas.width = SHEET_W;
|
|
212
|
+
canvas.height = SHEET_H;
|
|
213
|
+
const ctx = canvas.getContext('2d');
|
|
214
|
+
|
|
215
|
+
// Row 0: IDLE (4 frames, head bob on frames 1,2)
|
|
216
|
+
for (let f = 0; f < 4; f++) {
|
|
217
|
+
const hb = (f === 1 || f === 2) ? -1 : 0;
|
|
218
|
+
drawCharacter(ctx, f * FRAME_SIZE, 0, def, hb, false, 0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Row 1: WORKING (4 frames, typing arm offset alternating)
|
|
222
|
+
for (let f = 0; f < 4; f++) {
|
|
223
|
+
const ao = (f % 2 === 0) ? -2 : 2;
|
|
224
|
+
drawCharacter(ctx, f * FRAME_SIZE, FRAME_SIZE, def, 0, true, ao);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Row 2: MOVING (4 frames, walk cycle leg offset)
|
|
228
|
+
const legOffsets = [-2, 0, 2, 0];
|
|
229
|
+
for (let f = 0; f < 4; f++) {
|
|
230
|
+
drawWalkingCharacter(ctx, f * FRAME_SIZE, FRAME_SIZE * 2, def, legOffsets[f]);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Row 3: DONE (4 frames, checkmark popup)
|
|
234
|
+
for (let f = 0; f < 4; f++) {
|
|
235
|
+
drawCharacter(ctx, f * FRAME_SIZE, FRAME_SIZE * 3, def, 0, false, 0);
|
|
236
|
+
drawCheckmark(ctx, f * FRAME_SIZE, FRAME_SIZE * 3, f);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return canvas;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/* ---- State manager ---- */
|
|
243
|
+
|
|
244
|
+
const STATUS_TO_ROW = {
|
|
245
|
+
idle: 0,
|
|
246
|
+
working: 1,
|
|
247
|
+
moving: 2,
|
|
248
|
+
done: 3,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const agentSprites = {};
|
|
252
|
+
|
|
253
|
+
const initAgentSprites = () => {
|
|
254
|
+
for (const name of Object.keys(AGENT_DEFS)) {
|
|
255
|
+
const pos = window.OfficeCanvas.getAgentDeskPosition(name);
|
|
256
|
+
if (!pos) continue;
|
|
257
|
+
agentSprites[name] = {
|
|
258
|
+
sheet: generateSpriteSheet(name),
|
|
259
|
+
x: pos.x,
|
|
260
|
+
y: pos.y,
|
|
261
|
+
status: 'idle',
|
|
262
|
+
frame: 0,
|
|
263
|
+
frameTimer: 0,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const updateAgentState = (name, status) => {
|
|
269
|
+
if (!agentSprites[name]) return;
|
|
270
|
+
agentSprites[name].status = status;
|
|
271
|
+
agentSprites[name].frame = 0;
|
|
272
|
+
agentSprites[name].frameTimer = 0;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const updateAgentSprites = (dt) => {
|
|
276
|
+
for (const sprite of Object.values(agentSprites)) {
|
|
277
|
+
sprite.frameTimer += dt;
|
|
278
|
+
const frameDuration = 1 / ANIM_FPS;
|
|
279
|
+
if (sprite.frameTimer >= frameDuration) {
|
|
280
|
+
sprite.frameTimer -= frameDuration;
|
|
281
|
+
sprite.frame = (sprite.frame + 1) % SHEET_COLS;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const STATUS_DOT_COLORS = {
|
|
287
|
+
idle: '#9E9E9E',
|
|
288
|
+
working: '#4CAF50',
|
|
289
|
+
done: '#2196F3',
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const drawAgentSprites = (ctx) => {
|
|
293
|
+
for (const [name, sprite] of Object.entries(agentSprites)) {
|
|
294
|
+
const row = STATUS_TO_ROW[sprite.status] || 0;
|
|
295
|
+
const sx = sprite.frame * FRAME_SIZE;
|
|
296
|
+
const sy = row * FRAME_SIZE;
|
|
297
|
+
|
|
298
|
+
ctx.drawImage(
|
|
299
|
+
sprite.sheet,
|
|
300
|
+
sx, sy, FRAME_SIZE, FRAME_SIZE,
|
|
301
|
+
sprite.x - FRAME_SIZE / 2, sprite.y - FRAME_SIZE / 2,
|
|
302
|
+
FRAME_SIZE, FRAME_SIZE
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Status dot
|
|
306
|
+
const dotColor = STATUS_DOT_COLORS[sprite.status] || '#9E9E9E';
|
|
307
|
+
ctx.fillStyle = dotColor;
|
|
308
|
+
ctx.beginPath();
|
|
309
|
+
ctx.arc(sprite.x, sprite.y - FRAME_SIZE / 2 - 4, 3, 0, Math.PI * 2);
|
|
310
|
+
ctx.fill();
|
|
311
|
+
|
|
312
|
+
// Name label
|
|
313
|
+
ctx.fillStyle = '#E0E0E0';
|
|
314
|
+
ctx.font = '8px monospace';
|
|
315
|
+
ctx.textAlign = 'center';
|
|
316
|
+
const shortName = name.length > 12 ? name.substring(0, 11) + '\u2026' : name;
|
|
317
|
+
ctx.fillText(shortName, sprite.x, sprite.y + FRAME_SIZE / 2 + 8);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
window.AgentSprites = {
|
|
322
|
+
AGENT_DEFS,
|
|
323
|
+
agentSprites,
|
|
324
|
+
initAgentSprites,
|
|
325
|
+
updateAgentState,
|
|
326
|
+
updateAgentSprites,
|
|
327
|
+
drawAgentSprites,
|
|
328
|
+
};
|
|
329
|
+
})();
|