/* * Haunted House — XZBT Exhibit Contract 5.3 adapter. * * This adapter is the one that has to be honest about capabilities. The * exhibit's audio engine cannot start until the browser has seen a user * gesture, so `audio` is declared `available` at boot and moves to * `loading` then `ready` when it really does. The adapter mirrors the * exhibit's real state into the capability registry; it never invents a * transition (Authoring Guide §M). * * The audio-dependent actions are gated on that capability, so invoking * `action.moan` before the engine is ready returns CAPABILITY_UNAVAILABLE * rather than silently doing nothing. */ (function () { 'use strict'; var Core = window.XZBTContractCore; var IDENTITY = { product: 'Haunted House', version: '1.0.0', build: 'reference-exhibit' }; var DESCRIPTORS = [ { id: 'room.current', kind: 'selection', label: 'Room', description: 'Room the visitor is standing in.', options: [ { value: 'foyer', label: 'Foyer' }, { value: 'library', label: 'Library' }, { value: 'cellar', label: 'Cellar' }, { value: 'attic', label: 'Attic' }, { value: 'nursery', label: 'Nursery' } ], readable: true, writable: true, restorable: true, category: 'room', requires: [] }, { id: 'room.spirit', kind: 'selection', label: 'Spirit', description: 'Which presence is currently resident.', options: [ { value: 'restless', label: 'Restless' }, { value: 'vengeful', label: 'Vengeful' }, { value: 'mourning', label: 'Mourning' }, { value: 'mischievous', label: 'Mischievous' } ], readable: true, writable: true, restorable: true, category: 'room', requires: [] }, { id: 'room.haunt-level', kind: 'range', label: 'Haunt level', description: 'Intensity of the haunting, 0 (dormant) to 1 (manifest).', min: 0, max: 1, step: 0.01, readable: true, writable: true, restorable: true, category: 'room', requires: [] }, { id: 'room.fog-density', kind: 'range', label: 'Fog density', description: 'Density of the ground fog.', min: 0, max: 1, step: 0.01, readable: true, writable: true, restorable: true, category: 'room', requires: [] }, { id: 'room.candlelight', kind: 'range', label: 'Candlelight', description: 'Strength of the candle pool of light.', min: 0, max: 1, step: 0.01, readable: true, writable: true, restorable: true, category: 'room', requires: [] }, { id: 'room.audio-enabled', kind: 'state', valueType: 'boolean', label: 'Audio enabled', description: 'Whether the house is permitted to make sound.', readable: true, writable: true, restorable: true, category: 'room', requires: [] }, { id: 'room.paused', kind: 'state', valueType: 'boolean', label: 'Held', description: 'Whether the haunt simulation is frozen.', readable: true, writable: true, restorable: true, category: 'room', requires: [] }, { id: 'action.moan', kind: 'impulse', label: 'Moan', description: 'Emit a long descending moan.', readable: false, writable: false, restorable: false, category: 'action', requires: ['audio'] }, { id: 'action.slam-door', kind: 'impulse', label: 'Slam door', description: 'Slam a door somewhere in the house.', readable: false, writable: false, restorable: false, category: 'action', requires: ['audio'] }, { id: 'action.manifest', kind: 'impulse', label: 'Manifest', description: 'Bring the apparition to full manifestation.', readable: false, writable: false, restorable: false, category: 'action', requires: [] }, { id: 'action.calm', kind: 'impulse', label: 'Calm', description: 'Settle the house back to dormancy.', readable: false, writable: false, restorable: false, category: 'action', requires: [] }, { id: 'telemetry.audio-state', kind: 'state', valueType: 'string', label: 'Audio engine state', description: 'Live state of the Web Audio engine.', readable: true, writable: false, restorable: false, category: 'telemetry', requires: [] }, { id: 'telemetry.room-damp', kind: 'state', valueType: 'number', label: 'Room damping', description: 'How strongly the current room suppresses the haunt.', readable: true, writable: false, restorable: false, category: 'telemetry', requires: [] } ]; function createHauntedHouseContract(exhibit, ui) { var catalog = new Core.Catalog(DESCRIPTORS); var capabilities = new Core.CapabilityRegistry(); capabilities.declare('render', 'ready'); /* Honest initial state: the engine exists but the browser will not let * it start until a gesture arrives. */ capabilities.declare('audio', 'available'); var setters = { 'room.current': function (v) { return exhibit.setRoom(v); }, 'room.spirit': function (v) { return exhibit.setSpirit(v); }, 'room.haunt-level': function (v) { return exhibit.setHauntLevel(v); }, 'room.fog-density': function (v) { return exhibit.setFogDensity(v); }, 'room.candlelight': function (v) { return exhibit.setCandlelight(v); }, 'room.audio-enabled': function (v) { return exhibit.setAudioEnabled(v); }, 'room.paused': function (v) { return exhibit.setPaused(v); } }; var readers = {}; var ids = catalog.ids(); for (var i = 0; i < ids.length; i++) { (function (id) { readers[id] = function () { return exhibit.read(id); }; })(ids[i]); } var actions = { 'action.moan': function () { return exhibit.moan(); }, 'action.slam-door': function () { return exhibit.slamDoor(); }, 'action.manifest': function () { return exhibit.manifest(); }, 'action.calm': function () { return exhibit.calm(); } }; var core = new Core.ContractCore({ identity: IDENTITY, catalog: catalog, capabilities: capabilities, setters: setters, readers: readers, actions: actions }); /* Mirror the exhibit's real audio lifecycle into the contract. This is * a genuine state change driven by the browser, not a scripted one. */ exhibit.audio.onStateChange = function (state) { core.setCapabilityState('audio', state); ui.sync(); }; ui.dispatchOverride = function (targetId, value) { core.applyMutation(targetId, value, 'ui'); ui.sync(); }; ui.invokeOverride = function (targetId) { core.invokeAction(targetId, {}, 'ui'); ui.sync(); }; exhibit.onChange = function () { ui.sync(); }; return core; } window.HauntedHouseContract = { create: createHauntedHouseContract, IDENTITY: IDENTITY }; })();