// 3rd party. import React from 'react'; import ReactDOM from 'react-dom'; // 2nd party. import {create as createClient} from '@avocado/client/socket'; import {compose} from '@avocado/core'; import {Stage} from '@avocado/graphics'; import {ActionRegistry, InputPacket} from '@avocado/input'; import {Vector} from '@avocado/math'; import {EventEmitter, Property} from '@avocado/mixins'; import {SocketIoParser} from '@avocado/packet'; import { StateKeysPacket, StatePacket, Synchronizer, Unpacker, } from '@avocado/state'; import {clearAnimation, setAnimation} from '@avocado/timing'; import {World} from '@avocado/physics/matter/world'; import {Room, RoomView} from '@avocado/topdown'; // 1st party. import {augmentParserWithThroughput} from '../common/parser-throughput'; import {WorldTime} from '../common/world-time'; import Ui from './ui'; import DebugUi from './ui/debug'; const decorate = compose( EventEmitter, Property('isDebugging', { track: true, }), Property('selfEntity', { track: true, }), Property('selfEntityUuid', { track: true, }), ); export class App extends decorate(class {}) { constructor() { super(); const config = this.readConfig(); // Room. this.room = new Room(); // World time. this.worldTime = new WorldTime(); // Graphics. this.lastIntensity = undefined; this.renderHandle = undefined; this.stage = new Stage(config.visibleSize, config.visibleScale); const roomView = new RoomView(this.room, this.stage.renderer); this.stage.addChild(roomView); // Listen for new entities. this.room.on('entityAdded', this.onRoomEntityAdded, this); // Input. this.actionRegistry = new ActionRegistry(); this.actionRegistry.mapKeysToActions(config.actionKeyMap); this.actionState = this.actionRegistry.state; this.inputHandle = undefined; this.isFocused = false; this.lastIsFocused = undefined; this.onBlur = this.onBlur.bind(this); this.onFocus = this.onFocus.bind(this); // Global keys. this.onGlobalKeydown = this.onGlobalKeydown.bind(this); window.addEventListener('keydown', this.onGlobalKeydown); this.pointingAt = [-1, -1]; this.pointerMovementHandle = undefined; // Net. this.AugmentedParser = augmentParserWithThroughput(SocketIoParser); this.hasReceivedState = false; this.isConnected = false; this.socket = undefined; // Debugging. this.isDebugging = false; // Simulation. this.simulationHandle = undefined; this.world = config.doPhysicsSimulation ? new World() : undefined; this.room.world = this.world; // State synchronization. this.state = undefined; this.synchronizer = new Synchronizer({ room: this.room, worldTime: this.worldTime, }); this.unpacker = new Unpacker(); } applyLighting() { let intensity = 0; if (this.worldTime.hour >= 21 || this.worldTime.hour < 4) { intensity = 0.8; } if (this.worldTime.hour >= 4 && this.worldTime.hour < 7) { intensity = 0.8 * ((7 - this.worldTime.hour) / 3); } if (this.worldTime.hour >= 18 && this.worldTime.hour < 21) { intensity = 0.8 * ((3 - (21 - this.worldTime.hour)) / 3); } intensity = Math.floor(intensity * 1000) / 1000; if (intensity !== this.lastIntensity || this.isFocused !== this.lastIsFocused) { this.stage.removeAllFilters(); if (!this.isFocused) { this.stage.paused(intensity); } else { this.stage.night(intensity); } this.lastIntensity = intensity; this.lastIsFocused = this.isFocused; } } connect() { const config = this.readConfig(); this.socket = createClient(config.connectionUrl, { parser: this.AugmentedParser, }); this.socket.on('connect', () => { this.room.layers.destroy(); this.selfEntity = undefined; this.selfEntityUuid = undefined; this.isConnected = true; }); this.socket.on('disconnect', () => { this.hasReceivedState = false; this.isConnected = false; this.stopRendering(); this.stopSimulation(); this.stopProcessingInput(); }); this.socket.on('packet', this.onPacket, this); } createMoveToNormal(position) { if (!this.selfEntity) { return; } const realEntityPosition = Vector.sub( this.selfEntity.position, this.selfEntity.camera.realOffset, ); const magnitude = Vector.magnitude(position, realEntityPosition); if (magnitude < 8) { return; } const diff = Vector.sub(position, realEntityPosition); return Vector.normalize(diff); } createReactContainer() { const reactContainer = window.document.createElement('div'); reactContainer.className = 'react'; reactContainer.style.width = '100%'; reactContainer.style.height = '100%'; return reactContainer; } eventIsInUi(event) { let walk = event.target; while (walk) { if (walk === this.stage.ui) { return true; } walk = walk.parentNode; } return false; } isPointingAtAnything() { return -1 !== this.pointingAt[0] && -1 !== this.pointingAt[1]; } onBlur() { this.isFocused = false; } onFocus() { this.isFocused = true; } onGlobalKeydown(event) { switch (event.key) { case 'F2': this.isDebugging = !this.isDebugging; break; } } onPacket(packet) { if (packet instanceof StateKeysPacket) { this.unpacker.registerKeys(packet.data); } if (packet instanceof StatePacket) { const patch = this.unpacker.unpack(packet.data); // Yank out self entity. if (!this.selfEntityUuid) { for (const step of patch) { const {op, path, value} = step; if ('add' === op && '/selfEntity' === path) { this.selfEntityUuid = value; } } } this.synchronizer.patchState(patch); if (!this.hasReceivedState) { this.startProcessingInput(); this.startSimulation(); this.startRendering(); this.hasReceivedState = true; } } } onPointerDown(event) { if (!this.eventIsInUi(event)) { return; } this.pointingAt = event.position; } onPointerMove(event) { if (!this.eventIsInUi(event)) { return; } if (!this.isPointingAtAnything()) { return; } this.pointingAt = event.position; } onPointerUp(event) { this.pointingAt = [-1, -1]; } onRoomEntityAdded(entity) { // Traits that shouldn't be on client. const noClientTraits = [ 'behaved', 'informed', ]; // If there's no physics, then remove physical trait. if (!this.room.world) { noClientTraits.push('physical'); } // Traits that only make sense for our self entity on the client. const selfEntityOnlyTraits = [ 'controllable', 'followed', ]; const nixedTraits = selfEntityOnlyTraits.concat(noClientTraits); for (let i = 0; i < nixedTraits.length; ++i) { const type = nixedTraits[i]; if (entity.is(type)) { entity.removeTrait(type); } } // Client only traits. const clientOnlyTraits = [ 'staged', ]; for (let i = 0; i < clientOnlyTraits.length; ++i) { const type = clientOnlyTraits[i]; entity.addTrait(type); } entity.stage = this.stage; // Set self entity. if (this.selfEntityUuid) { if (entity === this.room.findEntity(this.selfEntityUuid)) { this.selfEntity = entity; this.selfEntityUuid = undefined; // Add back our self entity traits. for (const type of selfEntityOnlyTraits) { entity.addTrait(type); } const {camera} = entity; this.stage.camera = camera; // Avoid the initial 'lerp. camera.realPosition = camera.position; } } } // Stub for now! readConfig() { return { actionKeyMap: { 'w': 'MoveUp', 'a': 'MoveLeft', 's': 'MoveDown', 'd': 'MoveRight', }, connectionUrl: window.location.href, doPhysicsSimulation: true, inputFrequency: 1 / 60, pointerMovementFrequency: 1 / 15, simulationFrequency: 1 / 60, visibleScale: [1, 1], visibleSize: [320, 180], }; } renderIntoDom(node) { // Lol, Apple... node.addEventListener('touchmove', (event) => { event.preventDefault(); }); // Add graphics stage. this.stage.addToDom(node); // UI. const reactContainer = this.createReactContainer(); this.stage.ui.appendChild(reactContainer); const UiComponent = ; ReactDOM.render(UiComponent, reactContainer); // Debug UI. const debugUiNode = node.querySelector('.debug-container'); const DebugUiComponent = ; ReactDOM.render(DebugUiComponent, debugUiNode, () => { this.stage.resolveUiRendered(); }); } startProcessingInput() { const config = this.readConfig(); this.actionRegistry.listen(this.stage.element); // Pointer input. this.stage.on('pointerDown', this.onPointerDown, this); this.stage.on('pointerMove', this.onPointerMove, this); this.stage.on('pointerUp', this.onPointerUp, this); // Blur/focus. this.stage.element.addEventListener('blur', this.onBlur); this.stage.element.addEventListener('focus', this.onFocus); // Input messages. this.inputHandle = setInterval(() => { if (this.actionState !== this.actionRegistry.state) { this.actionState = this.actionRegistry.state; this.socket.send(InputPacket.fromState(this.actionState)); } }, 1000 * config.inputFrequency); // Mouse/touch movement. this.pointerMovementHandle = setInterval(() => { do { let normal; if (this.isPointingAtAnything()) { const toVector = Vector.scale(this.pointingAt, this.isDebugging ? 2 : 1); normal = this.createMoveToNormal(toVector); if (normal) { this.actionRegistry.state = this.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; } } this.actionRegistry.state = this.actionRegistry.state.withMutations((state) => { if (!normal || 0 === normal[0]) { state.delete('MoveToX'); } if (!normal || 0 === normal[1]) { state.delete('MoveToY'); } }); } while (false); }, 1000 * config.pointerMovementFrequency); // Focus the stage. this.stage.focus(); this.isFocused = true; } startRendering() { let lastTime = performance.now(); this.renderHandle = setAnimation(() => { const now = performance.now(); const elapsed = (now - lastTime) / 1000; lastTime = now; this.applyLighting(); this.stage.render(); }); } startSimulation() { const config = this.readConfig(); let lastTime = performance.now(); this.simulationHandle = setInterval(() => { const now = performance.now(); const elapsed = (now - lastTime) / 1000; lastTime = now; // Inject input. if (this.selfEntity) { this.selfEntity.inputState = this.actionState.toJS(); } // Tick synchronized. this.synchronizer.tick(elapsed); this.state = this.synchronizer.state; }, 1000 * config.simulationFrequency); } stopProcessingInput() { clearInterval(this.pointerMovementHandle); this.pointerMovementHandle = undefined; clearInterval(this.inputHandle); this.inputHandle = undefined; this.stage.element.removeEventListener('focus', this.onFocus); this.stage.element.removeEventListener('blur', this.onBlur); this.stage.off('pointerUp', this.onPointerUp, this); this.stage.off('pointerMove', this.onPointerMove, this); this.stage.off('pointerDown', this.onPointerDown, this); this.actionRegistry.stopListening(); } stopRendering() { clearAnimation(this.renderHandle); this.renderHandle = undefined; } stopSimulation() { clearInterval(this.simulationHandle); this.simulationHandle = undefined; } }