// 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', { default: false, track: true, }), Property('isMenuOpened', { default: false, track: true, }), Property('isFocused', { default: false, track: true, }), Property('darkness', { default: 0, 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.debugUiNode = undefined; this.reactContainer = undefined; this.lastIntensity = undefined; this.renderHandle = undefined; this.stage = new Stage(config.visibleSize, config.visibleScale); this.roomView = new RoomView(this.room, this.stage.renderer); this.stage.addChild(this.roomView); this.on('darknessChanged', this.applyLighting, this); this.on('isFocusedChanged', this.applyMuting, this) this.on('isMenuOpenedChanged', this.applyMuting, this) // 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.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; // 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() { const roomView = this.roomView; if (!roomView) { return; } const layersView = roomView.layersView; if (!layersView) { return; } const layerViews = layersView.layerViews; if (!layerViews) { return; } for (const layerIndex in layerViews) { const layerView = layerViews[layerIndex]; if (!layerView) { return; } const entityListView = layerView.entityListView; const entityList = entityListView.entityList; for (const entity of entityList) { if (entity.isDarkened) { entity.container.night(this.darkness); } } const layerContainer = layerView.layerContainer; layerContainer.night(this.darkness); } } applyMuting() { if (!this.isFocused || this.isMenuOpened) { this.stage.sepia(); } else { this.stage.removeAllFilters(); } } calculateDarkness() { let darkness = 0; if (this.worldTime.hour >= 21 || this.worldTime.hour < 4) { darkness = 0.8; } if (this.worldTime.hour >= 4 && this.worldTime.hour < 7) { darkness = 0.8 * ((7 - this.worldTime.hour) / 3); } if (this.worldTime.hour >= 18 && this.worldTime.hour < 21) { darkness = 0.8 * ((3 - (21 - this.worldTime.hour)) / 3); } this.darkness = Math.floor(darkness * 1000) / 1000; } connect() { const config = this.readConfig(); this.socket = createClient(config.connectionUrl, { parser: this.AugmentedParser, }); this.socket.on('connect', () => { this.removeFromDom(document.querySelector('.app')); 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; case 'e': this.isMenuOpened = !this.isMenuOpened; 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.renderIntoDom(document.querySelector('.app')); 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 = [ 'darkened', 'staged', ]; for (let i = 0; i < clientOnlyTraits.length; ++i) { const type = clientOnlyTraits[i]; if (!entity.is(type)) { entity.addTrait(type); } } // Set darkness up front. if (entity.isDarkened) { entity.container.night(this.darkness); } 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], }; } removeFromDom(node) { if (this.debugUiNode) { ReactDOM.unmountComponentAtNode(this.debugUiNode); } if (this.reactContainer) { ReactDOM.unmountComponentAtNode(this.reactContainer); this.stage.ui.removeChild(this.reactContainer); this.reactContainer = undefined; } this.stage.removeFromDom(node); } renderIntoDom(node) { // Add graphics stage. this.stage.addToDom(node); // UI. this.reactContainer = this.createReactContainer(); this.stage.ui.appendChild(this.reactContainer); const UiComponent = ; ReactDOM.render(UiComponent, this.reactContainer, () => { this.stage.flushUiElements(); }); // Debug UI. this.debugUiNode = node.querySelector('.debug-container'); const DebugUiComponent = ; ReactDOM.render(DebugUiComponent, this.debugUiNode); } 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); // Lol, Apple... window.addEventListener('touchmove', (event) => { event.preventDefault(); }); // 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.calculateDarkness(); 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; } // 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; } }