diff --git a/MANIFEST.sha256 b/MANIFEST.sha256 index fb9ec65..99680b6 100644 --- a/MANIFEST.sha256 +++ b/MANIFEST.sha256 @@ -33,15 +33,15 @@ efd1af2bcdd0939bda2432e6823f09135f6d8345796e8a6f9690886b0b104b05 site/assets/wo 27eaa1da60c9ae26c54512c6b986e2d568f5ec9a968d6a46d3d785f9117e5e4e site/cinematic-world.css 6c2c82fb1de7d8a0051edaa1e5f5711fc8881865ad8638fa0c41a709dce54696 site/data/sovereign-genesis.json 0670d7be3b270c8e627c5f0089e25ba065c3326f6bcf0f06ce9ae5cf3c5cf64b site/icon.svg -a2c950a45727b3527c5541f0347c74fa422ed103e073a2dabef6df37dfc7b6c0 site/index.html +37edda29fd3d7dc28535024733ec4e770cf16b8e2e8dc1ceeda73d1c1bc154b2 site/index.html a31f15ff86c87f9eafb1723fb3fc59186f27fb32a75b00d287fc7efbdeec5b1c site/medieval-art.js 49897b8d946b85c84a4e0f177a3c7773a0c6f9acfef0025d308ccec2ac3a7ef1 site/medieval-world.css 82197a81454575b5c941ec0c52924c0a53f1d92bf70371e9c16df0912a29dcf0 site/observer-connection.js -73d7b00031479ba5d4a2093567c4d9628b50cf3e9a8a6c4be646ad3d73497bd4 site/pixi-observer.js +f57bf13a527ada7c6c792760f6ab976412df64a4a125f305be7e86dda6a32cbf site/pixi-observer.js 5edab258856c44d89ea1df51472037697db95909c2d696607ef7c30a503a4838 site/preview.png -513b703d86b97ed70136c2c1a050f5810f89f14cfba8ec340fd055639413cbad site/sovereign-renderer.js +772ee873b012f6ddf0f36b79c8c95b737c1a45b4362f0b997afcb7a68b77b233 site/sovereign-renderer.js 59d83fee261298ef4c90a650ab3b350d4533ab61e7ad3f315e45cdd83443de74 site/sovereign-world.css -572984f1295862408b6e810619583bbb866ab3130daecbdd968c3865f863d9dd site/sovereign-world.js +d639481c2258cfabfaa97bead83e2009cffa3a5535fca4c8c14729dade54fce4 site/sovereign-world.js aaa5a63cab5926e0355e8257bd562ec2db4c8890904ef1894767063925ceadf1 site/terrain-model.js c98fca62534d141f285a6c92bfd6257c26d462a3daff72672a4d76f2399d1f21 site/transient-physics.js d179b80fb34aae0db437dfa72c1a6e6704209a8119b8d991ad49321d8584da8f tests/browser-art.mjs @@ -116,3 +116,9 @@ fba432ea0721d0c250d7b91aa9df1c914219740354dcd941f92ea65354d00a96 world/terrain. e56357ae087309b86d670288998026568373b7268778157b38612fb38d81eba7 worker/src/persistence.js 279876578ed24eade882ad165e12f0952f1ac985d4b22065f24067dd720c886e tests/test_sovereign_persistence_budget.mjs 1219637898a43e4d623897ea15f1ff495554b4b6f2279e6e2466eba072e7b59e tests/test_sovereign_ledger_compaction.mjs +9942697150f9cc3be949b4869f67c678731db377e284602df4faef8bc6c4fb5e site/observer-concepts.js +ab0d9a1e3aa3efbc09f241543d9a0714a196d997849049e62fb0a22eae73b1c9 site/citizen-animation.js +dab555575cd5a79e1e1be63a155033788d40995f6b5c69b3b82fe87132233009 site/spine-citizen-adapter.js +4fd200df9a9fc14a16eb12445665f4dfd58777fdba8426da4f00ca99b21d8fbe docs/SPINE_OBSERVER_INTEGRATION.md +164c56738f05469e36d0cb3c913656154381bda6a177505d5d2f06d87d1665ad tests/test_sovereign_observer_readability_upgrade.mjs +98d88f126a1bca9443485b2cbc2f4614dbc975f9f96d0dfb72ed37465e2c3e56 tests/test_sovereign_citizen_animation_upgrade.mjs diff --git a/docs/SPINE_OBSERVER_INTEGRATION.md b/docs/SPINE_OBSERVER_INTEGRATION.md new file mode 100644 index 0000000..cd2437d --- /dev/null +++ b/docs/SPINE_OBSERVER_INTEGRATION.md @@ -0,0 +1,42 @@ +# Optional Spine Citizen renderer + +CYMONIA's canonical world does not depend on animation middleware. Citizen animation is an Observer concern: canonical actions select a visual state, while the browser renderer may never create or modify world state. + +## Current integration + +The public MIT repository contains an action-to-animation contract, improved native PixiJS procedural Citizen motion, and `site/spine-citizen-adapter.js`, an optional adapter for a licensed Spine PixiJS v8 runtime and exported Citizen skeleton assets. The native renderer remains the automatic fallback. + +No Spine Runtime source, package, CDN script, skeleton export, atlas, or proprietary asset is included or enabled by default. + +## Licensing boundary + +Spine Runtimes are governed by the Spine Runtimes License, not CYMONIA's MIT license. A valid Spine license is required when integrating or distributing the runtime under Esoteric Software's published terms. Keep the optional adapter disabled unless the project owner has satisfied those terms. + +## Enabling a licensed rig + +Configure the Observer before `sovereign-world.js` starts: + +```js +window.CYMONIA_SPINE = { + Spine: LicensedSpineConstructor, + skeletonAlias: 'citizen-skeleton', + atlasAlias: 'citizen-atlas', + skeletonUrl: '/assets/licensed/citizen.skel', + atlasUrl: '/assets/licensed/citizen.atlas', + scale: 0.22, + animations: { + idle: 'idle', + walk: 'walk', + gather: 'gather', + build: 'build', + sleep: 'sleep', + communicate: 'communicate', + attack: 'attack', + defend: 'defend' + } +}; +``` + +The adapter preloads the aliases through `PIXI.Assets`, creates one Spine container per living Citizen, and maps only canonical `currentAction.type` values to animation tracks. Missing assets or animations fall back safely. + +A useful first rig should export at least `idle`, `walk`, `gather`, `build`, `sleep`, and `communicate`. Skins may represent Observer-side interpretations of canonical clothing, equipment and injuries, but they must never write back into the world. diff --git a/site/citizen-animation.js b/site/citizen-animation.js new file mode 100644 index 0000000..b1b5b23 --- /dev/null +++ b/site/citizen-animation.js @@ -0,0 +1,38 @@ +const ACTION_ANIMATION={ + MOVE:'walk',OBSERVE:'observe',REST:'idle',SLEEP:'sleep',EAT:'eat',DRINK:'drink', + GATHER:'gather',CARRY:'carry',CUT:'cut',DIG:'dig',HEAT:'work',COOL:'work',MIX:'work', + ASSEMBLE:'build',BUILD:'build',CARE:'care',TEACH:'communicate',COMMUNICATE:'communicate', + EXPERIMENT:'experiment',ATTACK:'attack',DEFEND:'defend',TRANSFER:'carry', + PROMISE:'communicate',CLAIM:'communicate',REPRODUCE:'idle', +}; +function hashUnit(value){ + let h=2166136261>>>0; + for(const ch of String(value||'')){h^=ch.charCodeAt(0);h=Math.imul(h,16777619)>>>0;} + return(h>>>0)/4294967295; +} +export function animationForCitizen(citizen){ + return ACTION_ANIMATION[citizen?.currentAction?.type]||'idle'; +} +export function citizenVisualPose(citizen,nowMs=Date.now()){ + const action=animationForCitizen(citizen); + const phase=Number(nowMs)/180+hashUnit(citizen?.id)*Math.PI*2; + const wave=Math.sin(phase),pulse=Math.sin(phase*.5); + let y=0,rotation=0,scaleX=1,scaleY=1,shadowScale=1; + if(action==='walk'){ + y=-Math.abs(wave)*1.6;rotation=wave*.035;scaleX=1+Math.abs(wave)*.025; + scaleY=1-Math.abs(wave)*.03;shadowScale=1-Math.abs(wave)*.08; + }else if(['gather','cut','dig','build','work'].includes(action)){ + y=Math.abs(wave)*.55;rotation=wave*.065;scaleY=1-Math.abs(wave)*.025;shadowScale=1.03; + }else if(action==='carry'){ + y=-Math.abs(wave)*.7;rotation=wave*.018;scaleY=.98; + }else if(action==='sleep'){ + y=2.2;rotation=-.11;scaleX=1.05;scaleY=.86;shadowScale=1.18; + }else if(['communicate','care','observe','experiment'].includes(action)){ + y=pulse*.25;rotation=wave*.025;scaleY=1+pulse*.015; + }else if(['attack','defend'].includes(action)){ + y=-Math.abs(wave)*.7;rotation=wave*.09;scaleX=1+Math.abs(wave)*.04;scaleY=.97; + }else{ + y=pulse*.18;scaleX=1-pulse*.006;scaleY=1+pulse*.012; + } + return {animation:action,y,rotation,scaleX,scaleY,shadowScale}; +} diff --git a/site/index.html b/site/index.html index 3fbe096..4a98e26 100644 --- a/site/index.html +++ b/site/index.html @@ -20,7 +20,7 @@
Completed at world minute ${esc(b.createdWorldMinute)}. It exists only because its project consumed material and labor.
This site is canonical. Completion requires the project's remaining work and reserved materials.
`;return}if(d){$('inspectorBody').innerHTML=`NATURAL RESOURCEThis marker represents a canonical resource deposit, not decorative scenery.
`;return} - const k=Array.isArray(c.knowledge)?c.knowledge:[],age=(c.body.ageMinutes/525600).toFixed(1),a=c.currentAction,rels=relationRows(c),poss=(c.possessions||[]).slice(0,12),psych=Object.entries(c.psychology||{}).filter(([,v])=>typeof v==='number').sort((a,b)=>b[1]-a[1]).slice(0,7); - $('inspectorBody').innerHTML=`${esc(c.kind)} · ${c.alive?'ALIVE':'DEAD'}${esc(a?.purpose||'No public physical action recorded.')}
${a?``:''}${esc(goalLabel(c.activeGoal))}
No public trait state.
'}${c.knowledge?.count!=null?`${c.knowledge.count} private concepts`:'No exposed concepts yet.'}
`}No stable coined lexicon yet.
'}No significant public relationship data yet.
'}No individually held objects.
'}${esc(a?.purpose||'No public physical action recorded.')}
${a?``:''}${esc(goalLabel(c.activeGoal))}
No public trait state.
'}${c.knowledge?.count!=null?`${c.knowledge.count} private concepts`:'No exposed concepts yet.'}
`}No stable coined lexicon yet.
'}No significant public relationship data yet.
'}No individually held objects.
'}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?.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 TRACEWorld 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();} @@ -30,7 +42,7 @@ async function sendIntent(){const intent=$('humanIntent').value.trim();if(!inten 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();$('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==='+'||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=()=>$('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 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(){ diff --git a/site/spine-citizen-adapter.js b/site/spine-citizen-adapter.js new file mode 100644 index 0000000..975212c --- /dev/null +++ b/site/spine-citizen-adapter.js @@ -0,0 +1,57 @@ +import {animationForCitizen} from './citizen-animation.js'; + +const DEFAULT_ANIMATIONS={ + idle:'idle',walk:'walk',observe:'observe',sleep:'sleep',eat:'eat',drink:'drink', + gather:'gather',carry:'carry',cut:'cut',dig:'dig',work:'work',build:'build', + care:'care',communicate:'communicate',experiment:'experiment',attack:'attack',defend:'defend', +}; + +export class SpineCitizenAdapter{ + constructor(PIXI,config=globalThis.CYMONIA_SPINE){ + this.PIXI=PIXI;this.config=config||null;this.Spine=this.config?.Spine||null;this.ready=false; + this.animations={...DEFAULT_ANIMATIONS,...(this.config?.animations||{})}; + } + get enabled(){ + return Boolean(this.PIXI&&this.Spine&&this.config?.skeletonAlias&&this.config?.atlasAlias); + } + async preload(){ + if(!this.enabled)return false; + try{ + const {skeletonAlias,atlasAlias,skeletonUrl,atlasUrl}=this.config; + if(skeletonUrl){try{this.PIXI.Assets.add({alias:skeletonAlias,src:skeletonUrl});}catch{}} + if(atlasUrl){try{this.PIXI.Assets.add({alias:atlasAlias,src:atlasUrl});}catch{}} + await this.PIXI.Assets.load([skeletonAlias,atlasAlias]); + this.ready=true;return true; + }catch(error){ + console.warn('Optional Spine Citizen assets unavailable; using native CYMONIA sprites.',error); + this.ready=false;return false; + } + } + create(citizen){ + if(!this.ready)return null; + try{ + const PIXI=this.PIXI,container=new PIXI.Container(),shadow=new PIXI.Graphics(); + shadow.ellipse(1,1,6,2.8).fill({color:0x1d281c,alpha:.22}); + const body=new this.Spine({skeleton:this.config.skeletonAlias,atlas:this.config.atlasAlias,autoUpdate:true}); + const baseScale=Number(this.config.scale||.22);body.scale.set(baseScale); + const task=new PIXI.Text({text:'',style:{fontFamily:'Georgia',fontSize:10,fill:0xf8e8b5,stroke:{color:0x283021,width:2}}}); + task.anchor.set(.5);task.position.set(0,-41);container.addChild(shadow,body,task); + const entry={container,body,task,shadow,baseScale,spine:true,currentAnimation:null}; + this.update(entry,citizen,{flip:false});return entry; + }catch(error){ + console.warn('Optional Spine Citizen instance failed; using native CYMONIA sprite.',error);return null; + } + } + update(entry,citizen,{flip=false}={}){ + if(!entry?.spine)return; + const body=entry.body,semantic=animationForCitizen(citizen),requested=this.animations[semantic]||this.animations.idle||'idle'; + const available=body?.skeleton?.data?.findAnimation?.(requested); + if(available&&entry.currentAnimation!==requested){ + body.state?.setAnimation?.(0,requested,true);entry.currentAnimation=requested; + }else if(!available&&entry.currentAnimation!==this.animations.idle){ + const idle=this.animations.idle||'idle'; + if(body?.skeleton?.data?.findAnimation?.(idle)){body.state?.setAnimation?.(0,idle,true);entry.currentAnimation=idle;} + } + const scale=Math.abs(Number(entry.baseScale)||.22);body.scale.x=(flip?-1:1)*scale;body.scale.y=scale; + } +} diff --git a/tests/test_sovereign_citizen_animation_upgrade.mjs b/tests/test_sovereign_citizen_animation_upgrade.mjs new file mode 100644 index 0000000..96c6bfd --- /dev/null +++ b/tests/test_sovereign_citizen_animation_upgrade.mjs @@ -0,0 +1,25 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import {animationForCitizen,citizenVisualPose} from '../site/citizen-animation.js'; + +test('canonical actions map to Observer animation states only',()=>{ + assert.equal(animationForCitizen({currentAction:{type:'MOVE'}}),'walk'); + assert.equal(animationForCitizen({currentAction:{type:'BUILD'}}),'build'); + assert.equal(animationForCitizen({currentAction:{type:'SLEEP'}}),'sleep'); + assert.equal(animationForCitizen({currentAction:null}),'idle'); + const c={id:'c:1',currentAction:{type:'MOVE'}},before=JSON.stringify(c),pose=citizenVisualPose(c,1000); + assert.equal(pose.animation,'walk'); + for(const key of ['y','rotation','scaleX','scaleY','shadowScale'])assert.ok(Number.isFinite(pose[key]),key); + assert.equal(JSON.stringify(c),before); +}); + +test('Spine support is optional and does not vendor proprietary runtime into the MIT shell',()=>{ + const adapter=fs.readFileSync(new URL('../site/spine-citizen-adapter.js',import.meta.url),'utf8'); + const pixi=fs.readFileSync(new URL('../site/pixi-observer.js',import.meta.url),'utf8'); + const html=fs.readFileSync(new URL('../site/index.html',import.meta.url),'utf8'); + const docs=fs.readFileSync(new URL('../docs/SPINE_OBSERVER_INTEGRATION.md',import.meta.url),'utf8'); + assert.match(adapter,/CYMONIA_SPINE/);assert.match(adapter,/animationForCitizen/); + assert.match(pixi,/SpineCitizenAdapter/);assert.match(pixi,/citizenVisualPose/); + assert.doesNotMatch(html,/@esotericsoftware|spine-pixi-v8/i);assert.match(docs,/Spine Runtimes License/);assert.match(docs,/fallback/i); +}); diff --git a/tests/test_sovereign_observer_readability_upgrade.mjs b/tests/test_sovereign_observer_readability_upgrade.mjs new file mode 100644 index 0000000..5cee53f --- /dev/null +++ b/tests/test_sovereign_observer_readability_upgrade.mjs @@ -0,0 +1,28 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import {observerConceptView} from '../site/observer-concepts.js'; + +test('Observer translates opaque resource concepts without mutating canonical knowledge',()=>{ + const world={resourceDeposits:[{id:'dep:water',type:'water'}],buildings:[],projects:[],objects:[]}; + const citizen={language:{lexicon:{'k:water':'mira'}}}; + const entry={concept:'k:water',confidence:.91,provenance:[{kind:'observation',entityId:'dep:water',evidence:{appearance:'moving reflective fluid'}}]}; + const before=JSON.stringify({world,citizen,entry}),view=observerConceptView(world,citizen,entry); + assert.equal(view.label,'Water source');assert.equal(view.canonicalId,'k:water');assert.equal(view.token,'mira'); + assert.match(view.tooltip,/Canonical concept: k:water/);assert.equal(JSON.stringify({world,citizen,entry}),before); +}); + +test('Observer gives readable structure and sensory fallbacks without guessing canonical state',()=>{ + const world={resourceDeposits:[],buildings:[{id:'hut:1'}],projects:[],objects:[]}; + assert.equal(observerConceptView(world,{language:{lexicon:{}}},{concept:'kstruct:1',provenance:[{kind:'observation',entityId:'hut:1'}]}).label,'Constructed structure'); + assert.equal(observerConceptView(world,{language:{lexicon:{}}},{concept:'k:unknown',provenance:[{kind:'observation',evidence:{appearance:'fibrous rigid material'}}]}).label,'Observed: fibrous rigid material'); +}); + +test('Observer exposes population framing and readable concept UI',()=>{ + const renderer=fs.readFileSync(new URL('../site/sovereign-renderer.js',import.meta.url),'utf8'); + const world=fs.readFileSync(new URL('../site/sovereign-world.js',import.meta.url),'utf8'); + const html=fs.readFileSync(new URL('../site/index.html',import.meta.url),'utf8'); + assert.match(renderer,/fitPopulation\(\)/);assert.match(renderer,/populationVisibility\(\)/); + assert.match(world,/observerConceptView/);assert.match(world,/fitPopulation/); + assert.match(html,/id="fitPopulation"/);assert.match(html,/id="populationLabel"/); +});