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, context) { const code = await this.readAsset(uri); if (code.byteLength > 0) { return Script.fromCode((new TextDecoder()).decode(code), context); } } } 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, Ecs, Interacts, 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; } case 'interact': { if (!Controlled.locked) { if (payload.value) { if (Interacts.willInteractWith) { const ecs = this.ecses[Ecs.path]; const subject = ecs.get(Interacts.willInteractWith); subject.Interactive.interact(entity); } } } 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}, } ], }, Water: {water: {}}, }); await ecs.create({ Position: {x: 100, y: 100}, Sprite: { anchor: {x: 0.5, y: 0.8}, source: '/assets/shit-shack/shit-shack.png', }, VisibleAabb: {}, }); const defaultSystems = [ 'ResetForces', 'ApplyControlMovement', 'IntegratePhysics', 'ClampPositions', 'PlantGrowth', 'FollowCamera', 'CalculateAabbs', 'UpdateSpatialHash', 'ControlDirection', 'SpriteDirection', 'RunAnimations', 'RunTickingPromises', 'Water', 'Interactions', ]; defaultSystems.forEach((defaultSystem) => { const System = ecs.system(defaultSystem); if (System) { System.active = true; } }); await this.saveEcs(join('homesteads', `${id}`), ecs); } async createPlayer(id) { const player = { Camera: {}, Controlled: {}, Direction: {direction: 2}, Ecs: {path: join('homesteads', `${id}`)}, Emitter: {}, Forces: {}, Interacts: {}, Inventory: { slots: { // 1: { // qty: 10, // source: '/assets/potion/potion.json', // }, 2: { qty: 1, source: '/assets/watering-can/watering-can.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}, 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: {}, VisibleAabb: {}, 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; } }