import {create as createClient} from '@avocado/client'; import {EntityList} from '@avocado/entity'; import {ActionRegistry} from '@avocado/input'; import {Container, Renderer} 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'; let canvasRatio = 1; const renderer = new Renderer([640, 360]); const stage = new Container(); stage.scale = [2, 2]; const room = new Room(); room.world = new World(); const roomView = new RoomView(room, renderer); stage.addChild(roomView); const stateSynchronizer = new StateSynchronizer({ room, }); let selfEntity; function hasSelfEntity() { return selfEntity && 'string' !== typeof selfEntity; } 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'); } } }); // Create the graphics canvas. const appNode = document.querySelector('.app'); renderer.element.tabIndex = 0; appNode.appendChild(renderer.element); // Accept input. const canvasNode = document.querySelector('.app canvas'); const actionRegistry = new ActionRegistry(); actionRegistry.mapKeysToActions({ 'w': 'MoveUp', 'a': 'MoveLeft', 's': 'MoveDown', 'd': 'MoveRight', }); function createMoveToNormal(position) { if (selfEntity) { const scaledEntityPosition = Vector.mul( selfEntity.position, stage.scale, ); const diff = Vector.sub( position, scaledEntityPosition, ); const magnitude = Vector.magnitude(position, scaledEntityPosition); if (magnitude < 8) { return; } const normal = Vector.normalize(diff); return normal; } } let pointingAt = [-1, -1]; function isPointingAtAnything() { return -1 !== pointingAt[0] && -1 !== pointingAt[1]; } function pointingAtFromEvent(event) { const rect = event.target.getBoundingClientRect(); return Vector.scale( [ event.clientX - rect.x, event.clientY - rect.y, ], canvasRatio, ); } canvasNode.addEventListener('pointerdown', (event) => { pointingAt = pointingAtFromEvent(event); }); canvasNode.addEventListener('pointermove', (event) => { if (!isPointingAtAnything()) { return; } pointingAt = pointingAtFromEvent(event); }); canvasNode.addEventListener('pointerup', (event) => { pointingAt = [-1, -1]; }); actionRegistry.listen(canvasNode); 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); }, 1000 / 80); // State updates. let dirty = false; 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; } renderer.render(stage); 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(); appNode.removeChild(renderer.element); stage.destroy(); renderer.destroy(); }); } // Make sure dis boi always correct ratio and always maximally visible. function onResize() { const ratio = window.innerWidth / window.innerHeight; const axe = ratio > (16 / 9) ? 'height' : 'width'; const nodes = window.document.querySelectorAll('.app canvas'); nodes.forEach((node) => { for (const key of ['height', 'width']) { node.style[key] = key === axe ? '100%' : 'auto'; } canvasRatio = 640 / node.clientWidth; }); } window.addEventListener('resize', onResize); onResize();