// 3rd party. import msgpack from 'msgpack-lite'; import React from 'react'; import ReactDOM from 'react-dom'; // 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 {SocketIoParser} from '@avocado/packet'; import {Synchronizer, Unpacker} from '@avocado/state'; import {Room, RoomView} from '@avocado/topdown'; import {World} from '@avocado/physics/matter/world'; import {clearAnimation, setAnimation, Ticker} from '@avocado/timing'; // 1st party. import {InputPacket, KeysPacket, StatePacket} from '../common/packet'; import {augmentParserWithThroughput} from '../common/parser-throughput'; import {WorldTime} from '../common/world-time'; import {App} from './app'; import Ui from './ui'; import DebugUi from './ui/debug'; // Application. const app = new App(); // DOM. const appNode = document.querySelector('.app'); // Graphics stage. const visibleSize = [320, 180]; const visibleScale = [2, 2]; const stage = new Stage(Vector.mul(visibleSize, visibleScale)); stage.scale = visibleScale; // 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(); // Synchronize state. let state = undefined; const synchronizer = new Synchronizer({ room, worldTime, }); 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; app.selfEntity = entity; // Add back our self entity traits. for (const type of selfEntityOnlyTraits) { entity.addTrait(type); } const {camera} = entity; camera.on('realPositionChanged', () => { roomView.position = Vector.scale(selfEntity.camera.realOffset, -1); }); // Avoid the initial 'lerp. camera.realPosition = camera.position; } } }); // Accept input. window.addEventListener('keydown', (event) => { event = event || window.event; if ('F2' === event.key) { app.isDebugging = !app.isDebugging; } }); const actionRegistry = new ActionRegistry(); actionRegistry.mapKeysToActions({ 'w': 'MoveUp', 'a': 'MoveLeft', 's': 'MoveDown', 'd': 'MoveRight', }); actionRegistry.listen(stage.element); // Lol Apple appNode.addEventListener('touchmove', (event) => { event.preventDefault(); }); // Mouse/touch movement. function createMoveToNormal(position) { if (!selfEntity) { return; } const realEntityPosition = Vector.sub( selfEntity.position, selfEntity.camera.realOffset, ); const magnitude = Vector.magnitude(position, realEntityPosition); if (magnitude < 8) { return; } const diff = Vector.sub(position, realEntityPosition); 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 AugmentedParser = augmentParserWithThroughput(SocketIoParser); const socket = createClient(window.location.href, { parser: AugmentedParser, }); let isConnected = false; socket.on('connect', () => { room.layers.destroy(); selfEntity = undefined; isConnected = true; }); socket.on('disconnect', () => { isConnected = false; }); // Mouse/touch movement. const pointerMovementHandle = setInterval(() => { do { let normal; if (isPointingAtAnything()) { const toVector = Vector.scale(pointingAt, app.isDebugging ? 2 : 1); normal = createMoveToNormal(toVector); if (normal) { actionRegistry.state = actionRegistry.state.withMutations((state) => { if (normal[0]) { state.set('MoveToX', Math.floor(normal[0] * 127)); } if (normal[1]) { state.set('MoveToY', Math.floor(normal[1] * 127)); } }); break; } } actionRegistry.state = actionRegistry.state.withMutations((state) => { if (!normal || 0 === normal[0]) { state.delete('MoveToX'); } if (!normal || 0 === normal[1]) { state.delete('MoveToY'); } }); } while (false); }, 1000 / 15); // Input messages. const inputHandle = setInterval(() => { if (!isConnected) { return; } if (actionState !== actionRegistry.state) { actionState = actionRegistry.state; socket.send(InputPacket.fromState(actionState)); } }, 1000 / 60); // Prediction. let mayRender = false; const renderTicker = new Ticker(1 / 60); renderTicker.on('tick', () => { mayRender = true; }); let lastTime = performance.now(); const predictionHandle = setInterval(() => { if (!isConnected) { return; } const now = performance.now(); const elapsed = (now - lastTime) / 1000; lastTime = now; if (hasSelfEntity()) { selfEntity.inputState = actionState.toJS(); } // Tick synchronized. synchronizer.tick(elapsed); state = synchronizer.state; // Render timing. renderTicker.tick(elapsed); }, 1000 / 60); // State updates. const unpacker = new Unpacker(); function onPacket(packet) { if (packet instanceof KeysPacket) { unpacker.registerKeys(packet.data); } if (packet instanceof StatePacket) { const patch = unpacker.unpack(packet.data); // Yank out self entity. if (!selfEntity) { for (const step of patch) { const {op, path, value} = step; if ('add' === op && '/selfEntity' === path) { selfEntity = value; } } } synchronizer.patchState(patch); } } socket.on('packet', onPacket); // Apply stage lighting. let isFocused = false; stage.element.addEventListener('blur', () => { isFocused = false; }); stage.element.addEventListener('focus', () => { isFocused = true; }); let lastIntensity = undefined; let lastIsFocused = undefined; function applyStageLighting() { 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; } } // Render. function render() { if (!isConnected) { return; } stage.tick(); if (!mayRender) { return false; } mayRender = false; // Lighting. applyStageLighting(); stage.render(); } const renderHandle = setAnimation(render); // UI. const UiComponent = ; ReactDOM.render(UiComponent, stage.ui); // Debug UI. const debugUiNode = document.querySelector('.debug-container'); const DebugUiComponent = ; ReactDOM.render(DebugUiComponent, debugUiNode); // Inject the stage last. stage.addToDom(appNode); // Eval. const evaluationContext = { room, selfEntity, stage, }; app.on('selfEntityChanged', (selfEntity) => { evaluationContext.selfEntity = selfEntity; }); // Just inject it into global for now. window.humus = evaluationContext;