2019-04-12 11:04:59 -05:00
|
|
|
// 3rd party.
|
2019-04-13 18:42:53 -05:00
|
|
|
import msgpack from 'msgpack-lite';
|
2019-04-12 11:04:59 -05:00
|
|
|
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-04-14 00:24:21 -05:00
|
|
|
import DebugUi from './ui/debug';
|
2019-03-30 05:08:58 -05:00
|
|
|
// DOM.
|
|
|
|
const appNode = document.querySelector('.app');
|
|
|
|
// Graphics stage.
|
2019-04-05 08:14:39 -05:00
|
|
|
const visibleSize = [320, 180];
|
|
|
|
const visibleScale = [2, 2];
|
|
|
|
const stage = new Stage(Vector.mul(visibleSize, visibleScale));
|
|
|
|
stage.scale = visibleScale;
|
|
|
|
// Handle "own" entity.
|
|
|
|
let selfEntity;
|
2019-04-14 01:03:09 -05:00
|
|
|
let selfEntityResolver;
|
|
|
|
const selfEntityPromise = new Promise((resolve) => {
|
|
|
|
selfEntityResolver = resolve;
|
|
|
|
});
|
2019-04-05 08:14:39 -05:00
|
|
|
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 20:16:47 -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-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-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');
|
|
|
|
}
|
2019-04-05 11:59:29 -05:00
|
|
|
// 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)) {
|
2019-04-05 11:59:29 -05:00
|
|
|
if (entity.is(type)) {
|
|
|
|
entity.removeTrait(type);
|
|
|
|
}
|
|
|
|
}
|
2019-03-21 18:33:20 -05:00
|
|
|
// Set self entity.
|
2019-03-20 22:11:11 -05:00
|
|
|
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;
|
2019-04-14 01:03:09 -05:00
|
|
|
selfEntityResolver(entity);
|
2019-04-05 11:59:29 -05:00
|
|
|
// 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-13 18:13:38 -05:00
|
|
|
roomView.position = Vector.scale(selfEntity.camera.realOffset, -1);
|
2019-04-05 08:14:39 -05:00
|
|
|
});
|
|
|
|
// Avoid the initial 'lerp.
|
|
|
|
camera.realPosition = camera.position;
|
2019-03-21 18:33:20 -05:00
|
|
|
}
|
2019-03-20 22:11:11 -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-04-14 13:28:41 -05:00
|
|
|
// Lol Apple
|
|
|
|
appNode.addEventListener('touchmove', (event) => {
|
|
|
|
event.preventDefault();
|
|
|
|
});
|
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-04-11 15:22:34 -05:00
|
|
|
const realEntityPosition = Vector.sub(
|
2019-04-13 18:20:26 -05:00
|
|
|
selfEntity.position,
|
2019-04-13 18:13:38 -05:00
|
|
|
selfEntity.camera.realOffset,
|
2019-04-11 15:22:34 -05:00
|
|
|
);
|
|
|
|
const magnitude = Vector.magnitude(position, realEntityPosition);
|
2019-04-13 18:22:49 -05:00
|
|
|
if (magnitude < 8) {
|
2019-03-30 05:08:58 -05:00
|
|
|
return;
|
|
|
|
}
|
2019-04-13 17:14:55 -05:00
|
|
|
const diff = Vector.sub(position, realEntityPosition);
|
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,
|
|
|
|
});
|
2019-04-14 16:14:03 -05:00
|
|
|
let isConnected = false;
|
|
|
|
socket.on('connect', () => {
|
|
|
|
room.layers.destroy();
|
|
|
|
selfEntity = undefined;
|
|
|
|
isConnected = true;
|
|
|
|
});
|
|
|
|
socket.on('disconnect', () => {
|
|
|
|
isConnected = false;
|
|
|
|
});
|
2019-04-12 12:19:06 -05:00
|
|
|
// Mouse/touch movement.
|
|
|
|
const pointerMovementHandle = setInterval(() => {
|
2019-03-28 20:36:31 -05:00
|
|
|
do {
|
2019-04-13 19:16:42 -05:00
|
|
|
let normal;
|
2019-03-28 20:36:31 -05:00
|
|
|
if (isPointingAtAnything()) {
|
2019-04-13 19:16:42 -05:00
|
|
|
normal = createMoveToNormal(pointingAt);
|
2019-03-28 20:36:31 -05:00
|
|
|
if (normal) {
|
2019-04-13 19:16:42 -05:00
|
|
|
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));
|
|
|
|
}
|
|
|
|
});
|
2019-03-28 20:36:31 -05:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2019-04-13 19:16:42 -05:00
|
|
|
actionRegistry.state = actionRegistry.state.withMutations((state) => {
|
|
|
|
if (!normal || 0 === normal[0]) {
|
|
|
|
state.delete('MoveToX');
|
|
|
|
}
|
|
|
|
if (!normal || 0 === normal[1]) {
|
|
|
|
state.delete('MoveToY');
|
|
|
|
}
|
|
|
|
});
|
2019-03-28 20:36:31 -05:00
|
|
|
} while (false);
|
2019-04-12 12:19:06 -05:00
|
|
|
}, 1000 / 15);
|
|
|
|
// Input messages.
|
|
|
|
const inputHandle = setInterval(() => {
|
2019-04-14 16:14:03 -05:00
|
|
|
if (!isConnected) {
|
|
|
|
return;
|
|
|
|
}
|
2019-03-28 20:36:31 -05:00
|
|
|
if (actionState !== actionRegistry.state) {
|
|
|
|
actionState = actionRegistry.state;
|
2019-04-13 19:16:42 -05:00
|
|
|
socket.send(InputPacket.fromState(actionState));
|
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-03-23 23:38:14 -05:00
|
|
|
let lastTime = performance.now();
|
|
|
|
const predictionHandle = setInterval(() => {
|
2019-04-14 16:14:03 -05:00
|
|
|
if (!isConnected) {
|
|
|
|
return;
|
|
|
|
}
|
2019-03-23 23:38:14 -05:00
|
|
|
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);
|
|
|
|
state = synchronizer.state;
|
2019-04-10 21:09:29 -05:00
|
|
|
// Render timing.
|
|
|
|
renderTicker.tick(elapsed);
|
2019-04-13 17:23:42 -05:00
|
|
|
}, 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);
|
2019-04-13 17:59:39 -05:00
|
|
|
// Yank out self entity.
|
|
|
|
if (!selfEntity) {
|
|
|
|
for (const step of patch) {
|
|
|
|
const {op, path, value} = step;
|
|
|
|
if ('add' === op && '/selfEntity' === path) {
|
|
|
|
selfEntity = value;
|
|
|
|
}
|
2019-04-13 17:23:42 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
synchronizer.patchState(patch);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
socket.on('packet', onPacket);
|
|
|
|
// Apply stage lighting.
|
2019-04-13 18:19:07 -05:00
|
|
|
let isFocused = false;
|
|
|
|
stage.element.addEventListener('blur', () => {
|
|
|
|
isFocused = false;
|
|
|
|
});
|
|
|
|
stage.element.addEventListener('focus', () => {
|
|
|
|
isFocused = true;
|
|
|
|
});
|
|
|
|
let lastIntensity = undefined;
|
|
|
|
let lastIsFocused = undefined;
|
2019-04-13 17:23:42 -05:00
|
|
|
function applyStageLighting() {
|
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-03-20 15:28:18 -05:00
|
|
|
}
|
|
|
|
// Render.
|
|
|
|
function render() {
|
2019-04-14 16:14:03 -05:00
|
|
|
if (!isConnected) {
|
|
|
|
return;
|
|
|
|
}
|
2019-03-20 15:28:18 -05:00
|
|
|
stage.tick();
|
2019-04-10 21:09:29 -05:00
|
|
|
if (!mayRender) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
mayRender = false;
|
2019-04-13 17:23:42 -05:00
|
|
|
// Lighting.
|
|
|
|
applyStageLighting();
|
2019-03-30 05:08:58 -05:00
|
|
|
stage.render();
|
2019-03-20 15:28:18 -05:00
|
|
|
}
|
2019-03-28 12:45:22 -05:00
|
|
|
const renderHandle = setAnimation(render);
|
2019-04-12 12:10:40 -05:00
|
|
|
// UI.
|
|
|
|
const UiComponent = <Ui
|
2019-04-14 16:14:03 -05:00
|
|
|
socket={socket}
|
2019-04-13 16:35:44 -05:00
|
|
|
worldTime={worldTime}
|
2019-04-12 12:10:40 -05:00
|
|
|
/>;
|
|
|
|
ReactDOM.render(UiComponent, stage.ui);
|
2019-04-14 00:24:21 -05:00
|
|
|
// Debug UI.
|
|
|
|
const debugUiNode = document.querySelector('.debug-container');
|
|
|
|
const DebugUiComponent = <DebugUi
|
|
|
|
actionRegistry={actionRegistry}
|
|
|
|
Parser={AugmentedParser}
|
2019-04-14 01:03:09 -05:00
|
|
|
selfEntityPromise={selfEntityPromise}
|
2019-04-14 00:24:21 -05:00
|
|
|
stage={stage}
|
|
|
|
/>;
|
|
|
|
ReactDOM.render(DebugUiComponent, debugUiNode);
|
|
|
|
// Inject the stage last.
|
|
|
|
stage.addToDom(appNode);
|
2019-04-13 16:20:22 -05:00
|
|
|
// Eval.
|
|
|
|
const evaluationContext = {
|
|
|
|
room,
|
|
|
|
selfEntity,
|
|
|
|
stage,
|
|
|
|
};
|
2019-04-14 01:03:09 -05:00
|
|
|
selfEntityPromise.then((entity) => {
|
|
|
|
evaluationContext.selfEntity = entity;
|
|
|
|
});
|
2019-04-13 16:20:22 -05:00
|
|
|
// Just inject it into global for now.
|
|
|
|
window.humus = evaluationContext;
|