humus-old/client/index.js

279 lines
7.6 KiB
JavaScript
Raw Normal View History

2019-04-12 11:04:59 -05:00
// 3rd party.
import React from 'react';
import ReactDOM from 'react-dom';
2019-04-04 07:31:21 -05:00
// 2nd party.
2019-04-11 12:55:21 -05:00
import {create as createClient} from '@avocado/client/socket';
2019-03-21 18:33:20 -05:00
import {EntityList} from '@avocado/entity';
2019-03-20 15:28:18 -05:00
import {ActionRegistry} from '@avocado/input';
2019-03-30 05:08:58 -05:00
import {Stage} from '@avocado/graphics';
2019-03-28 20:36:31 -05:00
import {Vector} from '@avocado/math';
2019-04-12 12:10:40 -05:00
import {SocketIoParser} from '@avocado/packet';
2019-04-07 20:04:34 -05:00
import {Synchronizer, Unpacker} from '@avocado/state';
2019-03-27 01:53:10 -05:00
import {Room, RoomView} from '@avocado/topdown';
2019-04-12 09:50:31 -05:00
import {World} from '@avocado/physics/matter/world';
2019-04-10 21:09:29 -05:00
import {clearAnimation, setAnimation, Ticker} from '@avocado/timing';
2019-04-04 07:31:21 -05:00
// 1st party.
2019-04-11 15:27:39 -05:00
import {InputPacket, KeysPacket, StatePacket} from '../common/packet';
2019-04-12 12:10:40 -05:00
import {augmentParserWithThroughput} from '../common/parser-throughput';
2019-04-04 07:31:21 -05:00
import {WorldTime} from '../common/world-time';
2019-04-12 11:04:59 -05:00
import Ui from './ui';
2019-03-30 05:08:58 -05:00
// DOM.
const appNode = document.querySelector('.app');
2019-04-10 12:42:29 -05:00
// Lol Apple
appNode.addEventListener('touchmove', (event) => {
event.preventDefault();
});
2019-03-30 05:08:58 -05:00
// Graphics stage.
2019-04-05 08:14:39 -05:00
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;
2019-03-30 07:00:07 -05:00
let isFocused = false;
2019-03-30 05:08:58 -05:00
stage.element.addEventListener('blur', () => {
2019-03-30 07:00:07 -05:00
isFocused = false;
2019-03-30 05:08:58 -05:00
});
stage.element.addEventListener('focus', () => {
2019-03-30 07:00:07 -05:00
isFocused = true;
2019-03-30 05:08:58 -05:00
});
2019-03-30 07:00:07 -05:00
stage.addToDom(appNode);
2019-04-12 11:04:59 -05:00
// World time ticker.
const worldTimeTicker = new Ticker(1 / 10);
2019-04-05 08:14:39 -05:00
// Handle "own" entity.
let selfEntity;
function hasSelfEntity() {
return selfEntity && 'string' !== typeof selfEntity;
}
2019-03-30 05:08:58 -05:00
// Create room.
2019-03-27 01:53:10 -05:00
const room = new Room();
2019-04-12 12:18:48 -05:00
room.world = new World();
2019-03-30 05:08:58 -05:00
const roomView = new RoomView(room, stage.renderer);
2019-03-27 01:53:10 -05:00
stage.addChild(roomView);
2019-04-04 07:31:21 -05:00
// Time.
2019-04-04 17:18:55 -05:00
const worldTime = new WorldTime();
2019-04-12 11:04:59 -05:00
worldTimeTicker.on('tick', () => {
worldTimeTicker.emit('updateTime', worldTime.hour)
});
2019-03-30 05:08:58 -05:00
// Synchronize state.
2019-04-07 16:02:31 -05:00
let state = undefined;
2019-04-07 20:04:34 -05:00
const synchronizer = new Synchronizer({
2019-03-27 01:53:10 -05:00
room,
2019-04-04 17:18:55 -05:00
worldTime,
2019-03-20 15:28:18 -05:00
});
2019-04-05 22:40:33 -05:00
const unpacker = new Unpacker();
2019-03-27 01:53:10 -05:00
room.on('entityAdded', (entity) => {
2019-04-09 09:43:11 -05:00
// Traits that shouldn't be on client.
const noClientTraits = [
'behaved',
];
2019-04-10 19:28:13 -05:00
// 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',
];
2019-04-09 09:43:11 -05:00
for (const type of selfEntityOnlyTraits.concat(noClientTraits)) {
if (entity.is(type)) {
entity.removeTrait(type);
}
}
2019-03-21 18:33:20 -05:00
// Set self entity.
if ('string' === typeof selfEntity) {
2019-03-27 17:37:09 -05:00
if (entity === room.findEntity(selfEntity)) {
2019-03-21 18:33:20 -05:00
selfEntity = entity;
// Add back our self entity traits.
for (const type of selfEntityOnlyTraits) {
entity.addTrait(type);
}
2019-04-05 08:14:39 -05:00
const {camera} = entity;
camera.on('realPositionChanged', () => {
2019-04-11 12:20:50 -05:00
const offset = Vector.sub(halfVisibleSize, camera.realPosition);
2019-04-10 19:27:54 -05:00
roomView.position = offset;
2019-04-05 08:14:39 -05:00
dirty = true;
});
// Avoid the initial 'lerp.
camera.realPosition = camera.position;
2019-03-21 18:33:20 -05:00
}
}
2019-03-20 15:28:18 -05:00
});
2019-03-21 18:33:20 -05:00
// Accept input.
2019-03-20 15:28:18 -05:00
const actionRegistry = new ActionRegistry();
actionRegistry.mapKeysToActions({
'w': 'MoveUp',
'a': 'MoveLeft',
's': 'MoveDown',
'd': 'MoveRight',
});
2019-04-04 17:14:49 -05:00
actionRegistry.listen(stage.element);
2019-03-30 05:08:58 -05:00
// Mouse/touch movement.
2019-03-28 20:36:31 -05:00
function createMoveToNormal(position) {
2019-03-30 05:08:58 -05:00
if (!selfEntity) {
return;
2019-03-28 20:36:31 -05:00
}
2019-03-30 05:08:58 -05:00
const entityPosition = selfEntity.position;
2019-04-11 15:22:34 -05:00
const realEntityPosition = Vector.sub(
entityPosition,
Vector.sub(selfEntity.camera.realPosition, halfVisibleSize)
);
const magnitude = Vector.magnitude(position, realEntityPosition);
if (magnitude < 4) {
2019-03-30 05:08:58 -05:00
return;
}
2019-04-09 15:59:16 -05:00
const diff = Vector.sub(
position,
2019-04-11 15:22:34 -05:00
realEntityPosition,
2019-04-09 15:59:16 -05:00
);
2019-03-30 05:08:58 -05:00
return Vector.normalize(diff);
2019-03-28 20:36:31 -05:00
}
let pointingAt = [-1, -1];
function isPointingAtAnything() {
return -1 !== pointingAt[0] && -1 !== pointingAt[1];
}
2019-03-30 05:08:58 -05:00
stage.on('pointerDown', (event) => {
2019-04-04 17:14:49 -05:00
if (event.target !== stage.ui) {
return;
}
2019-03-30 05:08:58 -05:00
pointingAt = event.position;
2019-03-28 20:36:31 -05:00
});
2019-03-30 05:08:58 -05:00
stage.on('pointerMove', (event) => {
2019-04-04 17:14:49 -05:00
if (event.target !== stage.ui) {
return;
}
2019-03-28 20:36:31 -05:00
if (!isPointingAtAnything()) {
return;
}
2019-03-30 05:08:58 -05:00
pointingAt = event.position;
2019-03-28 20:36:31 -05:00
});
2019-03-30 05:08:58 -05:00
stage.on('pointerUp', (event) => {
2019-03-28 20:36:31 -05:00
pointingAt = [-1, -1];
});
let actionState = actionRegistry.state;
2019-03-21 18:33:20 -05:00
// Create the socket connection.
2019-04-12 12:10:40 -05:00
const AugmentedParser = augmentParserWithThroughput(SocketIoParser);
const socket = createClient(window.location.href, {
parser: AugmentedParser,
});
// Throughput ticker.
const throughputSampleTime = 0.5;
const throughputTicker = new Ticker(throughputSampleTime);
const {Decoder, Encoder} = AugmentedParser;
throughputTicker.on('tick', () => {
throughputTicker.emit('updateThroughput', Vector.scale([
Decoder.throughput,
Encoder.throughput,
], 1 / throughputSampleTime));
Decoder.throughput = 0;
Encoder.throughput = 0;
});
2019-03-21 18:33:20 -05:00
// Input messages.
const messageHandle = setInterval(() => {
2019-03-28 20:36:31 -05:00
// 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;
2019-04-11 15:27:39 -05:00
socket.send(new InputPacket(JSON.stringify(actionRegistry.state.toJS())));
2019-03-20 15:28:18 -05:00
}
2019-03-21 18:33:20 -05:00
}, 1000 / 60);
// Prediction.
2019-04-10 21:09:29 -05:00
let mayRender = false;
const renderTicker = new Ticker(1 / 60);
renderTicker.on('tick', () => {
mayRender = true;
});
2019-04-10 21:07:54 -05:00
let lastIntensity = undefined;
let lastIsFocused = undefined;
2019-03-23 23:38:14 -05:00
let lastTime = performance.now();
const predictionHandle = setInterval(() => {
const now = performance.now();
const elapsed = (now - lastTime) / 1000;
lastTime = now;
2019-03-27 17:40:25 -05:00
if (hasSelfEntity()) {
selfEntity.inputState = actionState.toJS();
}
2019-04-07 15:16:12 -05:00
// Tick synchronized.
2019-04-07 20:04:34 -05:00
synchronizer.tick(elapsed);
dirty = dirty || synchronizer.state !== state;
state = synchronizer.state;
2019-04-10 21:09:29 -05:00
// Render timing.
renderTicker.tick(elapsed);
2019-04-12 11:04:59 -05:00
// World time ticker.
worldTimeTicker.tick(elapsed);
2019-04-12 12:10:40 -05:00
// Throughput ticker.
throughputTicker.tick(elapsed);
2019-04-07 15:16:12 -05:00
// Apply environmental lighting.
2019-04-04 07:31:21 -05:00
let intensity = 0;
2019-04-04 17:18:55 -05:00
if (worldTime.hour >= 21 || worldTime.hour < 4) {
2019-04-04 07:31:21 -05:00
intensity = 0.8;
}
2019-04-04 17:18:55 -05:00
if (worldTime.hour >= 4 && worldTime.hour < 7) {
intensity = 0.8 * ((7 - worldTime.hour) / 3);
2019-04-04 07:31:21 -05:00
}
2019-04-04 17:18:55 -05:00
if (worldTime.hour >= 18 && worldTime.hour < 21) {
intensity = 0.8 * ((3 - (21 - worldTime.hour)) / 3);
2019-04-04 07:31:21 -05:00
}
2019-04-10 21:07:54 -05:00
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;
2019-03-30 07:00:07 -05:00
}
2019-04-11 12:53:47 -05:00
}, 1000 / 60);
2019-03-21 18:33:20 -05:00
// State updates.
2019-04-11 15:27:39 -05:00
function onPacket(packet) {
if (packet instanceof KeysPacket) {
unpacker.registerKeys(packet.data);
2019-04-11 12:55:21 -05:00
}
2019-04-11 15:27:39 -05:00
if (packet instanceof StatePacket) {
const patch = unpacker.unpack(packet.data);
2019-04-08 07:31:13 -05:00
for (const step of patch) {
const {op, path, value} = step;
if ('add' === op && '/selfEntity' === path) {
selfEntity = value;
2019-03-20 21:09:32 -05:00
}
2019-04-08 07:31:13 -05:00
}
synchronizer.patchState(patch);
dirty = true;
2019-03-20 15:28:18 -05:00
}
}
2019-04-11 15:27:39 -05:00
socket.on('packet', onPacket);
2019-03-20 15:28:18 -05:00
// Render.
function render() {
stage.tick();
if (!dirty) {
return;
}
2019-04-10 21:09:29 -05:00
if (!mayRender) {
return false;
}
mayRender = false;
2019-03-30 05:08:58 -05:00
stage.render();
2019-03-20 15:28:18 -05:00
dirty = false;
}
2019-03-28 12:45:22 -05:00
const renderHandle = setAnimation(render);
2019-04-12 12:10:40 -05:00
// UI.
const UiComponent = <Ui
worldTimeTicker={worldTimeTicker}
throughputTicker={throughputTicker}
/>;
ReactDOM.render(UiComponent, stage.ui);