185 lines
4.9 KiB
JavaScript
185 lines
4.9 KiB
JavaScript
import {create as createClient} from '@avocado/client';
|
|
import {EntityList} from '@avocado/entity';
|
|
import {ActionRegistry} from '@avocado/input';
|
|
import {Container, Renderer} from '@avocado/graphics';
|
|
import {Vector} from '@avocado/math';
|
|
import {World} from '@avocado/physics/matter/world';
|
|
import {StateSynchronizer} from '@avocado/state';
|
|
import {Room, RoomView} from '@avocado/topdown';
|
|
import {clearAnimation, setAnimation} from '@avocado/timing';
|
|
|
|
let canvasRatio = 1;
|
|
const renderer = new Renderer([640, 360]);
|
|
const stage = new Container();
|
|
stage.scale = [2, 2];
|
|
|
|
const room = new Room();
|
|
room.world = new World();
|
|
const roomView = new RoomView(room, renderer);
|
|
stage.addChild(roomView);
|
|
|
|
const stateSynchronizer = new StateSynchronizer({
|
|
room,
|
|
});
|
|
|
|
let selfEntity;
|
|
function hasSelfEntity() {
|
|
return selfEntity && 'string' !== typeof selfEntity;
|
|
}
|
|
room.on('entityAdded', (entity) => {
|
|
entity.removeTrait('controllable');
|
|
// Set self entity.
|
|
if ('string' === typeof selfEntity) {
|
|
if (entity === room.findEntity(selfEntity)) {
|
|
selfEntity = entity;
|
|
// Only self entity should be controllable.
|
|
entity.addTrait('controllable');
|
|
}
|
|
}
|
|
});
|
|
// Create the graphics canvas.
|
|
const appNode = document.querySelector('.app');
|
|
renderer.element.tabIndex = 0;
|
|
appNode.appendChild(renderer.element);
|
|
// Accept input.
|
|
const canvasNode = document.querySelector('.app canvas');
|
|
const actionRegistry = new ActionRegistry();
|
|
actionRegistry.mapKeysToActions({
|
|
'w': 'MoveUp',
|
|
'a': 'MoveLeft',
|
|
's': 'MoveDown',
|
|
'd': 'MoveRight',
|
|
});
|
|
function createMoveToNormal(position) {
|
|
if (selfEntity) {
|
|
const scaledEntityPosition = Vector.mul(
|
|
selfEntity.position,
|
|
stage.scale,
|
|
);
|
|
const diff = Vector.sub(
|
|
position,
|
|
scaledEntityPosition,
|
|
);
|
|
const magnitude = Vector.magnitude(position, scaledEntityPosition);
|
|
if (magnitude < 8) {
|
|
return;
|
|
}
|
|
const normal = Vector.normalize(diff);
|
|
return normal;
|
|
}
|
|
}
|
|
let pointingAt = [-1, -1];
|
|
function isPointingAtAnything() {
|
|
return -1 !== pointingAt[0] && -1 !== pointingAt[1];
|
|
}
|
|
function pointingAtFromEvent(event) {
|
|
const rect = event.target.getBoundingClientRect();
|
|
return Vector.scale(
|
|
[
|
|
event.clientX - rect.x,
|
|
event.clientY - rect.y,
|
|
],
|
|
canvasRatio,
|
|
);
|
|
}
|
|
canvasNode.addEventListener('pointerdown', (event) => {
|
|
pointingAt = pointingAtFromEvent(event);
|
|
});
|
|
canvasNode.addEventListener('pointermove', (event) => {
|
|
if (!isPointingAtAnything()) {
|
|
return;
|
|
}
|
|
pointingAt = pointingAtFromEvent(event);
|
|
});
|
|
canvasNode.addEventListener('pointerup', (event) => {
|
|
pointingAt = [-1, -1];
|
|
});
|
|
actionRegistry.listen(canvasNode);
|
|
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();
|
|
}
|
|
room.tick(elapsed);
|
|
}, 1000 / 80);
|
|
// State updates.
|
|
let dirty = false;
|
|
function onMessage({type, payload}) {
|
|
switch (type) {
|
|
case 'state-update':
|
|
if (payload.selfEntity) {
|
|
selfEntity = payload.selfEntity;
|
|
}
|
|
stateSynchronizer.acceptStateChange(payload);
|
|
dirty = true;
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
socket.on('message', onMessage);
|
|
// Render.
|
|
function render() {
|
|
stage.tick();
|
|
if (!dirty) {
|
|
return;
|
|
}
|
|
renderer.render(stage);
|
|
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();
|
|
appNode.removeChild(renderer.element);
|
|
stage.destroy();
|
|
renderer.destroy();
|
|
});
|
|
}
|
|
// Make sure dis boi always correct ratio and always maximally visible.
|
|
function onResize() {
|
|
const ratio = window.innerWidth / window.innerHeight;
|
|
const axe = ratio > (16 / 9) ? 'height' : 'width';
|
|
const nodes = window.document.querySelectorAll('.app canvas');
|
|
nodes.forEach((node) => {
|
|
for (const key of ['height', 'width']) {
|
|
node.style[key] = key === axe ? '100%' : 'auto';
|
|
}
|
|
canvasRatio = 640 / node.clientWidth;
|
|
});
|
|
}
|
|
window.addEventListener('resize', onResize);
|
|
onResize();
|