2019-04-19 22:29:38 -05:00
|
|
|
// 3rd party.
|
|
|
|
import React from 'react';
|
|
|
|
import ReactDOM from 'react-dom';
|
|
|
|
// 2nd party.
|
|
|
|
import {create as createClient} from '@avocado/client/socket';
|
2019-04-14 16:33:26 -05:00
|
|
|
import {compose} from '@avocado/core';
|
2019-04-19 21:21:41 -05:00
|
|
|
import {Stage} from '@avocado/graphics';
|
2019-04-19 22:29:38 -05:00
|
|
|
import {ActionRegistry, InputPacket} from '@avocado/input';
|
|
|
|
import {Vector} from '@avocado/math';
|
2019-04-14 16:33:26 -05:00
|
|
|
import {EventEmitter, Property} from '@avocado/mixins';
|
2019-04-19 22:29:38 -05:00
|
|
|
import {SocketIoParser} from '@avocado/packet';
|
|
|
|
import {
|
|
|
|
StateKeysPacket,
|
|
|
|
StatePacket,
|
|
|
|
Synchronizer,
|
|
|
|
Unpacker,
|
|
|
|
} from '@avocado/state';
|
|
|
|
import {clearAnimation, setAnimation} from '@avocado/timing';
|
2019-04-19 21:21:41 -05:00
|
|
|
import {World} from '@avocado/physics/matter/world';
|
|
|
|
import {Room, RoomView} from '@avocado/topdown';
|
2019-04-19 22:29:38 -05:00
|
|
|
// 1st party.
|
|
|
|
import {augmentParserWithThroughput} from '../common/parser-throughput';
|
2019-04-19 21:21:41 -05:00
|
|
|
import {WorldTime} from '../common/world-time';
|
2019-04-19 22:29:38 -05:00
|
|
|
import Ui from './ui';
|
|
|
|
import DebugUi from './ui/debug';
|
2019-04-14 16:33:26 -05:00
|
|
|
|
|
|
|
const decorate = compose(
|
|
|
|
EventEmitter,
|
2019-04-14 16:46:22 -05:00
|
|
|
Property('isDebugging', {
|
|
|
|
track: true,
|
|
|
|
}),
|
2019-04-14 16:33:26 -05:00
|
|
|
Property('selfEntity', {
|
|
|
|
track: true,
|
|
|
|
}),
|
2019-04-19 19:48:53 -05:00
|
|
|
Property('selfEntityUuid', {
|
|
|
|
track: true,
|
|
|
|
}),
|
2019-04-14 16:33:26 -05:00
|
|
|
);
|
|
|
|
|
2019-04-19 20:01:06 -05:00
|
|
|
export class App extends decorate(class {}) {
|
|
|
|
|
2019-04-19 21:21:41 -05:00
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
const config = this.readConfig();
|
|
|
|
// Room.
|
|
|
|
this.room = new Room();
|
|
|
|
// World time.
|
|
|
|
this.worldTime = new WorldTime();
|
2019-04-19 22:29:38 -05:00
|
|
|
// Graphics.
|
|
|
|
this.lastIntensity = undefined;
|
|
|
|
this.renderHandle = undefined;
|
|
|
|
this.stage = new Stage(config.visibleSize, config.visibleScale);
|
2019-04-19 21:21:41 -05:00
|
|
|
const roomView = new RoomView(this.room, this.stage.renderer);
|
|
|
|
this.stage.addChild(roomView);
|
|
|
|
// Listen for new entities.
|
|
|
|
this.room.on('entityAdded', this.onRoomEntityAdded, this);
|
2019-04-19 22:29:38 -05:00
|
|
|
// Input.
|
|
|
|
this.actionRegistry = new ActionRegistry();
|
2019-04-20 01:20:05 -05:00
|
|
|
this.actionRegistry.mapKeysToActions(config.actionKeyMap);
|
2019-04-19 22:29:38 -05:00
|
|
|
this.actionState = this.actionRegistry.state;
|
|
|
|
this.inputHandle = undefined;
|
|
|
|
this.isFocused = false;
|
|
|
|
this.lastIsFocused = undefined;
|
|
|
|
this.onBlur = this.onBlur.bind(this);
|
|
|
|
this.onFocus = this.onFocus.bind(this);
|
|
|
|
// Global keys.
|
|
|
|
this.onGlobalKeydown = this.onGlobalKeydown.bind(this);
|
|
|
|
window.addEventListener('keydown', this.onGlobalKeydown);
|
|
|
|
this.pointingAt = [-1, -1];
|
|
|
|
this.pointerMovementHandle = undefined;
|
|
|
|
// Net.
|
|
|
|
this.AugmentedParser = augmentParserWithThroughput(SocketIoParser);
|
2019-04-20 14:16:06 -05:00
|
|
|
this.hasReceivedState = false;
|
2019-04-19 22:29:38 -05:00
|
|
|
this.isConnected = false;
|
|
|
|
this.socket = undefined;
|
|
|
|
// Debugging.
|
|
|
|
this.isDebugging = false;
|
|
|
|
// Simulation.
|
|
|
|
this.simulationHandle = undefined;
|
|
|
|
this.world = config.doPhysicsSimulation ? new World() : undefined;
|
|
|
|
this.room.world = this.world;
|
|
|
|
// State synchronization.
|
|
|
|
this.state = undefined;
|
|
|
|
this.synchronizer = new Synchronizer({
|
|
|
|
room: this.room,
|
|
|
|
worldTime: this.worldTime,
|
|
|
|
});
|
|
|
|
this.unpacker = new Unpacker();
|
2019-04-19 21:21:41 -05:00
|
|
|
}
|
|
|
|
|
2019-04-19 22:29:38 -05:00
|
|
|
applyLighting() {
|
|
|
|
let intensity = 0;
|
|
|
|
if (this.worldTime.hour >= 21 || this.worldTime.hour < 4) {
|
|
|
|
intensity = 0.8;
|
|
|
|
}
|
|
|
|
if (this.worldTime.hour >= 4 && this.worldTime.hour < 7) {
|
|
|
|
intensity = 0.8 * ((7 - this.worldTime.hour) / 3);
|
|
|
|
}
|
|
|
|
if (this.worldTime.hour >= 18 && this.worldTime.hour < 21) {
|
|
|
|
intensity = 0.8 * ((3 - (21 - this.worldTime.hour)) / 3);
|
|
|
|
}
|
|
|
|
intensity = Math.floor(intensity * 1000) / 1000;
|
|
|
|
if (intensity !== this.lastIntensity || this.isFocused !== this.lastIsFocused) {
|
|
|
|
this.stage.removeAllFilters();
|
|
|
|
if (!this.isFocused) {
|
|
|
|
this.stage.paused(intensity);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
this.stage.night(intensity);
|
|
|
|
}
|
|
|
|
this.lastIntensity = intensity;
|
|
|
|
this.lastIsFocused = this.isFocused;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
connect() {
|
|
|
|
const config = this.readConfig();
|
|
|
|
this.socket = createClient(config.connectionUrl, {
|
|
|
|
parser: this.AugmentedParser,
|
2019-04-19 21:21:41 -05:00
|
|
|
});
|
2019-04-19 22:29:38 -05:00
|
|
|
this.socket.on('connect', () => {
|
|
|
|
this.room.layers.destroy();
|
|
|
|
this.selfEntity = undefined;
|
|
|
|
this.selfEntityUuid = undefined;
|
|
|
|
this.isConnected = true;
|
|
|
|
});
|
|
|
|
this.socket.on('disconnect', () => {
|
2019-04-20 14:16:06 -05:00
|
|
|
this.hasReceivedState = false;
|
2019-04-19 22:29:38 -05:00
|
|
|
this.isConnected = false;
|
|
|
|
this.stopRendering();
|
|
|
|
this.stopSimulation();
|
|
|
|
this.stopProcessingInput();
|
|
|
|
});
|
|
|
|
this.socket.on('packet', this.onPacket, this);
|
|
|
|
}
|
|
|
|
|
|
|
|
createMoveToNormal(position) {
|
|
|
|
if (!this.selfEntity) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const realEntityPosition = Vector.sub(
|
|
|
|
this.selfEntity.position,
|
|
|
|
this.selfEntity.camera.realOffset,
|
|
|
|
);
|
|
|
|
const magnitude = Vector.magnitude(position, realEntityPosition);
|
|
|
|
if (magnitude < 8) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const diff = Vector.sub(position, realEntityPosition);
|
|
|
|
return Vector.normalize(diff);
|
|
|
|
}
|
|
|
|
|
|
|
|
createReactContainer() {
|
|
|
|
const reactContainer = window.document.createElement('div');
|
|
|
|
reactContainer.className = 'react';
|
|
|
|
reactContainer.style.width = '100%';
|
|
|
|
reactContainer.style.height = '100%';
|
|
|
|
return reactContainer;
|
|
|
|
}
|
|
|
|
|
|
|
|
eventIsInUi(event) {
|
|
|
|
let walk = event.target;
|
|
|
|
while (walk) {
|
|
|
|
if (walk === this.stage.ui) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
walk = walk.parentNode;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
isPointingAtAnything() {
|
|
|
|
return -1 !== this.pointingAt[0] && -1 !== this.pointingAt[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
onBlur() {
|
|
|
|
this.isFocused = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
onFocus() {
|
|
|
|
this.isFocused = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
onGlobalKeydown(event) {
|
|
|
|
switch (event.key) {
|
|
|
|
case 'F2':
|
|
|
|
this.isDebugging = !this.isDebugging;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onPacket(packet) {
|
|
|
|
if (packet instanceof StateKeysPacket) {
|
|
|
|
this.unpacker.registerKeys(packet.data);
|
|
|
|
}
|
|
|
|
if (packet instanceof StatePacket) {
|
|
|
|
const patch = this.unpacker.unpack(packet.data);
|
|
|
|
// Yank out self entity.
|
|
|
|
if (!this.selfEntityUuid) {
|
|
|
|
for (const step of patch) {
|
|
|
|
const {op, path, value} = step;
|
|
|
|
if ('add' === op && '/selfEntity' === path) {
|
|
|
|
this.selfEntityUuid = value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.synchronizer.patchState(patch);
|
2019-04-20 14:16:06 -05:00
|
|
|
if (!this.hasReceivedState) {
|
|
|
|
this.startProcessingInput();
|
|
|
|
this.startSimulation();
|
|
|
|
this.startRendering();
|
|
|
|
this.hasReceivedState = true;
|
|
|
|
}
|
2019-04-19 22:29:38 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onPointerDown(event) {
|
|
|
|
if (!this.eventIsInUi(event)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.pointingAt = event.position;
|
|
|
|
}
|
|
|
|
|
|
|
|
onPointerMove(event) {
|
|
|
|
if (!this.eventIsInUi(event)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!this.isPointingAtAnything()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.pointingAt = event.position;
|
|
|
|
}
|
|
|
|
|
|
|
|
onPointerUp(event) {
|
|
|
|
this.pointingAt = [-1, -1];
|
2019-04-19 21:21:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
onRoomEntityAdded(entity) {
|
|
|
|
// Traits that shouldn't be on client.
|
|
|
|
const noClientTraits = [
|
|
|
|
'behaved',
|
|
|
|
'informed',
|
|
|
|
];
|
|
|
|
// If there's no physics, then remove physical trait.
|
|
|
|
if (!this.room.world) {
|
|
|
|
noClientTraits.push('physical');
|
|
|
|
}
|
|
|
|
// Traits that only make sense for our self entity on the client.
|
|
|
|
const selfEntityOnlyTraits = [
|
|
|
|
'controllable',
|
|
|
|
'followed',
|
|
|
|
];
|
|
|
|
const nixedTraits = selfEntityOnlyTraits.concat(noClientTraits);
|
|
|
|
for (let i = 0; i < nixedTraits.length; ++i) {
|
|
|
|
const type = nixedTraits[i];
|
|
|
|
if (entity.is(type)) {
|
|
|
|
entity.removeTrait(type);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Client only traits.
|
|
|
|
const clientOnlyTraits = [
|
|
|
|
'staged',
|
|
|
|
];
|
|
|
|
for (let i = 0; i < clientOnlyTraits.length; ++i) {
|
|
|
|
const type = clientOnlyTraits[i];
|
|
|
|
entity.addTrait(type);
|
|
|
|
}
|
|
|
|
entity.stage = this.stage;
|
|
|
|
// Set self entity.
|
|
|
|
if (this.selfEntityUuid) {
|
|
|
|
if (entity === this.room.findEntity(this.selfEntityUuid)) {
|
|
|
|
this.selfEntity = entity;
|
|
|
|
this.selfEntityUuid = undefined;
|
|
|
|
// Add back our self entity traits.
|
|
|
|
for (const type of selfEntityOnlyTraits) {
|
|
|
|
entity.addTrait(type);
|
|
|
|
}
|
|
|
|
const {camera} = entity;
|
|
|
|
this.stage.camera = camera;
|
|
|
|
// Avoid the initial 'lerp.
|
|
|
|
camera.realPosition = camera.position;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-19 20:01:06 -05:00
|
|
|
// Stub for now!
|
|
|
|
readConfig() {
|
|
|
|
return {
|
2019-04-19 22:29:38 -05:00
|
|
|
actionKeyMap: {
|
|
|
|
'w': 'MoveUp',
|
|
|
|
'a': 'MoveLeft',
|
|
|
|
's': 'MoveDown',
|
|
|
|
'd': 'MoveRight',
|
|
|
|
},
|
|
|
|
connectionUrl: window.location.href,
|
2019-04-19 21:21:41 -05:00
|
|
|
doPhysicsSimulation: true,
|
2019-04-19 22:29:38 -05:00
|
|
|
inputFrequency: 1 / 60,
|
|
|
|
pointerMovementFrequency: 1 / 15,
|
|
|
|
simulationFrequency: 1 / 60,
|
2019-04-20 02:14:40 -05:00
|
|
|
visibleScale: [1, 1],
|
2019-04-19 20:01:06 -05:00
|
|
|
visibleSize: [320, 180],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-04-19 22:29:38 -05:00
|
|
|
renderIntoDom(node) {
|
|
|
|
// Lol, Apple...
|
|
|
|
node.addEventListener('touchmove', (event) => {
|
|
|
|
event.preventDefault();
|
|
|
|
});
|
|
|
|
// Add graphics stage.
|
|
|
|
this.stage.addToDom(node);
|
|
|
|
// UI.
|
|
|
|
const reactContainer = this.createReactContainer();
|
|
|
|
this.stage.ui.appendChild(reactContainer);
|
|
|
|
const UiComponent = <Ui
|
|
|
|
socket={this.socket}
|
|
|
|
worldTime={this.worldTime}
|
|
|
|
/>;
|
|
|
|
ReactDOM.render(UiComponent, reactContainer);
|
|
|
|
// Debug UI.
|
|
|
|
const debugUiNode = node.querySelector('.debug-container');
|
|
|
|
const DebugUiComponent = <DebugUi
|
|
|
|
actionRegistry={this.actionRegistry}
|
|
|
|
app={this}
|
|
|
|
Parser={this.AugmentedParser}
|
|
|
|
socket={this.socket}
|
|
|
|
stage={this.stage}
|
|
|
|
/>;
|
|
|
|
ReactDOM.render(DebugUiComponent, debugUiNode, () => {
|
|
|
|
this.stage.resolveUiRendered();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
startProcessingInput() {
|
|
|
|
const config = this.readConfig();
|
|
|
|
this.actionRegistry.listen(this.stage.element);
|
|
|
|
// Pointer input.
|
|
|
|
this.stage.on('pointerDown', this.onPointerDown, this);
|
|
|
|
this.stage.on('pointerMove', this.onPointerMove, this);
|
|
|
|
this.stage.on('pointerUp', this.onPointerUp, this);
|
|
|
|
// Blur/focus.
|
|
|
|
this.stage.element.addEventListener('blur', this.onBlur);
|
|
|
|
this.stage.element.addEventListener('focus', this.onFocus);
|
|
|
|
// Input messages.
|
|
|
|
this.inputHandle = setInterval(() => {
|
|
|
|
if (this.actionState !== this.actionRegistry.state) {
|
|
|
|
this.actionState = this.actionRegistry.state;
|
|
|
|
this.socket.send(InputPacket.fromState(this.actionState));
|
|
|
|
}
|
2019-04-19 23:35:33 -05:00
|
|
|
}, 1000 * config.inputFrequency);
|
2019-04-19 22:29:38 -05:00
|
|
|
// Mouse/touch movement.
|
|
|
|
this.pointerMovementHandle = setInterval(() => {
|
|
|
|
do {
|
|
|
|
let normal;
|
|
|
|
if (this.isPointingAtAnything()) {
|
|
|
|
const toVector = Vector.scale(this.pointingAt, this.isDebugging ? 2 : 1);
|
|
|
|
normal = this.createMoveToNormal(toVector);
|
|
|
|
if (normal) {
|
|
|
|
this.actionRegistry.state = this.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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.actionRegistry.state = this.actionRegistry.state.withMutations((state) => {
|
|
|
|
if (!normal || 0 === normal[0]) {
|
|
|
|
state.delete('MoveToX');
|
|
|
|
}
|
|
|
|
if (!normal || 0 === normal[1]) {
|
|
|
|
state.delete('MoveToY');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} while (false);
|
2019-04-19 23:35:33 -05:00
|
|
|
}, 1000 * config.pointerMovementFrequency);
|
2019-04-19 22:29:38 -05:00
|
|
|
// Focus the stage.
|
|
|
|
this.stage.focus();
|
2019-04-20 01:20:05 -05:00
|
|
|
this.isFocused = true;
|
2019-04-19 22:29:38 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
startRendering() {
|
2019-04-19 23:15:15 -05:00
|
|
|
let lastTime = performance.now();
|
2019-04-19 22:29:38 -05:00
|
|
|
this.renderHandle = setAnimation(() => {
|
2019-04-19 23:15:15 -05:00
|
|
|
const now = performance.now();
|
|
|
|
const elapsed = (now - lastTime) / 1000;
|
|
|
|
lastTime = now;
|
2019-04-19 22:29:38 -05:00
|
|
|
this.applyLighting();
|
|
|
|
this.stage.render();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
startSimulation() {
|
|
|
|
const config = this.readConfig();
|
|
|
|
let lastTime = performance.now();
|
|
|
|
this.simulationHandle = setInterval(() => {
|
|
|
|
const now = performance.now();
|
|
|
|
const elapsed = (now - lastTime) / 1000;
|
|
|
|
lastTime = now;
|
|
|
|
// Inject input.
|
|
|
|
if (this.selfEntity) {
|
|
|
|
this.selfEntity.inputState = this.actionState.toJS();
|
|
|
|
}
|
|
|
|
// Tick synchronized.
|
|
|
|
this.synchronizer.tick(elapsed);
|
|
|
|
this.state = this.synchronizer.state;
|
2019-04-19 23:35:33 -05:00
|
|
|
}, 1000 * config.simulationFrequency);
|
2019-04-19 22:29:38 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
stopProcessingInput() {
|
|
|
|
clearInterval(this.pointerMovementHandle);
|
|
|
|
this.pointerMovementHandle = undefined;
|
|
|
|
clearInterval(this.inputHandle);
|
|
|
|
this.inputHandle = undefined;
|
|
|
|
this.stage.element.removeEventListener('focus', this.onFocus);
|
|
|
|
this.stage.element.removeEventListener('blur', this.onBlur);
|
|
|
|
this.stage.off('pointerUp', this.onPointerUp, this);
|
|
|
|
this.stage.off('pointerMove', this.onPointerMove, this);
|
|
|
|
this.stage.off('pointerDown', this.onPointerDown, this);
|
|
|
|
this.actionRegistry.stopListening();
|
|
|
|
}
|
|
|
|
|
|
|
|
stopRendering() {
|
|
|
|
clearAnimation(this.renderHandle);
|
2019-04-19 22:36:39 -05:00
|
|
|
this.renderHandle = undefined;
|
2019-04-19 22:29:38 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
stopSimulation() {
|
|
|
|
clearInterval(this.simulationHandle);
|
2019-04-19 22:36:39 -05:00
|
|
|
this.simulationHandle = undefined;
|
2019-04-19 22:29:38 -05:00
|
|
|
}
|
|
|
|
|
2019-04-19 20:01:06 -05:00
|
|
|
}
|