// 2nd party. import {create as createClient} from '@avocado/client/socket'; import {EntityList} from '@avocado/entity'; import {ActionRegistry} from '@avocado/input'; import {Stage} from '@avocado/graphics'; import {Vector} from '@avocado/math'; import {World} from '@avocado/physics/matter/world'; import {Synchronizer, Unpacker} from '@avocado/state'; import {Room, RoomView} from '@avocado/topdown'; import {clearAnimation, setAnimation, Ticker} from '@avocado/timing'; // 1st party. import {WorldTime} from '../common/world-time'; // DOM. const appNode = document.querySelector('.app'); // Lol Apple appNode.addEventListener('touchmove', (event) => { event.preventDefault(); }); // Graphics stage. let dirty = false; const visibleSize = [320, 180]; const halfVisibleSize = Vector.scale(visibleSize, 0.5); const visibleScale = [2, 2]; const stage = new Stage(Vector.mul(visibleSize, visibleScale)); stage.scale = visibleScale; let isFocused = false; stage.element.addEventListener('blur', () => { isFocused = false; }); stage.element.addEventListener('focus', () => { isFocused = true; }); stage.addToDom(appNode); // Time display. const timeUi = document.createElement('div'); timeUi.className = 'time unselectable'; stage.ui.appendChild(timeUi); // Handle "own" entity. let selfEntity; function hasSelfEntity() { return selfEntity && 'string' !== typeof selfEntity; } // Create room. const room = new Room(); // room.world = new World(); const roomView = new RoomView(room, stage.renderer); stage.addChild(roomView); // Time. const worldTime = new WorldTime(); let lastWorldTime = worldTime.humanReadable(); // Synchronize state. let state = undefined; const synchronizer = new Synchronizer({ room, worldTime, }); const unpacker = new Unpacker(); room.on('entityAdded', (entity) => { // Traits that shouldn't be on client. const noClientTraits = [ 'behaved', ]; // If there's no physics, then remove physical trait. if (!room.world) { noClientTraits.push('physical'); } // Traits that only make sense for our self entity on the client. const selfEntityOnlyTraits = [ 'controllable', 'followed', ]; for (const type of selfEntityOnlyTraits.concat(noClientTraits)) { if (entity.is(type)) { entity.removeTrait(type); } } // Set self entity. if ('string' === typeof selfEntity) { if (entity === room.findEntity(selfEntity)) { selfEntity = entity; // Add back our self entity traits. for (const type of selfEntityOnlyTraits) { entity.addTrait(type); } const {camera} = entity; camera.on('realPositionChanged', () => { const offset = Vector.sub(halfVisibleSize, camera.realPosition); roomView.position = offset; dirty = true; }); // Avoid the initial 'lerp. camera.realPosition = camera.position; } } }); // Accept input. const actionRegistry = new ActionRegistry(); actionRegistry.mapKeysToActions({ 'w': 'MoveUp', 'a': 'MoveLeft', 's': 'MoveDown', 'd': 'MoveRight', }); actionRegistry.listen(stage.element); // Mouse/touch movement. function createMoveToNormal(position) { if (!selfEntity) { return; } const entityPosition = selfEntity.position; const magnitude = Vector.magnitude(position, entityPosition); if (magnitude < 8) { return; } const diff = Vector.sub( position, Vector.sub( entityPosition, Vector.sub(selfEntity.camera.realPosition, halfVisibleSize) ), ); return Vector.normalize(diff); } let pointingAt = [-1, -1]; function isPointingAtAnything() { return -1 !== pointingAt[0] && -1 !== pointingAt[1]; } stage.on('pointerDown', (event) => { if (event.target !== stage.ui) { return; } pointingAt = event.position; }); stage.on('pointerMove', (event) => { if (event.target !== stage.ui) { return; } if (!isPointingAtAnything()) { return; } pointingAt = event.position; }); stage.on('pointerUp', (event) => { pointingAt = [-1, -1]; }); let actionState = actionRegistry.state; // Create the socket connection. const socket = createClient(window.location.href); // Input messages. const messageHandle = setInterval(() => { // Mouse/touch movement. do { if (isPointingAtAnything()) { const normal = createMoveToNormal(pointingAt); if (normal) { actionRegistry.state = actionRegistry.state.set('MoveTo', normal); break; } } actionRegistry.state = actionRegistry.state.delete('MoveTo'); } while (false); if (actionState !== actionRegistry.state) { actionState = actionRegistry.state; socket.send({ type: 'input', payload: actionState.toJS() }); } }, 1000 / 60); // Prediction. let mayRender = false; const renderTicker = new Ticker(1 / 60); renderTicker.on('tick', () => { mayRender = true; }); let lastIntensity = undefined; let lastIsFocused = undefined; let lastTime = performance.now(); const predictionHandle = setInterval(() => { const now = performance.now(); const elapsed = (now - lastTime) / 1000; lastTime = now; if (hasSelfEntity()) { selfEntity.inputState = actionState.toJS(); } // Tick synchronized. synchronizer.tick(elapsed); dirty = dirty || synchronizer.state !== state; state = synchronizer.state; // Render timing. renderTicker.tick(elapsed); // Apply environmental lighting. let intensity = 0; if (worldTime.hour >= 21 || worldTime.hour < 4) { intensity = 0.8; } if (worldTime.hour >= 4 && worldTime.hour < 7) { intensity = 0.8 * ((7 - worldTime.hour) / 3); } if (worldTime.hour >= 18 && worldTime.hour < 21) { intensity = 0.8 * ((3 - (21 - worldTime.hour)) / 3); } intensity = Math.floor(intensity * 1000) / 1000; if (intensity !== lastIntensity || isFocused !== lastIsFocused) { stage.removeAllFilters(); if (!isFocused) { stage.paused(intensity); } else { stage.night(intensity); } lastIntensity = intensity; lastIsFocused = isFocused; } }, 1000 / 60); // State updates. function onMessage(message) { if ('type' in message) { switch (message.type) { case 'keys': unpacker.registerKeys(message.payload); break; } } if (message instanceof window.ArrayBuffer) { const patch = unpacker.unpack(message); for (const step of patch) { const {op, path, value} = step; if ('add' === op && '/selfEntity' === path) { selfEntity = value; } } synchronizer.patchState(patch); dirty = true; } } socket.on('message', onMessage); // Render. function render() { stage.tick(); if (!dirty) { return; } if (!mayRender) { return false; } mayRender = false; if (worldTime.humanReadable() !== lastWorldTime) { timeUi.textContent = worldTime.humanReadable(); lastWorldTime = worldTime.humanReadable(); } stage.render(); dirty = false; } const renderHandle = setAnimation(render); // Hot reloading. if (module.hot) { module.hot.accept((error) => { console.error(error); }); module.hot.dispose(() => { clearAnimation(renderHandle); room.destroy(); stage.destroy(); }); }