v2.2.18: Dynamic Office Auto-Layout & Legacy Cleanup

This commit is contained in:
g1nation
2026-05-16 23:13:40 +09:00
parent 84b2b0670d
commit c7b596f17a
11 changed files with 138 additions and 116 deletions
+71 -62
View File
@@ -29,16 +29,13 @@ setTimeout(_fitStage, 0);
// 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]]},
];
// 기본 장면은 더 이상 "가짜 8인 사무실"을 깔지 않는다.
// 실제 roster 가 들어오면 그 인원만으로 장면을 짓고, roster 가 비면 빈 오피스 그대로 둔다.
const DEFAULT_STATIONS=[];
const LEGACY_SHOWROOM_POS={
ceo:[304,84],planner:[60,228],researcher:[236,214],designer:[402,240],
developer:[56,410],qa:[232,394],inspector:[408,420],support:[220,472],
};
// \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.
@@ -47,17 +44,14 @@ const AGENT_CHOICES=['','ceo','planner','researcher','designer','developer','qa'
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:'plant-tall',x:40,y:118,w:42},{name:'bookshelf',x:86,y:88,w:54},
{name:'plant-bushy',x:640,y:118,w:42},{name:'cooler',x:646,y:286,w:38},
{name:'filing',x:618,y:374,w:42},{name:'couch',x:584,y:452,w:96},
{name:'rug',x:560,y:514,w:126},{name:'printer',x:520,y:526,w:58},
];
// 후면 배경 자체가 이미 충분히 강하므로, 목적 없는 프랍은 기본 장면에서 제거.
// 커스텀 편집 모드에선 여전히 사용자가 원하는 프랍을 직접 추가할 수 있다.
const DEFAULT_PROPS=[];
let stations=[]; // mutable, \uC2DC\uC791 \uC2DC default \uB610\uB294 saved layout \uB85C \uCC44\uC6C0.
let _hasSavedLayout=false;
let _layoutSource='default';
let _lastPipelineLayoutSignature='';
let _lastAutoLayoutSignature='';
let _latestSnapshot=null;
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;
@@ -65,6 +59,8 @@ const stationByKey={}; // \uBE60\uB978 lookup. stations \uBCC0\uACBD \uC2DC rebu
const __deskWrap={}; // role \u2192 desk DOM wrap.
const chars={}; // role \u2192 char DOM.
const anim={}; // role \u2192 animation state.
// 한 번 처리된 agentId 는 여기에 기록 → 사용자가 그 desk 를 지워도 자동 재생성 안 함.
const _autoDeskedFor = new Set();
function png(name){return base+'/'+name+'.png'}
@@ -118,36 +114,47 @@ function _pipelineRosterOrder(roster, pipeline){
}
function _stationSpriteFor(slot, roleCategory){
if(roleCategory==='ceo') return 'desk-boss';
return slot >= 4 ? 'desk-dark-mirror' : (slot % 2 ? 'desk-dark' : 'desk-main');
return slot % 2 ? 'desk-dark' : 'desk-main';
}
function _stationSlot(slot){
const path=[
[72,220,'R'],[214,210,'R'],[356,220,'R'],[498,210,'R'],
[498,394,'L'],[356,404,'L'],[214,394,'L'],[72,404,'L'],
];
if(path[slot]) return path[slot];
const extra=slot-path.length;
const cols=4;
return [54+(extra%cols)*144, 42+Math.floor(extra/cols)*86, extra%2?'L':'R'];
function _stationSlots(count){
const rows=[];
if(count <= 0) return rows;
const cols = count <= 2 ? count : (count >= 7 ? 4 : 3);
const rowCount = Math.ceil(count / cols);
const xSets = {
1:[308],
2:[216,400],
3:[124,308,492],
4:[78,230,382,534],
};
for(let row=0; row<rowCount; row++){
const remaining = count - row*cols;
const inRow = Math.min(cols, remaining);
const xs = xSets[inRow] || xSets[3];
const rowYs = rowCount === 1 ? [330] : (rowCount === 2 ? [304,432] : [270,380,490]);
const y = rowYs[row] ?? (490 + Math.max(0,row-rowYs.length+1)*88);
xs.forEach((x)=>rows.push([x,y,'R']));
}
return rows;
}
function _makeStationForRosterAgent(agent, slot){
function _makeStationForRosterAgent(agent, slot, slots){
if(agent.roleCategory==='ceo' || agent.agentId==='ceo'){
return {
key:'ceo',agentKey:agent.agentId,label:agent.agentName||'CEO',charRow:_roleCategoryToCharRow(agent.roleCategory),
deskSprite:'desk-boss',deskX:292,deskY:82,deskW:136,seatX:319,seatY:113,face:'R',
dock:[350,164],roam:[[306,196],[396,196]],boss:true,
deskSprite:'desk-boss',deskX:294,deskY:188,deskW:124,seatX:319,seatY:218,face:'R',
dock:[350,268],roam:[[304,280],[402,280]],boss:true,
};
}
const [deskX,deskY,face]=_stationSlot(slot);
const deskW=112;
const seatX=face==='L' ? deskX+48 : deskX+4;
const [deskX,deskY,face]=(slots && slots[slot]) || [308,318,'R'];
const deskW=104;
const seatX=deskX+4;
const seatY=deskY+36;
return {
key:_safeDeskKey(agent.agentId),agentKey:agent.agentId,label:agent.agentName||agent.agentId,
charRow:_roleCategoryToCharRow(agent.roleCategory),deskSprite:_stationSpriteFor(slot,agent.roleCategory),
deskX,deskY,deskW,seatX,seatY,face,boss:false,
dock:[deskX+(face==='L'?72:32),deskY+82],
roam:[[deskX+18,deskY+116],[deskX+78,deskY+102]],
dock:[deskX+32,deskY+82],
roam:[[deskX+12,deskY+112],[deskX+74,deskY+104]],
};
}
function _rebuildScene(nextStations, props){
@@ -160,31 +167,41 @@ function _rebuildScene(nextStations, props){
function _restoreDefaultScene(){
_rebuildScene(DEFAULT_STATIONS,DEFAULT_PROPS);
_layoutSource='default';
_lastPipelineLayoutSignature='';
_lastAutoLayoutSignature='';
const shell=document.querySelector('.office-shell');
if(shell) shell.dataset.layout='default';
}
function _syncPipelineLayout(pipeline, roster){
function _looksLikeLegacyShowroomLayout(snap){
if(!snap || !Array.isArray(snap.cells) || snap.cells.length!==8) return false;
return snap.cells.every(c=>{
const legacy=LEGACY_SHOWROOM_POS[c && c.roleKey];
return !!legacy
&& Math.abs((c.deskX||0)-legacy[0])<=2
&& Math.abs((c.deskY||0)-legacy[1])<=2;
});
}
function _syncOfficeLayout(pipeline, roster){
if(_hasSavedLayout || _editMode) return;
const stages=pipeline && Array.isArray(pipeline.stages) ? pipeline.stages : [];
if(!stages.length){
if(_layoutSource==='pipeline') _restoreDefaultScene();
const ordered=stages.length ? _pipelineRosterOrder(roster,pipeline) : _uniqueAgentsById(roster||[]);
if(!ordered.length){
if(_layoutSource!=='default') _restoreDefaultScene();
return;
}
const ordered=_pipelineRosterOrder(roster,pipeline);
const signature=ordered.map(r=>r.agentId).join('|')+'::'+stages.map(s=>(s.label||'')+':'+(_resolvedStageAgentId(s,ordered)||'')).join('|');
if(_layoutSource==='pipeline' && signature===_lastPipelineLayoutSignature) return;
if(_layoutSource==='roster' && signature===_lastAutoLayoutSignature) return;
const ceo=ordered.find(r=>r.agentId==='ceo' || r.roleCategory==='ceo');
const workers=ordered.filter(r=>r!==ceo);
const slots=_stationSlots(workers.length);
const next=[];
if(ceo) next.push(_makeStationForRosterAgent(ceo,0));
workers.forEach((agent,idx)=>next.push(_makeStationForRosterAgent(agent,idx)));
if(ceo) next.push(_makeStationForRosterAgent(ceo,0,slots));
workers.forEach((agent,idx)=>next.push(_makeStationForRosterAgent(agent,idx,slots)));
if(next.length){
_rebuildScene(next,DEFAULT_PROPS);
_layoutSource='pipeline';
_lastPipelineLayoutSignature=signature;
_layoutSource='roster';
_lastAutoLayoutSignature=signature;
const shell=document.querySelector('.office-shell');
if(shell) shell.dataset.layout='pipeline';
if(shell) shell.dataset.layout=stages.length?'pipeline':'roster';
}
}
function _clearPipelineDecor(){
@@ -317,14 +334,7 @@ function addChar(st){
function buildStation(st){ addDesk(st); if(!st.noChar) 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();
_restoreDefaultScene();
// ── 4방향 dir 매핑 ──
// PNG 파일명 규약: walk-r<row>-d<DIR>-f<frame>.png
// 사용자의 sprite 컨벤션이 다르면 이 4 상수만 수정하면 됨.
@@ -898,8 +908,6 @@ function _previewAgent(agentId, on, item){
if(ch) ch.classList.toggle('preview', !!on);
}
// refactor #G-full — roster 에 있는 agent 중 desk 가 없는 경우 자동 생성.
// 한 번 처리된 agentId 는 _autoDeskedFor 에 기록 → 사용자가 그 desk 를 지워도 재생성 안 함.
const _autoDeskedFor = new Set();
function _roleCategoryToCharRow(role){
switch(role){
case 'ceo': return 0;
@@ -966,8 +974,8 @@ function applyFromSnapshot(snap){
const roster = Array.isArray(snap.roster) ? snap.roster : [];
const orderedRoster = _pipelineRosterOrder(roster, snap.pipeline);
_renderRoster(orderedRoster, snap.activeAgentId);
_syncPipelineLayout(snap.pipeline, orderedRoster);
if(_layoutSource!=='pipeline' || _hasSavedLayout) _ensureRosterDesks(orderedRoster);
_syncOfficeLayout(snap.pipeline, orderedRoster);
if(_layoutSource!=='roster' || _hasSavedLayout) _ensureRosterDesks(orderedRoster);
_renderPipelineDecor(snap.pipeline, orderedRoster);
const active = (snap.activeAgentId && roster.find(a => a.agentId === snap.activeAgentId)) || roster[0];
const activeStage = snap.pipeline && Array.isArray(snap.pipeline.stages)
@@ -1570,12 +1578,13 @@ window.addEventListener('message', e=>{
const d = e.data;
if(!d || typeof d !== 'object') return;
if(d.type === 'pixelOfficeLayoutLoaded'){
_hasSavedLayout = !!d.value;
if(d.value) {
const saved = d.value && !_looksLikeLegacyShowroomLayout(d.value) ? d.value : null;
_hasSavedLayout = !!saved;
if(saved) {
_layoutSource = 'saved';
_restoreLayout(d.value);
_restoreLayout(saved);
} else if(_latestSnapshot) {
_syncPipelineLayout(_latestSnapshot.pipeline, _pipelineRosterOrder(_latestSnapshot.roster || [], _latestSnapshot.pipeline));
_syncOfficeLayout(_latestSnapshot.pipeline, _pipelineRosterOrder(_latestSnapshot.roster || [], _latestSnapshot.pipeline));
_renderPipelineDecor(_latestSnapshot.pipeline, _latestSnapshot.roster || []);
}
}