// 2nd party. import {create as createClient} from '@avocado/client'; 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 {StateSynchronizer} from '@avocado/state'; import {Room, RoomView} from '@avocado/topdown'; import {clearAnimation, setAnimation} from '@avocado/timing'; // 1st party. import {WorldTime} from '../common/world-time'; // DOM. const appNode = document.querySelector('.app'); // 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. const stateSynchronizer = new StateSynchronizer({ room, worldTime, }); room.on('entityAdded', (entity) => { entity.removeTrait('controllable'); // Set self entity. if ('string' === typeof selfEntity) { if (entity === room.findEntity(selfEntity)) { selfEntity = entity; // Only self entity should be controllable. entity.addTrait('controllable'); // Camera tracking. entity.addTrait('followed'); const {camera} = entity; camera.on('realPositionChanged', () => { roomView.position = Vector.round( Vector.sub(halfVisibleSize, camera.realPosition) ); 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, entityPosition); 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 lastTime = performance.now(); const predictionHandle = setInterval(() => { const now = performance.now(); const elapsed = (now - lastTime) / 1000; lastTime = now; if (hasSelfEntity()) { selfEntity.inputState = actionState.toJS(); } room.tick(elapsed); worldTime.tick(elapsed); stage.removeAllFilters(); 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); } if (!isFocused) { stage.paused(intensity); } else { stage.night(intensity); } }, 1000 / 80); // State updates. function onMessage({type, payload}) { switch (type) { case 'state-update': if (payload.selfEntity) { selfEntity = payload.selfEntity; } stateSynchronizer.acceptStateChange(payload); dirty = true; break; default: } } socket.on('message', onMessage); // Render. function render() { stage.tick(); if (!dirty) { return; } 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(); }); }