// 3rd party. 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 Ui from './ui'; // DOM. const appNode = document.querySelector('.app'); // Lol Apple appNode.addEventListener('touchmove', (event) => { event.preventDefault(); }); // Graphics stage. 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); // 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; evaluationContext.selfEntity = selfEntity; // Add back our self entity traits. for (const type of selfEntityOnlyTraits) { entity.addTrait(type); } const {camera} = entity; camera.on('realPositionChanged', () => { const offset = Vector.sub(halfVisibleSize, camera.realPosition); roomView.position = offset; }); // 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 realEntityPosition = Vector.sub( entityPosition, Vector.sub(selfEntity.camera.realPosition, halfVisibleSize) ); const magnitude = Vector.magnitude(position, realEntityPosition); if (magnitude < 4) { 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, }); // Mouse/touch movement. const pointerMovementHandle = setInterval(() => { 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); }, 1000 / 15); // Input messages. const inputHandle = setInterval(() => { if (actionState !== actionRegistry.state) { actionState = actionRegistry.state; socket.send(new InputPacket(JSON.stringify(actionRegistry.state.toJS()))); } }, 1000 / 60); // Prediction. let mayRender = false; const renderTicker = new Ticker(1 / 60); renderTicker.on('tick', () => { mayRender = true; }); let lastIntensity = undefined; let lastIsFocused = undefined; let lastTime = performance.now(); const predictionHandle = setInterval(() => { 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); // Apply environmental lighting. 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; } }, 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); for (const step of patch) { const {op, path, value} = step; if ('add' === op && '/selfEntity' === path) { selfEntity = value; } } synchronizer.patchState(patch); } } socket.on('packet', onPacket); // Render. function render() { stage.tick(); if (!mayRender) { return false; } mayRender = false; stage.render(); } const renderHandle = setAnimation(render); // UI. const UiComponent = ; ReactDOM.render(UiComponent, stage.ui); // Eval. const evaluationContext = { room, selfEntity, stage, }; // Just inject it into global for now. window.humus = evaluationContext;