v2.2.13: Pixel Office Interactive Editor & Core Refinement

This commit is contained in:
g1nation
2026-05-16 13:18:49 +09:00
parent dea106ce68
commit c4f01fd6af
11 changed files with 1030 additions and 655 deletions
+408 -44
View File
@@ -3873,8 +3873,6 @@ header{display:flex;justify-content:space-between;align-items:center;padding:10p
.office:before{content:'';position:absolute;left:0;right:0;top:16%;bottom:0;background-image:linear-gradient(rgba(255,255,255,.028) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.028) 1px,transparent 1px);background-size:48px 48px}
.office:after{content:'';position:absolute;left:0;right:0;top:15.5%;height:8px;background:linear-gradient(180deg,rgba(0,0,0,.36),transparent)}
.stage{position:relative;width:720px;height:585px;margin:0}
.zone{position:absolute;border:1px solid rgba(255,255,255,.05);background:rgba(255,255,255,.018);box-shadow:inset 0 1px 0 rgba(255,255,255,.035);border-radius:10px}
.zone.exec{left:220px;top:18px;width:280px;height:150px;background:rgba(124,131,255,.06)}.zone.core{left:28px;top:175px;width:520px;height:170px}.zone.ops{left:28px;top:355px;width:520px;height:170px}.zone.lounge{left:560px;top:355px;width:132px;height:170px;background:rgba(255,189,89,.045)}
.wall-window{position:absolute;top:16px;width:86px;height:42px;border:3px solid rgba(206,223,255,.35);background:linear-gradient(180deg,rgba(160,208,255,.3),rgba(110,150,210,.1));box-shadow:inset 0 0 0 2px rgba(15,20,31,.55)}
.wall-window.w1{left:84px}.wall-window.w2{left:550px}
.obj,.desk,.char{position:absolute;image-rendering:pixelated}
@@ -3885,26 +3883,26 @@ header{display:flex;justify-content:space-between;align-items:center;padding:10p
@keyframes po-pulse{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(1.5);opacity:.6}}
/* ── C. 직군별 페르소나 컬러 ── 책상 outline 가벼운 강조, 활성 캐릭터 위 점이 직군색.
data-role attribute로 자동 매핑. 사용자가 PNG sprite로 swap해도 컬러는 유지. */
.char[data-role="ceo"],.desk[data-role="ceo"] {--role-color:#A78BFA}
.char[data-role="planner"],.desk[data-role="planner"] {--role-color:#60A5FA}
.char[data-role="researcher"],.desk[data-role="researcher"] {--role-color:#10B981}
.char[data-role="designer"],.desk[data-role="designer"] {--role-color:#F472B6}
.char[data-role="developer"],.desk[data-role="developer"] {--role-color:#FBBF24}
.char[data-role="qa"],.desk[data-role="qa"] {--role-color:#22D3EE}
.char[data-role="inspector"],.desk[data-role="inspector"] {--role-color:#FB923C}
.char[data-role="support"],.desk[data-role="support"] {--role-color:#94A3B8}
.char[data-agent="ceo"],.desk[data-agent="ceo"] {--role-color:#A78BFA}
.char[data-agent="planner"],.desk[data-agent="planner"] {--role-color:#60A5FA}
.char[data-agent="researcher"],.desk[data-agent="researcher"] {--role-color:#10B981}
.char[data-agent="designer"],.desk[data-agent="designer"] {--role-color:#F472B6}
.char[data-agent="developer"],.desk[data-agent="developer"] {--role-color:#FBBF24}
.char[data-agent="qa"],.desk[data-agent="qa"] {--role-color:#22D3EE}
.char[data-agent="inspector"],.desk[data-agent="inspector"] {--role-color:#FB923C}
.char[data-agent="support"],.desk[data-agent="support"] {--role-color:#94A3B8}
.char.active::after{content:'';position:absolute;left:0;right:0;bottom:-4px;height:3px;background:var(--role-color,var(--accent));box-shadow:0 0 8px var(--role-color,var(--accent));border-radius:2px;animation:po-glow 1.6s ease-in-out infinite}
@keyframes po-glow{0%,100%{opacity:.7}50%{opacity:1}}
.desk{position:relative}
.desk::after{content:'';position:absolute;inset:-2px;border-radius:4px;border:2px solid transparent;pointer-events:none;transition:border-color .3s}
.char.active ~ .desk[data-role],.stage:has(.char.active[data-role="ceo"]) .desk[data-role="ceo"]::after,
.stage:has(.char.active[data-role="planner"]) .desk[data-role="planner"]::after,
.stage:has(.char.active[data-role="researcher"]) .desk[data-role="researcher"]::after,
.stage:has(.char.active[data-role="designer"]) .desk[data-role="designer"]::after,
.stage:has(.char.active[data-role="developer"]) .desk[data-role="developer"]::after,
.stage:has(.char.active[data-role="qa"]) .desk[data-role="qa"]::after,
.stage:has(.char.active[data-role="inspector"]) .desk[data-role="inspector"]::after,
.stage:has(.char.active[data-role="support"]) .desk[data-role="support"]::after
.stage:has(.char.active[data-agent="ceo"]) .desk[data-agent="ceo"]::after,
.stage:has(.char.active[data-agent="planner"]) .desk[data-agent="planner"]::after,
.stage:has(.char.active[data-agent="researcher"]) .desk[data-agent="researcher"]::after,
.stage:has(.char.active[data-agent="designer"]) .desk[data-agent="designer"]::after,
.stage:has(.char.active[data-agent="developer"]) .desk[data-agent="developer"]::after,
.stage:has(.char.active[data-agent="qa"]) .desk[data-agent="qa"]::after,
.stage:has(.char.active[data-agent="inspector"]) .desk[data-agent="inspector"]::after,
.stage:has(.char.active[data-agent="support"]) .desk[data-agent="support"]::after
{border-color:var(--role-color)}
.shadow{position:absolute;left:12px;bottom:0;width:28px;height:7px;background:radial-gradient(ellipse,rgba(0,0,0,.55),transparent 70%)}
.bubble{position:absolute;z-index:20;transform:translate(-50%,-100%);background:#fff;color:#222;padding:5px 8px;border-radius:8px;font-size:11px;box-shadow:2px 2px 0 rgba(0,0,0,.35);white-space:nowrap}
@@ -3949,7 +3947,29 @@ body:not([data-edit-mode="true"]) .char{cursor:pointer}
.ctx-detail dt{color:#94A3B8;font-weight:600;white-space:nowrap}
.ctx-detail dd{margin:0;color:#F1F4FB;overflow-wrap:anywhere}
.ctx-detail .cd-logs{margin-top:10px;padding:6px 8px;background:rgba(0,0,0,.3);border-radius:4px;font-family:ui-monospace,monospace;font-size:10.5px;max-height:120px;overflow-y:auto}
.edit-toolbar{display:flex;gap:8px;align-items:center;padding:6px 16px;background:rgba(99,102,241,.18);border-bottom:1px solid rgba(99,102,241,.4);font-size:11px}.edit-toolbar .et-hint{flex:1;color:#D7DBEA}.edit-toolbar button{background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.25);color:#F1F4FB;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:11px}.edit-toolbar button:hover{background:rgba(99,102,241,.35)}
.edit-toolbar{display:flex;gap:8px;align-items:center;padding:6px 16px;background:rgba(99,102,241,.18);border-bottom:1px solid rgba(99,102,241,.4);font-size:11px;flex-wrap:wrap}.edit-toolbar .et-hint{flex:1;color:#D7DBEA;min-width:160px}.edit-toolbar button{background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.25);color:#F1F4FB;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:11px}.edit-toolbar button:hover{background:rgba(99,102,241,.35)}
.edit-toolbar button.add{background:rgba(16,185,129,.22);border-color:rgba(16,185,129,.55)}.edit-toolbar button.add:hover{background:rgba(16,185,129,.4)}
.edit-toolbar button.del{background:rgba(239,68,68,.22);border-color:rgba(239,68,68,.55)}.edit-toolbar button.del:hover{background:rgba(239,68,68,.4)}.edit-toolbar button[disabled]{opacity:.4;cursor:not-allowed}
/* 선택된 item 의 속성 편집 패널 — 우측 슬라이드 */
.prop-panel{position:absolute;right:12px;top:90px;width:240px;background:#13162A;border:1px solid #2A2E3F;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.6);padding:12px;font-size:11px;color:#F1F4FB;z-index:25;display:none}
.prop-panel.show{display:block}
.prop-panel h4{margin:0 0 8px;font-size:12px;color:#A78BFA;text-transform:uppercase;letter-spacing:.04em}
.prop-panel .pp-row{margin-bottom:8px}
.prop-panel label{display:block;font-size:10px;color:#94A3B8;margin-bottom:2px}
.prop-panel select,.prop-panel input{width:100%;background:#0c1020;color:#F1F4FB;border:1px solid #2A2E3F;border-radius:4px;padding:3px 6px;font-size:11px}
.prop-panel .pp-thumbs{display:grid;grid-template-columns:repeat(4,1fr);gap:4px;margin-top:4px}
.prop-panel .pp-thumb{width:100%;aspect-ratio:1/1;background:#0c1020;border:1px solid #2A2E3F;border-radius:3px;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:2px}
.prop-panel .pp-thumb img{max-width:100%;max-height:100%;image-rendering:pixelated}
.prop-panel .pp-thumb.active{border-color:#A78BFA;box-shadow:0 0 0 2px rgba(167,139,250,.35)}
/* 프랍 추가 picker — 모달 grid */
.prop-picker{position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:1100;display:flex;align-items:center;justify-content:center}
.prop-picker-box{background:#13162A;border:1px solid #2A2E3F;border-radius:10px;padding:14px;max-width:520px;max-height:80vh;overflow-y:auto;color:#F1F4FB}
.prop-picker-box h3{margin:0 0 10px;font-size:13px;color:#A78BFA}
.prop-picker-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:8px}
.prop-pick{background:#0c1020;border:1px solid #2A2E3F;border-radius:4px;padding:6px;cursor:pointer;text-align:center}
.prop-pick:hover{border-color:#A78BFA}
.prop-pick img{max-width:60px;max-height:60px;image-rendering:pixelated}
.prop-pick .pp-name{font-size:10px;color:#94A3B8;margin-top:4px;word-break:break-all}
/* 편집 모드 — 드래그 가능 요소 강조 */
body[data-edit-mode="true"] .stage{background-image:linear-gradient(rgba(99,102,241,.15) 1px,transparent 1px),linear-gradient(90deg,rgba(99,102,241,.15) 1px,transparent 1px);background-size:32px 32px}
body[data-edit-mode="true"] .desk,body[data-edit-mode="true"] .char,body[data-edit-mode="true"] .obj{cursor:grab;outline:1px dashed rgba(99,102,241,.5)}
@@ -3963,7 +3983,10 @@ footer{padding:8px 16px 12px;border-top:1px solid rgba(255,255,255,.08);backgrou
<header><div><div class="h-title">🏢 ASTRA OFFICE</div><div class="h-sub" id="agent">Astra</div></div><div style="display:flex;gap:8px;align-items:center;"><button id="editBtn" class="edit-btn" title="배치 편집 모드 토글">✏️ 편집</button><div class="status" id="status">idle</div></div></header>
<div id="miniMap" class="mini-map" style="display:none;"></div>
<div id="editToolbar" class="edit-toolbar" style="display:none;">
<span class="et-hint">드래그로 이동 · <b>R</b> 회전(Shift+R 반시계) · <b>]</b> 위 / <b>[</b> 아래 · 4px snap</span>
<span class="et-hint">드래그로 이동 · <b>R</b> 회전 · <b>]</b>/<b>[</b> 레이어 · 4px snap</span>
<button id="addDeskBtn" class="add" title="책상 추가">+ 책상</button>
<button id="addPropBtn" class="add" title="프랍(소품) 추가">+ 프랍</button>
<button id="deleteSelBtn" class="del" title="선택 항목 삭제" disabled>🗑 삭제</button>
<button id="layerUpBtn" title="레이어 위로 (])">⬆</button>
<button id="layerDownBtn" title="레이어 아래로 ([)">⬇</button>
<button id="saveBtn">💾 저장</button>
@@ -3971,29 +3994,118 @@ footer{padding:8px 16px 12px;border-top:1px solid rgba(255,255,255,.08);backgrou
<button id="cancelBtn" title="저장 안 하고 종료">✕ 취소</button>
</div>
<div class="strip"><span><b>작업</b> <span id="task">—</span></span><span><b>단계</b> <span id="step">—</span></span></div>
<main class="office"><div class="stage" id="stage"><div class="zone exec"></div><div class="zone core"></div><div class="zone ops"></div><div class="zone lounge"></div><div class="wall-window w1"></div><div class="wall-window w2"></div></div></main>
<main class="office"><div class="stage" id="stage"><div class="wall-window w1"></div><div class="wall-window w2"></div></div><div id="propPanel" class="prop-panel"></div></main>
<div id="ticker" class="ticker" style="display:none;"><div class="tk-track" id="tickerTrack"></div></div>
<footer><div class="progress"><div class="bar" id="bar"></div></div><div id="log">—</div></footer>
<script>(function(){
const base='${assets.derivedBase}'; const stage=document.getElementById('stage');
const stations=[
{key:'ceo',label:'CEO',row:0,desk:'desk-boss',deskX:304,deskY:84,deskW:136,seatX:331,seatY:115,face:'R',dock:[362,164],roam:[[320,196],[396,196]],boss:true},
{key:'planner',label:'\uAE30\uD68D',row:1,desk:'desk-main',deskX:60,deskY:228,deskW:112,seatX:64,seatY:264,face:'R',dock:[96,322],roam:[[154,350],[200,330]]},
{key:'researcher',label:'\uB9AC\uC11C\uCE58',row:2,desk:'desk-dark-mirror',deskX:236,deskY:214,deskW:112,seatX:284,seatY:248,face:'L',dock:[304,310],roam:[[322,340],[344,322]]},
{key:'designer',label:'\uB514\uC790\uC778',row:3,desk:'desk-main',deskX:402,deskY:240,deskW:112,seatX:406,seatY:276,face:'R',dock:[438,334],roam:[[492,350],[520,326]]},
{key:'developer',label:'\uAC1C\uBC1C',row:4,desk:'desk-dark-mirror',deskX:56,deskY:410,deskW:112,seatX:104,seatY:442,face:'L',dock:[124,500],roam:[[150,534],[188,518]]},
{key:'qa',label:'QA',row:5,desk:'desk-main',deskX:232,deskY:394,deskW:112,seatX:236,seatY:430,face:'R',dock:[268,486],roam:[[320,520],[352,500]]},
{key:'inspector',label:'\uAC10\uB9AC',row:6,desk:'desk-dark-mirror',deskX:408,deskY:420,deskW:112,seatX:456,seatY:452,face:'L',dock:[476,506],roam:[[506,532],[540,502]]},
{key:'support',label:'\uC9C0\uC6D0',row:7,desk:'desk-main',deskX:220,deskY:472,deskW:112,seatX:224,seatY:504,face:'R',dock:[256,548],roam:[[360,548],[484,540]]},
// \u2500\u2500 \uB370\uC774\uD130 \uBAA8\uB378 \u2500\u2500
// stations: \uCC45\uC0C1 + \uCE90\uB9AD\uD130 \uC815\uC758 \uBC30\uC5F4 (let \u2014 \uCD94\uAC00/\uC81C\uAC70 \uAC00\uB2A5).
// key = \uC548\uC815\uC801 \uC2DD\uBCC4\uC790 (DOM dataset.role \uB85C\uB3C4 \uC0AC\uC6A9). \uC0AC\uC6A9\uC790\uAC00 \uC0C8\uB85C \uB9CC\uB4E0 \uCC45\uC0C1\uC740 \uC790\uB3D9 \uC0DD\uC131.
// agentKey = \uC774 \uCC45\uC0C1\uC5D0 \uB9E4\uD551\uB41C \uC5D0\uC774\uC804\uD2B8 ID (ceo/planner/researcher/...). \uC5C6\uC73C\uBA74 idle.
// charRow = \uC0AC\uC6A9\uD560 sprite row (0~7). \uCE90\uB9AD\uD130 \uC678\uBAA8/\uBC29\uD5A5.
// deskSprite= \uCC45\uC0C1 PNG \uC774\uB984 (desk-main / desk-boss / desk-dark-mirror \uB4F1).
// objs: \uD504\uB78D \uC815\uC758 \uBC30\uC5F4 (let). user-add/remove.
const DEFAULT_STATIONS=[
{key:'ceo',agentKey:'ceo',label:'CEO',charRow:0,deskSprite:'desk-boss',deskX:304,deskY:84,deskW:136,seatX:331,seatY:115,face:'R',dock:[362,164],roam:[[320,196],[396,196]],boss:true},
{key:'planner',agentKey:'planner',label:'\uAE30\uD68D',charRow:1,deskSprite:'desk-main',deskX:60,deskY:228,deskW:112,seatX:64,seatY:264,face:'R',dock:[96,322],roam:[[154,350],[200,330]]},
{key:'researcher',agentKey:'researcher',label:'\uB9AC\uC11C\uCE58',charRow:2,deskSprite:'desk-dark-mirror',deskX:236,deskY:214,deskW:112,seatX:284,seatY:248,face:'L',dock:[304,310],roam:[[322,340],[344,322]]},
{key:'designer',agentKey:'designer',label:'\uB514\uC790\uC778',charRow:3,deskSprite:'desk-main',deskX:402,deskY:240,deskW:112,seatX:406,seatY:276,face:'R',dock:[438,334],roam:[[492,350],[520,326]]},
{key:'developer',agentKey:'developer',label:'\uAC1C\uBC1C',charRow:4,deskSprite:'desk-dark-mirror',deskX:56,deskY:410,deskW:112,seatX:104,seatY:442,face:'L',dock:[124,500],roam:[[150,534],[188,518]]},
{key:'qa',agentKey:'qa',label:'QA',charRow:5,deskSprite:'desk-main',deskX:232,deskY:394,deskW:112,seatX:236,seatY:430,face:'R',dock:[268,486],roam:[[320,520],[352,500]]},
{key:'inspector',agentKey:'inspector',label:'\uAC10\uB9AC',charRow:6,deskSprite:'desk-dark-mirror',deskX:408,deskY:420,deskW:112,seatX:456,seatY:452,face:'L',dock:[476,506],roam:[[506,532],[540,502]]},
{key:'support',agentKey:'support',label:'\uC9C0\uC6D0',charRow:7,deskSprite:'desk-main',deskX:220,deskY:472,deskW:112,seatX:224,seatY:504,face:'R',dock:[256,548],roam:[[360,548],[484,540]]},
];
const roleMap={ceo:'ceo',writer:'planner',researcher:'researcher',designer:'designer',editor:'designer',developer:'developer',qa:'qa',inspector:'inspector',secretary:'support',business:'inspector'};
const stationByKey={}; stations.forEach(st=>stationByKey[st.key]=st);
// \uC5D0\uC774\uC804\uD2B8 ID alias \u2014 \uAC19\uC740 \uD398\uB974\uC18C\uB098\uC758 \uB2E4\uC591\uD55C \uD638\uCE6D\uC744 \uAC19\uC740 book agentKey \uB85C. (writer\u2192planner, editor\u2192designer, secretary\u2192support, business\u2192inspector)
const AGENT_ALIASES={writer:'planner',editor:'designer',secretary:'support',business:'inspector'};
// \uB9E4\uD551 dropdown \uC5D0 \uBCF4\uC5EC\uC904 \uC5D0\uC774\uC804\uD2B8 \uD6C4\uBCF4. agentKey \uAC00 unique \uD55C base set.
const AGENT_CHOICES=['','ceo','planner','researcher','designer','developer','qa','inspector','support'];
// \uCD94\uAC00 \uAC00\uB2A5\uD55C \uCC45\uC0C1 sprite \uD6C4\uBCF4 (assets/pixelOffice/derived \uC5D0 \uC788\uB294 desk-* PNG).
const DESK_SPRITE_CHOICES=['desk-main','desk-boss','desk-dark-mirror','desk-main-mirror','desk-dark','desk-boss-mirror','desk-main-front','desk-dark-front','desk-boss-front','desk-partition'];
// \uCD94\uAC00 \uAC00\uB2A5\uD55C \uD504\uB78D sprite \uD6C4\uBCF4.
const PROP_SPRITE_CHOICES=['board','plant-tall','bookshelf','plant-bushy','partition','cooler','filing','couch','rug','shelf','printer','monitor-blue','monitor-black','chair-blue','crt'];
const DEFAULT_PROPS=[
{name:'board',x:316,y:12,w:88},{name:'plant-tall',x:44,y:92,w:42},{name:'bookshelf',x:86,y:70,w:54},
{name:'plant-bushy',x:642,y:96,w:42},{name:'partition',x:520,y:208,w:72},{name:'cooler',x:640,y:248,w:38},
{name:'filing',x:620,y:330,w:42},{name:'couch',x:578,y:432,w:96},{name:'rug',x:560,y:510,w:126},
{name:'shelf',x:40,y:504,w:118},{name:'printer',x:520,y:520,w:58},{name:'monitor-blue',x:356,y:56,w:44},
];
let stations=[]; // mutable, \uC2DC\uC791 \uC2DC default \uB610\uB294 saved layout \uB85C \uCC44\uC6C0.
let __nextDeskN=100; // user-add \uCC45\uC0C1 id \uCE74\uC6B4\uD130 (default \uC640 \uCDA9\uB3CC \uD68C\uD53C \uC704\uD574 \uD070 \uC218\uC5D0\uC11C \uC2DC\uC791).
let __nextObjN=0;
const stationByKey={}; // \uBE60\uB978 lookup. stations \uBCC0\uACBD \uC2DC rebuild.
const __deskWrap={}; // role \u2192 desk DOM wrap.
const chars={}; // role \u2192 char DOM.
const anim={}; // role \u2192 animation state.
function png(name){return base+'/'+name+'.png'}
let __objN=0;function addImg(name,x,y,w,cls='obj'){const i=document.createElement('img');i.src=png(name);i.className=cls;i.dataset.objId='obj_'+(__objN++);i.dataset.objName=name;if(w)i.dataset.objW=w;i.style.left=x+'px';i.style.top=y+'px';if(w)i.style.width=w+'px';stage.appendChild(i);return i}
addImg('board',316,12,88); addImg('plant-tall',44,92,42); addImg('bookshelf',86,70,54); addImg('plant-bushy',642,96,42); addImg('partition',520,208,72); addImg('cooler',640,248,38); addImg('filing',620,330,42); addImg('couch',578,432,96); addImg('rug',560,510,126); addImg('shelf',40,504,118); addImg('printer',520,520,58); addImg('monitor-blue',356,56,44);
const __deskWrap={};function addDesk(st){const wrap=document.createElement('div');wrap.className='desk '+(st.boss?'boss':'');wrap.dataset.role=st.key;wrap.style.left=st.deskX+'px';wrap.style.top=st.deskY+'px';wrap.style.width=st.deskW+'px';const img=document.createElement('img');img.src=png(st.desk);img.style.width='100%';wrap.appendChild(img);const l=document.createElement('div');l.className='label';l.textContent=st.label;wrap.appendChild(l);stage.appendChild(wrap);__deskWrap[st.key]=wrap}
stations.forEach(addDesk);
const chars={}, anim={}; stations.forEach((st)=>{const ch=document.createElement('div');ch.className='char';ch.dataset.role=st.key;ch.style.left=st.seatX+'px';ch.style.top=st.seatY+'px';ch.dataset.homeX=st.seatX;ch.dataset.homeY=st.seatY;ch.dataset.row=st.row;ch.innerHTML='<img><div class="shadow"></div>';stage.appendChild(ch);chars[st.key]=ch;anim[st.key]={row:st.row,frame:0,dir:0,mode:'sit',face:st.face,route:0};const img=ch.querySelector('img');img.src=png('idle-r'+st.row+'-f0');img.style.transform=st.face==='R'?'scaleX(-1)':'none';});
function _rebuildStationIndex(){
Object.keys(stationByKey).forEach(k=>delete stationByKey[k]);
stations.forEach(st=>{ stationByKey[st.key]=st; });
}
// agentKey \u2192 station.key \uB85C \uB77C\uC6B0\uD305. roleMap \uC758 \uB3D9\uC801 \uBC84\uC804.
function findStationByAgent(agentId){
if(!agentId) return null;
const a = AGENT_ALIASES[agentId] || agentId;
for(const st of stations){ if(st.agentKey === a) return st; }
return null;
}
// \uC61B \uCF54\uB4DC \uD638\uD658 \u2014 roleMap[x] \uD638\uCD9C \uD328\uD134\uC744 \uD568\uC218\uB85C.
const roleMap=new Proxy({},{get:(_,k)=>{ const s=findStationByAgent(k); return s?s.key:null; }});
function addImg(name,x,y,w){
const i=document.createElement('img');
i.src=png(name); i.className='obj';
i.dataset.objId='obj_'+(__nextObjN++); i.dataset.objName=name;
if(w!=null) i.dataset.objW=w;
i.style.left=x+'px'; i.style.top=y+'px';
if(w!=null) i.style.width=w+'px';
stage.appendChild(i); return i;
}
function addDesk(st){
const wrap=document.createElement('div');
wrap.className='desk '+(st.boss?'boss':'');
wrap.dataset.role=st.key;
if(st.agentKey) wrap.dataset.agent=st.agentKey;
wrap.style.left=st.deskX+'px'; wrap.style.top=st.deskY+'px'; wrap.style.width=st.deskW+'px';
const img=document.createElement('img'); img.src=png(st.deskSprite); img.style.width='100%';
wrap.appendChild(img);
const l=document.createElement('div'); l.className='label'; l.textContent=st.label;
wrap.appendChild(l);
stage.appendChild(wrap);
__deskWrap[st.key]=wrap;
return wrap;
}
function addChar(st){
const ch=document.createElement('div');
ch.className='char'; ch.dataset.role=st.key;
if(st.agentKey) ch.dataset.agent=st.agentKey;
ch.style.left=st.seatX+'px'; ch.style.top=st.seatY+'px';
ch.dataset.homeX=st.seatX; ch.dataset.homeY=st.seatY; ch.dataset.row=st.charRow;
ch.innerHTML='<img><div class="shadow"></div>';
stage.appendChild(ch);
chars[st.key]=ch;
anim[st.key]={row:st.charRow,frame:0,dir:0,mode:'sit',face:st.face,route:0};
const img=ch.querySelector('img');
img.src=png('idle-r'+st.charRow+'-f0');
img.style.transform=st.face==='R'?'scaleX(-1)':'none';
return ch;
}
function buildStation(st){ addDesk(st); addChar(st); }
function _renderDefaultStations(){
// default 8 stations + default props.
stations = DEFAULT_STATIONS.map(s=>Object.assign({},s));
_rebuildStationIndex();
stations.forEach(buildStation);
DEFAULT_PROPS.forEach(p=>addImg(p.name,p.x,p.y,p.w));
}
_renderDefaultStations();
// ── 4방향 dir 매핑 ──
// PNG 파일명 규약: walk-r<row>-d<DIR>-f<frame>.png
// 사용자의 sprite 컨벤션이 다르면 이 4 상수만 수정하면 됨.
@@ -4186,12 +4298,13 @@ function apply(s){
} else {
mm.style.display = 'none';
}
// 활성 캐릭터 결정.
// 활성 캐릭터 결정. roleMap 은 agentKey → 실제 존재하는 station.key 로 lookup
// 하므로, 매핑된 책상이 없으면 null. 사용자가 default ceo 책상을 지워도 안전.
let role = null;
const m = (s?.message || '').match(/^([a-z0-9_-]+)/i);
if(m) role = roleMap[m[1]] || null;
if(['planning','analyzing','waiting_approval'].includes(st)) role = 'ceo';
if(st === 'reviewing') role = 'inspector';
if(['planning','analyzing','waiting_approval'].includes(st)) role = roleMap['ceo'] || role;
if(st === 'reviewing') role = roleMap['inspector'] || role;
// 활성 outline은 *항상* 즉시 반영 (시뮬레이션 없이도 누가 일하는지 보임).
activate(role);
// 시뮬레이션은 *status 전환 + critical 진입* 에서만.
@@ -4224,7 +4337,7 @@ function apply(s){
if(st === 'done'){
// 완료 — 모두 자리 복귀 후 단체 sit.
Object.keys(chars).forEach(k => sendHome(k, 'work'));
bubble('ceo', '완료!');
const ceoRole = roleMap['ceo']; if(ceoRole) bubble(ceoRole, '완료!');
setTimeout(() => Object.keys(chars).forEach(k => sendHome(k, 'sit')), 1800);
}
}
@@ -4373,10 +4486,21 @@ try{vscode.postMessage({type:'getPixelOfficeState'})}catch{}
let _editMode=false, _drag=null, _dragDX=0, _dragDY=0, _snapshotBeforeEdit=null, _selected=null;
function _snapshotLayout(){
// 현재 화면의 모든 좌표를 캡쳐 — 취소(Cancel) 시 복원용. 회전(rot) 도 포함.
// 새 schema v2: cells 가 단순 좌표 패치가 아니라 stations 전체 정의를 담는다.
// (책상 추가/제거가 가능하니 default 와의 delta 로는 표현 불가.) v1 호환을 위해
// _restoreLayout 이 양쪽 모두 처리.
return {
schema: 2,
cells: stations.map(st=>({
roleKey: st.key,
agentKey: st.agentKey || '',
label: st.label || '',
charRow: st.charRow ?? 0,
deskSprite: st.deskSprite || 'desk-main',
face: st.face || 'R',
boss: !!st.boss,
dock: st.dock,
roam: st.roam,
deskX: parseFloat(__deskWrap[st.key].style.left),
deskY: parseFloat(__deskWrap[st.key].style.top),
deskW: parseFloat(__deskWrap[st.key].style.width),
@@ -4399,6 +4523,15 @@ function _snapshotLayout(){
};
}
// stage 에 그려진 모든 desk/char/obj DOM 제거 (벽 창문 제외). 레이아웃 리빌드 직전에 호출.
function _clearStage(){
Array.from(stage.querySelectorAll('.desk,.char,img.obj')).forEach(el=>el.remove());
Object.keys(__deskWrap).forEach(k=>delete __deskWrap[k]);
Object.keys(chars).forEach(k=>delete chars[k]);
Object.keys(anim).forEach(k=>delete anim[k]);
stations = [];
}
function _applyRot(el, rot){
if(!el) return;
const r = typeof rot==='number' ? rot : 0;
@@ -4411,8 +4544,50 @@ function _applyZ(el, z){
el.dataset.z = String(z);
el.style.zIndex = z === 0 ? '' : String(z);
}
// v2 schema 또는 cell 에 desk 정의 필드가 있으면 stage 를 통째로 재구축.
// v1 (옛 포맷) 이면 좌표만 패치하는 in-place 갱신.
function _isV2Snap(snap){
if(!snap || !Array.isArray(snap.cells)) return false;
if(snap.schema === 2) return true;
return snap.cells.some(c => c && (typeof c.deskSprite === 'string' || typeof c.agentKey === 'string' || typeof c.charRow === 'number'));
}
function _restoreLayout(snap){
if(!snap) return;
if(_isV2Snap(snap)){
_clearStage();
(snap.cells||[]).forEach(c=>{
const st = {
key: c.roleKey,
agentKey: c.agentKey || '',
label: c.label || c.roleKey,
charRow: typeof c.charRow === 'number' ? c.charRow : 0,
deskSprite: c.deskSprite || 'desk-main',
face: c.face || 'R',
boss: !!c.boss,
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]],
deskX: c.deskX, deskY: c.deskY, deskW: c.deskW || 112,
seatX: c.seatX, seatY: c.seatY,
};
stations.push(st);
const m = String(st.key).match(/^desk_(\\d+)$/);
if(m){ const n = parseInt(m[1],10); if(n >= __nextDeskN) __nextDeskN = n + 1; }
});
_rebuildStationIndex();
stations.forEach(buildStation);
(snap.cells||[]).forEach(c=>{
const wrap=__deskWrap[c.roleKey], ch=chars[c.roleKey];
if(wrap){ _applyRot(wrap, c.deskRot); _applyZ(wrap, c.deskZ); }
if(ch){ _applyRot(ch, c.charRot); _applyZ(ch, c.charZ); }
});
(snap.objs||[]).forEach(o=>{
const el = addImg(o.name, o.x, o.y, o.w);
if(o.id){ el.dataset.objId = o.id; }
_applyRot(el, o.rot); _applyZ(el, o.z);
});
return;
}
// v1 — 옛 포맷, in-place patch.
(snap.cells||[]).forEach(c=>{
const wrap=__deskWrap[c.roleKey];
if(wrap){
@@ -4429,7 +4604,6 @@ function _restoreLayout(snap){
_applyRot(ch, c.charRot);
_applyZ(ch, c.charZ);
}
// stations 배열 자체도 갱신 — sendHome 등이 이걸 참조.
const st=stationByKey[c.roleKey];
if(st){
if(typeof c.deskX==='number') st.deskX=c.deskX;
@@ -4466,6 +4640,185 @@ function _setEdit(on){
// 편집 종료 시 selected 강조 해제.
if(_selected){ _selected.classList.remove('selected'); _selected = null; }
}
_onSelectionChanged();
}
// ── 선택 변화 시 호출 — 속성 패널 / 삭제 버튼 상태 sync ──
function _onSelectionChanged(){
const panel = document.getElementById('propPanel');
const delBtn = document.getElementById('deleteSelBtn');
if(!_editMode || !_selected){
panel.classList.remove('show'); panel.innerHTML='';
if(delBtn) delBtn.disabled = true;
return;
}
// 캐릭터를 선택해도 그 책상의 속성을 편집한다 (캐릭터와 책상은 한 쌍).
let targetForProps = _selected;
if(targetForProps.classList.contains('char')){
const role = Object.keys(chars).find(k=>chars[k]===targetForProps);
if(role && __deskWrap[role]) targetForProps = __deskWrap[role];
}
if(targetForProps.classList.contains('desk')){
if(delBtn) delBtn.disabled = false;
_renderDeskProps(targetForProps);
} else if(_selected.classList.contains('obj')){
if(delBtn) delBtn.disabled = false;
_renderObjProps(_selected);
} else {
panel.classList.remove('show'); panel.innerHTML='';
if(delBtn) delBtn.disabled = true;
}
}
function _renderDeskProps(deskEl){
const role = deskEl.dataset.role;
const st = stationByKey[role]; if(!st) return;
const panel = document.getElementById('propPanel');
panel.classList.add('show');
// 에이전트 매핑 dropdown.
const agentOpts = AGENT_CHOICES.map(a=>'<option value="'+a+'"'+(a===(st.agentKey||'')?' selected':'')+'>'+(a||'(매핑 없음)')+'</option>').join('');
// 책상 sprite picker.
const deskOpts = DESK_SPRITE_CHOICES.map(s=>'<option value="'+s+'"'+(s===st.deskSprite?' selected':'')+'>'+s+'</option>').join('');
// charRow 썸네일 picker (idle-r<n>-f0.png).
let thumbs='';
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>';
}
panel.innerHTML =
'<h4>책상 속성</h4>'+
'<div class="pp-row"><label>라벨</label><input id="ppLabel" value="'+(st.label||'').replace(/"/g,'&quot;')+'"></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>착석 캐릭터 (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>';
// 핸들러
panel.querySelector('#ppLabel').oninput = (ev)=>{
st.label = ev.target.value;
const lbl = deskEl.querySelector('.label'); if(lbl) lbl.textContent = st.label;
};
panel.querySelector('#ppAgent').onchange = (ev)=>{
st.agentKey = ev.target.value || '';
// CSS data-role 색깔은 agentKey 기준 — 매핑 변경 시 swap.
if(st.agentKey){ deskEl.dataset.agent = st.agentKey; if(chars[role]) chars[role].dataset.agent = st.agentKey; }
else { delete deskEl.dataset.agent; if(chars[role]) delete chars[role].dataset.agent; }
};
panel.querySelector('#ppDesk').onchange = (ev)=>{
st.deskSprite = ev.target.value;
const img = deskEl.querySelector('img'); if(img) img.src = png(st.deskSprite);
};
panel.querySelectorAll('.pp-thumb').forEach(t=>{
t.onclick = ()=>{
const r = parseInt(t.dataset.charrow,10);
st.charRow = r;
if(anim[role]){ anim[role].row = r; }
const ch = chars[role]; if(ch){ ch.dataset.row = r; const img=ch.querySelector('img'); if(img) img.src = png('idle-r'+r+'-f0'); }
panel.querySelectorAll('.pp-thumb').forEach(x=>x.classList.toggle('active', x===t));
};
});
panel.querySelector('#ppFace').onchange = (ev)=>{
st.face = ev.target.value;
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'; }
};
}
function _renderObjProps(el){
const panel = document.getElementById('propPanel');
panel.classList.add('show');
const name = el.dataset.objName || '';
const w = el.dataset.objW || '';
const opts = PROP_SPRITE_CHOICES.map(s=>'<option value="'+s+'"'+(s===name?' selected':'')+'>'+s+'</option>').join('');
panel.innerHTML =
'<h4>프랍 속성</h4>'+
'<div class="pp-row"><label>sprite</label><select id="ppObjName">'+opts+'</select></div>'+
'<div class="pp-row"><label>너비 (px, 비우면 원본)</label><input id="ppObjW" type="number" value="'+w+'" placeholder="원본"></div>';
panel.querySelector('#ppObjName').onchange = (ev)=>{
el.dataset.objName = ev.target.value;
el.src = png(ev.target.value);
};
panel.querySelector('#ppObjW').oninput = (ev)=>{
const v = ev.target.value.trim();
if(v === ''){ el.style.removeProperty('width'); delete el.dataset.objW; }
else { el.style.width = parseFloat(v)+'px'; el.dataset.objW = String(parseFloat(v)); }
};
}
// ── 신규 책상 추가 ──
function _addNewDesk(){
const id = 'desk_'+(__nextDeskN++);
// 기본 위치: stage 중앙 근처, 다른 책상과 안 겹치게 살짝 오프셋.
const baseX = 280 + ((__nextDeskN%5)*16);
const baseY = 260 + ((__nextDeskN%5)*16);
const st = {
key: id, agentKey: '', label: '새 책상', charRow: 0, deskSprite: 'desk-main', face: 'R',
boss: false,
deskX: baseX, deskY: baseY, deskW: 112,
seatX: baseX+4, seatY: baseY+36,
dock: [baseX+32, baseY+80],
roam: [[baseX-20, baseY+120],[baseX+60, baseY+100]],
};
stations.push(st); _rebuildStationIndex();
buildStation(st);
// 새 책상을 자동 선택.
if(_selected) _selected.classList.remove('selected');
_selected = __deskWrap[id]; _selected.classList.add('selected');
_onSelectionChanged();
}
// ── 신규 프랍 추가 — sprite picker 모달 ──
function _openPropPicker(){
const overlay = document.createElement('div');
overlay.className = 'prop-picker';
const box = document.createElement('div');
box.className = 'prop-picker-box';
box.innerHTML = '<h3>프랍 추가 — sprite 선택</h3>'+
'<div class="prop-picker-grid">'+
PROP_SPRITE_CHOICES.map(n=>'<div class="prop-pick" data-name="'+n+'"><img src="'+png(n)+'"><div class="pp-name">'+n+'</div></div>').join('')+
'</div>';
overlay.appendChild(box);
overlay.onclick = (e)=>{ if(e.target === overlay) overlay.remove(); };
box.querySelectorAll('.prop-pick').forEach(p=>{
p.onclick = ()=>{
const name = p.dataset.name;
// stage 중앙 근처에 배치.
const x = 300 + ((__nextObjN%4)*12);
const y = 280 + ((__nextObjN%4)*12);
const el = addImg(name, x, y);
overlay.remove();
// 자동 선택.
if(_selected) _selected.classList.remove('selected');
_selected = el; _selected.classList.add('selected');
_onSelectionChanged();
};
});
document.body.appendChild(overlay);
}
// ── 선택 항목 삭제 ──
function _deleteSelected(){
if(!_editMode || !_selected) return;
// char 가 선택돼 있으면 그 desk 도 함께 묶어서 처리.
let target = _selected;
if(target.classList.contains('char')){
const role = Object.keys(chars).find(k=>chars[k]===target);
if(role && __deskWrap[role]) target = __deskWrap[role];
}
if(target.classList.contains('desk')){
const role = target.dataset.role;
if(!confirm('"'+(stationByKey[role]?.label||role)+'" 책상을 삭제할까요? 이 자리에 매핑된 에이전트도 자리가 사라집니다.')) return;
target.remove();
if(chars[role]) chars[role].remove();
delete __deskWrap[role]; delete chars[role]; delete anim[role];
const idx = stations.findIndex(s=>s.key===role);
if(idx>=0) stations.splice(idx,1);
_rebuildStationIndex();
} else if(target.classList.contains('obj')){
target.remove();
} else {
return;
}
_selected = null;
_onSelectionChanged();
}
function _findDraggable(el){
@@ -4485,7 +4838,7 @@ stage.addEventListener('mousedown', e=>{
const target = _findDraggable(e.target);
// 빈 공간 클릭 → 선택 해제.
if(!target){
if(_selected){ _selected.classList.remove('selected'); _selected=null; }
if(_selected){ _selected.classList.remove('selected'); _selected=null; _onSelectionChanged(); }
return;
}
e.preventDefault();
@@ -4493,6 +4846,7 @@ stage.addEventListener('mousedown', e=>{
if(_selected && _selected !== target) _selected.classList.remove('selected');
_selected = target;
target.classList.add('selected');
_onSelectionChanged();
_drag = target;
const rect = stage.getBoundingClientRect();
const tx = parseFloat(target.style.left)||0;
@@ -4595,6 +4949,16 @@ document.getElementById('resetBtn').addEventListener('click', ()=>{
if(!confirm('현재 배치를 버리고 기본 배치로 되돌릴까요?')) return;
try{ vscode.postMessage({type:'resetPixelOfficeLayout'}); }catch{}
});
document.getElementById('addDeskBtn').addEventListener('click', _addNewDesk);
document.getElementById('addPropBtn').addEventListener('click', _openPropPicker);
document.getElementById('deleteSelBtn').addEventListener('click', _deleteSelected);
// Delete / Backspace 키도 같은 동작 — 단 입력 필드 포커스 시 무시.
document.addEventListener('keydown', e=>{
if(!_editMode) return;
const tag = (e.target && e.target.tagName) || '';
if(tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
if(e.key === 'Delete' || e.key === 'Backspace'){ e.preventDefault(); _deleteSelected(); }
});
// 저장된 layout 로드 요청 + 응답 처리.
window.addEventListener('message', e=>{