import { is, embed, stringify, Dataflow, Dataspace, Embedded, Reader, Ref, Schemas, Sturdy, Turn, Value } from "@syndicate-lang/core"; import * as html from "@syndicate-lang/html"; import * as timer from "@syndicate-lang/timer"; import * as wsRelay from "@syndicate-lang/ws-relay"; import * as wakeDetector from './wake-detector.js'; import * as Shapes from './gen/shapes.js'; import * as SceneProtocol from './gen/scene.js'; import { md5 } from './md5.js'; import { AbstractMesh, Engine, FreeCamera, Mesh, Scene, TargetCamera, Vector3, WebXRCamera, } from '@babylonjs/core/Legacy/legacy'; import { activeFloorMeshes, activeTouchableMeshes, ShapeTree, builder as B } from './shapes.js'; import { RunningEngine } from './engine.js'; import { uuid } from './uuid.js'; assertion type SceneHandle(ds: Embedded); function interpretScene(myId: string, scene: Scene, rootMesh: Mesh, sceneDs: Ref) { at sceneDs { during Shapes.Sprite({ "name": $name: string }) => spawn named `sprite:${name}` { if (name === myId) { console.log('ignoring sprite', name); } else { console.log('+shape', name); on stop console.log('-shape', name); spriteMain(name, scene, rootMesh, sceneDs); } } } } function spriteMain(spriteName: string, scene: Scene, rootMesh: Mesh, sceneDs: Ref) { at sceneDs { let currentShape = ShapeTree.empty(spriteName, scene); currentShape.node.parent = rootMesh; on stop currentShape.remove(); during Shapes.Sprite({ "name": spriteName, "shape": $shape: Shapes.Shape }) => { currentShape = currentShape.reconcile(spriteName, spriteName, shape); currentShape.node.parent = rootMesh; } } } async function enterScene( id: string, runningEngine: RunningEngine, ds: Ref, sceneDs: Ref, email: Dataflow.Field, ) { const currentSceneFacet = Turn.activeFacet; console.log('enterScene', sceneDs); const rootMesh = new Mesh('--root-' + (+new Date()), runningEngine.scene); interpretScene(id, runningEngine.scene, rootMesh, sceneDs); const camera = runningEngine.camera; camera.applyGravity = false; camera.position = runningEngine.options.initialPos; let lastTouchTime = 0; let lastTouchSpriteName = ""; camera.onCollide = (other: AbstractMesh) => { if (other.metadata?.touchable) { const now = +new Date(); const touched = other.metadata?.spriteName ?? ""; if ((now - lastTouchTime > 500) || (lastTouchSpriteName !== touched)) { currentSceneFacet.turn(() => { at sceneDs { send message SceneProtocol.Touch({ subject: id, object: touched, }); } }); lastTouchTime = now; lastTouchSpriteName = touched; } } }; const currentPosition = () => Shapes.Vector3(camera.position); const currentRotation = () => Shapes.Vector3( camera instanceof WebXRCamera ? camera.rotationQuaternion.toEulerAngles() : camera.rotation); field position: Shapes.Vector3 = currentPosition(); field rotation: Shapes.Vector3 = currentRotation(); const refreshPeriod = Math.floor(1000 / 10); at ds { on message timer.PeriodicTick(refreshPeriod) => { const newPosition = currentPosition(); const newRotation = currentRotation(); if (!is(Shapes.fromVector3(position.value), Shapes.fromVector3(newPosition))) { position.value = newPosition; } if (!is(Shapes.fromVector3(rotation.value), Shapes.fromVector3(newRotation))) { rotation.value = newRotation; } } } at sceneDs { during SceneProtocol.Gravity($direction: Shapes.Vector3) => { camera.applyGravity = true; on stop camera.applyGravity = false; const frameRate = 60; runningEngine.scene.gravity = new Vector3(direction.x / frameRate, direction.y / frameRate, direction.z / frameRate); } on message SceneProtocol.Touch({ "subject": id, "object": $o }) => { console.log('touched', o); react { let needStop = true; on asserted SceneProtocol.Portal({ "name": o, "destination": $dest: SceneProtocol.PortalDestination }) => { needStop = false; switch (dest._variant) { case "local": runningEngine.scene.removeMesh(rootMesh, true); Turn.active.stop(currentSceneFacet, () => { react { enterScene(id, runningEngine, ds, dest.value, email); } }); break; default: break; } } const checkFacet = Turn.activeFacet; Turn.active.sync(sceneDs).then(() => checkFacet.turn(() => { if (needStop) { stop {} } })); } } assert Shapes.Sprite({ name: id, shape: B.nonphysical( B.move(position.value, B.many([ B.move({ x: 0, y: -0.9, z: 0 }, B.rotate({ x: 0, y: rotation.value.y, z: 0 }, B.scale({ x: 0.4, y: 1.4, z: 0.1 }, B.box()))), B.rotate(rotation.value, B.scale({ x: 0.15, y: 0.23, z: 0.18 }, B.many([ B.box(), B.move({ x: 0, y: 0, z: 0.501 }, B.texture( Shapes.TextureSpec.uvAlpha({ path: `https://www.gravatar.com/avatar/${md5(new TextEncoder().encode(email.value.trim()))}?s=256&d=wavatar`, scale: Shapes.Vector2({ x:1, y:1 }), offset: Shapes.Vector2({ x:0, y:0 }), alpha: 1 }), B.rotate({ x: 0, y: Math.PI, z: 0 }, B.plane()))), ]))), ]))), }); } } function wsurl(): string { const scheme = (document.location.protocol.toLowerCase() === 'https:') ? 'wss' : 'ws'; return `${scheme}://${document.location.host}/ws`; } function bootApp(ds: Ref, runningEngine: RunningEngine) { spawn named 'app' { at ds { const id = uuid(); const url = wsurl(); const serverCap = Sturdy.asSturdyRef(new Reader( '').next()); const relayAddr = wsRelay.RelayAddress(Schemas.transportAddress.WebSocket(url)); field email: string = localStorage.getItem('userEmail') ?? id; const outerFacet = Turn.activeFacet; const emailInput = document.getElementById('emailInput') as HTMLInputElement; emailInput.value = email.value; emailInput.addEventListener('keyup', () => outerFacet.turn(() => { email.value = emailInput.value; localStorage.setItem('userEmail', emailInput.value); })); during wsRelay.Resolved({ "addr": relayAddr, "sturdyref": serverCap, "resolved": $remoteDs_e: Embedded, }) => { const remoteDs = remoteDs_e.embeddedValue; on message wakeDetector.WakeEvent() => { send message wsRelay.ForceRelayDisconnect(relayAddr); } react { at remoteDs { stop on asserted SceneHandle($sceneDs_e: Embedded) => { react { enterScene(id, runningEngine, ds, sceneDs_e.embeddedValue, email); } } } } } } } } window.addEventListener('load', async () => { const runningEngine = await RunningEngine.start({ floorMeshes: () => activeFloorMeshes, touchableMeshes: () => activeTouchableMeshes, }); Dataspace.boot(ds => { html.boot(ds); timer.boot(ds); wsRelay.boot(ds, false); wakeDetector.boot(ds); bootApp(ds, runningEngine); }); });