365 lines
8.8 KiB
JavaScript
365 lines
8.8 KiB
JavaScript
import {
|
|
TPS,
|
|
} from '@/constants.js';
|
|
import Ecs from '@/ecs/ecs.js';
|
|
import Components from '@/ecs-components/index.js';
|
|
import Systems from '@/ecs-systems/index.js';
|
|
import {decode, encode} from '@/packets/index.js';
|
|
import Script from '@/util/script.js';
|
|
|
|
function join(...parts) {
|
|
return parts.join('/');
|
|
}
|
|
|
|
export default class Engine {
|
|
|
|
connections = [];
|
|
connectedPlayers = new Map();
|
|
ecses = {};
|
|
frame = 0;
|
|
handle;
|
|
incomingActions = [];
|
|
last = Date.now();
|
|
server;
|
|
|
|
constructor(Server) {
|
|
this.ecses = {};
|
|
class SilphiusServer extends Server {
|
|
accept(connection, packed) {
|
|
super.accept(connection, decode(packed));
|
|
}
|
|
transmit(connection, packet) {
|
|
super.transmit(connection, encode(packet));
|
|
}
|
|
}
|
|
const server = this.server = new SilphiusServer();
|
|
this.Ecs = class EngineEcs extends Ecs {
|
|
async readAsset(uri) {
|
|
return server.readAsset(uri);
|
|
}
|
|
async readJson(uri) {
|
|
const chars = await this.readAsset(uri);
|
|
return chars.byteLength > 0 ? JSON.parse((new TextDecoder()).decode(chars)) : {};
|
|
}
|
|
async readScript(uri) {
|
|
const code = await this.readAsset(uri);
|
|
if (code.byteLength > 0) {
|
|
return Script.fromCode((new TextDecoder()).decode(code));
|
|
}
|
|
}
|
|
}
|
|
this.server.addPacketListener('Action', (connection, payload) => {
|
|
this.incomingActions.push([this.connectedPlayers.get(connection).entity, payload]);
|
|
});
|
|
}
|
|
|
|
acceptActions() {
|
|
for (const [
|
|
entity,
|
|
payload,
|
|
] of this.incomingActions) {
|
|
const {Controlled, Inventory, Wielder} = entity;
|
|
switch (payload.type) {
|
|
case 'changeSlot': {
|
|
if (!Controlled.locked) {
|
|
Wielder.activeSlot = payload.value - 1;
|
|
}
|
|
break;
|
|
}
|
|
case 'moveUp':
|
|
case 'moveRight':
|
|
case 'moveDown':
|
|
case 'moveLeft': {
|
|
Controlled[payload.type] = payload.value;
|
|
break;
|
|
}
|
|
case 'swapSlots': {
|
|
if (!Controlled.locked) {
|
|
Inventory.swapSlots(...payload.value);
|
|
}
|
|
break;
|
|
}
|
|
case 'use': {
|
|
if (!Controlled.locked) {
|
|
Wielder.useActiveItem(payload.value);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
this.incomingActions = [];
|
|
}
|
|
|
|
async connectPlayer(connection, id) {
|
|
const entityJson = await this.loadPlayer(id);
|
|
if (!this.ecses[entityJson.Ecs.path]) {
|
|
await this.loadEcs(entityJson.Ecs.path);
|
|
}
|
|
const ecs = this.ecses[entityJson.Ecs.path];
|
|
const entity = await ecs.create(entityJson);
|
|
this.connections.push(connection);
|
|
this.connectedPlayers.set(
|
|
connection,
|
|
{
|
|
entity: ecs.get(entity),
|
|
id,
|
|
memory: new Set(),
|
|
},
|
|
);
|
|
}
|
|
|
|
createEcs() {
|
|
return new this.Ecs({Components, Systems});
|
|
}
|
|
|
|
async createHomestead(id) {
|
|
const ecs = this.createEcs();
|
|
const area = {x: 100, y: 60};
|
|
await ecs.create({
|
|
AreaSize: {x: area.x * 16, y: area.y * 16},
|
|
Engine: {},
|
|
TileLayers: {
|
|
layers: [
|
|
{
|
|
area,
|
|
data: Array(area.x * area.y).fill(0).map(() => 1 + Math.floor(Math.random() * 4)),
|
|
source: '/assets/tileset.json',
|
|
tileSize: {x: 16, y: 16},
|
|
}
|
|
],
|
|
},
|
|
});
|
|
const plant = {
|
|
Plant: {
|
|
growScript: '/assets/tomato-plant/grow.js',
|
|
mayGrowScript: '/assets/tomato-plant/may-grow.js',
|
|
stages: Array(5).fill(60),
|
|
},
|
|
Sprite: {
|
|
anchor: {x: 0.5, y: 0.75},
|
|
animation: 'stage/0',
|
|
frame: 0,
|
|
frames: 1,
|
|
source: '/assets/tomato-plant/tomato-plant.json',
|
|
speed: 0,
|
|
},
|
|
Ticking: {},
|
|
VisibleAabb: {},
|
|
};
|
|
const promises = [];
|
|
for (let y = 0; y < 5; ++y) {
|
|
for (let x = 0; x < 5; ++x) {
|
|
promises.push(ecs.create({
|
|
...plant,
|
|
Plant: {
|
|
...plant.Plant,
|
|
growthFactor: Math.floor(Math.random() * 256),
|
|
},
|
|
Position: {x: 8 + x * 16, y: 8 + y * 16},
|
|
}));
|
|
}
|
|
}
|
|
await Promise.all(promises);
|
|
const defaultSystems = [
|
|
'ResetForces',
|
|
'ApplyControlMovement',
|
|
'ApplyForces',
|
|
'ClampPositions',
|
|
'PlantGrowth',
|
|
'FollowCamera',
|
|
'CalculateAabbs',
|
|
'UpdateSpatialHash',
|
|
'ControlDirection',
|
|
'SpriteDirection',
|
|
'RunAnimations',
|
|
'RunTickingPromises',
|
|
];
|
|
defaultSystems.forEach((defaultSystem) => {
|
|
const System = ecs.system(defaultSystem);
|
|
if (System) {
|
|
System.active = true;
|
|
}
|
|
});
|
|
this.saveEcs(join('homesteads', `${id}`), ecs);
|
|
}
|
|
|
|
async createPlayer(id) {
|
|
const player = {
|
|
Camera: {},
|
|
Controlled: {},
|
|
Direction: {direction: 2},
|
|
Ecs: {path: join('homesteads', `${id}`)},
|
|
Emitter: {},
|
|
Forces: {},
|
|
Inventory: {
|
|
slots: {
|
|
1: {
|
|
qty: 10,
|
|
source: '/assets/potion/potion.json',
|
|
},
|
|
3: {
|
|
qty: 1,
|
|
source: '/assets/tomato-seeds/tomato-seeds.json',
|
|
},
|
|
4: {
|
|
qty: 1,
|
|
source: '/assets/hoe/hoe.json',
|
|
},
|
|
},
|
|
},
|
|
Health: {health: 100},
|
|
Position: {x: 368, y: 368},
|
|
VisibleAabb: {},
|
|
Speed: {speed: 100},
|
|
Sound: {},
|
|
Sprite: {
|
|
anchor: {x: 0.5, y: 0.8},
|
|
animation: 'moving:down',
|
|
frame: 0,
|
|
frames: 8,
|
|
source: '/assets/dude/dude.json',
|
|
speed: 0.115,
|
|
},
|
|
Ticking: {},
|
|
Wielder: {
|
|
activeSlot: 0,
|
|
},
|
|
};
|
|
const buffer = (new TextEncoder()).encode(JSON.stringify(player));
|
|
await this.server.writeData(
|
|
join('players', `${id}`),
|
|
buffer,
|
|
);
|
|
return buffer;
|
|
}
|
|
|
|
async disconnectPlayer(connection) {
|
|
const {entity, id} = this.connectedPlayers.get(connection);
|
|
const ecs = this.ecses[entity.Ecs.path];
|
|
await this.savePlayer(id, entity);
|
|
ecs.destroy(entity.id);
|
|
this.connectedPlayers.delete(connection);
|
|
this.connections.splice(this.connections.indexOf(connection), 1);
|
|
}
|
|
|
|
async load() {
|
|
}
|
|
|
|
async loadEcs(path) {
|
|
this.ecses[path] = await this.Ecs.deserialize(
|
|
this.createEcs(),
|
|
await this.server.readData(path),
|
|
);
|
|
}
|
|
|
|
async loadPlayer(id) {
|
|
let buffer;
|
|
try {
|
|
buffer = await this.server.readData(['players', `${id}`].join('/'))
|
|
}
|
|
catch (error) {
|
|
if ('ENOENT' !== error.code) {
|
|
throw error;
|
|
}
|
|
await this.createHomestead(id);
|
|
buffer = await this.createPlayer(id);
|
|
}
|
|
return JSON.parse((new TextDecoder()).decode(buffer));
|
|
}
|
|
|
|
async saveEcs(path, ecs) {
|
|
const view = this.Ecs.serialize(ecs);
|
|
await this.server.writeData(path, view);
|
|
}
|
|
|
|
async saveEcses() {
|
|
const promises = []
|
|
for (const i in this.ecses) {
|
|
promises.push(this.saveEcs(i, this.ecses[i]));
|
|
}
|
|
await Promise.all(promises);
|
|
}
|
|
|
|
async savePlayer(id, entity) {
|
|
const encoder = new TextEncoder();
|
|
const buffer = encoder.encode(JSON.stringify(entity.toJSON()));
|
|
await this.server.writeData(['players', `${id}`].join('/'), buffer);
|
|
}
|
|
|
|
setClean() {
|
|
for (const i in this.ecses) {
|
|
this.ecses[i].setClean();
|
|
}
|
|
}
|
|
|
|
start() {
|
|
this.handle = setInterval(() => {
|
|
const elapsed = (Date.now() - this.last) / 1000;
|
|
this.last = Date.now();
|
|
this.acceptActions();
|
|
this.tick(elapsed);
|
|
this.update(elapsed);
|
|
this.setClean();
|
|
this.frame += 1;
|
|
}, 1000 / TPS);
|
|
}
|
|
|
|
stop() {
|
|
clearInterval(this.handle);
|
|
this.handle = undefined;
|
|
}
|
|
|
|
tick(elapsed) {
|
|
for (const i in this.ecses) {
|
|
this.ecses[i].tick(elapsed);
|
|
}
|
|
}
|
|
|
|
update(elapsed) {
|
|
for (const connection of this.connections) {
|
|
this.server.send(
|
|
connection,
|
|
{
|
|
type: 'Tick',
|
|
payload: {
|
|
ecs: this.updateFor(connection),
|
|
elapsed,
|
|
frame: this.frame,
|
|
},
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
updateFor(connection) {
|
|
const update = {};
|
|
const {entity, memory} = this.connectedPlayers.get(connection);
|
|
const mainEntityId = entity.id;
|
|
const ecs = this.ecses[entity.Ecs.path];
|
|
const nearby = ecs.system('UpdateSpatialHash').nearby(entity);
|
|
// Master entity.
|
|
nearby.add(ecs.get(1));
|
|
const lastMemory = new Set(memory.values());
|
|
for (const entity of nearby) {
|
|
const {id} = entity;
|
|
lastMemory.delete(id);
|
|
if (!memory.has(id)) {
|
|
update[id] = entity.toJSON();
|
|
if (mainEntityId === id) {
|
|
update[id].MainEntity = {};
|
|
}
|
|
}
|
|
else if (ecs.diff[id]) {
|
|
update[id] = ecs.diff[id];
|
|
}
|
|
memory.add(id);
|
|
}
|
|
for (const id of lastMemory) {
|
|
memory.delete(id);
|
|
update[id] = false;
|
|
}
|
|
return update;
|
|
}
|
|
|
|
}
|