humus-old/client/index.js
2019-04-19 21:21:41 -05:00

280 lines
7.2 KiB
JavaScript

// 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, InputPacket} from '@avocado/input';
import {Vector} from '@avocado/math';
import {SocketIoParser} from '@avocado/packet';
import {
StateKeysPacket,
StatePacket,
Synchronizer,
Unpacker,
} from '@avocado/state';
import {clearAnimation, setAnimation, Ticker} from '@avocado/timing';
// 1st party.
import {augmentParserWithThroughput} from '../common/parser-throughput';
import {App} from './app';
import Ui from './ui';
import DebugUi from './ui/debug';
// Application.
const app = new App();
// Synchronize state.
let state = undefined;
const synchronizer = new Synchronizer({
room: app.room,
worldTime: app.worldTime,
});
// 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(app.stage.element);
// Mouse/touch movement.
function createMoveToNormal(position) {
if (!app.selfEntity) {
return;
}
const realEntityPosition = Vector.sub(
app.selfEntity.position,
app.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];
}
function isInUi(node) {
let walk = node;
while (walk) {
if (walk === app.stage.ui) {
return true;
}
walk = walk.parentNode;
}
return false;
}
app.stage.on('pointerDown', (event) => {
if (!isInUi(event.target)) {
return;
}
pointingAt = event.position;
});
app.stage.on('pointerMove', (event) => {
if (!isInUi(event.target)) {
return;
}
if (!isPointingAtAnything()) {
return;
}
pointingAt = event.position;
});
app.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', () => {
app.room.layers.destroy();
app.selfEntity = undefined;
app.selfEntityUuid = 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 (app.selfEntity) {
app.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 StateKeysPacket) {
unpacker.registerKeys(packet.data);
}
if (packet instanceof StatePacket) {
const patch = unpacker.unpack(packet.data);
// Yank out self entity.
if (!app.selfEntityUuid) {
for (const step of patch) {
const {op, path, value} = step;
if ('add' === op && '/selfEntity' === path) {
app.selfEntityUuid = value;
}
}
}
synchronizer.patchState(patch);
}
}
socket.on('packet', onPacket);
// Apply stage lighting.
let isFocused = false;
app.stage.element.addEventListener('blur', () => {
isFocused = false;
});
app.stage.element.addEventListener('focus', () => {
isFocused = true;
});
let lastIntensity = undefined;
let lastIsFocused = undefined;
function applyStageLighting() {
let intensity = 0;
if (app.worldTime.hour >= 21 || app.worldTime.hour < 4) {
intensity = 0.8;
}
if (app.worldTime.hour >= 4 && app.worldTime.hour < 7) {
intensity = 0.8 * ((7 - app.worldTime.hour) / 3);
}
if (app.worldTime.hour >= 18 && app.worldTime.hour < 21) {
intensity = 0.8 * ((3 - (21 - app.worldTime.hour)) / 3);
}
intensity = Math.floor(intensity * 1000) / 1000;
if (intensity !== lastIntensity || isFocused !== lastIsFocused) {
app.stage.removeAllFilters();
if (!isFocused) {
app.stage.paused(intensity);
}
else {
app.stage.night(intensity);
}
lastIntensity = intensity;
lastIsFocused = isFocused;
}
}
// Render.
function render() {
if (!isConnected) {
return;
}
if (!mayRender) {
return false;
}
mayRender = false;
// Lighting.
applyStageLighting();
app.stage.render();
}
const renderHandle = setAnimation(render);
// UI.
const UiComponent = <Ui
socket={socket}
worldTime={app.worldTime}
/>;
// Create UI layer for react.
const reactContainer = window.document.createElement('div');
reactContainer.className = 'react';
app.stage.on('displaySizeChanged', () => {
const displaySize = app.stage.displaySize;
reactContainer.style.width = '100%';
reactContainer.style.height = '100%';
})
app.stage.ui.appendChild(reactContainer);
ReactDOM.render(UiComponent, reactContainer);
// Debug UI.
const debugUiNode = document.querySelector('.debug-container');
const DebugUiComponent = <DebugUi
actionRegistry={actionRegistry}
app={app}
Parser={AugmentedParser}
socket={socket}
stage={app.stage}
/>;
ReactDOM.render(DebugUiComponent, debugUiNode, () => {
app.stage.resolveUiRendered();
});
// Inject the stage last.
app.injectStageIntoDom(document.querySelector('.app'));
// Eval.
const evaluationContext = {
room: app.room,
selfEntity: app.selfEntity,
stage: app.stage,
};
app.on('selfEntityChanged', () => {
evaluationContext.selfEntity = app.selfEntity;
});
// Just inject it into global for now.
window.humus = evaluationContext;