v2.2.14: Advanced Pixel Office Customization & Face Directions
This commit is contained in:
@@ -3,15 +3,15 @@
|
|||||||
<!-- ASTRA:AUTO-START -->
|
<!-- ASTRA:AUTO-START -->
|
||||||
|
|
||||||
## Snapshot
|
## Snapshot
|
||||||
- **Workspace**: `ConnectAI` `v2.2.12` _(absolute path varies by environment; resolved from the active VS Code workspace)_
|
- **Workspace**: `ConnectAI` `v2.2.13` _(absolute path varies by environment; resolved from the active VS Code workspace)_
|
||||||
- **Description**: The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.
|
- **Description**: The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.
|
||||||
- **Stack**: TypeScript, Node.js, VS Code Extension, LM Studio SDK, Test runner
|
- **Stack**: TypeScript, Node.js, VS Code Extension, LM Studio SDK, Test runner
|
||||||
- **Stats**: 224 source files, ~45,925 lines across 5 top-level modules.
|
- **Stats**: 224 source files, ~46,311 lines across 5 top-level modules.
|
||||||
|
|
||||||
## Last Refresh
|
## Last Refresh
|
||||||
- **Time**: 2026-05-16T03:37:49.405Z
|
- **Time**: 2026-05-16T04:18:55.379Z
|
||||||
- **Files newly analysed**: 0
|
- **Files newly analysed**: 2
|
||||||
- **Files reused from cache**: 224
|
- **Files reused from cache**: 222
|
||||||
|
|
||||||
## Directory Map
|
## Directory Map
|
||||||
```mermaid
|
```mermaid
|
||||||
@@ -64,7 +64,7 @@ flowchart LR
|
|||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
### `src/` — 110 files, ~30,926 lines
|
### `src/` — 110 files, ~31,312 lines
|
||||||
|
|
||||||
**Sub-directories**
|
**Sub-directories**
|
||||||
- `src/features/` (37) — 기본 에이전트 로스터 — 1인 기업 모드의 출고 디폴트. 설계 의도: 소프트웨어/게임 개발 IT 회사의 1인 기업 운영을 가정. 한 사람이 기획 → 디자인 → 개발 → QA → 출시 → 운영/마케팅을 모두 책임질 때
|
- `src/features/` (37) — 기본 에이전트 로스터 — 1인 기업 모드의 출고 디폴트. 설계 의도: 소프트웨어/게임 개발 IT 회사의 1인 기업 운영을 가정. 한 사람이 기획 → 디자인 → 개발 → QA → 출시 → 운영/마케팅을 모두 책임질 때
|
||||||
@@ -87,7 +87,7 @@ flowchart LR
|
|||||||
- `src/core/services.ts` (164 lines)
|
- `src/core/services.ts` (164 lines)
|
||||||
- `src/lib/paths.ts` (151 lines)
|
- `src/lib/paths.ts` (151 lines)
|
||||||
- `src/features/company/companyConfig.ts` (896 lines) — State + config plumbing for 1인 기업 모드. Two surfaces: - CompanyState (runtime data: enabled flag, company name, which agents are active, per-agent model overrides). Persisted in VS Code's globalState so
|
- `src/features/company/companyConfig.ts` (896 lines) — State + config plumbing for 1인 기업 모드. Two surfaces: - CompanyState (runtime data: enabled flag, company name, which agents are active, per-agent model overrides). Persisted in VS Code's globalState so
|
||||||
- `src/sidebarProvider.ts` (5119 lines)
|
- `src/sidebarProvider.ts` (5505 lines)
|
||||||
- `src/memory/types.ts` (126 lines) — Memory Type Definitions (메모리 타입 정의) Astra의 5-Layer Cognitive Memory System의 모든 타입을 정의합니다. ① Short-Term ② Long-Term ③ Project ④ Procedural ⑤ Episodic
|
- `src/memory/types.ts` (126 lines) — Memory Type Definitions (메모리 타입 정의) Astra의 5-Layer Cognitive Memory System의 모든 타입을 정의합니다. ① Short-Term ② Long-Term ③ Project ④ Procedural ⑤ Episodic
|
||||||
- `src/retrieval/scoring.ts` (518 lines) — Scoring Engine — TF-IDF + Bilingual Tokenizer 단순 includes() 키워드 매칭을 넘어서, TF-IDF 가중치 기반의 문서 스코어링을 제공합니다. 한국어/영어 양국어 토크나이저를 포함합니다.
|
- `src/retrieval/scoring.ts` (518 lines) — Scoring Engine — TF-IDF + Bilingual Tokenizer 단순 includes() 키워드 매칭을 넘어서, TF-IDF 가중치 기반의 문서 스코어링을 제공합니다. 한국어/영어 양국어 토크나이저를 포함합니다.
|
||||||
- `src/skills/agentKnowledgeMap.ts` (374 lines)
|
- `src/skills/agentKnowledgeMap.ts` (374 lines)
|
||||||
@@ -319,7 +319,7 @@ Astra는 대표님의 명시적인 승인 하에 로컬 시스템의 강력한
|
|||||||
**Designed for High-Performance Decision Making.**
|
**Designed for High-Performance Decision Making.**
|
||||||
Copyright (C) **g1nation**. All rights reserved.
|
Copyright (C) **g1nation**. All rights reserved.
|
||||||
|
|
||||||
_Last auto-scan: 2026-05-16T03:37:49.405Z · signature `2e3db319`_
|
_Last auto-scan: 2026-05-16T04:18:55.379Z · signature `b1025fa`_
|
||||||
<!-- ASTRA:AUTO-END -->
|
<!-- ASTRA:AUTO-END -->
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"generatedAt": "2026-05-16T03:37:49.410Z",
|
"generatedAt": "2026-05-16T04:18:55.389Z",
|
||||||
"files": {
|
"files": {
|
||||||
"src/agent.ts": {
|
"src/agent.ts": {
|
||||||
"mtimeMs": 1778902489000,
|
"mtimeMs": 1778902489000,
|
||||||
@@ -1030,9 +1030,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/sidebarProvider.ts": {
|
"src/sidebarProvider.ts": {
|
||||||
"mtimeMs": 1778902489000,
|
"mtimeMs": 1778904191000,
|
||||||
"size": 228676,
|
"size": 245949,
|
||||||
"lines": 5119,
|
"lines": 5505,
|
||||||
"role": "",
|
"role": "",
|
||||||
"imports": [
|
"imports": [
|
||||||
"src/utils",
|
"src/utils",
|
||||||
@@ -1638,8 +1638,8 @@
|
|||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
"docs/records/ConnectAI/chronicle.config.json": {
|
"docs/records/ConnectAI/chronicle.config.json": {
|
||||||
"mtimeMs": 1778902507000,
|
"mtimeMs": 1778902789000,
|
||||||
"size": 371,
|
"size": 416,
|
||||||
"lines": 11,
|
"lines": 11,
|
||||||
"role": "JSON configuration",
|
"role": "JSON configuration",
|
||||||
"imports": []
|
"imports": []
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
|
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
|
||||||
"createdAt": 1778902731275,
|
"createdAt": 1778905142810,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
||||||
"createdAt": 1778902731267,
|
"createdAt": 1778905142809,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
||||||
"createdAt": 1778902731263,
|
"createdAt": 1778905142808,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "---\nid: stress_conflict_1778902731247\ndate: 2026-05-16T03:38:51.279Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (11ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (5ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (8ms)\n",
|
"result": "---\nid: stress_conflict_1778905142797\ndate: 2026-05-16T04:19:02.810Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (11ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (1ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (1ms)\n",
|
||||||
"createdAt": 1778902731279,
|
"createdAt": 1778905142810,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
+10
-10
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"missionId": "stress_conflict_1778902731247",
|
"missionId": "stress_conflict_1778905142797",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"startTime": "2026-05-16T03:38:51.247Z",
|
"startTime": "2026-05-16T04:19:02.797Z",
|
||||||
"totalElapsedMs": 33,
|
"totalElapsedMs": 13,
|
||||||
"results": {
|
"results": {
|
||||||
"planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
"planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
||||||
"researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
"researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
||||||
@@ -18,28 +18,28 @@
|
|||||||
"to": "planner",
|
"to": "planner",
|
||||||
"durationMs": 11,
|
"durationMs": 11,
|
||||||
"message": "전략 수립 중...",
|
"message": "전략 수립 중...",
|
||||||
"ts": "2026-05-16T03:38:51.258Z"
|
"ts": "2026-05-16T04:19:02.808Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "planner",
|
"from": "planner",
|
||||||
"to": "researcher",
|
"to": "researcher",
|
||||||
"durationMs": 5,
|
"durationMs": 1,
|
||||||
"message": "핵심 정보 수집 및 분석 중...",
|
"message": "핵심 정보 수집 및 분석 중...",
|
||||||
"ts": "2026-05-16T03:38:51.263Z"
|
"ts": "2026-05-16T04:19:02.809Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "researcher",
|
"from": "researcher",
|
||||||
"to": "writer",
|
"to": "writer",
|
||||||
"durationMs": 8,
|
"durationMs": 1,
|
||||||
"message": "최종 리포트 작성 및 편집 중...",
|
"message": "최종 리포트 작성 및 편집 중...",
|
||||||
"ts": "2026-05-16T03:38:51.271Z"
|
"ts": "2026-05-16T04:19:02.810Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "writer",
|
"from": "writer",
|
||||||
"to": "completed",
|
"to": "completed",
|
||||||
"durationMs": 9,
|
"durationMs": 0,
|
||||||
"message": "미션 완료",
|
"message": "미션 완료",
|
||||||
"ts": "2026-05-16T03:38:51.280Z"
|
"ts": "2026-05-16T04:19:02.810Z"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"resilienceMetrics": {
|
"resilienceMetrics": {
|
||||||
@@ -1,5 +1,17 @@
|
|||||||
# Astra Patch Notes
|
# Astra Patch Notes
|
||||||
|
|
||||||
|
## v2.2.14 (2026-05-16)
|
||||||
|
### 🎭 Advanced Pixel Office Customization & Face Directions
|
||||||
|
- **캐릭터 방향성 고도화:** 기존 좌우(Left/Right) 방향에 더해 상하(Up/Down) 방향 스프라이트 지원을 추가하여 더욱 다채로운 사무실 연출이 가능해졌습니다.
|
||||||
|
- **캐릭터-책상 독립 제어:** 책상은 유지하면서 캐릭터만 삭제하거나, 캐릭터가 없는 책상에 새 팀원을 다시 추가할 수 있는 기능을 도입했습니다.
|
||||||
|
- **레이아웃 스키마 V2 도입:** 캐릭터 존재 여부 및 정밀한 위치/회전 정보를 보존하는 최신 레이아웃 스냅샷 엔진을 적용했습니다.
|
||||||
|
- **UI/UX 편의성 개선:** 속성 패널에서 방향 전환과 캐릭터 추가/삭제가 즉시 반영되도록 인터랙션을 강화했습니다.
|
||||||
|
- **신규 패키징:** `astra-2.2.14.vsix` 패키지를 통해 더욱 유연해진 사무실 모델링 환경을 제공합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## v2.2.13 (2026-05-16)
|
## v2.2.13 (2026-05-16)
|
||||||
### 🏗️ Pixel Office Interactive Editor & Core Refinement
|
### 🏗️ Pixel Office Interactive Editor & Core Refinement
|
||||||
- **픽셀 오피스 편집 모드 도입:** 책상 추가/삭제, 에이전트 매핑 변경, 가구(프랍) 배치 및 크기 조절이 가능한 인터랙티브 편집 기능을 추가했습니다.
|
- **픽셀 오피스 편집 모드 도입:** 책상 추가/삭제, 에이전트 매핑 변경, 가구(프랍) 배치 및 크기 조절이 가능한 인터랙티브 편집 기능을 추가했습니다.
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
"corePurpose": "",
|
"corePurpose": "",
|
||||||
"detailLevel": "standard",
|
"detailLevel": "standard",
|
||||||
"createdAt": "2026-05-13T13:09:33.788Z",
|
"createdAt": "2026-05-13T13:09:33.788Z",
|
||||||
"updatedAt": "2026-05-16T03:39:49.319Z"
|
"updatedAt": "2026-05-16T04:20:09.223Z"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "astra",
|
"name": "astra",
|
||||||
"displayName": "Astra",
|
"displayName": "Astra",
|
||||||
"description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.",
|
"description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.",
|
||||||
"version": "2.2.13",
|
"version": "2.2.14",
|
||||||
"publisher": "g1nation",
|
"publisher": "g1nation",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
|
|||||||
+111
-44
@@ -4091,12 +4091,17 @@ function addChar(st){
|
|||||||
chars[st.key]=ch;
|
chars[st.key]=ch;
|
||||||
anim[st.key]={row:st.charRow,frame:0,dir:0,mode:'sit',face:st.face,route:0};
|
anim[st.key]={row:st.charRow,frame:0,dir:0,mode:'sit',face:st.face,route:0};
|
||||||
const img=ch.querySelector('img');
|
const img=ch.querySelector('img');
|
||||||
img.src=png('idle-r'+st.charRow+'-f0');
|
if(st.face === 'U' || st.face === 'D'){
|
||||||
img.style.transform=st.face==='R'?'scaleX(-1)':'none';
|
img.src=png('walk-r'+st.charRow+'-d'+_faceToWalkDir(st.face)+'-f0');
|
||||||
|
img.style.transform='none';
|
||||||
|
} else {
|
||||||
|
img.src=png('idle-r'+st.charRow+'-f0');
|
||||||
|
img.style.transform=st.face==='R'?'scaleX(-1)':'none';
|
||||||
|
}
|
||||||
return ch;
|
return ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildStation(st){ addDesk(st); addChar(st); }
|
function buildStation(st){ addDesk(st); if(!st.noChar) addChar(st); }
|
||||||
|
|
||||||
function _renderDefaultStations(){
|
function _renderDefaultStations(){
|
||||||
// default 8 stations + default props.
|
// default 8 stations + default props.
|
||||||
@@ -4114,8 +4119,17 @@ const DIR_DOWN = 0; // 정면 (카메라/사용자 쪽)
|
|||||||
const DIR_LEFT = 1;
|
const DIR_LEFT = 1;
|
||||||
const DIR_RIGHT = 2;
|
const DIR_RIGHT = 2;
|
||||||
const DIR_UP = 3; // 뒤
|
const DIR_UP = 3; // 뒤
|
||||||
|
// idle/work 의 정면(D) / 후면(U) sprite 는 별도로 없으므로 walk 의 같은 방향
|
||||||
|
// frame 0 (정지 포즈) 를 그대로 빌려쓴다.
|
||||||
|
function _faceToWalkDir(face){
|
||||||
|
if(face === 'D') return DIR_DOWN;
|
||||||
|
if(face === 'U') return DIR_UP;
|
||||||
|
if(face === 'L') return DIR_LEFT;
|
||||||
|
return DIR_RIGHT;
|
||||||
|
}
|
||||||
function setSprite(role,mode,frame=0,dir=0){
|
function setSprite(role,mode,frame=0,dir=0){
|
||||||
const ch=chars[role],a=anim[role];
|
const ch=chars[role]; if(!ch) return;
|
||||||
|
const a=anim[role]; if(!a) return;
|
||||||
a.mode=mode;a.frame=frame;a.dir=dir;
|
a.mode=mode;a.frame=frame;a.dir=dir;
|
||||||
ch.classList.toggle('walking',mode==='walk');
|
ch.classList.toggle('walking',mode==='walk');
|
||||||
const img=ch.querySelector('img');
|
const img=ch.querySelector('img');
|
||||||
@@ -4123,6 +4137,10 @@ function setSprite(role,mode,frame=0,dir=0){
|
|||||||
// 4방향 walk sprite — 좌우 sprite를 따로 제공하므로 scaleX 반전은 *안* 한다.
|
// 4방향 walk sprite — 좌우 sprite를 따로 제공하므로 scaleX 반전은 *안* 한다.
|
||||||
img.src=png('walk-r'+a.row+'-d'+dir+'-f'+frame);
|
img.src=png('walk-r'+a.row+'-d'+dir+'-f'+frame);
|
||||||
img.style.transform='none';
|
img.style.transform='none';
|
||||||
|
} else if(a.face === 'U' || a.face === 'D'){
|
||||||
|
// 위/아래 face 는 idle/work sprite 가 없으므로 walk 의 같은 방향 정지 포즈로.
|
||||||
|
img.src=png('walk-r'+a.row+'-d'+_faceToWalkDir(a.face)+'-f0');
|
||||||
|
img.style.transform='none';
|
||||||
} else if(mode==='work'){
|
} else if(mode==='work'){
|
||||||
img.src=png('work-r'+a.row+'-f'+frame);
|
img.src=png('work-r'+a.row+'-f'+frame);
|
||||||
img.style.transform=(a.face==='R'?'scaleX(-1)':'none');
|
img.style.transform=(a.face==='R'?'scaleX(-1)':'none');
|
||||||
@@ -4132,8 +4150,9 @@ function setSprite(role,mode,frame=0,dir=0){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function move(role,x,y){
|
function move(role,x,y){
|
||||||
const ch=chars[role],a=anim[role],
|
const ch=chars[role]; if(!ch) return; // 캐릭터 삭제된 자리면 무시.
|
||||||
cx=parseFloat(ch.style.left),cy=parseFloat(ch.style.top),
|
const a=anim[role]; if(!a) return;
|
||||||
|
const cx=parseFloat(ch.style.left),cy=parseFloat(ch.style.top),
|
||||||
dx=x-cx,dy=y-cy;
|
dx=x-cx,dy=y-cy;
|
||||||
// 주축 결정: 큰 쪽을 우선해 4방향 중 하나로 매핑. 동률이면 가로 우선.
|
// 주축 결정: 큰 쪽을 우선해 4방향 중 하나로 매핑. 동률이면 가로 우선.
|
||||||
let dir;
|
let dir;
|
||||||
@@ -4232,8 +4251,22 @@ function walkPath(role,points,done,route){
|
|||||||
const tail=planned.slice(1).concat(rest);
|
const tail=planned.slice(1).concat(rest);
|
||||||
setTimeout(()=>walkPath(role,tail,done,token),950);
|
setTimeout(()=>walkPath(role,tail,done,token),950);
|
||||||
}
|
}
|
||||||
function sendHome(role,mode='sit'){const st=stationByKey[role],ch=chars[role],hx=st.seatX,hy=st.seatY,cx=parseFloat(ch.style.left),cy=parseFloat(ch.style.top);anim[role].route++;if(Math.abs(cx-hx)<1&&Math.abs(cy-hy)<1){setSprite(role,mode);return;}walkPath(role,[st.dock,[hx,hy]],()=>setSprite(role,mode))}
|
function sendHome(role,mode='sit'){
|
||||||
setInterval(()=>{const free=Object.keys(chars).filter(k=>anim[k].mode==='sit'&&!chars[k].classList.contains('active'));if(!free.length)return;const k=free[Math.floor(Math.random()*free.length)],st=stationByKey[k],pt=st.roam[Math.floor(Math.random()*st.roam.length)];walkPath(k,[st.dock,pt,st.dock,[st.seatX,st.seatY]],()=>setSprite(k,'sit'))},5600)
|
const st=stationByKey[role],ch=chars[role];
|
||||||
|
if(!st || !ch) return; // 책상/캐릭터 부재 시 no-op.
|
||||||
|
const hx=st.seatX,hy=st.seatY,cx=parseFloat(ch.style.left),cy=parseFloat(ch.style.top);
|
||||||
|
anim[role].route++;
|
||||||
|
if(Math.abs(cx-hx)<1&&Math.abs(cy-hy)<1){setSprite(role,mode);return;}
|
||||||
|
walkPath(role,[st.dock,[hx,hy]],()=>setSprite(role,mode));
|
||||||
|
}
|
||||||
|
setInterval(()=>{
|
||||||
|
const free=Object.keys(chars).filter(k=>anim[k]?.mode==='sit'&&!chars[k].classList.contains('active'));
|
||||||
|
if(!free.length)return;
|
||||||
|
const k=free[Math.floor(Math.random()*free.length)],st=stationByKey[k];
|
||||||
|
if(!st || !Array.isArray(st.roam) || !st.roam.length || !Array.isArray(st.dock)) return;
|
||||||
|
const pt=st.roam[Math.floor(Math.random()*st.roam.length)];
|
||||||
|
walkPath(k,[st.dock,pt,st.dock,[st.seatX,st.seatY]],()=>setSprite(k,'sit'));
|
||||||
|
},5600);
|
||||||
function activate(role){Object.keys(chars).forEach(k=>chars[k].classList.toggle('active',k===role))}
|
function activate(role){Object.keys(chars).forEach(k=>chars[k].classList.toggle('active',k===role))}
|
||||||
function bubble(role,text){const ch=chars[role];if(!ch||!text)return;const b=document.createElement('div');b.className='bubble';b.textContent=text;b.style.left=(parseFloat(ch.style.left)+28)+'px';b.style.top=(parseFloat(ch.style.top)-6)+'px';stage.appendChild(b);setTimeout(()=>b.remove(),1600)}
|
function bubble(role,text){const ch=chars[role];if(!ch||!text)return;const b=document.createElement('div');b.className='bubble';b.textContent=text;b.style.left=(parseFloat(ch.style.left)+28)+'px';b.style.top=(parseFloat(ch.style.top)-6)+'px';stage.appendChild(b);setTimeout(()=>b.remove(),1600)}
|
||||||
// ── A. 상태 계층화 ──
|
// ── A. 상태 계층화 ──
|
||||||
@@ -4491,26 +4524,30 @@ function _snapshotLayout(){
|
|||||||
// _restoreLayout 이 양쪽 모두 처리.
|
// _restoreLayout 이 양쪽 모두 처리.
|
||||||
return {
|
return {
|
||||||
schema: 2,
|
schema: 2,
|
||||||
cells: stations.map(st=>({
|
cells: stations.map(st=>{
|
||||||
roleKey: st.key,
|
const ch = chars[st.key];
|
||||||
agentKey: st.agentKey || '',
|
return {
|
||||||
label: st.label || '',
|
roleKey: st.key,
|
||||||
charRow: st.charRow ?? 0,
|
agentKey: st.agentKey || '',
|
||||||
deskSprite: st.deskSprite || 'desk-main',
|
label: st.label || '',
|
||||||
face: st.face || 'R',
|
charRow: st.charRow ?? 0,
|
||||||
boss: !!st.boss,
|
deskSprite: st.deskSprite || 'desk-main',
|
||||||
dock: st.dock,
|
face: st.face || 'R',
|
||||||
roam: st.roam,
|
boss: !!st.boss,
|
||||||
deskX: parseFloat(__deskWrap[st.key].style.left),
|
noChar: !!st.noChar,
|
||||||
deskY: parseFloat(__deskWrap[st.key].style.top),
|
dock: st.dock,
|
||||||
deskW: parseFloat(__deskWrap[st.key].style.width),
|
roam: st.roam,
|
||||||
deskRot: parseFloat(__deskWrap[st.key].dataset.rot || '0'),
|
deskX: parseFloat(__deskWrap[st.key].style.left),
|
||||||
deskZ: parseFloat(__deskWrap[st.key].dataset.z || '0'),
|
deskY: parseFloat(__deskWrap[st.key].style.top),
|
||||||
seatX: parseFloat(chars[st.key].style.left),
|
deskW: parseFloat(__deskWrap[st.key].style.width),
|
||||||
seatY: parseFloat(chars[st.key].style.top),
|
deskRot: parseFloat(__deskWrap[st.key].dataset.rot || '0'),
|
||||||
charRot: parseFloat(chars[st.key].dataset.rot || '0'),
|
deskZ: parseFloat(__deskWrap[st.key].dataset.z || '0'),
|
||||||
charZ: parseFloat(chars[st.key].dataset.z || '0'),
|
seatX: ch ? parseFloat(ch.style.left) : (st.seatX ?? 0),
|
||||||
})),
|
seatY: ch ? parseFloat(ch.style.top) : (st.seatY ?? 0),
|
||||||
|
charRot: ch ? parseFloat(ch.dataset.rot || '0') : 0,
|
||||||
|
charZ: ch ? parseFloat(ch.dataset.z || '0') : 0,
|
||||||
|
};
|
||||||
|
}),
|
||||||
objs: Array.from(stage.querySelectorAll('img.obj')).map(el=>({
|
objs: Array.from(stage.querySelectorAll('img.obj')).map(el=>({
|
||||||
id: el.dataset.objId,
|
id: el.dataset.objId,
|
||||||
name: el.dataset.objName,
|
name: el.dataset.objName,
|
||||||
@@ -4564,6 +4601,7 @@ function _restoreLayout(snap){
|
|||||||
deskSprite: c.deskSprite || 'desk-main',
|
deskSprite: c.deskSprite || 'desk-main',
|
||||||
face: c.face || 'R',
|
face: c.face || 'R',
|
||||||
boss: !!c.boss,
|
boss: !!c.boss,
|
||||||
|
noChar: !!c.noChar,
|
||||||
dock: Array.isArray(c.dock) ? c.dock : [c.deskX+32, c.deskY+80],
|
dock: Array.isArray(c.dock) ? c.dock : [c.deskX+32, c.deskY+80],
|
||||||
roam: Array.isArray(c.roam) ? c.roam : [[c.deskX, c.deskY+120],[c.deskX+60, c.deskY+100]],
|
roam: Array.isArray(c.roam) ? c.roam : [[c.deskX, c.deskY+120],[c.deskX+60, c.deskY+100]],
|
||||||
deskX: c.deskX, deskY: c.deskY, deskW: c.deskW || 112,
|
deskX: c.deskX, deskY: c.deskY, deskW: c.deskW || 112,
|
||||||
@@ -4684,13 +4722,20 @@ function _renderDeskProps(deskEl){
|
|||||||
for(let r=0;r<8;r++){
|
for(let r=0;r<8;r++){
|
||||||
thumbs += '<div class="pp-thumb'+(r===st.charRow?' active':'')+'" data-charrow="'+r+'" title="row '+r+'"><img src="'+png('idle-r'+r+'-f0')+'"></div>';
|
thumbs += '<div class="pp-thumb'+(r===st.charRow?' active':'')+'" data-charrow="'+r+'" title="row '+r+'"><img src="'+png('idle-r'+r+'-f0')+'"></div>';
|
||||||
}
|
}
|
||||||
|
const hasChar = !!chars[role];
|
||||||
panel.innerHTML =
|
panel.innerHTML =
|
||||||
'<h4>책상 속성</h4>'+
|
'<h4>책상 속성</h4>'+
|
||||||
'<div class="pp-row"><label>라벨</label><input id="ppLabel" value="'+(st.label||'').replace(/"/g,'"')+'"></div>'+
|
'<div class="pp-row"><label>라벨</label><input id="ppLabel" value="'+(st.label||'').replace(/"/g,'"')+'"></div>'+
|
||||||
'<div class="pp-row"><label>에이전트 매핑</label><select id="ppAgent">'+agentOpts+'</select></div>'+
|
'<div class="pp-row"><label>에이전트 매핑</label><select id="ppAgent">'+agentOpts+'</select></div>'+
|
||||||
'<div class="pp-row"><label>책상 sprite</label><select id="ppDesk">'+deskOpts+'</select></div>'+
|
'<div class="pp-row"><label>책상 sprite</label><select id="ppDesk">'+deskOpts+'</select></div>'+
|
||||||
|
(hasChar ? '' : '<div class="pp-row"><button id="ppAddChar" style="width:100%;padding:6px;background:rgba(16,185,129,.22);border:1px solid rgba(16,185,129,.55);color:#F1F4FB;border-radius:4px;cursor:pointer;font-size:11px">+ 이 책상에 캐릭터 추가</button></div>')+
|
||||||
'<div class="pp-row"><label>착석 캐릭터 (row 0~7)</label><div class="pp-thumbs" id="ppThumbs">'+thumbs+'</div></div>'+
|
'<div class="pp-row"><label>착석 캐릭터 (row 0~7)</label><div class="pp-thumbs" id="ppThumbs">'+thumbs+'</div></div>'+
|
||||||
'<div class="pp-row"><label>방향 (앉은 face)</label><select id="ppFace"><option value="L"'+(st.face==='L'?' selected':'')+'>← Left</option><option value="R"'+(st.face==='R'?' selected':'')+'>Right →</option></select></div>';
|
'<div class="pp-row"><label>방향 (앉은 face)</label><select id="ppFace">'+
|
||||||
|
'<option value="L"'+(st.face==='L'?' selected':'')+'>← Left</option>'+
|
||||||
|
'<option value="R"'+(st.face==='R'?' selected':'')+'>Right →</option>'+
|
||||||
|
'<option value="U"'+(st.face==='U'?' selected':'')+'>↑ Up (뒷모습)</option>'+
|
||||||
|
'<option value="D"'+(st.face==='D'?' selected':'')+'>↓ Down (정면)</option>'+
|
||||||
|
'</select></div>';
|
||||||
// 핸들러
|
// 핸들러
|
||||||
panel.querySelector('#ppLabel').oninput = (ev)=>{
|
panel.querySelector('#ppLabel').oninput = (ev)=>{
|
||||||
st.label = ev.target.value;
|
st.label = ev.target.value;
|
||||||
@@ -4718,8 +4763,17 @@ function _renderDeskProps(deskEl){
|
|||||||
panel.querySelector('#ppFace').onchange = (ev)=>{
|
panel.querySelector('#ppFace').onchange = (ev)=>{
|
||||||
st.face = ev.target.value;
|
st.face = ev.target.value;
|
||||||
if(anim[role]) anim[role].face = st.face;
|
if(anim[role]) anim[role].face = st.face;
|
||||||
const ch = chars[role]; if(ch){ const img=ch.querySelector('img'); if(img) img.style.transform = st.face==='R'?'scaleX(-1)':'none'; }
|
// 현재 mode 기준 sprite 즉시 다시 그리기 — U/D 도 한 번에 반영.
|
||||||
|
const a = anim[role]; if(a) setSprite(role, a.mode || 'sit', 0, 0);
|
||||||
};
|
};
|
||||||
|
const addCharBtn = panel.querySelector('#ppAddChar');
|
||||||
|
if(addCharBtn){
|
||||||
|
addCharBtn.onclick = ()=>{
|
||||||
|
st.noChar = false;
|
||||||
|
addChar(st);
|
||||||
|
_renderDeskProps(deskEl); // 패널 재렌더 — "캐릭터 추가" 버튼 제거.
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _renderObjProps(el){
|
function _renderObjProps(el){
|
||||||
@@ -4795,30 +4849,43 @@ function _openPropPicker(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── 선택 항목 삭제 ──
|
// ── 선택 항목 삭제 ──
|
||||||
|
// 캐릭터 / 책상 / 프랍 각각 분리해서 처리:
|
||||||
|
// · 캐릭터 선택 → 캐릭터만 삭제 (책상은 유지). 속성 패널에서 "+ 캐릭터" 로 재추가 가능.
|
||||||
|
// · 책상 선택 → 책상 + 캐릭터 모두 삭제 (station 자체 제거).
|
||||||
|
// · 프랍 선택 → 프랍 삭제.
|
||||||
function _deleteSelected(){
|
function _deleteSelected(){
|
||||||
if(!_editMode || !_selected) return;
|
if(!_editMode || !_selected) return;
|
||||||
// char 가 선택돼 있으면 그 desk 도 함께 묶어서 처리.
|
if(_selected.classList.contains('char')){
|
||||||
let target = _selected;
|
const role = Object.keys(chars).find(k=>chars[k]===_selected);
|
||||||
if(target.classList.contains('char')){
|
if(!role) return;
|
||||||
const role = Object.keys(chars).find(k=>chars[k]===target);
|
const st = stationByKey[role];
|
||||||
if(role && __deskWrap[role]) target = __deskWrap[role];
|
if(!confirm('"'+(st?.label||role)+'" 책상의 캐릭터를 삭제할까요? 책상은 그대로 남습니다.')) return;
|
||||||
|
_selected.remove();
|
||||||
|
delete chars[role]; delete anim[role];
|
||||||
|
if(st) st.noChar = true;
|
||||||
|
_selected = null;
|
||||||
|
_onSelectionChanged();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if(target.classList.contains('desk')){
|
if(_selected.classList.contains('desk')){
|
||||||
const role = target.dataset.role;
|
const role = _selected.dataset.role;
|
||||||
if(!confirm('"'+(stationByKey[role]?.label||role)+'" 책상을 삭제할까요? 이 자리에 매핑된 에이전트도 자리가 사라집니다.')) return;
|
if(!confirm('"'+(stationByKey[role]?.label||role)+'" 책상을 삭제할까요? 캐릭터도 함께 사라집니다.')) return;
|
||||||
target.remove();
|
_selected.remove();
|
||||||
if(chars[role]) chars[role].remove();
|
if(chars[role]) chars[role].remove();
|
||||||
delete __deskWrap[role]; delete chars[role]; delete anim[role];
|
delete __deskWrap[role]; delete chars[role]; delete anim[role];
|
||||||
const idx = stations.findIndex(s=>s.key===role);
|
const idx = stations.findIndex(s=>s.key===role);
|
||||||
if(idx>=0) stations.splice(idx,1);
|
if(idx>=0) stations.splice(idx,1);
|
||||||
_rebuildStationIndex();
|
_rebuildStationIndex();
|
||||||
} else if(target.classList.contains('obj')){
|
_selected = null;
|
||||||
target.remove();
|
_onSelectionChanged();
|
||||||
} else {
|
return;
|
||||||
|
}
|
||||||
|
if(_selected.classList.contains('obj')){
|
||||||
|
_selected.remove();
|
||||||
|
_selected = null;
|
||||||
|
_onSelectionChanged();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_selected = null;
|
|
||||||
_onSelectionChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _findDraggable(el){
|
function _findDraggable(el){
|
||||||
|
|||||||
Reference in New Issue
Block a user