REPOSITORY SOURCE

game.ts

games/cooperative-puzzle/game.ts

1import {defineGame,authoredEntityId,SDK_API,PlaceBuilder,SKY_MOODS,type Place,type Material,type WorldText} from '../../packages/sdk';
2/** The Meridian: independent content, using only the public SDK. */
3const stone:Material={color:[.58,.51,.39],roughness:.92,texture:2},ivory:Material={color:[.8,.73,.57],roughness:.85},ink:Material={color:[.055,.16,.17],roughness:.7},brass:Material={color:[.62,.42,.15],metalness:.65,roughness:.3};
4function sign(id:string,text:string,position:[number,number,number],height=.28):WorldText{return {id,text,position,height,color:[.94,.82,.55],plate:false,facing:'fixed',yaw:0,font:'serif',maxDistance:20};}
5const court:Place={id:'court',name:'The Meridian',subtitle:'Move the sunstone home, then the moonstone. Open the observatory together.',spawn:[-2.5,.85,5.5],facing:0,ambient:[.25,.3,.29],fog:[.24,.32,.31],fogDensity:.012,sun:[-.5,.8,.3],sunIntensity:1.7,sky:SKY_MOODS.golden,lights:[{position:[0,6,-4],color:[1,.81,.47],intensity:35}],shadowLight:0,objects:[],interactions:[],lettering:[sign('title','T H E   M E R I D I A N',[0,4,-7.7],.5),sign('sun-label','I  /  SUNSTONE',[-2.5,1.3,-3.4],.22),sign('moon-label','II  /  MOONSTONE',[2.5,1.3,-3.4],.22),sign('invitation','Two travelers. One small discovery.',[0,3.2,-7.65],.22)]};
6const b=new PlaceBuilder(court);
7b.box([0,-.2,0],[19,.4,19],stone,{id:'floor',collider:true});
8for(const x of [-9.5,9.5])b.box([x,1.2,0],[.4,2.4,19.4],ink,{id:`wall.x${x}`,collider:true});
9for(const z of [-9.5,9.5])b.box([0,1.2,z],[19.4,2.4,.4],ink,{id:`wall.z${z}`,collider:true});
10b.box([0,2.5,-8],[13,5,.5],ink,{id:'observatory.back',collider:true});
11for(const x of [-6,-3,3,6]){
12 b.box([x,.2,-6],[.9,.4,.9],ivory,{id:`base.${x}`,collider:true});b.cylinder([x,2.4,-6],[.35,4.2,.35],ivory,{id:`column.${x}`,collider:true});b.box([x,4.6,-6],[.7,.2,.7],brass,{id:`capital.${x}`});
13}
14b.box([0,4.85,-6],[13,.3,.7],ivory,{id:'lintel'});
15for(const x of [-7,7])for(const z of [-3,3,7]){b.cylinder([x,.4,z],[.8,.8,.8],ink,{id:`urn.${x}.${z}`,collider:true});b.sphere([x,1,z],[1.15,1.1,1.15],{color:[.18,.3,.18],roughness:1},{id:`leaf.${x}.${z}`});}
16for(let i=0;i<40;i++){const a=i*Math.PI/20;b.box([Math.sin(a)*5,.007,Math.cos(a)*5],[.06,.012,.35],brass,{id:`dial.${i}`,rotation:[0,a,0]});}
17for(const [name,x,color,flag,requires] of [['sun',-2.5,[.85,.55,.19],'sun-home',undefined],['moon',2.5,[.28,.64,.66],'moon-home','sun-home']] as const){
18 const material:Material={color:[...color],roughness:.32,metalness:.3};
19 b.box([x,.5,3],[.9,1,.9],material,{id:`${name}.stone`,collider:true,interaction:name});b.sphere([x,1.12,3],[.35,.35,.35],brass,{id:`${name}.crown`,interaction:name});
20 for(const side of [-1,1])b.box([x+side*.65,.012,1.5],[.025,.024,4.2],brass,{id:`${name}.rail.${side}`});
21 b.box([x,.006,0],[1.25,.012,1.25],ink,{id:`${name}.target`,flagMaterial:{flag,material:{color:[...color],emission:1.2,roughness:.5}}});
22 court.interactions.push({id:name,kind:'push',label:`Move the ${name}stone`,position:[x,.5,3],radius:1.7,push:{offset:[0,0,-.75],steps:4},flag,...(requires?{requires,denial:'First settle the sunstone into its brass square.'}:{}),message:{title:name==='sun'?'The sun is home.':'The instruments are aligned.',text:name==='sun'?'Now help the moonstone follow.':'The observatory is ready. Meet beneath the golden dial.'}});
23}
24b.cylinder([0,.55,-5.6],[1.3,1.1,1.3],ivory,{id:'dial.pedestal',collider:true});b.sphere([0,1.5,-5.6],[.75,.75,.75],brass,{id:'dial.globe',flagMaterial:{flag:'meridian-awake',material:{color:[1,.72,.25],emission:2,metalness:.5}}});
25court.interactions.push({id:'wake',kind:'beacon',label:'Wake the observatory',position:[0,1,-4.5],radius:1.5,requires:'moon-home',flag:'meridian-awake',denial:'Set both stones in their squares first.',message:{title:'A small discovery, shared.',text:'The Meridian is awake. The terrace beyond is open to everyone.'}},{id:'terrace',kind:'portal',label:'Step onto the star terrace',position:[0,.85,-6.8],radius:1.4,requires:'meridian-awake',destination:'terrace',denial:'Wake the observatory to open the way.'},{id:'instructions',kind:'dialogue',label:'Read the observatory notes',position:[-5,.85,5.5],radius:1.8,speaker:'Notes from the astronomer',text:['Stand behind the amber sunstone and press Interact. Follow it and push four times until it rests in its square.','Then do the same with the blue moonstone. Leave the path clear for your friend.','When both stones are home, wake the golden globe beneath the dial. Your shared discovery opens the star terrace.']});
26b.box([-5,.7,5.5],[.8,1.4,.3],ink,{id:'notes',collider:true});
27const terrace:Place={...structuredClone(court),id:'terrace',name:'The star terrace',subtitle:'A small discovery, shared. Rest here together, or return to the instruments.',spawn:[0,.85,4],facing:0,objects:[],interactions:[{id:'return',kind:'portal',label:'Return to the observatory',position:[0,.85,5],radius:1.4,destination:'court'}],lettering:[sign('finish','A SMALL DISCOVERY, SHARED',[0,2,-4],.35)],sky:SKY_MOODS.moonlit,ambient:[.2,.26,.34],fog:[.04,.07,.12],sunIntensity:.5};const t=new PlaceBuilder(terrace);
28t.box([0,-.2,0],[14,.4,14],stone,{id:'terrace.floor',collider:true});
29for(const x of [-7,7])t.box([x,.8,0],[.3,1.6,14],ink,{id:`terrace.wall.x${x}`,collider:true});for(const z of [-7,7])t.box([0,.8,z],[14,1.6,.3],ink,{id:`terrace.wall.z${z}`,collider:true});
30for(const x of [-4,4]){t.box([x,.5,-1],[1.2,1,4],ivory,{id:`bench.${x}`,collider:true});t.sphere([x,1.5,-4],[.4,.4,.4],{color:[1,.74,.32],emission:2},{id:`light.${x}`});}
31for(const place of [court,terrace])for(const object of place.objects)object.entityId=await authoredEntityId('meridian.1',place.id+'/'+object.id);
32export const game=defineGame({api:SDK_API,world:{format:18,id:'The Meridian',release:'meridian.1',entry:'court',models:{},authority:'cooperative',places:[court,terrace]}});
33