humus-old/client/index.js
2019-04-10 13:42:29 -04:00

240 lines
6.2 KiB
JavaScript

// 2nd party.
import {create as createClient} from '@avocado/client';
import {EntityList} from '@avocado/entity';
import {ActionRegistry} from '@avocado/input';
import {Stage} from '@avocado/graphics';
import {Vector} from '@avocado/math';
import {World} from '@avocado/physics/matter/world';
import {Synchronizer, Unpacker} from '@avocado/state';
import {Room, RoomView} from '@avocado/topdown';
import {clearAnimation, setAnimation} from '@avocado/timing';
// 1st party.
import {WorldTime} from '../common/world-time';
// DOM.
const appNode = document.querySelector('.app');
// Lol Apple
appNode.addEventListener('touchmove', (event) => {
event.preventDefault();
});
// Graphics stage.
let dirty = false;
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);
// Time display.
const timeUi = document.createElement('div');
timeUi.className = 'time unselectable';
stage.ui.appendChild(timeUi);
// 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();
let lastWorldTime = worldTime.humanReadable();
// Synchronize state.
let state = undefined;
const synchronizer = new Synchronizer({
room,
worldTime,
});
const unpacker = new Unpacker();
room.on('entityAdded', (entity) => {
// Traits that shouldn't be on client.
const noClientTraits = [
'behaved',
];
// 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;
// Add back our self entity traits.
for (const type of selfEntityOnlyTraits) {
entity.addTrait(type);
}
const {camera} = entity;
camera.on('realPositionChanged', () => {
roomView.position = Vector.round(
Vector.sub(halfVisibleSize, camera.realPosition)
);
dirty = true;
});
// 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 magnitude = Vector.magnitude(position, entityPosition);
if (magnitude < 8) {
return;
}
const diff = Vector.sub(
position,
Vector.sub(
entityPosition,
Vector.sub(selfEntity.camera.realPosition, halfVisibleSize)
),
);
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 socket = createClient(window.location.href);
// Input messages.
const messageHandle = setInterval(() => {
// Mouse/touch movement.
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);
if (actionState !== actionRegistry.state) {
actionState = actionRegistry.state;
socket.send({
type: 'input',
payload: actionState.toJS()
});
}
}, 1000 / 60);
// Prediction.
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);
dirty = dirty || synchronizer.state !== state;
state = synchronizer.state;
// Apply environmental lighting.
stage.removeAllFilters();
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);
}
if (!isFocused) {
stage.paused(intensity);
}
else {
stage.night(intensity);
}
}, 1000 / 80);
// State updates.
function onMessage(message) {
if ('s' in message) {
const patch = unpacker.unpack(message.s);
for (const step of patch) {
const {op, path, value} = step;
if ('add' === op && '/selfEntity' === path) {
selfEntity = value;
}
}
synchronizer.patchState(patch);
dirty = true;
return;
}
}
socket.on('message', onMessage);
// Render.
function render() {
stage.tick();
if (!dirty) {
return;
}
if (worldTime.humanReadable() !== lastWorldTime) {
timeUi.textContent = worldTime.humanReadable();
lastWorldTime = worldTime.humanReadable();
}
stage.render();
dirty = false;
}
const renderHandle = setAnimation(render);
// Hot reloading.
if (module.hot) {
module.hot.accept((error) => {
console.error(error);
});
module.hot.dispose(() => {
clearAnimation(renderHandle);
room.destroy();
stage.destroy();
});
}