silphius/app/client/predictor.js

169 lines
4.5 KiB
JavaScript

import {LRUCache} from 'lru-cache';
import Components from '@/ecs/components/index.js';
import Ecs from '@/ecs/ecs.js';
import Systems from '@/ecs/systems/index.js';
import {withResolvers} from '@/util/promise.js';
const cache = new LRUCache({
max: 128,
});
class PredictionEcs extends Ecs {
async readAsset(uri) {
if (!cache.has(uri)) {
const {promise, resolve, reject} = withResolvers();
cache.set(uri, promise);
fetch(uri)
.then((response) => resolve(response.ok ? response.arrayBuffer() : new ArrayBuffer(0)))
.catch(reject);
}
return cache.get(uri);
}
}
const Flow = {
UP: 0,
DOWN: 1,
};
const actions = new Map();
let ecs = new PredictionEcs({Components, Systems});
let mainEntityId = 0;
function applyClientActions(elapsed) {
if (actions.size > 0) {
const main = ecs.get(mainEntityId);
const {Controlled} = main;
const finished = [];
for (const [id, action] of actions) {
if (0 === action.finished && !action.ack) {
if (!Controlled.locked) {
switch (action.action.type) {
case 'moveUp':
case 'moveRight':
case 'moveDown':
case 'moveLeft': {
Controlled[action.action.type] = action.action.value;
break;
}
}
}
action.steps.push(elapsed);
}
if (1 === action.finished) {
if (!Controlled.locked) {
switch (action.action.type) {
case 'moveUp':
case 'moveRight':
case 'moveDown':
case 'moveLeft': {
Controlled[action.action.type] = 0;
break;
}
}
}
action.finished = 2;
}
if (action.ack && 2 === action.finished) {
action.steps.shift();
if (0 === action.steps.length) {
finished.push(id);
continue;
}
}
let leap = 0;
for (const step of action.steps) {
leap += step;
}
if (leap > 0) {
ecs.predict(main, leap);
}
}
for (const id of finished) {
actions.delete(id);
}
}
}
let downPromise;
const pending = new Map();
onmessage = async (event) => {
const [flow, packet] = event.data;
switch (flow) {
case Flow.UP: {
switch (packet.type) {
case 'Action': {
switch (packet.payload.type) {
case 'moveUp':
case 'moveRight':
case 'moveDown':
case 'moveLeft': {
if (0 === packet.payload.value) {
const ack = pending.get(packet.payload.type);
const action = actions.get(ack);
action.finished = 1;
pending.delete(packet.payload.type);
}
else {
const tx = {
action: packet.payload,
ack: false,finished: 0,
steps: [],
};
packet.payload.ack = Math.random();
pending.set(packet.payload.type, packet.payload.ack);
actions.set(packet.payload.ack, tx);
}
}
}
break;
}
}
postMessage([0, packet]);
break;
}
case Flow.DOWN: {
downPromise = Promise.resolve(downPromise).then(async () => {
switch (packet.type) {
case 'ActionAck': {
const action = actions.get(packet.payload.ack);
action.ack = true;
return;
}
case 'EcsChange': {
ecs = new PredictionEcs({Components, Systems});
mainEntityId = 0;
break;
}
case 'Tick': {
for (const entityId in packet.payload.ecs) {
if (packet.payload.ecs[entityId]) {
if (packet.payload.ecs[entityId].MainEntity) {
mainEntityId = parseInt(entityId);
}
}
}
await ecs.apply(packet.payload.ecs);
if (actions.size > 0) {
const main = ecs.get(mainEntityId);
const authoritative = structuredClone(main.toNet(main));
applyClientActions(packet.payload.elapsed);
if (ecs.diff[mainEntityId]) {
packet.payload.ecs[mainEntityId] = ecs.diff[mainEntityId];
}
await ecs.apply({[mainEntityId]: authoritative});
}
ecs.setClean();
break;
}
}
postMessage([1, packet]);
});
break;
}
}
};