import { is, fromJS, Dataflow, Dataspace, Embedded, Reader, Ref, Schemas, Sturdy, Turn } 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 { setupLog, log } from './log.js'; import { AbstractMesh, Mesh, Vector3, } from '@babylonjs/core/Legacy/legacy'; import { activeFloorMeshes, activeTouchableMeshes, ShapeTree, v3, scale3, buildSound, 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, runningEngine: RunningEngine, 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, runningEngine, rootMesh, sceneDs); } } during SceneProtocol.AmbientSound({ "name": $name: string, "spec": $spec: SceneProtocol.SoundSpec }) => spawn named `sound:${name}` { const sound = buildSound(name, runningEngine.scene, spec, false); on stop sound.dispose(); } } } function spriteMain(spriteName: string, runningEngine: RunningEngine, rootMesh: Mesh, sceneDs: Ref) { at sceneDs { let currentShape = ShapeTree.empty(spriteName, runningEngine.scene); currentShape.rootnode.parent = rootMesh; on stop currentShape.remove(); during Shapes.Sprite({ "name": spriteName, "shape": $shape: Shapes.Shape }) => { currentShape = currentShape.reconcile(spriteName, spriteName, shape); currentShape.rootnode.parent = rootMesh; } during SceneProtocol.Gravity($direction: Shapes.Vector3) => { runningEngine.applyGravity = true; on stop runningEngine.applyGravity = false; runningEngine.gravity = v3(direction); } } } async function enterScene( routeField: Dataflow.Field>, id: string, runningEngine: RunningEngine, ds: Ref, sceneDs: Ref, initialPosition: Vector3, email: Dataflow.Field, ) { const currentSceneFacet = Turn.activeFacet; console.log('enterScene', sceneDs); const rootMesh = new Mesh('--root-' + (+new Date()), runningEngine.scene); runningEngine.applyGravity = false; runningEngine.camera.position = initialPosition.clone(); interpretScene(id, runningEngine, rootMesh, sceneDs); let lastTouchTime = 0; let lastTouchSpriteName = ""; runningEngine.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(runningEngine.position); const currentRotation = () => Shapes.Vector3(runningEngine.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 { on message SceneProtocol.Touch({ "subject": id, "object": $o }) => { react { let needStop = true; on asserted SceneProtocol.Portal({ "name": o, "destination": $dest: SceneProtocol.PortalDestination, "position": $targetPosition: Shapes.Vector3, }) => { const newPos = new Vector3(targetPosition.x, targetPosition.y, targetPosition.z) .add(runningEngine.options.initialPos); needStop = false; switch (dest._variant) { case "local": if (dest.value === sceneDs) { runningEngine.camera.position = newPos; } else { runningEngine.scene.removeMesh(rootMesh, true); Turn.active.stop(currentSceneFacet, () => { react { enterScene(routeField, id, runningEngine, ds, dest.value, newPos, email); } }); } break; default: console.log('jumping to remote portal', dest); routeField.value = dest.value; 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(scale3({ x: 0, y: rotation.value.y, z: 0 }, 1 / (2 * Math.PI)), B.scale({ x: 0.4, y: 1.4, z: 0.1 }, B.box()))), B.rotate(scale3(rotation.value, 1 / (2 * Math.PI)), 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: 0.5, 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 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); })); field route: wsRelay.Noise.Route = wsRelay.Noise.Route({ "transports": [fromJS(relayAddr)], "steps": [], }); during wsRelay.Resolved({ "route": route.value, "addr": $addr: wsRelay.RelayAddress, "resolved": $remoteDs_e: Embedded, }) => { const remoteDs = remoteDs_e.embeddedValue; setupLog(remoteDs, id, Symbol.for('vr-demo')); log('connected'); on message wakeDetector.WakeEvent() => { send message wsRelay.ForceRelayDisconnect(addr); } react { at remoteDs { stop on asserted SceneHandle($sceneDs_e: Embedded) => { react { enterScene(route, id, runningEngine, ds, sceneDs_e.embeddedValue, runningEngine.options.initialPos, 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); }); });