.05){ctx.fillStyle=`rgba(4,11,18,${light.night*.16})`;ctx.fillRect(0,0,r.width,r.height);}if(light.dawn>.08){ctx.fillStyle=`rgba(225,145,84,${light.dawn*.08})`;ctx.fillRect(0,0,r.width,r.height);}const vignette=ctx.createRadialGradient(r.width/2,r.height/2,Math.min(r.width,r.height)*.28,r.width/2,r.height/2,Math.max(r.width,r.height)*.68);vignette.addColorStop(0,'rgba(0,0,0,0)');vignette.addColorStop(1,'rgba(3,8,6,.16)');ctx.fillStyle=vignette;ctx.fillRect(0,0,r.width,r.height);}
- drawMini(minute){if(!this.mini||!this.state)return;const ctx=this.mini.getContext('2d'),w=this.mini.width,h=this.mini.height;ctx.clearRect(0,0,w,h);for(let y=0;y<100;y+=4)for(let x=0;x<100;x+=4){const t=terrainAtPublic(this.state,x+2,y+2);ctx.fillStyle=terrainPalette(t.kind,.12);ctx.fillRect(x/100*w,y/100*h,4/100*w+1,4/100*h+1);}for(const b of this.state.buildings||[])if(b.massKg>0&&b.condition>0){ctx.fillStyle='#e3ca8b';ctx.fillRect(b.position.x/100*w-1,b.position.y/100*h-1,3,3);}for(const p of this.state.projects||[])if(p.status==='construction'){ctx.fillStyle='#c78c56';ctx.fillRect(p.site.x/100*w-1,p.site.y/100*h-1,3,3);}for(const c of this.state.citizens||[])if(c.alive){const p=citizenPosition(c,this.state);ctx.fillStyle=c.id===this.selected?'#ffe09a':c.kind==='HUMAN_LINKED'?'#8fcfff':'#d7e5bd';ctx.fillRect(p.x/100*w,p.y/100*h,2,2);}ctx.strokeStyle='#f3dda0';ctx.lineWidth=1;ctx.beginPath();for(const [i,xy] of [[0,[0,0]],[1,[this.camera.width,0]],[2,[this.camera.width,this.camera.height]],[3,[0,this.camera.height]]]){const p=this.unproject(...xy);if(i===0)ctx.moveTo(p.x/100*w,p.y/100*h);else ctx.lineTo(p.x/100*w,p.y/100*h);}ctx.closePath();ctx.stroke();}
+ drawMini(minute){if(!this.mini||!this.state)return;const ctx=this.mini.getContext('2d'),w=this.mini.width,h=this.mini.height;ctx.clearRect(0,0,w,h);for(let y=0;y<100;y+=4)for(let x=0;x<100;x+=4){const t=terrainAtPublic(this.state,x+2,y+2);ctx.fillStyle=terrainPalette(t.kind,.12);ctx.fillRect(x/100*w,y/100*h,4/100*w+1,4/100*h+1);}for(const b of this.state.buildings||[])if(b.massKg>0&&b.condition>0){ctx.fillStyle='#e3ca8b';ctx.fillRect(b.position.x/100*w-1,b.position.y/100*h-1,3,3);}for(const p of this.state.projects||[])if(p.status==='construction'){ctx.fillStyle='#c78c56';ctx.fillRect(p.site.x/100*w-1,p.site.y/100*h-1,3,3);}for(const c of this.state.citizens||[])if(c.alive){const p=citizenPosition(c,this.state);ctx.fillStyle=c.id===this.ownedCitizen?'#ffffff':c.id===this.selected?'#ffe09a':c.kind==='HUMAN_LINKED'?'#8fcfff':'#d7e5bd';const size=c.id===this.ownedCitizen?3:2;ctx.fillRect(p.x/100*w-size/2,p.y/100*h-size/2,size,size);}ctx.strokeStyle='#f3dda0';ctx.lineWidth=1;ctx.beginPath();for(const [i,xy] of [[0,[0,0]],[1,[this.camera.width,0]],[2,[this.camera.width,this.camera.height]],[3,[0,this.camera.height]]]){const p=this.unproject(...xy);if(i===0)ctx.moveTo(p.x/100*w,p.y/100*h);else ctx.lineTo(p.x/100*w,p.y/100*h);}ctx.closePath();ctx.stroke();}
}
diff --git a/site/sovereign-world.css b/site/sovereign-world.css
index e141b2c..6b8016d 100644
--- a/site/sovereign-world.css
+++ b/site/sovereign-world.css
@@ -6,7 +6,7 @@
.inspector{position:absolute;z-index:16;right:12px;top:91px;width:min(360px,calc(100vw - 24px));max-height:calc(100vh - 190px);overflow:auto;padding:18px;border-radius:3px}.inspector>.panel-close{position:sticky;float:right;top:0;margin:-8px -8px 4px 8px}.eyebrow{display:block;font-size:8px;letter-spacing:.18em;color:#765b32;margin:6px 0}.inspector h2,.game-window h2{font:700 25px/1.05 var(--ui-font);margin:4px 0 8px;color:#2c2517}.inspector h3{font:700 13px var(--ui-font);margin:9px 0 4px}.inspector p{font-size:11px;line-height:1.45}.inspector-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:4px;margin:12px 0}.inspector-grid div{background:#8e764233;border:1px solid #7d653633;padding:7px;text-align:center}.inspector-grid b{display:block;font:700 13px var(--ui-font)}.inspector-grid small{display:block;font-size:7px;text-transform:uppercase}.inspector-section{border-top:1px solid #876b3a55;margin-top:10px;padding-top:7px}.tag{display:inline-block;border:1px solid #836b3e;background:#fff8d943;border-radius:2px;padding:3px 5px;margin:2px;font-size:8px;color:#362d1c}
.mini-map-shell{position:absolute;z-index:13;left:12px;bottom:18px;width:260px;padding:7px;border-radius:3px}.mini-title{height:24px;display:flex;justify-content:space-between;align-items:center;padding:0 4px;font:700 9px var(--ui-font);letter-spacing:.12em;color:#dfc787}.mini-title small{font-size:7px;color:#a8956a}#miniMap{display:block;width:100%;height:148px;border:2px inset #6e5936;background:#0d160f;image-rendering:auto}
.game-dock{position:absolute;z-index:14;left:50%;bottom:15px;transform:translateX(-50%);height:67px;display:flex;align-items:stretch;padding:5px;border-radius:4px}.game-dock button{width:78px;border:0;border-right:1px solid #6e5938;background:linear-gradient(#4c3b25,#302719);color:#e8d49c;text-shadow:0 1px #000}.game-dock button:last-child{border-right:0}.game-dock button:hover{background:linear-gradient(#6b5330,#44351f);color:#fff0bd}.game-dock button span{display:block;font:700 20px var(--ui-font);line-height:28px}.game-dock button b{font:700 8px var(--ui-font);text-transform:uppercase;letter-spacing:.05em}
-.game-window{position:absolute;z-index:30;left:50%;top:50%;transform:translate(-50%,-50%);width:min(920px,calc(100vw - 40px));height:min(680px,calc(100vh - 80px));border-radius:4px;padding:20px;overflow:hidden}.game-window[hidden]{display:none}.game-window>header{display:flex;justify-content:space-between;border-bottom:1px solid #7d6338;padding-bottom:12px}.game-window>header span{font-size:8px;letter-spacing:.2em;color:#775d32}.game-window>header p{font-size:10px;margin:4px 0}.history-toolbar{display:flex;gap:8px;padding:10px 0}.history-toolbar input,.history-toolbar select,.agent-copy textarea{background:#f4e8c5;border:1px solid #7c6135;color:#2b2417;padding:8px}.history-toolbar input{flex:1}.history-timeline{height:calc(100% - 130px);overflow:auto;padding-right:8px}.history-entry{display:grid;grid-template-columns:105px 16px 1fr;gap:8px;padding:9px 0;border-bottom:1px solid #80694142;cursor:pointer}.history-entry:hover{background:#fff5d632}.history-date{font:700 8px var(--ui-font);color:#765d34}.history-node{width:7px;height:7px;border-radius:50%;background:#71562f;margin-top:4px;box-shadow:0 0 0 3px #8b704033}.history-entry b{font:700 11px var(--ui-font)}.history-entry small{display:block;font-size:8px;margin-top:3px;opacity:.75}.why-panel{position:absolute;right:18px;bottom:18px;width:min(390px,45%);max-height:45%;overflow:auto;background:#241d13;color:#ead9ab;border:1px solid #9d7d45;padding:13px;box-shadow:0 10px 30px #0008}.why-panel pre{white-space:pre-wrap;font-size:8px}.agent-window{width:min(640px,calc(100vw - 40px));height:auto;max-height:80vh}.agent-copy{padding-top:14px}.agent-copy textarea{width:100%;resize:vertical}.agent-copy .primary{display:block;margin-top:8px;padding:10px 16px;border:1px solid #604923;background:#72562d;color:#fff0bd}.agent-copy a{display:inline-block;margin-top:10px;color:#644a24}
+.game-window{position:absolute;z-index:30;left:50%;top:50%;transform:translate(-50%,-50%);width:min(920px,calc(100vw - 40px));height:min(680px,calc(100vh - 80px));border-radius:4px;padding:20px;overflow:hidden}.game-window[hidden]{display:none}.game-window>header{display:flex;justify-content:space-between;border-bottom:1px solid #7d6338;padding-bottom:12px}.game-window>header span{font-size:8px;letter-spacing:.2em;color:#775d32}.game-window>header p{font-size:10px;margin:4px 0}.history-toolbar{display:flex;gap:8px;padding:10px 0}.history-toolbar input,.history-toolbar select,.agent-copy textarea{background:#f4e8c5;border:1px solid #7c6135;color:#2b2417;padding:8px}.history-toolbar input{flex:1}.history-timeline{height:calc(100% - 130px);overflow:auto;padding-right:8px}.history-entry{display:grid;grid-template-columns:105px 16px 1fr;gap:8px;padding:9px 0;border-bottom:1px solid #80694142;cursor:pointer}.history-entry:hover{background:#fff5d632}.history-date{font:700 8px var(--ui-font);color:#765d34}.history-node{width:7px;height:7px;border-radius:50%;background:#71562f;margin-top:4px;box-shadow:0 0 0 3px #8b704033}.history-entry b{font:700 11px var(--ui-font)}.history-entry small{display:block;font-size:8px;margin-top:3px;opacity:.75}.why-panel{position:absolute;right:18px;bottom:18px;width:min(390px,45%);max-height:45%;overflow:auto;background:#241d13;color:#ead9ab;border:1px solid #9d7d45;padding:13px;box-shadow:0 10px 30px #0008}.why-panel pre{white-space:pre-wrap;font-size:8px}.agent-window{width:min(640px,calc(100vw - 40px));height:auto;max-height:80vh}.agent-copy{padding-top:14px}.agent-copy textarea{width:100%;resize:vertical}.agent-copy .primary{display:block;margin-top:8px;padding:10px 16px;border:1px solid #604923;background:#72562d;color:#fff0bd}.agent-copy a{display:inline-block;margin-top:10px;color:#644a24}.game-dock button small{display:block;font-size:6px;line-height:8px;letter-spacing:.08em;color:#9da98a}.game-dock button small[data-state="alive"]{color:#9be095}.game-dock button small[data-state="dead"]{color:#d88976}.avatar-login{padding:9px 12px;border:1px solid #765a31;background:#e7d7aa}.avatar-identity{display:flex;align-items:center;gap:12px;padding:8px 0 12px;border-bottom:1px solid #876b3a55}.avatar-identity img{width:44px;height:44px;border-radius:50%;border:2px solid #765a31}.avatar-identity h3{margin:2px 0;font:700 18px var(--ui-font)}.avatar-identity small{font-size:8px;letter-spacing:.06em}.avatar-actions{display:flex;gap:6px;padding:10px 0}.avatar-actions button{flex:1;padding:8px;border:1px solid #765a31;background:#5f492d;color:#f4e4b4}.avatar-stats,.avatar-evolution-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:5px}.avatar-stats div,.avatar-evolution-grid span{background:#8e764233;border:1px solid #7d653633;padding:8px;text-align:center}.avatar-stats b,.avatar-evolution-grid b{display:block;font:700 14px var(--ui-font)}.avatar-stats small,.avatar-evolution-grid small{display:block;font-size:7px;text-transform:uppercase}.avatar-section{border-top:1px solid #876b3a55;margin-top:10px;padding-top:8px}.avatar-section h3{font:700 13px var(--ui-font);margin:0 0 5px}.avatar-life-row{display:grid;grid-template-columns:125px 1fr;gap:8px;padding:5px 0;border-bottom:1px solid #80694133;font-size:8px}.avatar-life-row span{color:#765d34}.avatar-direction textarea{margin-top:5px}
.truth-badge{position:absolute;z-index:12;right:13px;bottom:18px;display:flex;align-items:center;gap:7px;padding:6px 9px;background:#1a2117dd;border:1px solid #675a3b;color:#cdbf92;font-size:7px;letter-spacing:.12em}.truth-badge i{width:7px;height:7px;border-radius:50%;background:#d1a84d}.truth-badge[data-mode="LIVE"] i{background:#73b56d;box-shadow:0 0 7px #73b56d}.truth-badge[data-mode="DEGRADED"] i,.truth-badge[data-mode="RECONNECTING"] i{background:#d38b45}.truth-badge[data-mode="REPLAY"] i{background:#aa6254}.control-hints{position:absolute;z-index:9;left:50%;top:84px;transform:translateX(-50%);font-size:7px;color:#d8c996aa;letter-spacing:.08em;text-shadow:0 1px #000}.control-hints span{color:#f1dfaa}.control-hints i:after{content:'·';margin:0 8px}.toast-layer{position:absolute;right:15px;top:88px;z-index:50;display:grid;gap:6px}.toast{background:#2f2719;color:#ecdca9;border:1px solid #8b7041;padding:8px 12px;box-shadow:0 6px 20px #0007;font-size:9px}
.world-loader{position:absolute;inset:0;z-index:100;background:#0c120d;display:grid;place-content:center;text-align:center;transition:opacity .45s,visibility .45s}.world-loader.ready{opacity:0;visibility:hidden}.world-loader strong{font:700 25px var(--ui-font);letter-spacing:.13em;color:#e0c77f}.world-loader span{font-size:9px;letter-spacing:.15em;color:#968662;margin-top:8px}.world-loader-mark{display:flex;gap:5px;justify-content:center;margin-bottom:12px}.world-loader-mark i{width:8px;height:18px;background:#947541;animation:loader 1s infinite alternate}.world-loader-mark i:nth-child(2){animation-delay:.2s}.world-loader-mark i:nth-child(3){animation-delay:.4s}@keyframes loader{to{transform:translateY(-7px);background:#d8bd73}}
@media(max-width:900px){.game-topbar{height:58px;gap:5px}.brand{min-width:110px}.brand b{font-size:17px}.brand span{display:none}.world-clock{min-width:150px;padding:4px 8px}.top-metrics span{min-width:55px;padding:4px}.environment-metric{display:none}.activity-rail{top:78px;max-height:45vh;width:225px}.mini-map-shell{width:190px;bottom:83px}.mini-map-shell canvas{height:110px}.game-dock{bottom:8px;width:calc(100vw - 14px);height:60px}.game-dock button{flex:1;width:auto}.game-dock button b{font-size:6px}.truth-badge{bottom:75px}.control-hints{display:none}.inspector{top:72px;max-height:calc(100vh - 150px)}}
diff --git a/site/sovereign-world.js b/site/sovereign-world.js
index d34ec27..32e7a51 100644
--- a/site/sovereign-world.js
+++ b/site/sovereign-world.js
@@ -4,7 +4,7 @@ import {observerConceptView} from './observer-concepts.js';
const $=id=>document.getElementById(id);
const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const ICON={MOVE:'→',OBSERVE:'◉',REST:'·',SLEEP:'z',EAT:'•',DRINK:'≈',GATHER:'⌁',CARRY:'▣',CUT:'╱',DIG:'⌄',BUILD:'⌂',CARE:'+',TEACH:'◇',COMMUNICATE:'◇',EXPERIMENT:'✦',ATTACK:'⚠',DEFEND:'◈',TRANSFER:'↔',PROMISE:'∞',CLAIM:'⌁',REPRODUCE:'◌'};
-const state={world:null,selected:null,renderer:null,keys:new Set(),lastFrame:performance.now(),lastHud:0,connection:null,mode:CONNECTION.CONNECTING};
+const state={world:null,selected:null,renderer:null,myAvatar:null,keys:new Set(),lastFrame:performance.now(),lastHud:0,connection:null,mode:CONNECTION.CONNECTING};
async function getJSON(url,opts={}){const r=await fetch(url,{cache:'no-store',credentials:'same-origin',...opts});if(!r.ok)throw new Error(`${r.status}`);return r.json();}
function dateLabel(min){const m=Math.max(0,Math.floor(min)),year=Math.floor(m/525600)+1,rem=m%525600,day=Math.floor(rem/1440)+1,hour=Math.floor((rem%1440)/60),minute=rem%60;return `YEAR ${year} · DAY ${day} · ${String(hour).padStart(2,'0')}:${String(minute).padStart(2,'0')}`;}
function climateState(w){const minute=worldMinute(w),dayPhase=Number(w.environment?.dayPhase??((minute%1440)/1440)),seasonPhase=Number(w.environment?.seasonPhase??((minute%525600)/525600)),temperature=Number(w.environment?.temperatureC??18),rain=Number(w.environment?.precipitation??0),seasons=['DEEPWINTER','THAW','HIGHSUN','HARVEST'],season=seasons[Math.floor((((seasonPhase+.125)%1)+1)%1*4)%4];return{dayPhase,season,temperature,rain,night:dayPhase<.21||dayPhase>.79};}
@@ -12,18 +12,19 @@ function citizenName(c){return c.selfName||c.observerDisplayName||c.id.replace('
function goalLabel(g){if(!g)return 'No long-term goal exposed';if(typeof g==='string')return g;const actions=Array.isArray(g.actionTypes)?g.actionTypes:[],concepts=Array.isArray(g.concepts)?g.concepts:[];return [actions.length?actions.join(' → '):'',concepts.length?`concepts ${concepts.join(', ')}`:''].filter(Boolean).join(' · ')||'Self-directed';}
function modeLabel(mode){return({[CONNECTION.LIVE]:'LIVE · CANONICAL WORLD',[CONNECTION.DEGRADED]:'DEGRADED · LAST CANONICAL STATE',[CONNECTION.REPLAY]:'OFFLINE · GENESIS REPLAY',[CONNECTION.RECONNECTING]:'RECONNECTING TO CANONICAL WORLD',[CONNECTION.CONNECTING]:'CONNECTING TO CANONICAL WORLD'})[mode]||mode;}
function setConnectionMode(mode,detail){state.mode=mode;const badge=document.querySelector('.truth-badge');if(badge)badge.dataset.mode=mode;$('truthText').textContent=modeLabel(mode);$('worldStatus').textContent=mode===CONNECTION.LIVE?'CANONICAL WORLD':mode;if(mode===CONNECTION.DEGRADED&&detail?.retryInMs)$('worldStatus').textContent=`RETRY ${Math.ceil(detail.retryInMs/1000)}s`;}
-function acceptWorld(world,meta={}){state.world=world;state.renderer?.setState(world);$('worldLoader')?.classList.add('ready');renderAll();}
+function acceptWorld(world,meta={}){state.world=world;state.renderer?.setState(world);state.renderer?.setOwnedCitizen?.(state.myAvatar?.citizenId||null);$('worldLoader')?.classList.add('ready');renderAll();}
function renderTop(){
const w=state.world;if(!w)return;
- const living=w.citizens.filter(c=>c.alive).length,climate=climateState(w),visibility=state.renderer?.populationVisibility?.()||{visible:living,total:living};
+ const living=w.citizens.filter(c=>c.alive).length,embodied=w.citizens.length,human=w.citizens.filter(c=>c.kind==='HUMAN_LINKED').length,climate=climateState(w),visibility=state.renderer?.populationVisibility?.()||{visible:living,total:living};
$('populationValue').textContent=living;
- if($('populationLabel'))$('populationLabel').textContent=`PEOPLE · ${visibility.visible} VISIBLE`;
+ if($('populationLabel'))$('populationLabel').textContent=`LIVING · ${embodied} TOTAL · ${human} HUMAN · ${visibility.visible} VISIBLE`;
$('structureValue').textContent=(w.buildings||[]).length;
$('organizationValue').textContent=(w.organizations||[]).length;
$('environmentValue').textContent=`${Math.round(climate.temperature)}° ${climate.season}`;
$('weatherValue').textContent=climate.rain>.08?`RAIN ${Math.round(climate.rain*100)}%`:climate.night?'NIGHT':'CLEAR';
$('worldDate').textContent=dateLabel(worldMinute(w));
$('miniMode').textContent=state.mode===CONNECTION.LIVE?'LIVE':'OBSERVER';
+ renderMyAvatarStatus();
}
function significantHistory(w){const rows=[...(w.history||[])];const rank={conflict:9,life:8,construction:8,society:8,culture:8,language:7,knowledge:7,genesis:6};return rows.sort((a,b)=>(b.worldMinute-a.worldMinute)||(rank[b.category]||0)-(rank[a.category]||0)).slice(0,14);}
function renderActivity(){const w=state.world;if(!w)return;const history=significantHistory(w);if(history.length){$('activityList').innerHTML=history.map(e=>`${e.category==='life'?'✦':e.category==='construction'?'⌂':e.category==='conflict'?'⚠':e.category==='language'?'◇':'•'} ${esc(e.label)} ${esc(e.date?.label||dateLabel(e.worldMinute))} · ${esc(e.category)}
`).join('');return;}const now=worldMinute(w),rows=w.citizens.filter(c=>c.alive&&c.currentAction).sort((a,b)=>{const am=a.currentAction.type==='MOVE'?1:0,bm=b.currentAction.type==='MOVE'?1:0;return am-bm||a.currentAction.endsWorldMinute-b.currentAction.endsWorldMinute;}).slice(0,14);$('activityList').innerHTML=rows.map(c=>{const a=c.currentAction,span=Math.max(1,a.endsWorldMinute-a.startedWorldMinute),progress=clamp((now-a.startedWorldMinute)/span*100,0,100);return `${ICON[a.type]||'·'} ${esc(citizenName(c))} ${esc(a.type.toLowerCase())} · ${esc(a.purpose||'self-directed')}
`;}).join('')||'… Quiet interval No significant canonical event right now.
';}
@@ -35,14 +36,98 @@ function renderInspector(){const w=state.world;if(!w)return;const c=w.citizens.f
$('inspectorBody').innerHTML=`${esc(c.kind)} · ${c.alive?'ALIVE':'DEAD'} ${esc(citizenName(c))} ${age} years
${Math.round(c.body.health)}% health
${Math.round(c.body.hydration)}% water
${Math.round(c.body.calories)}% energy
CURRENT ACTION ${esc(a?.type||'IDLE / THINKING')} ${esc(a?.purpose||'No public physical action recorded.')}
${a?`
WHY? `:''}
Current goal ${esc(goalLabel(c.activeGoal))}
Body / exposure sleep ${Math.round(c.body.sleepPressure||0)}% temperature ${Number(c.body.temperatureC||36.6).toFixed(1)}°C terrain ${esc(c.body.exposure?.terrain||'unknown')} rain exposure ${Math.round((c.body.exposure?.rainExposure||0)*100)}% injuries ${Number(c.body.injuries||0)} diseases ${Number(c.body.diseases||0)} Mind / traits ${psych.map(([name,v])=>`
${esc(name)} ${Math.round(v*100)}% `).join('')||'
No public trait state.
'}
Known concepts ${conceptViews.length?conceptViews.slice(0,28).map(v=>`
${esc(v.label)} `).join(''):`
${c.knowledge?.count!=null?`${c.knowledge.count} private concepts`:'No exposed concepts yet.'}
`}
Language ${Object.entries(c.language?.lexicon||{}).slice(0,18).map(([concept,token])=>{const v=conceptById.get(concept)||observerConceptView(w,c,{concept,confidence:null,provenance:[]});return `
${esc(token)} ↔ ${esc(v.label)} `;}).join('')||'
No stable coined lexicon yet.
'}
Relationships ${rels.map(([id,r])=>`
${esc(id.replace('genesis:','C'))} · trust ${Math.round((r.trust||0)*100)} · affection ${Math.round((r.affection||0)*100)} · fear ${Math.round((r.fear||0)*100)} `).join('')||'
No significant public relationship data yet.
'}
Possessions ${poss.map(id=>`
${esc(objectLabel(id))} `).join('')||'
No individually held objects.
'}
`;}
function renderHistory(){const history=state.world?.history||[],q=$('historySearch').value.trim().toLowerCase(),cat=$('historyCategory').value,rows=history.filter(e=>(cat==='all'||e.category===cat)&&(!q||JSON.stringify(e).toLowerCase().includes(q))).sort((a,b)=>b.worldMinute-a.worldMinute);$('historyTimeline').innerHTML=rows.length?rows.map(e=>`${esc(e.date?.label||dateLabel(e.worldMinute))} ${esc(e.label)} ${esc(e.era||'Emergent history')} · ${esc(e.category)} · ${esc(e.actorId||'world')}
`).join(''):'No canonical history matches this filter.
';}
async function showWhy(id){const p=$('whyPanel');p.hidden=false;p.innerHTML='WHY? Tracing canonical causes…
';try{if(String(id).startsWith('observer:')){const e=(state.world.history||[]).find(x=>x.eventId===id);p.innerHTML=`OBSERVER CLASSIFICATION ${esc(e?.label||'Derived historical label')} ${esc(e?.era||'')} ${e?.confidence!=null?`· confidence ${Math.round(e.confidence*100)}%`:''}
This label is descriptive. It never mutates world state.
${e?.payload?`${esc(JSON.stringify(e.payload,null,2))} `:''}${(e?.causes||[]).length} canonical cause(s) `;return}const d=await getJSON(`/api/v2/why/${encodeURIComponent(id)}`);p.innerHTML=`CANONICAL CAUSAL TRACE ${esc(d.why.event.type)} World minute ${d.why.event.worldMinute} · actor ${esc(d.why.event.actorId)}
${esc(JSON.stringify(d.why.event.payload,null,2))} ${d.why.causes?.length||0} direct causal parent(s) `;}catch(e){p.innerHTML=`WHY? Trace unavailable: ${esc(e.message)}
`;}}
-function renderAll(){renderTop();renderActivity();renderInspector();if(!$('societyHistory').hidden)renderHistory();}
+function worldDuration(minutes){
+ const m=Math.max(0,Math.floor(Number(minutes)||0)),days=Math.floor(m/1440),hours=Math.floor((m%1440)/60);
+ return `${days}d ${hours}h`;
+}
+function currentOwnedCitizen(){
+ const id=state.myAvatar?.citizenId;
+ return id?state.world?.citizens?.find(c=>c.id===id)||null:null;
+}
+function renderMyAvatarStatus(){
+ const el=$('myAvatarStatus');if(!el)return;
+ if(!state.myAvatar?.authenticated){el.textContent='SIGN IN';el.dataset.state='guest';return;}
+ const c=currentOwnedCitizen();
+ if(!c){el.textContent='LINKED';el.dataset.state='linked';return;}
+ el.textContent=c.alive?'● ALIVE':'† DEAD';el.dataset.state=c.alive?'alive':'dead';
+}
+function avatarRecentEvents(c){
+ const rows=[...(state.world?.recentLedger||[]),...(state.world?.history||[])].filter(e=>e?.actorId===c.id);
+ const seen=new Set();
+ return rows.sort((a,b)=>Number(b.worldMinute||0)-Number(a.worldMinute||0)).filter(e=>{
+ const key=e.eventId||`${e.type}|${e.worldMinute}`;if(seen.has(key))return false;seen.add(key);return true;
+ }).slice(0,8);
+}
+function renderMyAvatar(){
+ const guest=$('avatarGuest'),dash=$('avatarDashboard');
+ if(!guest||!dash)return;
+ const linked=Boolean(state.myAvatar?.authenticated&&state.myAvatar?.citizenId);
+ guest.hidden=linked;dash.hidden=!linked;renderMyAvatarStatus();
+ if(!linked)return;
+ const c=currentOwnedCitizen(),actor=state.myAvatar.actor||{};
+ if(!c){$('avatarIdentityName').textContent=actor.displayName||actor.githubLogin||'Linked Citizen';$('avatarIdentityStatus').textContent='Synchronizing canonical Citizen…';return;}
+ const img=$('avatarIdentityImage');
+ if(actor.avatarUrl){img.src=actor.avatarUrl;img.hidden=false;}else img.hidden=true;
+ $('avatarIdentityName').textContent=citizenName(c)||actor.displayName||actor.githubLogin||'My Citizen';
+ $('avatarIdentityStatus').textContent=`${c.kind} · ${c.alive?'ALIVE':'DEAD'} · embodied ${worldDuration(worldMinute(state.world)-Number(c.birthWorldMinute||0))} ago`;
+ const knowledgeCount=Array.isArray(c.knowledge)?c.knowledge.length:Number(c.knowledge?.count||0);
+ const vocabularyCount=Object.keys(c.language?.lexicon||{}).length;
+ const relationshipsCount=Object.keys(c.relationships||{}).length;
+ const possessionsCount=(c.possessions||[]).length;
+ $('avatarStats').innerHTML=`${Math.round(c.body.health||0)}% health
${knowledgeCount} knowledge
${vocabularyCount} words
${relationshipsCount} relations
`;
+ const a=c.currentAction;
+ $('avatarNow').innerHTML=`${esc(a?.type||'IDLE / THINKING')} — ${esc(a?.purpose||'No public physical action recorded.')}
water ${Math.round(c.body.hydration||0)}% energy ${Math.round(c.body.calories||0)}% sleep ${Math.round(c.body.sleepPressure||0)}% possessions ${possessionsCount} Goal: ${esc(goalLabel(c.activeGoal))}
`;
+ $('avatarEvolution').innerHTML=`${knowledgeCount} concepts learned ${vocabularyCount} coined/shared words ${relationshipsCount} known relationships ${possessionsCount} possessions
`;
+ const events=avatarRecentEvents(c);
+ $('avatarTimeline').innerHTML=events.length?events.map(e=>`${esc(dateLabel(e.worldMinute||0))} ${esc(e.label||e.type||'Canonical event')}
`).join(''):'No recent public canonical events for this Citizen yet.
';
+}
+async function refreshMyAvatar({quiet=false}={}){
+ try{
+ const d=await getJSON('/api/v2/avatar',{
+ method:'POST',
+ headers:{'content-type':'application/json'},
+ body:'{}'
+ });
+
+ state.myAvatar={
+ authenticated:true,
+ citizenId:d.avatar?.citizenId||null,
+ actor:d.actor||{}
+ };
+
+ state.renderer?.setOwnedCitizen?.(state.myAvatar.citizenId);
+ renderMyAvatar();
+ renderTop();
+
+ return state.myAvatar;
+ }catch(error){
+ if(error.message==='401'){
+ state.myAvatar=null;
+ state.renderer?.setOwnedCitizen?.(null);
+ renderMyAvatar();
+ renderMyAvatarStatus();
+ return null;
+ }
+
+ if(!quiet)toast('Avatar link unavailable');
+ return null;
+ }
+}
+function locateMyAvatar({follow=false,inspect=false}={}){
+ const id=state.myAvatar?.citizenId;if(!id){toast('Sign in with GitHub first');return false;}
+ const ok=state.renderer?.locateCitizen?.(id,{follow});
+ if(!ok){toast(currentOwnedCitizen()?.alive===false?'Your Citizen is dead':'Avatar position unavailable');return false;}
+ if(inspect){state.selected=id;state.renderer.setSelected(id);renderInspector();}
+ toast(follow?'Following your Citizen':'Located your Citizen');return true;
+}
+function renderAll(){renderTop();renderActivity();renderInspector();renderMyAvatar();if(!$('societyHistory').hidden)renderHistory();}
function createSocket({onWorld,onOpen,onClose,onError}){const url=new URL('/api/v2/stream',location.href);url.protocol=location.protocol==='https:'?'wss:':'ws:';const ws=new WebSocket(url);ws.onopen=onOpen;ws.onmessage=e=>{try{const m=JSON.parse(e.data);if(m.state)onWorld(m.state);}catch(error){onError(error);}};ws.onclose=onClose;ws.onerror=e=>onError(e);return ws;}
function toast(text){const d=document.createElement('div');d.className='toast';d.textContent=text;$('toastLayer').append(d);setTimeout(()=>d.remove(),3200);}
async function sendIntent(){const intent=$('humanIntent').value.trim();if(!intent)return;const feedback=$('agentFeedback');feedback.textContent='Sending direction through the epistemic boundary…';try{const d=await getJSON('/api/v2/intent',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({intent})});feedback.textContent=d.ok?'Direction queued. Your avatar still decides and can fail.':'Direction rejected.';toast('Avatar direction queued');}catch(e){feedback.textContent=e.message==='401'?'Sign in with GitHub first.':`Unavailable: ${e.message}`;}}
function toggleFullscreen(){document.fullscreenElement?document.exitFullscreen?.():$('game').requestFullscreen?.();}
function closeWindows(){let closed=false;for(const id of ['societyHistory','agentWindow']){const el=$(id);if(el&&!el.hidden){el.hidden=true;closed=true;}}$('whyPanel').hidden=true;if(!closed&&state.selected){state.selected=null;state.renderer.setSelected(null);renderInspector();}else if(!closed&&state.renderer.follow)state.renderer.setFollow(null);}
function isTyping(el){return ['INPUT','TEXTAREA','SELECT'].includes(el?.tagName)||el?.isContentEditable;}
-function wire(){const canvas=$('worldCanvas');canvas.addEventListener('worldselect',e=>{state.selected=e.detail.id;state.renderer.setSelected(state.selected);renderInspector();});canvas.addEventListener('dblclick',()=>state.renderer.center());document.addEventListener('click',e=>{const c=e.target.closest('[data-citizen]');if(c){state.selected=c.dataset.citizen;state.renderer.setSelected(state.selected);renderInspector();}const h=e.target.closest('[data-event]');if(h)showWhy(h.dataset.event);const why=e.target.closest('[data-why-action]');if(why)showWhy(why.dataset.whyAction);});$('closeInspector').onclick=()=>{state.selected=null;state.renderer.setSelected(null);renderInspector();};$('toggleRail').onclick=()=>$('activityRail').classList.toggle('collapsed');$('historyOpen').onclick=()=>{$('societyHistory').hidden=false;renderHistory();};$('historyClose').onclick=()=>{$('societyHistory').hidden=true;$('whyPanel').hidden=true;};$('historySearch').oninput=renderHistory;$('historyCategory').onchange=renderHistory;$('agentOpen').onclick=()=>$('agentWindow').hidden=false;$('agentClose').onclick=()=>$('agentWindow').hidden=true;$('sendIntent').onclick=sendIntent;$('centerWorld').onclick=()=>state.renderer.center();$('fitPopulation').onclick=()=>{const ok=state.renderer.fitPopulation();toast(ok?'Framing all living Citizens':'No living Citizens to frame');};$('followSelected').onclick=()=>{state.renderer.setFollow(state.selected);toast(state.selected?'Following selected Citizen':'Select a Citizen first');};$('overlayKnowledge').onclick=()=>state.renderer.toggleOverlay('knowledge');$('overlayRelations').onclick=()=>state.renderer.toggleOverlay('relations');$('fullscreenWorld').onclick=toggleFullscreen;document.addEventListener('keydown',e=>{if(isTyping(e.target)){if(e.key==='Escape')e.target.blur();return}const k=e.key.toLowerCase();if(['arrowup','arrowdown','arrowleft','arrowright','w','a','s','d'].includes(k)){state.keys.add(k);e.preventDefault();}if(k==='f'){toggleFullscreen();e.preventDefault();}else if(k==='escape')closeWindows();else if(k==='h'){$('societyHistory').hidden=!$('societyHistory').hidden;if(!$('societyHistory').hidden)renderHistory();}else if(k==='c')state.renderer.center();else if(k==='v'){state.renderer.fitPopulation();e.preventDefault();}else if(k==='+'||k==='=')state.renderer.zoomBy(1.12);else if(k==='-')state.renderer.zoomBy(.89);});document.addEventListener('keyup',e=>state.keys.delete(e.key.toLowerCase()));window.addEventListener('blur',()=>state.keys.clear());}
+function wire(){const canvas=$('worldCanvas');canvas.addEventListener('worldselect',e=>{state.selected=e.detail.id;state.renderer.setSelected(state.selected);renderInspector();});canvas.addEventListener('dblclick',()=>state.renderer.center());document.addEventListener('click',e=>{const c=e.target.closest('[data-citizen]');if(c){state.selected=c.dataset.citizen;state.renderer.setSelected(state.selected);renderInspector();}const h=e.target.closest('[data-event]');if(h)showWhy(h.dataset.event);const why=e.target.closest('[data-why-action]');if(why)showWhy(why.dataset.whyAction);});$('closeInspector').onclick=()=>{state.selected=null;state.renderer.setSelected(null);renderInspector();};$('toggleRail').onclick=()=>$('activityRail').classList.toggle('collapsed');$('historyOpen').onclick=()=>{$('societyHistory').hidden=false;renderHistory();};$('historyClose').onclick=()=>{$('societyHistory').hidden=true;$('whyPanel').hidden=true;};$('historySearch').oninput=renderHistory;$('historyCategory').onchange=renderHistory;$('agentOpen').onclick=async()=>{$('agentWindow').hidden=false;await refreshMyAvatar({quiet:true});renderMyAvatar();};$('agentClose').onclick=()=>$('agentWindow').hidden=true;$('avatarLocate').onclick=()=>locateMyAvatar();$('avatarFollow').onclick=()=>locateMyAvatar({follow:true});$('avatarInspect').onclick=()=>locateMyAvatar({inspect:true});$('sendIntent').onclick=sendIntent;$('centerWorld').onclick=()=>state.renderer.center();$('fitPopulation').onclick=()=>{const ok=state.renderer.fitPopulation();toast(ok?'Framing all living Citizens':'No living Citizens to frame');};$('followSelected').onclick=()=>{state.renderer.setFollow(state.selected);toast(state.selected?'Following selected Citizen':'Select a Citizen first');};$('overlayKnowledge').onclick=()=>state.renderer.toggleOverlay('knowledge');$('overlayRelations').onclick=()=>state.renderer.toggleOverlay('relations');$('fullscreenWorld').onclick=toggleFullscreen;document.addEventListener('keydown',e=>{if(isTyping(e.target)){if(e.key==='Escape')e.target.blur();return}const k=e.key.toLowerCase();if(['arrowup','arrowdown','arrowleft','arrowright','w','a','s','d'].includes(k)){state.keys.add(k);e.preventDefault();}if(k==='f'){toggleFullscreen();e.preventDefault();}else if(k==='escape')closeWindows();else if(k==='h'){$('societyHistory').hidden=!$('societyHistory').hidden;if(!$('societyHistory').hidden)renderHistory();}else if(k==='c')state.renderer.center();else if(k==='v'){state.renderer.fitPopulation();e.preventDefault();}else if(k==='+'||k==='=')state.renderer.zoomBy(1.12);else if(k==='-')state.renderer.zoomBy(.89);});document.addEventListener('keyup',e=>state.keys.delete(e.key.toLowerCase()));window.addEventListener('blur',()=>state.keys.clear());}
function frame(now){const dt=Math.min(.05,(now-state.lastFrame)/1000||.016);state.lastFrame=now;const speed=260*dt;if(state.keys.has('w')||state.keys.has('arrowup'))state.renderer.panBy(0,speed);if(state.keys.has('s')||state.keys.has('arrowdown'))state.renderer.panBy(0,-speed);if(state.keys.has('a')||state.keys.has('arrowleft'))state.renderer.panBy(speed,0);if(state.keys.has('d')||state.keys.has('arrowright'))state.renderer.panBy(-speed,0);state.renderer.draw();if(now-state.lastHud>300){state.lastHud=now;renderTop();if(!$('inspector').hidden)renderInspector();}requestAnimationFrame(frame);}
function installTestHooks(){window.render_game_to_text=()=>{if(!state.world)return JSON.stringify({status:'loading'});const w=state.world;return JSON.stringify({mode:state.mode,worldMinute:worldMinute(w),selectedId:state.selected,citizens:w.citizens.filter(c=>c.alive).map(c=>{const p=citizenPosition(c,w);return{id:c.id,x:Number(p.x.toFixed(3)),y:Number(p.y.toFixed(3)),action:c.currentAction?.type||null};}),structures:(w.buildings||[]).length,projects:(w.projects||[]).filter(p=>p.status==='construction').length});};}
async function boot(){
@@ -53,6 +138,10 @@ async function boot(){
// Rendering must never depend on network availability.
requestAnimationFrame(frame);
+ // Session ownership is resolved before the first canonical state fetch so
+ // an authenticated user's idempotent HUMAN_LINKED embodiment is visible immediately.
+ await refreshMyAvatar({quiet:true});
+
state.connection=new ObserverConnection({
fetchState:async()=>{
const d=await getJSON('/api/v2/state');
diff --git a/tests/test_sovereign_avatar_observer_upgrade.mjs b/tests/test_sovereign_avatar_observer_upgrade.mjs
new file mode 100644
index 0000000..36c13fa
--- /dev/null
+++ b/tests/test_sovereign_avatar_observer_upgrade.mjs
@@ -0,0 +1,134 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import {createSovereignGenesis} from '../world/genesis.js';
+import {createHumanAvatar} from '../world/engine.js';
+
+const read=p=>fs.readFileSync(new URL(`../${p}`,import.meta.url),'utf8');
+
+test('human avatar embodiment is idempotent and population distinguishes embodiment from living count',()=>{
+ const world=createSovereignGenesis({seed:31,realEpochMs:0});
+
+ const reserve=world.reserves.observerEmbodimentKg;
+
+ const a=createHumanAvatar(
+ world,
+ {externalId:'github:owner-1',displayName:'Owner'},
+ 0
+ );
+
+ const afterFirst=world.reserves.observerEmbodimentKg;
+
+ const b=createHumanAvatar(
+ world,
+ {externalId:'github:owner-1',displayName:'Owner'},
+ 10
+ );
+
+ assert.equal(a.id,b.id);
+ assert.equal(world.citizens.length,101);
+ assert.equal(afterFirst,world.reserves.observerEmbodimentKg);
+ assert.ok(afterFirst{
+ const api=read('functions/api/v2/[[path]].js');
+ const worker=read('worker/src/index.js');
+
+ assert.ok(
+ api.includes("request.method==='POST'&&p==='/avatar'")
+ );
+
+ assert.equal(
+ api.includes("request.method==='GET'&&p==='/avatar'"),
+ false
+ );
+
+ assert.match(api,/requireActor\(request,env\)/);
+ assert.ok(api.includes("path:'/avatar'"));
+ assert.ok(api.includes('authenticated:true'));
+
+ assert.match(
+ worker,
+ /const existing=this\.world\.citizens\.find\(c=>c\.externalId===externalId\)/
+ );
+
+ assert.match(worker,/created:!existing/);
+ assert.match(worker,/if\(!existing\)\{/);
+});
+
+test('Observer exposes owned-avatar marker, locate/follow controls and personal evolution panel',()=>{
+ const html=read('site/index.html');
+ const js=read('site/sovereign-world.js');
+ const renderer=read('site/sovereign-renderer.js');
+ const pixi=read('site/pixi-observer.js');
+
+ assert.match(html,/id="myAvatarStatus"/);
+ assert.match(html,/id="avatarDashboard"/);
+
+ for(const id of [
+ 'avatarLocate',
+ 'avatarFollow',
+ 'avatarInspect',
+ 'avatarEvolution',
+ 'avatarTimeline'
+ ]){
+ assert.match(html,new RegExp(`id="${id}"`));
+ }
+
+ assert.match(js,/refreshMyAvatar/);
+ assert.match(js,/renderMyAvatar/);
+
+ assert.ok(
+ js.includes(
+ "LIVING · ${embodied} TOTAL · ${human} HUMAN · ${visibility.visible} VISIBLE"
+ )
+ );
+
+ assert.match(
+ js,
+ /getJSON\('\/api\/v2\/avatar',\{/
+ );
+
+ assert.match(renderer,/setOwnedCitizen/);
+ assert.match(renderer,/locateCitizen/);
+ assert.match(pixi,/ownedCitizenId/);
+ assert.match(pixi,/'YOU'/);
+});
+
+test('public HUMAN_LINKED state remains privacy-minimal',()=>{
+ const engine=read('world/engine.js');
+ const js=read('site/sovereign-world.js');
+
+ assert.match(
+ engine,
+ /c\.kind==='HUMAN_LINKED'\?\{count:c\.knowledge\.filter/
+ );
+
+ const start=engine.indexOf('export function publicWorld');
+
+ assert.ok(
+ start>=0,
+ 'publicWorld must exist'
+ );
+
+ const next=engine.indexOf(
+ '\nexport function ',
+ start+'export function publicWorld'.length
+ );
+
+ const publicBlock=engine.slice(
+ start,
+ next>=0?next:engine.length
+ );
+
+ assert.doesNotMatch(
+ publicBlock,
+ /privateHumanIntents/
+ );
+
+ assert.match(js,/knowledgeCount/);
+ assert.match(js,/vocabularyCount/);
+ assert.match(js,/relationshipsCount/);
+ assert.match(js,/possessionsCount/);
+});
diff --git a/worker/src/index.js b/worker/src/index.js
index a84524a..326fe56 100644
--- a/worker/src/index.js
+++ b/worker/src/index.js
@@ -426,9 +426,14 @@ export class SovereignWorld {
if(request.method==='POST'&&path==='/avatar'){
const body=await request.json();
if(!body?.actor?.id)return json({ok:false,error:'actor_required'},400);
- const c=createHumanAvatar(this.world,{externalId:`github:${body.actor.github_id||body.actor.id}`,displayName:body.actor.display_name||body.actor.github_login||null},this.world.clock.worldMinute);
- await this.persist({forceSeal:true});
- return json({ok:true,citizenId:c.id});
+ const externalId=`github:${body.actor.github_id||body.actor.id}`;
+ const existing=this.world.citizens.find(c=>c.externalId===externalId);
+ const c=existing||createHumanAvatar(this.world,{externalId,displayName:body.actor.display_name||body.actor.github_login||null},this.world.clock.worldMinute);
+ if(!existing){
+ await this.persist({forceSeal:true});
+ this.broadcast({type:'world_delta',state:publicWorld(this.world,Date.now())});
+ }
+ return json({ok:true,citizenId:c.id,created:!existing});
}
if(request.method==='POST'&&path==='/intent'){
const body=await request.json();