diff --git a/packages/behavior/src/traits/behaved.js b/packages/behavior/src/traits/behaved.js index da4d6d5..9fadf1d 100644 --- a/packages/behavior/src/traits/behaved.js +++ b/packages/behavior/src/traits/behaved.js @@ -118,6 +118,7 @@ export default (latus) => class Behaved extends decorate(Trait) { } destroy() { + super.destroy(); this.#context.destroy(); this.#currentRoutine = undefined; this.#routines = undefined; diff --git a/packages/dialog/src/traits/decorators/initiator.js b/packages/dialog/src/traits/decorators/initiator.js index 9a967b3..6af0685 100644 --- a/packages/dialog/src/traits/decorators/initiator.js +++ b/packages/dialog/src/traits/decorators/initiator.js @@ -85,8 +85,8 @@ export default (Trait, latus) => class DialogInitiator extends Trait { }; } - packets(informed) { - return super.packets(informed).concat( + packetsFor(informed) { + return super.packetsFor(informed).concat( informed === this.entity ? this.#dialogs.map((dialog) => ['OpenDialog', dialog]) : [], diff --git a/packages/entity/src/accessors.js b/packages/entity/src/accessors.js index 7473392..4925e8d 100644 --- a/packages/entity/src/accessors.js +++ b/packages/entity/src/accessors.js @@ -1,4 +1,5 @@ const blacklistedAccessorKeys = [ + 's13nId', 'state', 'uri', 'instanceUuid', diff --git a/packages/entity/src/packets/entity-list-update-entity.js b/packages/entity/src/packets/entity-list-update-entity.js deleted file mode 100644 index c2b39de..0000000 --- a/packages/entity/src/packets/entity-list-update-entity.js +++ /dev/null @@ -1,32 +0,0 @@ -import {Packet} from '@latus/socket'; - -export default (latus) => class EntityListUpdateEntityPacket extends Packet { - - static pack(data) { - const {Bundle} = latus.get('%packets'); - for (let i = 0; i < data.length; i++) { - // eslint-disable-next-line no-param-reassign - data[i].packets = Bundle.encode(data[i].packets); - } - return data; - } - - static get data() { - return [ - { - uuid: 'uint32', - packets: 'buffer', - }, - ]; - } - - static unpack(data) { - const {Bundle} = latus.get('%packets'); - for (let i = 0; i < data.length; i++) { - // eslint-disable-next-line no-param-reassign - data[i].packets = Bundle.decode(data[i].packets); - } - return data; - } - -}; diff --git a/packages/entity/src/packets/entity-update-trait.js b/packages/entity/src/packets/entity-update-trait.js deleted file mode 100644 index 1ddb144..0000000 --- a/packages/entity/src/packets/entity-update-trait.js +++ /dev/null @@ -1,44 +0,0 @@ -import {SynchronizedUpdatePacket} from '@avocado/s13n'; - -export default (latus) => class EntityUpdateTraitPacket extends SynchronizedUpdatePacket { - - static pack(data) { - const {Bundle} = latus.get('%packets'); - const Traits = latus.get('%traits'); - for (let i = 0; i < data.traits.length; i++) { - const {[data.traits[i].type]: Trait} = Traits; - // eslint-disable-next-line no-param-reassign - data.traits[i] = { - type: Trait.id, - packets: Bundle.encode(data.traits[i].packets), - }; - } - return data; - } - - static get s13nSchema() { - return { - traits: [ - { - type: 'uint8', - packets: 'buffer', - }, - ], - }; - } - - static unpack(data) { - const {Bundle} = latus.get('%packets'); - const Traits = latus.get('%traits'); - for (let i = 0; i < data.traits.length; i++) { - const {[data.traits[i].type]: Trait} = Traits; - // eslint-disable-next-line no-param-reassign - data.traits[i] = { - type: Trait.type, - packets: Bundle.decode(data.traits[i].packets), - }; - } - return data; - } - -}; diff --git a/packages/entity/src/resources/entity-list.js b/packages/entity/src/resources/entity-list.js index 85b681c..272e867 100644 --- a/packages/entity/src/resources/entity-list.js +++ b/packages/entity/src/resources/entity-list.js @@ -2,300 +2,272 @@ import {compile, Context} from '@avocado/behavior'; import {compose, EventEmitter} from '@latus/core'; import {QuadTree, Rectangle} from '@avocado/math'; import {JsonResource} from '@avocado/resource'; -import {Serializer} from '@avocado/s13n'; +import {Synchronized} from '@avocado/s13n'; -const decorate = compose( - EventEmitter, -); +export default (latus) => { + const decorate = compose( + EventEmitter, + Synchronized(latus), + ); + return class EntityList extends decorate(JsonResource) { -export default (latus) => class EntityList extends decorate(JsonResource) { + #afterDestructionTickers = []; - #afterDestructionTickers = []; + #entities = {}; - #entities = {}; + #entityTickers = []; - #entityTickers = []; + #flatEntities = []; - #flatEntities = []; + #informedEntities = new Map(); - #informedEntities = new Map(); + #quadTree = new QuadTree(); - #quadTree = new QuadTree(); - - #serializer = new Serializer(); - - async acceptPacket(packet) { - if ('EntityListUpdateEntity' === packet.constructor.type) { - for (let i = 0; i < packet.data.length; i++) { - const {uuid, packets} = packet.data[i]; - for (let j = 0; j < packets.length; j++) { - if (this.#entities[uuid]) { - this.#entities[uuid].acceptPacket(packets[j]); - } - else { - this.#serializer.later(uuid, (entity) => { - if (entity) { - entity.acceptPacket(packets[j]); - } - }); - } - } - } - } - const {constructor: {s13nType}} = packet; - switch (s13nType) { - case 'create': { - const uuid = packet.data.synchronized.id; + async acceptPacket(packet) { + await super.acceptPacket(packet); + const {s13nType} = packet; + if ('create' === s13nType) { const {Entity} = latus.get('%resources'); - this.#serializer.create(uuid, Entity.load(packet.data.spec)); - this.#serializer.later(uuid, (entity) => { - this.addEntity(entity); - }); - break; - } - case 'destroy': { - const uuid = packet.data.synchronized.id; - this.#serializer.cancelIfPending(uuid); - if (this.#entities[uuid]) { - this.#entities[uuid].destroy(); - } - break; - } - default: - } - } - - async addEntity(entity) { - const uuid = entity.instanceUuid; - // Already exists? - if (this.#entities[uuid]) { - return; - } - this.#entities[uuid] = entity; - this.#flatEntities.push(entity); - this.#entityTickers.push(entity.tick); - if ('client' !== process.env.SIDE) { - this.#informedEntities.set(entity, []); - } - // eslint-disable-next-line no-param-reassign - entity.list = this; - entity.emit('addedToList'); - entity.once('destroying', () => this.onEntityDestroying(entity)); - this.emit('entityAdded', entity); - } - - cleanPackets() { - for (let i = 0; i < this.#flatEntities.length; i++) { - this.#flatEntities[i].cleanPackets(); - } - } - - destroy() { - for (let i = 0; i < this.#flatEntities.length; i++) { - this.#flatEntities[i].destroy(); - } - } - - get entities() { - return this.#entities; - } - - findEntity(uuid) { - return uuid in this.#entities - ? this.#entities[uuid] - : undefined; - } - - async load(json = []) { - await super.load(json); - const {Entity} = latus.get('%resources'); - const entityInstances = await Promise.all(json.map((entity) => Entity.load(entity))); - for (let i = 0; i < entityInstances.length; i++) { - this.addEntity(entityInstances[i]); - } - } - - onEntityDestroying(entity) { - this.removeEntity(entity); - // In the process of destroying, allow entities to specify tickers that - // must live on past destruction. - const tickers = entity.invokeHookFlat('afterDestructionTickers'); - for (let i = 0; i < tickers.length; i++) { - const ticker = tickers[i]; - this.#afterDestructionTickers.push(ticker); - } - } - - packets(informed) { - const packets = []; - // Visible entities. - const {areaToInform} = informed; - const previousVisibleEntities = this.#informedEntities.get(informed); - const visibleEntities = this.visibleEntities(areaToInform); - const updates = []; - for (let i = 0; i < visibleEntities.length; i++) { - const entity = visibleEntities[i]; - // Newly visible entity. - const index = previousVisibleEntities.indexOf(entity); - if (-1 === index) { - packets.push(entity.createPacket(informed)); - } - else { - previousVisibleEntities.splice(index, 1); - } - const entityPackets = entity.packets(informed); - if (entityPackets.length > 0) { - updates.push({ - uuid: entity.instanceUuid, - packets: entityPackets, - }); + const {id} = packet.data.synchronized; + this.addEntity(this.synchronized(Entity.resourceId, id)); } } - // Send updates. - this.#informedEntities.set(informed, visibleEntities); - if (updates.length > 0) { - packets.push([ - 'EntityListUpdateEntity', - updates, - ]); - } - // Send destroys. - for (let i = 0; i < previousVisibleEntities.length; i++) { - const entity = previousVisibleEntities[i]; - // Newly removed entity. - if (-1 === visibleEntities.indexOf(entity)) { - packets.push(entity.destroyPacket(informed)); + + async addEntity(entity) { + const uuid = entity.instanceUuid; + // Already exists? + if (this.#entities[uuid]) { + return; } - } - return packets; - } - - get quadTree() { - return this.#quadTree; - } - - queryEntities(query, condition, context) { - if (!context) { + this.#entities[uuid] = entity; + this.#flatEntities.push(entity); + this.#entityTickers.push(entity.tick); // eslint-disable-next-line no-param-reassign - context = new Context({}, latus); + entity.list = this; + entity.emit('addedToList'); + entity.once('destroying', () => this.onEntityDestroying(entity)); + this.emit('entityAdded', entity); + if ('client' !== process.env.SIDE) { + this.#informedEntities.set(entity, []); + } + this.startSynchronizing(entity); } - const check = compile(condition, latus); - const candidates = this.visibleEntities(query, true); - const fails = []; - for (let i = 0; i < candidates.length; ++i) { - const entity = candidates[i]; - context.add('query', entity); - if (!check(context)) { - fails.push(entity); + + cleanPackets() { + super.cleanPackets(); + for (let i = 0; i < this.#flatEntities.length; i++) { + this.#flatEntities[i].cleanPackets(); } } - for (let i = 0; i < fails.length; ++i) { - candidates.splice(candidates.indexOf(fails[i]), 1); - } - return candidates; - } - queryPoint(query, condition, context) { - return this.queryEntities(Rectangle.centerOn(query, [1, 1]), condition, context); - } - - removeEntity(entity) { - const uuid = entity.instanceUuid; - if (!(uuid in this.#entities)) { - return; - } - if ('client' !== process.env.SIDE) { - this.#informedEntities.delete(entity); - } - // eslint-disable-next-line no-param-reassign - entity.list = null; - entity.emit('removedFromList', this); - delete this.#entities[uuid]; - this.#flatEntities.splice(this.#flatEntities.indexOf(entity), 1); - this.#entityTickers.splice(this.#entityTickers.indexOf(entity.tick), 1); - this.emit('entityRemoved', entity); - } - - tick(elapsed) { - // Run after destruction tickers. - if (this.#afterDestructionTickers.length > 0) { - this.tickAfterDestructionTickers(elapsed); - } - // Run normal tickers. - this.tickEntities(elapsed); - } - - tickAfterDestructionTickers(elapsed) { - const finishedTickers = []; - for (let i = 0; i < this.#afterDestructionTickers.length; ++i) { - const ticker = this.#afterDestructionTickers[i]; - if (ticker(elapsed)) { - finishedTickers.push(ticker); + destroy() { + for (let i = 0; i < this.#flatEntities.length; i++) { + this.#flatEntities[i].destroy(); } } - for (let i = 0; i < finishedTickers.length; ++i) { - const ticker = finishedTickers[i]; - const index = this.#afterDestructionTickers.indexOf(ticker); - this.#afterDestructionTickers.splice(index, 1); + + get entities() { + return this.#entities; } - } - tickEntities(elapsed) { - for (let i = 0; i < this.#entityTickers.length; i++) { - this.#entityTickers[i](elapsed); + findEntity(uuid) { + return uuid in this.#entities + ? this.#entities[uuid] + : undefined; } - } - toJSON() { - const json = []; - for (let i = 0; i < this.#flatEntities.length; i++) { - const entity = this.#flatEntities[i]; - json.push(entity.mergeDiff(entity.toJSON())); - } - return json; - } - - toNetwork(informed) { - const {areaToInform} = informed; - const visibleEntities = this.visibleEntities(areaToInform); - // Mark as notified. - this.#informedEntities.set(informed, visibleEntities); - return visibleEntities.map((entity) => entity.toNetwork(informed)); - } - - visibleEntities(query) { - const entities = []; - const quadTree = this.#quadTree; - const entries = Array.from(quadTree.query(query)); - for (let i = 0; i < entries.length; i++) { - const [entity] = entries[i]; - const {visibleAabb} = entity; - if (entity.isVisible && Rectangle.intersects(query, visibleAabb)) { - entities.push(entity); + async load(json = []) { + await super.load(json); + const {Entity} = latus.get('%resources'); + const entityInstances = await Promise.all(json.map((entity) => Entity.load(entity))); + for (let i = 0; i < entityInstances.length; i++) { + this.addEntity(entityInstances[i]); } } - return entities; - } - visibleEntitiesWithUri(query, uri) { - return this.visibleEntities(query).filter((entity) => entity.uri === uri); - } - - async waitForEntity(uuid) { - const entity = this.findEntity(uuid); - if (entity) { - return entity; + onEntityDestroying(entity) { + this.removeEntity(entity); + // In the process of destroying, allow entities to specify tickers that + // must live on past destruction. + const tickers = entity.invokeHookFlat('afterDestructionTickers'); + for (let i = 0; i < tickers.length; i++) { + const ticker = tickers[i]; + this.#afterDestructionTickers.push(ticker); + } } - return new Promise((resolve) => { - const find = () => { - const entity = this.findEntity(uuid); - if (entity) { - this.off('entityAdded', find); - resolve(entity); + + packetsFor(informed) { + const packets = []; + const {Entity} = latus.get('%resources'); + // Visible entities. + const {areaToInform} = informed; + const previousVisibleEntities = this.#informedEntities.get(informed); + const visibleEntities = this.visibleEntities(areaToInform); + for (let i = 0; i < visibleEntities.length; i++) { + const entity = visibleEntities[i]; + // Newly visible entity. + const index = previousVisibleEntities.indexOf(entity); + if (-1 === index) { + packets.push(entity.createPacket(informed)); } - }; - this.on('entityAdded', find); - }); - } + else { + previousVisibleEntities.splice(index, 1); + } + const entityPackets = entity.packetsFor(informed); + if (entityPackets.length > 0) { + packets.push([ + 'SynchronizedUpdate', + { + packets: entityPackets, + synchronized: { + id: entity.s13nId, + type: Entity.resourceId, + }, + }, + ]); + } + } + // Send updates. + this.#informedEntities.set(informed, visibleEntities); + // Send destroys. + for (let i = 0; i < previousVisibleEntities.length; i++) { + const entity = previousVisibleEntities[i]; + // Newly removed entity. + if (-1 === visibleEntities.indexOf(entity)) { + packets.push(entity.destroyPacket(informed)); + } + } + return packets; + } + get quadTree() { + return this.#quadTree; + } + + queryEntities(query, condition, context) { + if (!context) { + // eslint-disable-next-line no-param-reassign + context = new Context({}, latus); + } + const check = compile(condition, latus); + const candidates = this.visibleEntities(query, true); + const fails = []; + for (let i = 0; i < candidates.length; ++i) { + const entity = candidates[i]; + context.add('query', entity); + if (!check(context)) { + fails.push(entity); + } + } + for (let i = 0; i < fails.length; ++i) { + candidates.splice(candidates.indexOf(fails[i]), 1); + } + return candidates; + } + + queryPoint(query, condition, context) { + return this.queryEntities(Rectangle.centerOn(query, [1, 1]), condition, context); + } + + removeEntity(entity) { + const uuid = entity.instanceUuid; + if (!(uuid in this.#entities)) { + return; + } + this.stopSynchronizing(entity); + if ('client' !== process.env.SIDE) { + this.#informedEntities.delete(entity); + } + // eslint-disable-next-line no-param-reassign + entity.list = null; + entity.emit('removedFromList', this); + delete this.#entities[uuid]; + this.#flatEntities.splice(this.#flatEntities.indexOf(entity), 1); + this.#entityTickers.splice(this.#entityTickers.indexOf(entity.tick), 1); + this.emit('entityRemoved', entity); + } + + tick(elapsed) { + // Run after destruction tickers. + if (this.#afterDestructionTickers.length > 0) { + this.tickAfterDestructionTickers(elapsed); + } + // Run normal tickers. + this.tickEntities(elapsed); + } + + tickAfterDestructionTickers(elapsed) { + const finishedTickers = []; + for (let i = 0; i < this.#afterDestructionTickers.length; ++i) { + const ticker = this.#afterDestructionTickers[i]; + if (ticker(elapsed)) { + finishedTickers.push(ticker); + } + } + for (let i = 0; i < finishedTickers.length; ++i) { + const ticker = finishedTickers[i]; + const index = this.#afterDestructionTickers.indexOf(ticker); + this.#afterDestructionTickers.splice(index, 1); + } + } + + tickEntities(elapsed) { + for (let i = 0; i < this.#entityTickers.length; i++) { + this.#entityTickers[i](elapsed); + } + } + + toJSON() { + const json = []; + for (let i = 0; i < this.#flatEntities.length; i++) { + const entity = this.#flatEntities[i]; + json.push(entity.mergeDiff(entity.toJSON())); + } + return json; + } + + toNetwork(informed) { + const {areaToInform} = informed; + const visibleEntities = this.visibleEntities(areaToInform); + // Mark as notified. + this.#informedEntities.set(informed, visibleEntities); + return visibleEntities.map((entity) => entity.toNetwork(informed)); + } + + visibleEntities(query) { + const entities = []; + const quadTree = this.#quadTree; + const entries = Array.from(quadTree.query(query)); + for (let i = 0; i < entries.length; i++) { + const [entity] = entries[i]; + const {visibleAabb} = entity; + if (entity.isVisible && Rectangle.intersects(query, visibleAabb)) { + entities.push(entity); + } + } + return entities; + } + + visibleEntitiesWithUri(query, uri) { + return this.visibleEntities(query).filter((entity) => entity.uri === uri); + } + + async waitForEntity(uuid) { + const entity = this.findEntity(uuid); + if (entity) { + return entity; + } + return new Promise((resolve) => { + const find = () => { + const entity = this.findEntity(uuid); + if (entity) { + this.off('entityAdded', find); + resolve(entity); + } + }; + this.on('entityAdded', find); + }); + } + + }; }; diff --git a/packages/entity/src/resources/entity.js b/packages/entity/src/resources/entity.js index f1845f9..2e12692 100644 --- a/packages/entity/src/resources/entity.js +++ b/packages/entity/src/resources/entity.js @@ -14,428 +14,412 @@ import BaseEntity from '../base-entity'; const debug = D('@avocado/entity'); -const decorate = compose( - EventEmitter, - Synchronized, -); - let numericUid = 'client' !== process.env.SIDE ? 1 : 1000000000; -export default (latus) => class Entity extends decorate(BaseEntity) { +export default (latus) => { + const decorate = compose( + EventEmitter, + Synchronized(latus), + ); + return class Entity extends decorate(BaseEntity) { - #hooks = {}; + #hooks = {}; - #isDestroying = false; + #isDestroying = false; - #markedAsDirty = true; + #markedAsDirty = true; - #originalJson; + #originalJson; - #tickingPromisesTickers = []; + #tickingPromisesTickers = []; - #traits = {}; + #traits = {}; - #traitsFlat = []; + #traitsFlat = []; - #traitTickers = []; + #traitTickers = []; - #traitRenderTickers = []; + #traitRenderTickers = []; - #traitsAcceptingPackets = []; + #traitsAcceptingPackets = []; - constructor() { - super(); - this.once('destroyed', () => { - this.removeAllTraits(); - }); - // Bind to prevent lookup overhead. - this.tick = this.tick.bind(this); - // Fast props. - this.elapsed = 0; - this.list = null; - this.numericUid = numericUid++; - this.position = [0, 0]; - this.room = null; - this.uptime = 0; - this.visibleAabb = [0, 0, 0, 0]; - } + constructor() { + super(); + this.once('destroyed', () => { + this.removeAllTraits(); + }); + // Bind to prevent lookup overhead. + this.tick = this.tick.bind(this); + // Fast props. + this.elapsed = 0; + this.list = null; + this.numericUid = numericUid++; + this.position = [0, 0]; + this.room = null; + this.uptime = 0; + this.visibleAabb = [0, 0, 0, 0]; + } - acceptPacket(packet) { - if ('EntityUpdateTrait' === packet.constructor.type) { - const {traits} = packet.data; - for (let i = 0; i < traits.length; i++) { - const {type, packets} = traits[i]; - for (let j = 0; j < packets.length; j++) { - this.#traits[type].acceptPacket(packets[j]); + addTickingPromise(tickingPromise) { + const ticker = tickingPromise.tick.bind(tickingPromise); + this.#tickingPromisesTickers.push(ticker); + return tickingPromise.then(() => { + const index = this.#tickingPromisesTickers.indexOf(ticker); + if (-1 !== index) { + this.#tickingPromisesTickers.splice(index, 1); } - } + }); } - } - addTickingPromise(tickingPromise) { - const ticker = tickingPromise.tick.bind(tickingPromise); - this.#tickingPromisesTickers.push(ticker); - return tickingPromise.then(() => { - const index = this.#tickingPromisesTickers.indexOf(ticker); - if (-1 !== index) { - this.#tickingPromisesTickers.splice(index, 1); + async _addTrait(type, json = {}) { + const {[type]: Trait} = latus.get('%traits'); + if (!Trait) { + // eslint-disable-next-line no-console + console.error(`Tried to add trait "${type}" which isn't registered!`); + return undefined; } - }); - } - - async _addTrait(type, json = {}) { - const {[type]: Trait} = latus.get('%traits'); - if (!Trait) { - // eslint-disable-next-line no-console - console.error(`Tried to add trait "${type}" which isn't registered!`); - return undefined; - } - if (this.is(type)) { - await this.#traits[type].load({ + if (this.is(type)) { + await this.#traits[type].load({ + entity: this, + ...json, + }); + this.emit('traitAdded', type, this.#traits[type]); + return undefined; + } + // Ensure dependencies. + const dependencies = Trait.dependencies(); + const allTypes = this.traitTypes(); + const lacking = without(dependencies, ...allTypes); + if (lacking.length > 0) { + // eslint-disable-next-line no-console + console.error( + `Tried to add trait "${type}" but lack one or more dependents: "${lacking.join('", "')}"!`, + ); + return undefined; + } + // Instantiate. + const trait = await Trait.load({ entity: this, ...json, }); - this.emit('traitAdded', type, this.#traits[type]); - return undefined; - } - // Ensure dependencies. - const dependencies = Trait.dependencies(); - const allTypes = this.traitTypes(); - const lacking = without(dependencies, ...allTypes); - if (lacking.length > 0) { - // eslint-disable-next-line no-console - console.error( - `Tried to add trait "${type}" but lack one or more dependents: "${lacking.join('", "')}"!`, - ); - return undefined; - } - // Instantiate. - const trait = await Trait.load({ - entity: this, - ...json, - }); - // Proxy properties. - defineTraitAccessors(Trait.prototype, this, trait); - // Attach listeners. - const listeners = Object.entries(trait.memoizedListeners()); - for (let i = 0; i < listeners.length; i++) { - const [event, listener] = listeners[i]; - this.on(event, listener); - } - // Proxy methods. - const methods = Object.entries(trait.methods()); - for (let i = 0; i < methods.length; i++) { - const [key, method] = methods[i]; - this[key] = method; - } - // Register hook listeners. - const hooks = Object.entries(trait.hooks()); - for (let i = 0; i < hooks.length; i++) { - const [key, fn] = hooks[i]; - this.#hooks[key] = this.#hooks[key] || []; - this.#hooks[key].push({ - fn, - type, - }); - } - // Track trait. - this.#traits[type] = trait; - this.#traitsFlat.push(trait); - if ('tick' in trait) { - this.#traitTickers.push(trait.tick); - } - if ('renderTick' in trait) { - this.#traitRenderTickers.push(trait.renderTick); - } - if ('acceptPacket' in trait) { - this.#traitsAcceptingPackets.push(trait); - } - this.emit('traitAdded', type, trait); - return trait; - } - - async addTrait(type, json) { - return this.addTraits({[type]: json}); - } - - async addTraits(traits) { - const Traits = latus.get('%traits'); - const reorganized = {}; - const add = (type) => { - const deps = Traits[type]?.dependencies() || []; - if (deps.length > 0) { - deps.forEach((type) => { - if (!this.is(type)) { - add(type); - } + // Proxy properties. + defineTraitAccessors(Trait.prototype, this, trait); + // Attach listeners. + const listeners = Object.entries(trait.memoizedListeners()); + for (let i = 0; i < listeners.length; i++) { + const [event, listener] = listeners[i]; + this.on(event, listener); + } + // Proxy methods. + const methods = Object.entries(trait.methods()); + for (let i = 0; i < methods.length; i++) { + const [key, method] = methods[i]; + this[key] = method; + } + // Register hook listeners. + const hooks = Object.entries(trait.hooks()); + for (let i = 0; i < hooks.length; i++) { + const [key, fn] = hooks[i]; + this.#hooks[key] = this.#hooks[key] || []; + this.#hooks[key].push({ + fn, + type, }); } - if (!reorganized[type]) { - reorganized[type] = traits[type] || {}; + // Track trait. + this.#traits[type] = trait; + this.#traitsFlat.push(trait); + if ('tick' in trait) { + this.#traitTickers.push(trait.tick); } - }; - Object.keys(traits).forEach(add); - const entries = Object.entries(reorganized); - const instances = {}; - for (let i = 0; i < entries.length; i++) { - const [type, json] = entries[i]; - // eslint-disable-next-line no-await-in-loop - instances[type] = await this._addTrait(type, json); + if ('renderTick' in trait) { + this.#traitRenderTickers.push(trait.renderTick); + } + if ('acceptPacket' in trait) { + this.#traitsAcceptingPackets.push(trait); + } + this.startSynchronizing(trait); + this.emit('traitAdded', type, trait); + return trait; } - return instances; - } - cleanPackets() { - if (!this.#markedAsDirty) { - return; + async addTrait(type, json) { + return this.addTraits({[type]: json}); } - for (let i = 0; i < this.#traitsFlat.length; i++) { - this.#traitsFlat[i].cleanPackets(); - } - this.#markedAsDirty = false; - } - async destroy() { - if (this.#isDestroying) { - return; + async addTraits(traits) { + const Traits = latus.get('%traits'); + const reorganized = {}; + const add = (type) => { + const deps = Traits[type]?.dependencies() || []; + if (deps.length > 0) { + deps.forEach((type) => { + if (!this.is(type)) { + add(type); + } + }); + } + if (!reorganized[type]) { + reorganized[type] = traits[type] || {}; + } + }; + Object.keys(traits).forEach(add); + const entries = Object.entries(reorganized); + const instances = {}; + for (let i = 0; i < entries.length; i++) { + const [type, json] = entries[i]; + // eslint-disable-next-line no-await-in-loop + instances[type] = await this._addTrait(type, json); + } + return instances; } - this.#isDestroying = true; - const destroyers = this.invokeHookFlat('destroy'); - if (destroyers.length > 0) { - await this.addTickingPromise(new TickingPromise( - () => {}, - (elapsed, resolve) => { - if (destroyers.every((destroy) => destroy(elapsed), (r) => !!r)) { - resolve(); - } - }, - )); - } - this.emit('destroying'); - this.emit('destroyed'); - } - invokeHook(hook, ...args) { - const results = {}; - if (!(hook in this.#hooks)) { + cleanPackets() { + super.cleanPackets(); + if (!this.#markedAsDirty) { + return; + } + for (let i = 0; i < this.#traitsFlat.length; i++) { + this.#traitsFlat[i].cleanPackets(); + } + this.#markedAsDirty = false; + } + + async destroy() { + if (this.#isDestroying) { + return; + } + this.#isDestroying = true; + const destroyers = this.invokeHookFlat('destroy'); + if (destroyers.length > 0) { + await this.addTickingPromise(new TickingPromise( + () => {}, + (elapsed, resolve) => { + if (destroyers.every((destroy) => destroy(elapsed), (r) => !!r)) { + resolve(); + } + }, + )); + } + this.emit('destroying'); + this.emit('destroyed'); + } + + invokeHook(hook, ...args) { + const results = {}; + if (!(hook in this.#hooks)) { + return results; + } + const values = Object.values(this.#hooks[hook]); + for (let i = 0; i < values.length; i++) { + const {fn, type} = values[i]; + results[type] = fastApply(null, fn, args); + } return results; } - const values = Object.values(this.#hooks[hook]); - for (let i = 0; i < values.length; i++) { - const {fn, type} = values[i]; - results[type] = fastApply(null, fn, args); + + invokeHookFlat(hook, ...args) { + return Object.values(this.invokeHook(hook, ...args)); } - return results; - } - invokeHookFlat(hook, ...args) { - return Object.values(this.invokeHook(hook, ...args)); - } - - invokeHookReduced(hook, ...args) { - return this.invokeHookFlat(hook, ...args) - .reduce((r, result) => ({...r, ...result}), {}); - } - - is(type) { - return type in this.#traits; - } - - async load(json = {}) { - await super.load(json); - const {instanceUuid, traits = {}} = json; - if (!this.#originalJson) { - this.#originalJson = json; + invokeHookReduced(hook, ...args) { + return this.invokeHookFlat(hook, ...args) + .reduce((r, result) => ({...r, ...result}), {}); } - this.instanceUuid = instanceUuid || this.numericUid; - await this.addTraits(traits); - } - markAsDirty() { - this.#markedAsDirty = true; - } + is(type) { + return type in this.#traits; + } - packets(informed) { - const packets = []; - const updates = []; - const traits = Object.entries(this.#traits); - for (let i = 0; i < traits.length; i++) { - const [type, trait] = traits[i]; - const traitPackets = trait.packets(informed); - if (traitPackets.length > 0) { - updates.push({ - type, - packets: traitPackets, - }); + async load(json = {}) { + await super.load(json); + const {instanceUuid, traits = {}} = json; + if (!this.#originalJson) { + this.#originalJson = json; + } + this.instanceUuid = instanceUuid || this.numericUid; + await this.addTraits(traits); + } + + markAsDirty() { + this.#markedAsDirty = true; + } + + packetsFor(informed) { + const packets = []; + const traits = Object.values(this.#traits); + for (let i = 0; i < traits.length; i++) { + const trait = traits[i]; + const traitPackets = trait.packetsFor(informed); + if (traitPackets.length > 0) { + packets.push([ + 'SynchronizedUpdate', + { + packets: traitPackets, + synchronized: { + id: trait.s13nId, + type: trait.constructor.resourceId, + }, + }, + ]); + } + } + return packets; + } + + renderTick(elapsed) { + for (let i = 0; i < this.#traitRenderTickers.length; i++) { + this.#traitRenderTickers[i](elapsed); } } - if (updates.length > 0) { - packets.push([ - 'EntityUpdateTrait', - { - synchronized: { - id: 0, - type: 0, - }, - traits: updates, - }, - ]); - } - return packets; - } - renderTick(elapsed) { - for (let i = 0; i < this.#traitRenderTickers.length; i++) { - this.#traitRenderTickers[i](elapsed); + removeAllTraits() { + const types = this.traitTypes(); + this.removeTraits(types); } - } - removeAllTraits() { - const types = this.traitTypes(); - this.removeTraits(types); - } - - removeTrait(type) { - if (!this.is(type)) { - debug(`Tried to remove trait "${type}" when it doesn't exist!`); - return; + removeTrait(type) { + if (!this.is(type)) { + debug(`Tried to remove trait "${type}" when it doesn't exist!`); + return; + } + // Destroy instance. + const instance = this.#traits[type]; + this.stopSynchronizing(instance); + // Remove methods, hooks, and properties. + const methods = instance.methods(); + const keys = Object.keys(methods); + for (let i = 0; i < keys.length; i++) { + delete this[keys[i]]; + } + const hooks = Object.keys(instance.hooks()); + for (let i = 0; i < hooks.length; i++) { + const hook = hooks[i]; + const implementation = this.#hooks[hook].find(({type: hookType}) => hookType === type); + this.#hooks[hook].splice(this.#hooks[hook].indexOf(implementation), 1); + } + const {[type]: Trait} = latus.get('%traits'); + const properties = enumerateTraitAccessorKeys(Trait.prototype); + for (let i = 0; i < properties.length; ++i) { + const property = properties[i]; + delete this[property]; + } + // Remove all event listeners. + const listeners = Object.entries(instance.memoizedListeners()); + for (let i = 0; i < listeners.length; i++) { + const [event, listener] = listeners[i]; + this.off(event, listener); + } + instance.destroy(); + // Remove instance. + delete this.#traits[type]; + this.#traitsFlat.splice(this.#traitsFlat.indexOf(instance), 1); + if ('tick' in instance) { + this.#traitTickers.splice(this.#traitTickers.indexOf(instance.tick), 1); + } + if ('renderTick' in instance) { + this.#traitRenderTickers.splice(this.#traitRenderTickers.indexOf(instance.renderTick), 1); + } + const acceptPacketIndex = this.#traitsAcceptingPackets.indexOf(instance); + if (-1 !== acceptPacketIndex) { + this.#traitsAcceptingPackets.splice(acceptPacketIndex, 1); + } + // Unloop. + instance.entity = undefined; } - // Destroy instance. - const instance = this.#traits[type]; - instance.destroy(); - // Remove methods, hooks, and properties. - const methods = instance.methods(); - const keys = Object.keys(methods); - for (let i = 0; i < keys.length; i++) { - delete this[keys[i]]; - } - const hooks = Object.keys(instance.hooks()); - for (let i = 0; i < hooks.length; i++) { - const hook = hooks[i]; - const implementation = this.#hooks[hook].find(({type: hookType}) => hookType === type); - this.#hooks[hook].splice(this.#hooks[hook].indexOf(implementation), 1); - } - const {[type]: Trait} = latus.get('%traits'); - const properties = enumerateTraitAccessorKeys(Trait.prototype); - for (let i = 0; i < properties.length; ++i) { - const property = properties[i]; - delete this[property]; - } - // Remove all event listeners. - const listeners = Object.entries(instance.memoizedListeners()); - for (let i = 0; i < listeners.length; i++) { - const [event, listener] = listeners[i]; - this.off(event, listener); - } - instance._memoizedListeners = {}; - // Remove instance. - delete this.#traits[type]; - this.#traitsFlat.splice(this.#traitsFlat.indexOf(instance), 1); - if ('tick' in instance) { - this.#traitTickers.splice(this.#traitTickers.indexOf(instance.tick), 1); - } - if ('renderTick' in instance) { - this.#traitRenderTickers.splice(this.#traitRenderTickers.indexOf(instance.renderTick), 1); - } - const acceptPacketIndex = this.#traitsAcceptingPackets.indexOf(instance); - if (-1 !== acceptPacketIndex) { - this.#traitsAcceptingPackets.splice(acceptPacketIndex, 1); - } - // Unloop. - instance.entity = undefined; - } - removeTraits(types) { - types.forEach((type) => this.removeTrait(type)); - } - - s13nId() { - return this.instanceUuid; - } - - tick(elapsed) { - this.elapsed = elapsed; - this.uptime += elapsed; - for (let i = 0; i < this.#traitTickers.length; i++) { - this.#traitTickers[i](elapsed); + removeTraits(types) { + types.forEach((type) => this.removeTrait(type)); } - for (let i = 0; i < this.#tickingPromisesTickers.length; i++) { - this.#tickingPromisesTickers[i](elapsed); + + get s13nId() { + return this.instanceUuid; } - } - toJSON() { - const json = {}; - const traits = Object.entries(this.#traits); - for (let i = 0; i < traits.length; i++) { - const [type, trait] = traits[i]; - json[type] = trait.toJSON(); + tick(elapsed) { + this.elapsed = elapsed; + this.uptime += elapsed; + for (let i = 0; i < this.#traitTickers.length; i++) { + this.#traitTickers[i](elapsed); + } + for (let i = 0; i < this.#tickingPromisesTickers.length; i++) { + this.#tickingPromisesTickers[i](elapsed); + } } - return { - traits: json, - }; - } - toNetwork(informed) { - const pristine = {traits: {}}; - const json = { - traits: {}, - }; - const traits = Object.entries(this.#traits); - for (let i = 0; i < traits.length; i++) { - const [type, trait] = traits[i]; - pristine.traits[type] = trait.constructor.withDefaults( - this.#originalJson ? this.#originalJson.traits[type] : {}, - ); - json.traits[type] = trait.toNetwork(informed); + toJSON() { + const json = {}; + const traits = Object.entries(this.#traits); + for (let i = 0; i < traits.length; i++) { + const [type, trait] = traits[i]; + json[type] = trait.toJSON(); + } + return { + traits: json, + }; } - const merged = mergeDiff(pristine, json) || {}; - return { - instanceUuid: this.instanceUuid, - ...(this.uri ? {extends: this.uri} : {}), - ...merged, - }; - } - trait(type) { - return this.#traits[type]; - } + toNetwork(informed) { + const pristine = {traits: {}}; + const json = { + traits: {}, + }; + const traits = Object.entries(this.#traits); + for (let i = 0; i < traits.length; i++) { + const [type, trait] = traits[i]; + pristine.traits[type] = trait.constructor.withDefaults( + this.#originalJson ? this.#originalJson.traits[type] : {}, + ); + json.traits[type] = trait.toNetwork(informed); + } + const merged = mergeDiff(pristine, json) || {}; + return { + instanceUuid: this.instanceUuid, + ...(this.uri ? {extends: this.uri} : {}), + ...merged, + }; + } - get traits() { - return this.#traits; - } + trait(type) { + return this.#traits[type]; + } - traitTypes() { - return Object.keys(this.#traits); - } + get traits() { + return this.#traits; + } - static withDefaults(json = {}) { - const Traits = latus.get('%traits'); - return { - ...json, - traits: Object.entries(json.traits) - .reduce((r, [type, traitJson]) => ({ - ...r, - [type]: Traits[type] - ? Traits[type].withDefaults(traitJson) - : traitJson, - }), {}), - }; - } + traitTypes() { + return Object.keys(this.#traits); + } - static withoutDefaults(json) { - const Traits = latus.get('%traits'); - const without = { - ...json, - traits: Object.entries(json.traits) - .reduce((r, [type, traitJson]) => ({ - ...r, - [type]: Traits[type] - ? Traits[type].withoutDefaults(traitJson) - : traitJson, - }), {}), - }; - return without; - } + static withDefaults(json = {}) { + const Traits = latus.get('%traits'); + return { + ...json, + traits: Object.entries(json.traits) + .reduce((r, [type, traitJson]) => ({ + ...r, + [type]: Traits[type] + ? Traits[type].withDefaults(traitJson) + : traitJson, + }), {}), + }; + } + static withoutDefaults(json) { + const Traits = latus.get('%traits'); + const without = { + ...json, + traits: Object.entries(json.traits) + .reduce((r, [type, traitJson]) => ({ + ...r, + [type]: Traits[type] + ? Traits[type].withoutDefaults(traitJson) + : traitJson, + }), {}), + }; + return without; + } + + }; }; diff --git a/packages/entity/src/traits/alive.js b/packages/entity/src/traits/alive.js index ad9fbef..5ffb9b1 100644 --- a/packages/entity/src/traits/alive.js +++ b/packages/entity/src/traits/alive.js @@ -112,6 +112,7 @@ export default (latus) => class Alive extends decorate(Trait) { } destroy() { + super.destroy(); this.#context.destroy(); } @@ -193,7 +194,7 @@ export default (latus) => class Alive extends decorate(Trait) { }; } - packets() { + packetsFor() { const packets = this.#packets.concat(); const {life, maxLife} = this.stateDifferences(); if (life || maxLife) { diff --git a/packages/entity/src/traits/directional.js b/packages/entity/src/traits/directional.js index 995aee3..72c6869 100644 --- a/packages/entity/src/traits/directional.js +++ b/packages/entity/src/traits/directional.js @@ -90,7 +90,7 @@ export default () => class Directional extends decorate(Trait) { }; } - packets() { + packetsFor() { const {direction} = this.stateDifferences(); if (direction) { return [[ diff --git a/packages/entity/src/traits/dom-node.js b/packages/entity/src/traits/dom-node.js index 5af3e0c..d112dd2 100644 --- a/packages/entity/src/traits/dom-node.js +++ b/packages/entity/src/traits/dom-node.js @@ -37,6 +37,7 @@ export default () => class DomNode extends decorate(Trait) { } destroy() { + super.destroy(); super.parentNode?.removeChild(this.entity.node); } diff --git a/packages/entity/src/traits/mobile.js b/packages/entity/src/traits/mobile.js index 92f1060..8a4f0fc 100644 --- a/packages/entity/src/traits/mobile.js +++ b/packages/entity/src/traits/mobile.js @@ -224,7 +224,7 @@ export default () => class Mobile extends decorate(Trait) { }; } - packets() { + packetsFor() { const {isMobile, speed} = this.stateDifferences(); if (isMobile || speed) { return [[ diff --git a/packages/entity/src/traits/positioned.js b/packages/entity/src/traits/positioned.js index e7cbdbd..acd3523 100644 --- a/packages/entity/src/traits/positioned.js +++ b/packages/entity/src/traits/positioned.js @@ -75,6 +75,7 @@ export default () => class Positioned extends decorate(Trait) { } destroy() { + super.destroy(); this.off('trackedPositionChanged', this.ontrackedPositionChanged); if ('client' === process.env.SIDE) { this.off('serverPositionChanged', this.onServerPositionChanged); @@ -107,7 +108,7 @@ export default () => class Positioned extends decorate(Trait) { this.serverPositionDirty = true; } - packets() { + packetsFor() { const {x, y} = this.stateDifferences(); if (x || y) { return [[ diff --git a/packages/entity/src/traits/spawner.js b/packages/entity/src/traits/spawner.js index ba2b807..212df5a 100644 --- a/packages/entity/src/traits/spawner.js +++ b/packages/entity/src/traits/spawner.js @@ -143,6 +143,7 @@ export default (latus) => class Spawner extends decorate(Trait) { } destroy() { + super.destroy(); while (this.#children.length > 0) { const child = this.#children.pop(); if (child) { diff --git a/packages/entity/test/alive.js b/packages/entity/test/alive.js index 32dc2d2..d3f7685 100644 --- a/packages/entity/test/alive.js +++ b/packages/entity/test/alive.js @@ -112,7 +112,7 @@ describe('Alive', () => { it('generates and accepts life packets', async () => { entity.life = 80; entity.maxLife = 90; - const packets = entity.trait('Alive').packets(); + const packets = entity.trait('Alive').packetsFor(); expect(packets).to.have.lengthOf(1); expect(packets[0][0]).to.equal('TraitUpdateAlive'); expect(packets[0][1]).to.deep.equal({life: 80, maxLife: 90}); @@ -124,7 +124,7 @@ describe('Alive', () => { it('generates and accepts death packets', async () => { entity.life = 0; entity.tick(); - const packets = entity.trait('Alive').packets(); + const packets = entity.trait('Alive').packetsFor(); expect(packets).to.have.lengthOf(2); expect(packets[0][0]).to.equal('Died'); expect(packets[1][0]).to.equal('TraitUpdateAlive'); diff --git a/packages/entity/test/directional.js b/packages/entity/test/directional.js index 230e4fa..e759d1e 100644 --- a/packages/entity/test/directional.js +++ b/packages/entity/test/directional.js @@ -42,7 +42,7 @@ describe('Directional', () => { }); it('generates and accepts direction packets', async () => { entity.direction = 2; - const packets = entity.trait('Directional').packets(); + const packets = entity.trait('Directional').packetsFor(); expect(packets).to.have.lengthOf(1); expect(packets[0][0]).to.equal('TraitUpdateDirectionalDirection'); expect(packets[0][1]).to.equal(2); diff --git a/packages/entity/test/positioned.js b/packages/entity/test/positioned.js index 6f68069..068fd1d 100644 --- a/packages/entity/test/positioned.js +++ b/packages/entity/test/positioned.js @@ -30,7 +30,7 @@ describe('Positioned', () => { if ('client' !== process.env.SIDE) { it('generates and accepts movement packets', async () => { entity.setPosition([1, 1]); - const packets = entity.trait('Positioned').packets(); + const packets = entity.trait('Positioned').packetsFor(); expect(packets).to.have.lengthOf(1); expect(packets[0][0]).to.equal('TraitUpdatePositionedPosition'); expect(packets[0][1]).to.deep.equal([1, 1]); diff --git a/packages/graphics/src/traits/pictured.js b/packages/graphics/src/traits/pictured.js index 12e0419..4ab4a35 100644 --- a/packages/graphics/src/traits/pictured.js +++ b/packages/graphics/src/traits/pictured.js @@ -62,6 +62,7 @@ export default (latus) => class Pictured extends decorate(Trait) { } destroy() { + super.destroy(); if (this.#sprites) { const sprites = Object.entries(this.#sprites); for (let i = 0; i < sprites.length; i++) { diff --git a/packages/graphics/src/traits/rastered.js b/packages/graphics/src/traits/rastered.js index c2b6355..1766341 100644 --- a/packages/graphics/src/traits/rastered.js +++ b/packages/graphics/src/traits/rastered.js @@ -35,6 +35,7 @@ export default () => class Rastered extends Trait { } destroy() { + super.destroy(); if (this.#container) { this.#container.destroy(); } diff --git a/packages/graphics/src/traits/visible.js b/packages/graphics/src/traits/visible.js index 378b17a..9778c2b 100644 --- a/packages/graphics/src/traits/visible.js +++ b/packages/graphics/src/traits/visible.js @@ -98,6 +98,7 @@ export default () => class Visible extends decorate(Trait) { } destroy() { + super.destroy(); this.removeFromQuadTree(this.entity.list); } @@ -167,7 +168,7 @@ export default () => class Visible extends decorate(Trait) { }; } - packets() { + packetsFor() { const {isVisible, opacity, rotation} = this.stateDifferences(); if (isVisible || opacity || rotation) { return [[ diff --git a/packages/input/src/traits/controllable.js b/packages/input/src/traits/controllable.js index 3761239..98de09f 100644 --- a/packages/input/src/traits/controllable.js +++ b/packages/input/src/traits/controllable.js @@ -26,6 +26,7 @@ export default () => class Controllable extends Trait { } destroy() { + super.destroy(); this.#actionRegistry.stopListening(); } diff --git a/packages/input/src/traits/interactive.js b/packages/input/src/traits/interactive.js index 81d802a..9d7cd01 100644 --- a/packages/input/src/traits/interactive.js +++ b/packages/input/src/traits/interactive.js @@ -55,7 +55,7 @@ export default (latus) => class Interactive extends decorate(Trait) { }; } - packets() { + packetsFor() { const {isInteractive} = this.stateDifferences(); if (isInteractive) { return [[ diff --git a/packages/physics/src/traits/collider.js b/packages/physics/src/traits/collider.js index 2a83630..27e2f9e 100644 --- a/packages/physics/src/traits/collider.js +++ b/packages/physics/src/traits/collider.js @@ -126,6 +126,7 @@ export default (latus) => class Collider extends decorate(Trait) { } destroy() { + super.destroy(); this.releaseAllCollisions(); } diff --git a/packages/physics/src/traits/emitter.js b/packages/physics/src/traits/emitter.js index 995516c..dabe25a 100644 --- a/packages/physics/src/traits/emitter.js +++ b/packages/physics/src/traits/emitter.js @@ -322,7 +322,7 @@ export default (latus) => class Emitter extends decorate(Trait) { /* eslint-enable no-param-reassign */ } - packets() { + packetsFor() { return this.#emitting.length > 0 ? [['EmitParticles', this.#emitting]] : []; diff --git a/packages/physics/src/traits/physical.js b/packages/physics/src/traits/physical.js index cfbbba4..fbca25a 100644 --- a/packages/physics/src/traits/physical.js +++ b/packages/physics/src/traits/physical.js @@ -93,6 +93,7 @@ export default () => class Physical extends decorate(Trait) { } destroy() { + super.destroy(); this.world = undefined; } diff --git a/packages/physics/src/traits/shaped.js b/packages/physics/src/traits/shaped.js index fd6f331..399176c 100644 --- a/packages/physics/src/traits/shaped.js +++ b/packages/physics/src/traits/shaped.js @@ -34,6 +34,7 @@ export default () => class Shaped extends decorate(Trait) { } destroy() { + super.destroy(); this.#shape.destroy(); if (this.#shapeView) { this.#shapeView.destroy(); diff --git a/packages/s13n/src/index.js b/packages/s13n/src/index.js index 80809bb..0b306a4 100644 --- a/packages/s13n/src/index.js +++ b/packages/s13n/src/index.js @@ -4,19 +4,14 @@ import { SynchronizedUpdatePacket, } from './packets'; -export * from './packets'; - -export {default as ReceiverSynchronizer} from './receiver-synchronizer'; -export {default as SenderSynchronizer} from './sender-synchronizer'; -export {default as Serializer} from './serializer'; -export {default as Synchronized, synchronized} from './synchronized'; +export {default as Synchronized} from './synchronized'; export default { hooks: { - '@latus/socket/packets': () => ({ + '@latus/socket/packets': (latus) => ({ SynchronizedCreate: SynchronizedCreatePacket, SynchronizedDestroy: SynchronizedDestroyPacket, - SynchronizedUpdate: SynchronizedUpdatePacket, + SynchronizedUpdate: SynchronizedUpdatePacket(latus), }), }, }; diff --git a/packages/s13n/src/packets/synchronized-update.js b/packages/s13n/src/packets/synchronized-update.js index 3a10255..a41daff 100644 --- a/packages/s13n/src/packets/synchronized-update.js +++ b/packages/s13n/src/packets/synchronized-update.js @@ -1,9 +1,29 @@ import SynchronizedPacket from './synchronized'; -export default class SynchronizedUpdatePacket extends SynchronizedPacket { +export default (latus) => class SynchronizedUpdatePacket extends SynchronizedPacket { + + static pack(data) { + const {Bundle} = latus.get('%packets'); + // eslint-disable-next-line no-param-reassign + data.packets = Bundle.encode(data.packets); + return data; + } + + static get s13nSchema() { + return { + packets: 'buffer', + }; + } static get s13nType() { return 'update'; } -} + static unpack(data) { + const {Bundle} = latus.get('%packets'); + // eslint-disable-next-line no-param-reassign + data.packets = Bundle.decode(data.packets); + return data; + } + +}; diff --git a/packages/s13n/src/packets/synchronized.js b/packages/s13n/src/packets/synchronized.js index f2268d9..2b5ad99 100644 --- a/packages/s13n/src/packets/synchronized.js +++ b/packages/s13n/src/packets/synchronized.js @@ -16,6 +16,10 @@ export default class SynchronizedPacket extends Packet { return {}; } + get s13nType() { + return this.constructor.s13nType; + } + static get s13nType() { return 'none'; } diff --git a/packages/s13n/src/receiver-synchronizer.js b/packages/s13n/src/receiver-synchronizer.js deleted file mode 100644 index 6173a9c..0000000 --- a/packages/s13n/src/receiver-synchronizer.js +++ /dev/null @@ -1,95 +0,0 @@ -import {Class, compose, EventEmitter} from '@latus/core'; - -import Serializer from './serializer'; - -const decorate = compose( - EventEmitter, -); - -export default class ReceiverSynchronizer extends decorate(Class) { - - #serializer = new Serializer(); - - #synchronized = {}; - - constructor(latus) { - super(); - this.latus = latus; - } - - async acceptPacket(packet) { - const {constructor: {s13nType}} = packet; - if (!s13nType) { - return; - } - const {id, type} = packet.data.synchronized; - switch (s13nType) { - case 'create': { - this.createSynchronized(type, id, packet.data.spec); - break; - } - case 'destroy': { - this.deleteSynchronized(type, id); - break; - } - case 'update': { - this.#serializer.later(`${type}:${id}`, (resource) => { - resource.acceptPacket(packet); - }); - break; - } - default: - } - } - - addSynchronized(synchronized) { - const {resourceId: type} = synchronized.constructor; - if (!(type in this.#synchronized)) { - this.#synchronized[type] = {}; - } - const id = synchronized.s13nId(); - this.#synchronized[type][id] = synchronized; - } - - async createSynchronized(type, id, json) { - const {[type]: Resource} = this.latus.get('%resources'); - if (!(type in this.#synchronized)) { - this.#synchronized[type] = {}; - } - if (this.#synchronized[type][id]) { - await this.#synchronized[type][id].load(json); - } - else { - this.#serializer.create(`${type}:${id}`, Resource.load(json)); - await this.#serializer.later(`${type}:${id}`, (resource) => { - this.#synchronized[type][id] = resource; - }); - } - this.emit('created', type, this.#synchronized[type][id], id); - } - - async deleteSynchronized(type, id) { - if (!this.hasSynchronized(type, id)) { - return; - } - this.#serializer.cancelIfPending(`${type}:${id}`); - const resource = await this.#synchronized[type][id]; - if (resource) { - await resource.destroy(); - delete this.#synchronized[type][id]; - } - } - - hasSynchronized(type, id) { - return !!this.#synchronized[type][id]; - } - - synchronized(type, id) { - return this.synchronizedOfType(type)[id]; - } - - synchronizedOfType(type) { - return this.#synchronized[type] || {}; - } - -} diff --git a/packages/s13n/src/sender-synchronizer.js b/packages/s13n/src/sender-synchronizer.js deleted file mode 100644 index f6a37d3..0000000 --- a/packages/s13n/src/sender-synchronizer.js +++ /dev/null @@ -1,96 +0,0 @@ -export default class SenderSynchronizer { - - #added = []; - - #nextSyncPackets = []; - - #queuedPackets = []; - - #removed = []; - - #synchronized = {}; - - #synchronizedFlat = []; - - constructor(latus) { - this.latus = latus; - } - - addSynchronized(synchronized) { - if (this.hasSynchronized(synchronized)) { - return; - } - this.#added.push(synchronized); - const {resourceId: type} = synchronized.constructor; - if (!(type in this.#synchronized)) { - this.#synchronized[type] = {}; - } - this.#synchronizedFlat.push(synchronized); - const id = synchronized.s13nId(); - this.#synchronized[type][id] = synchronized; - } - - destroy() { - this.#queuedPackets = []; - this.#synchronized = {}; - } - - hasSynchronized(synchronized) { - return -1 !== this.#synchronizedFlat.indexOf(synchronized); - } - - packetsFor(informed) { - const payload = []; - for (let i = 0; i < this.#synchronizedFlat.length; i++) { - const synchronized = this.#synchronizedFlat[i]; - if (-1 !== this.#added.indexOf(synchronized)) { - payload.push(synchronized.createPacket(informed)); - } - else if (-1 !== this.#removed.indexOf(synchronized)) { - payload.push(synchronized.destroyPacket(informed)); - } - else { - const packets = synchronized.packetsFor(this.latus, informed); - for (let j = 0; j < packets.length; j++) { - payload.push(packets[j]); - } - } - } - this.#added = []; - this.#removed = []; - return payload; - } - - removeSynchronized(synchronized) { - if (!this.hasSynchronized(synchronized)) { - return; - } - this.#removed.push(synchronized); - const index = this.#synchronizedFlat.indexOf(synchronized); - this.#synchronizedFlat.splice(index, 1); - const {resourceId: type} = synchronized.constructor; - const id = synchronized.s13nId(); - delete this.#synchronized[type][id]; - } - - async send(socket, informed) { - const synchronizerPackets = this.packetsFor(informed); - for (let i = 0; i < synchronizerPackets.length; i++) { - this.#nextSyncPackets.push(synchronizerPackets[i]); - } - for (let i = 0; i < this.#queuedPackets.length; i++) { - this.#nextSyncPackets.push(this.#queuedPackets[i]); - } - this.#queuedPackets = []; - if (socket && this.#nextSyncPackets.length > 0) { - const nextSyncPackets = this.#nextSyncPackets; - this.#nextSyncPackets = []; - await socket.send(['Bundle', nextSyncPackets]); - } - } - - queuePacket(packet) { - this.#queuedPackets.push(packet); - } - -} diff --git a/packages/s13n/src/serializer.js b/packages/s13n/src/serializer.js index 0508ac0..4f422af 100644 --- a/packages/s13n/src/serializer.js +++ b/packages/s13n/src/serializer.js @@ -7,13 +7,6 @@ export default class Serializer { #promises = new Map(); - cancelIfPending(id) { - const pending = this.#pending.get(id); - if (pending) { - pending.cancel(); - } - } - create(id, creator) { const promise = creator.then(async (resource) => { if (!this.#pending.has(id)) { @@ -27,13 +20,18 @@ export default class Serializer { throw error; } }); - this.#pending.set(id, { - cancel: () => this.#pending.delete(id), - }); + this.#pending.set(id, () => this.#pending.delete(id)); this.#promises.set(id, promise); return promise; } + destroy(id) { + // Cancel... + this.#pending.get(id)?.(); + this.#pending.delete(id); + this.#promises.delete(id); + } + later(id, fn) { const promise = this.#promises.get(id); if (!promise) { diff --git a/packages/s13n/src/synchronized.js b/packages/s13n/src/synchronized.js deleted file mode 100644 index c931cbb..0000000 --- a/packages/s13n/src/synchronized.js +++ /dev/null @@ -1,92 +0,0 @@ -export const synchronized = ({config}) => config['%synchronized']; - -export default function SynchronizedMixin(Superclass) { - - return class Synchronized extends Superclass { - - constructor(...args) { - super(...args); - this._idempotentPackets = []; - } - - cleanPackets() { - this._idempotentPackets = []; - } - - createPacket(informed) { - const id = this.s13nId(); - return [ - 'SynchronizedCreate', - { - synchronized: { - id, - type: this.constructor.resourceId, - }, - spec: this.toNetwork(informed), - }, - ]; - } - - // eslint-disable-next-line class-methods-use-this - destroy() {} - - destroyPacket() { - const id = this.s13nId(); - return [ - 'SynchronizedDestroy', - { - synchronized: { - id, - type: this.constructor.resourceId, - }, - }, - ]; - } - - fromNetwork() { - throw new ReferenceError( - `${this.constructor.resourceType || this.constructor.name}::fromNetwork is undefined`, - ); - } - - // eslint-disable-next-line class-methods-use-this - packets() { - return []; - } - - // eslint-disable-next-line class-methods-use-this - packetsAreIdempotent() { - return true; - } - - packetsFor(latus, informed) { - if (this._idempotentPackets.length > 0) { - return this._idempotentPackets; - } - const packets = this.packets(informed); - // Embed synchronization info. - const id = this.s13nId(); - for (let i = 0; i < packets.length; i++) { - packets[i][1].synchronized = { - id, - type: this.constructor.resourceId, - }; - } - if (this.packetsAreIdempotent()) { - this._idempotentPackets = packets; - } - return packets; - } - - // eslint-disable-next-line class-methods-use-this - s13nId() { - return 0; - } - - toNetwork() { - return this.toJSON(); - } - - }; - -} diff --git a/packages/s13n/src/synchronized/client.js b/packages/s13n/src/synchronized/client.js new file mode 100644 index 0000000..35bf273 --- /dev/null +++ b/packages/s13n/src/synchronized/client.js @@ -0,0 +1,107 @@ +import Serializer from '../serializer'; + +export default (latus) => (Resource) => ( + class SynchronizedResource extends Resource { + + constructor() { + super(); + this._serializer = new Serializer(); + this._synchronized = {}; + } + + async acceptPacket(packet) { + const {s13nType} = packet; + // eslint-disable-next-line max-len + if (!s13nType) { + return; + } + const {id, type} = packet.data.synchronized; + switch (s13nType) { + case 'create': + await this.createSynchronized(type, id, packet.data.spec); + break; + case 'destroy': + await this.destroySynchronized(type, id); + break; + case 'update': + if (this._synchronized[type]?.[id]) { + const promises = []; + for (let i = 0; i < packet.data.packets.length; i++) { + promises.push(this._synchronized[type][id].acceptPacket(packet.data.packets[i])); + } + await Promise.all(promises); + } + else { + await this._serializer.later( + `${type}:${id}`, + (resource) => resource.acceptPacket(packet), + ); + } + break; + default: + } + } + + async createSynchronized(type, id, json) { + const {[type]: Resource} = latus.get('%resources'); + if (!(type in this._synchronized)) { + this._synchronized[type] = {}; + } + if (this._synchronized[type][id]) { + await this._synchronized[type][id].load(json); + } + else { + this._serializer.create(`${type}:${id}`, Resource.load(json)); + await this._serializer.later(`${type}:${id}`, (resource) => { + this._synchronized[type][id] = resource; + }); + } + } + + // eslint-disable-next-line class-methods-use-this + destroy() {} + + async destroySynchronized(type, id) { + if (!this._synchronized[type]?.[id]) { + return; + } + this._serializer.destroy(`${type}:${id}`); + const resource = await this._synchronized[type][id]; + if (resource) { + await resource.destroy(); + delete this._synchronized[type][id]; + } + } + + // eslint-disable-next-line class-methods-use-this + get s13nId() { + return 0; + } + + startSynchronizing(resource) { + const id = resource.s13nId; + const type = resource.constructor.resourceId; + if (this._synchronized[type]?.[id]) { + return; + } + if (!(type in this._synchronized)) { + this._synchronized[type] = {}; + } + this._synchronized[type][id] = resource; + } + + stopSynchronizing(resource) { + const id = resource.s13nId; + const type = resource.constructor.resourceId; + if (!this._synchronized[type]?.[id]) { + return; + } + delete this._synchronized[type][id]; + } + + synchronized(type, id) { + return this._synchronized[type]?.[id]; + } + + } +); diff --git a/packages/s13n/src/synchronized/index.js b/packages/s13n/src/synchronized/index.js new file mode 100644 index 0000000..60a8edd --- /dev/null +++ b/packages/s13n/src/synchronized/index.js @@ -0,0 +1,4 @@ +import Client from './client'; +import Server from './server'; + +export default process.env.SIDE === 'client' ? Client : Server; diff --git a/packages/s13n/src/synchronized/server.js b/packages/s13n/src/synchronized/server.js new file mode 100644 index 0000000..7a239b9 --- /dev/null +++ b/packages/s13n/src/synchronized/server.js @@ -0,0 +1,123 @@ +export default () => (Resource) => ( + class SynchronizedResource extends Resource { + + constructor() { + super(); + this._added = []; + this._removed = []; + this._synchronized = []; + } + + cleanPackets() { + this._added = []; + this._removed = []; + for (let i = 0; i < this._synchronized.length; i++) { + this._synchronized[i].cleanPackets(); + } + } + + createPacket(informed) { + return [ + 'SynchronizedCreate', + { + synchronized: { + id: this.s13nId, + type: this.constructor.resourceId, + }, + spec: this.toNetwork(informed), + }, + ]; + } + + destroy() { + super.destroy(); + this._added = []; + this._removed = []; + this._synchronized = []; + } + + destroyPacket() { + return [ + 'SynchronizedDestroy', + { + synchronized: { + id: this.s13nId, + type: this.constructor.resourceId, + }, + }, + ]; + } + + isSynchronizing(resource) { + return -1 !== this._synchronized.indexOf(resource); + } + + packetsFor(informed) { + const packets = []; + for (let i = 0; i < this._synchronized.length; i++) { + const synchronized = this._synchronized[i]; + if (-1 !== this._added.indexOf(synchronized)) { + packets.push(synchronized.createPacket(informed)); + } + else if (-1 !== this._removed.indexOf(synchronized)) { + packets.push(synchronized.destroyPacket(informed)); + } + else { + const updatePacket = synchronized.updatePacket(informed); + if (updatePacket) { + packets.push(updatePacket); + } + } + } + return packets; + } + + // eslint-disable-next-line class-methods-use-this + get s13nChildren() { + return []; + } + + // eslint-disable-next-line class-methods-use-this + get s13nId() { + return 0; + } + + startSynchronizing(synchronized) { + if (this.isSynchronizing(synchronized)) { + return; + } + this._added.push(synchronized); + this._synchronized.push(synchronized); + } + + stopSynchronizing(synchronized) { + if (!this.isSynchronizing(synchronized)) { + return; + } + this._removed.push(synchronized); + this._synchronized.splice(this._synchronized.indexOf(synchronized), 1); + } + + toNetwork() { + return this.toJSON(); + } + + updatePacket(informed) { + const packets = this.packetsFor(informed); + if (0 === packets.length) { + return undefined; + } + return [ + 'SynchronizedUpdate', + { + packets, + synchronized: { + id: this.s13nId, + type: this.constructor.resourceId, + }, + }, + ]; + } + + } +); diff --git a/packages/sound/src/traits/audible.js b/packages/sound/src/traits/audible.js index 188fad5..b062c73 100644 --- a/packages/sound/src/traits/audible.js +++ b/packages/sound/src/traits/audible.js @@ -68,6 +68,7 @@ export default (latus) => class Audible extends Trait { } destroy() { + super.destroy(); Object.values(this.#sounds).forEach((sound) => { Promise.resolve(sound).then((sound) => { sound.destroy(); @@ -122,7 +123,7 @@ export default (latus) => class Audible extends Trait { })); } - packets() { + packetsFor() { return Object.keys(this.#playing).map((key) => ['PlaySound', {sound: key}]); } diff --git a/packages/timing/src/traits/animated.js b/packages/timing/src/traits/animated.js index 3338a19..5588870 100644 --- a/packages/timing/src/traits/animated.js +++ b/packages/timing/src/traits/animated.js @@ -89,6 +89,7 @@ export default (latus) => class Animated extends decorate(Trait) { } destroy() { + super.destroy(); if (this.#animationViews) { const animationViews = Object.entries(this.#animationViews); for (let i = 0; i < animationViews.length; i++) { @@ -246,7 +247,7 @@ export default (latus) => class Animated extends decorate(Trait) { return this.params.animations[key].offset; } - packets() { + packetsFor() { const {currentAnimation, isAnimating} = this.stateDifferences(); if (this.#forceUpdate || currentAnimation || isAnimating) { this.#forceUpdate = false; diff --git a/packages/topdown/src/packets/layers-update-layer.js b/packages/topdown/src/packets/layers-update-layer.js deleted file mode 100644 index 89b140d..0000000 --- a/packages/topdown/src/packets/layers-update-layer.js +++ /dev/null @@ -1,32 +0,0 @@ -import {Packet} from '@latus/socket'; - -export default (latus) => class LayersUpdateLayerPacket extends Packet { - - static pack(data) { - const {Bundle} = latus.get('%packets'); - for (let i = 0; i < data.length; i++) { - // eslint-disable-next-line no-param-reassign - data[i].layerPackets = Bundle.encode(data[i].layerPackets); - } - return data; - } - - static get data() { - return [ - { - layerIndex: 'uint8', - layerPackets: 'buffer', - }, - ]; - } - - static unpack(data) { - const {Bundle} = latus.get('%packets'); - for (let i = 0; i < data.length; i++) { - // eslint-disable-next-line no-param-reassign - data[i].layerPackets = Bundle.decode(data[i].layerPackets); - } - return data; - } - -}; diff --git a/packages/topdown/src/packets/room-update-layers.js b/packages/topdown/src/packets/room-update-layers.js deleted file mode 100644 index 621cb48..0000000 --- a/packages/topdown/src/packets/room-update-layers.js +++ /dev/null @@ -1,25 +0,0 @@ -import {SynchronizedUpdatePacket} from '@avocado/s13n'; - -export default (latus) => class RoomUpdateLayers extends SynchronizedUpdatePacket { - - static pack(data) { - const {Bundle} = latus.get('%packets'); - // eslint-disable-next-line no-param-reassign - data.layersPackets = Bundle.encode(data.layersPackets); - return data; - } - - static get s13nSchema() { - return { - layersPackets: 'buffer', - }; - } - - static unpack(data) { - const {Bundle} = latus.get('%packets'); - // eslint-disable-next-line no-param-reassign - data.layersPackets = Bundle.decode(data.layersPackets); - return data; - } - -}; diff --git a/packages/topdown/src/resources/layer.js b/packages/topdown/src/resources/layer.js index 661c374..7324ba1 100644 --- a/packages/topdown/src/resources/layer.js +++ b/packages/topdown/src/resources/layer.js @@ -1,230 +1,215 @@ import {Property} from '@avocado/core'; import {JsonResource} from '@avocado/resource'; +import {Synchronized} from '@avocado/s13n'; import {compose, EventEmitter} from '@latus/core'; -const decorate = compose( - EventEmitter, - Property('tileset', { - track: true, - }), -); +export default (latus) => { + const decorate = compose( + EventEmitter, + Property('tileset', { + track: true, + }), + Synchronized(latus), + ); + return class Layer extends decorate(JsonResource) { -export default (latus) => class Layer extends decorate(JsonResource) { + #s13nId; - constructor() { - super(); - this.tileEntities = {}; - this.tileGeometry = []; - const {EntityList, Tiles} = latus.get('%resources'); - this.setEntityList(new EntityList()); - this.setTiles(new Tiles()); - } - - acceptPacket(packet) { - const {constructor: {s13nType}} = packet; - switch (s13nType) { - case 'create': - case 'destroy': - this.entityList.acceptPacket(packet); - break; - default: + constructor() { + super(); + this.tileEntities = {}; + this.tileGeometry = []; + const {EntityList, Tiles} = latus.get('%resources'); + this.setEntityList(new EntityList()); + this.setTiles(new Tiles()); } - switch (packet.constructor.type) { - case 'EntityListUpdateEntity': - this.entityList.acceptPacket(packet); - break; - case 'TilesUpdate': - this.tiles.acceptPacket(packet); - break; - default: + + addEntity(entity) { + this.entityList.addEntity(entity); } - } - addEntity(entity) { - this.entityList.addEntity(entity); - } - - addTileEntity(entity, index) { - if (!this.tileEntities[index]) { - this.tileEntities[index] = []; + addTileEntity(entity, index) { + if (!this.tileEntities[index]) { + this.tileEntities[index] = []; + } + if (-1 !== this.tileEntities[index].indexOf(entity)) { + return; + } + this.tileEntities[index].push(entity); } - if (-1 !== this.tileEntities[index].indexOf(entity)) { - return; + + cleanPackets() { + super.cleanPackets(); + this.entityList.cleanPackets(); + this.tiles.cleanPackets(); } - this.tileEntities[index].push(entity); - } - cleanPackets() { - this.entityList.cleanPackets(); - this.tiles.cleanPackets(); - } - - destroy() { - this.entityList.destroy(); - this.entityList.off('entityAdded', this.onEntityAddedToLayer); - this.entityList.off('entityRemoved', this.onEntityRemovedFromLayer); - this.tiles.off('dataChanged', this.onTileDataChanged); - if (this.tileset) { - this.tileset.destroy(); - } - } - - get entities() { - return this.entityList.entities; - } - - findEntity(uuid) { - return this.entityList.findEntity(uuid); - } - - hasTileEntityWithUriAt(tilePosition, uri) { - const tileEntities = this.tileEntitiesAt(tilePosition); - if (0 === tileEntities.length) { - return false; - } - const entitiesWithUri = tileEntities.filter((entity) => entity.uri === uri); - return entitiesWithUri.length > 0; - } - - indexAt(position) { - return this.tiles.indexAt(position); - } - - async load(json = {}) { - await super.load(json); - const { - entities, - tiles, - tilesetUri, - world, - } = json; - const {EntityList, Tiles, Tileset} = latus.get('%resources'); - this.setTiles(new Tiles(tiles)); - this.tileset = tilesetUri - ? await Tileset.load({extends: tilesetUri}) - : new Tileset(); - this.setEntityList( - entities - ? await EntityList.load(entities) - : new EntityList(), - ); - this.world = world; - } - - async onEntityAddedToLayer(entity) { - await entity.addTrait('Layered'); - // eslint-disable-next-line no-param-reassign - entity.layer = this; - entity.emit('addedToLayer'); - this.emit('entityAdded', entity); - } - - onEntityRemovedFromLayer(entity) { - // eslint-disable-next-line no-param-reassign - entity.layer = null; - entity.emit('removedFromLayer', this); - entity.removeTrait('Layered'); - this.emit('entityRemoved', entity); - } - - onTileDataChanged() { - this.emit('tileDataChanged'); - } - - packets(informed) { - const packets = []; - const entityListPackets = this.entityList.packets(informed); - for (let i = 0; i < entityListPackets.length; i++) { - packets.push(entityListPackets[i]); - } - const tilesPackets = this.tiles.packets(informed); - for (let i = 0; i < tilesPackets.length; i++) { - packets.push(tilesPackets[i]); - } - return packets; - } - - removeEntity(entity) { - this.entityList.removeEntity(entity); - } - - removeTileEntity(entity, index) { - if (!this.tileEntities[index]) { - return; - } - const entityIndex = this.tileEntities[index].indexOf(entity); - if (-1 === entityIndex) { - return; - } - this.tileEntities[index].splice(entityIndex, 1); - } - - setEntityList(entityList) { - if (this.entityList) { - Object.values(this.entityList.entities).forEach((entity) => { - this.onEntityRemovedFromLayer(entity); - }); - this.entityList.off('entityRemoved', this.onEntityRemovedFromLayer); + destroy() { + this.entityList.destroy(); this.entityList.off('entityAdded', this.onEntityAddedToLayer); - } - this.entityList = entityList; - this.entityList.on('entityAdded', this.onEntityAddedToLayer, this); - this.entityList.on('entityRemoved', this.onEntityRemovedFromLayer, this); - Object.values(this.entityList.entities).forEach((entity) => { - this.onEntityAddedToLayer(entity); - }); - } - - setTileAt(position, tile) { - this.tiles.setTileAt(position, tile); - } - - setTiles(tiles) { - if (this.tiles) { + this.entityList.off('entityRemoved', this.onEntityRemovedFromLayer); this.tiles.off('dataChanged', this.onTileDataChanged); + if (this.tileset) { + this.tileset.destroy(); + } } - this.tiles = tiles; - this.tiles.on('dataChanged', this.onTileDataChanged, this); - } - tick(elapsed) { - this.entityList.tick(elapsed); - } - - tileAt(position) { - return this.tiles.tileAt(position); - } - - tileEntitiesAt(tilePosition) { - const index = this.indexAt(tilePosition); - if (!this.tileEntities[index]) { - return []; + get entities() { + return this.entityList.entities; } - return this.tileEntities[index]; - } - toJSON() { - return { - entities: this.entityList.toJSON(), - tilesetUri: this.tileset.uri, - tiles: this.tiles.toJSON(), - }; - } + findEntity(uuid) { + return this.entityList.findEntity(uuid); + } - toNetwork(informed) { - return { - entities: this.entityList.toNetwork(informed), - tilesetUri: this.tileset.uri, - tiles: this.tiles.toNetwork(informed), - }; - } + hasTileEntityWithUriAt(tilePosition, uri) { + const tileEntities = this.tileEntitiesAt(tilePosition); + if (0 === tileEntities.length) { + return false; + } + const entitiesWithUri = tileEntities.filter((entity) => entity.uri === uri); + return entitiesWithUri.length > 0; + } - visibleEntities(query) { - return this.entityList.visibleEntities(query); - } + indexAt(position) { + return this.tiles.indexAt(position); + } - // visibleEntitiesWithUri(query, uri) { - // return this.entityList.visibleEntitiesWithUri(query, uri); - // } + async load(json = {}) { + await super.load(json); + const { + entities, + tiles, + tilesetUri, + world, + } = json; + const {EntityList, Tiles, Tileset} = latus.get('%resources'); + this.setTiles(new Tiles(tiles)); + this.tileset = tilesetUri + ? await Tileset.load({extends: tilesetUri}) + : new Tileset(); + this.setEntityList( + entities + ? await EntityList.load(entities) + : new EntityList(), + ); + this.world = world; + } + async onEntityAddedToLayer(entity) { + await entity.addTrait('Layered'); + // eslint-disable-next-line no-param-reassign + entity.layer = this; + entity.emit('addedToLayer'); + this.emit('entityAdded', entity); + } + + onEntityRemovedFromLayer(entity) { + // eslint-disable-next-line no-param-reassign + entity.layer = null; + entity.emit('removedFromLayer', this); + entity.removeTrait('Layered'); + this.emit('entityRemoved', entity); + } + + onTileDataChanged() { + this.emit('tileDataChanged'); + } + + removeEntity(entity) { + this.entityList.removeEntity(entity); + } + + removeTileEntity(entity, index) { + if (!this.tileEntities[index]) { + return; + } + const entityIndex = this.tileEntities[index].indexOf(entity); + if (-1 === entityIndex) { + return; + } + this.tileEntities[index].splice(entityIndex, 1); + } + + get s13nId() { + return this.#s13nId; + } + + set s13nId(s13nId) { + this.#s13nId = s13nId; + } + + setEntityList(entityList) { + if (this.entityList) { + this.stopSynchronizing(this.entityList); + Object.values(this.entityList.entities).forEach((entity) => { + this.onEntityRemovedFromLayer(entity); + }); + this.entityList.off('entityRemoved', this.onEntityRemovedFromLayer); + this.entityList.off('entityAdded', this.onEntityAddedToLayer); + } + this.entityList = entityList; + this.entityList.on('entityAdded', this.onEntityAddedToLayer, this); + this.entityList.on('entityRemoved', this.onEntityRemovedFromLayer, this); + Object.values(this.entityList.entities).forEach((entity) => { + this.onEntityAddedToLayer(entity); + }); + this.startSynchronizing(this.entityList); + } + + setTileAt(position, tile) { + this.tiles.setTileAt(position, tile); + } + + setTiles(tiles) { + if (this.tiles) { + this.stopSynchronizing(this.tiles); + this.tiles.off('dataChanged', this.onTileDataChanged); + } + this.tiles = tiles; + this.tiles.on('dataChanged', this.onTileDataChanged, this); + this.startSynchronizing(tiles); + } + + tick(elapsed) { + this.entityList.tick(elapsed); + } + + tileAt(position) { + return this.tiles.tileAt(position); + } + + tileEntitiesAt(tilePosition) { + const index = this.indexAt(tilePosition); + if (!this.tileEntities[index]) { + return []; + } + return this.tileEntities[index]; + } + + toJSON() { + return { + entities: this.entityList.toJSON(), + tilesetUri: this.tileset.uri, + tiles: this.tiles.toJSON(), + }; + } + + toNetwork(informed) { + return { + entities: this.entityList.toNetwork(informed), + tilesetUri: this.tileset.uri, + tiles: this.tiles.toNetwork(informed), + }; + } + + visibleEntities(query) { + return this.entityList.visibleEntities(query); + } + + // visibleEntitiesWithUri(query, uri) { + // return this.entityList.visibleEntitiesWithUri(query, uri); + // } + + }; }; diff --git a/packages/topdown/src/resources/layers.js b/packages/topdown/src/resources/layers.js index 3dee1ab..4722ec4 100644 --- a/packages/topdown/src/resources/layers.js +++ b/packages/topdown/src/resources/layers.js @@ -1,171 +1,147 @@ import {compose, EventEmitter} from '@latus/core'; import {JsonResource} from '@avocado/resource'; +import {Synchronized} from '@avocado/s13n'; -const decorate = compose( - EventEmitter, -); +export default (latus) => { + const decorate = compose( + EventEmitter, + Synchronized(latus), + ); + return class Layers extends decorate(JsonResource) { -export default (latus) => class Layers extends decorate(JsonResource) { + constructor() { + super(); + this.layers = []; + } - constructor() { - super(); - this.layers = []; - } + addEntityToLayer(entity, layerIndex) { + const layer = this.layers[layerIndex]; + if (!layer) { + return; + } + layer.addEntity(entity); + } - acceptPacket(packet) { - if ('LayersUpdateLayer' === packet.constructor.type) { - for (let i = 0; i < packet.data.length; i++) { - const {layerIndex, layerPackets} = packet.data[i]; - for (let j = 0; j < layerPackets.length; j++) { - this.layers[layerIndex].acceptPacket(layerPackets[j]); + addLayer(layer) { + // eslint-disable-next-line no-param-reassign + layer.s13nId = this.layers.length; + this.startSynchronizing(layer); + layer.on('entityAdded', this.onEntityAddedToLayers, this); + layer.on('entityRemoved', this.onEntityRemovedFromLayers, this); + this.layers.push(layer); + this.emit('layerAdded', layer); + } + + cleanPackets() { + super.cleanPackets(); + for (let i = 0; i < this.layers.length; i++) { + this.layers[i].cleanPackets(); + } + } + + destroy() { + for (let i = 0; i < this.layers.length; i++) { + const layer = this.layers[i]; + this.removeLayer(layer); + layer.destroy(); + } + } + + get entities() { + const entities = {}; + for (let i = 0; i < this.layers.length; i++) { + const layerEntities = Object.entries(this.layers[i].entities); + for (let j = 0; j < layerEntities.length; j++) { + const [uuid, entity] = layerEntities[j]; + entities[uuid] = entity; } } + return entities; } - } - addEntityToLayer(entity, layerIndex) { - const layer = this.layers[layerIndex]; - if (!layer) { - return; + findEntity(uuid) { + for (let i = 0; i < this.layers.length; i++) { + const foundEntity = this.layers[i].findEntity(uuid); + if (foundEntity) { + return foundEntity; + } + } + return undefined; } - layer.addEntity(entity); - } - addLayer(layer) { - layer.on('entityAdded', this.onEntityAddedToLayers, this); - layer.on('entityRemoved', this.onEntityRemovedFromLayers, this); - this.layers.push(layer); - this.emit('layerAdded', layer); - } - - cleanPackets() { - for (let i = 0; i < this.layers.length; i++) { - this.layers[i].cleanPackets(); + layer(index) { + return this.layers[index]; } - } - destroy() { - for (let i = 0; i < this.layers.length; i++) { - const layer = this.layers[i]; - this.removeLayer(layer); - layer.destroy(); - } - } - - get entities() { - const entities = {}; - for (let i = 0; i < this.layers.length; i++) { - const layerEntities = Object.entries(this.layers[i].entities); - for (let j = 0; j < layerEntities.length; j++) { - const [uuid, entity] = layerEntities[j]; - entities[uuid] = entity; + async load(json = []) { + await super.load(json); + this.removeAllLayers(); + const {Layer} = latus.get('%resources'); + const layers = await Promise.all(json.map((layer) => Layer.load(layer))); + for (let i = 0; i < layers.length; i++) { + this.addLayer(layers[i]); } } - return entities; - } - findEntity(uuid) { - for (let i = 0; i < this.layers.length; i++) { - const foundEntity = this.layers[i].findEntity(uuid); - if (foundEntity) { - return foundEntity; + onEntityAddedToLayers(entity) { + this.emit('entityAdded', entity); + } + + onEntityRemovedFromLayers(entity) { + this.emit('entityRemoved', entity); + } + + removeAllLayers() { + for (let i = 0; i < this.layers.length; i++) { + this.removeLayer(this.layers[i]); } } - return undefined; - } - layer(index) { - return this.layers[index]; - } - - async load(json = []) { - await super.load(json); - this.removeAllLayers(); - const {Layer} = latus.get('%resources'); - const layers = await Promise.all(json.map((layer) => Layer.load(layer))); - for (let i = 0; i < layers.length; i++) { - this.addLayer(layers[i]); + removeEntityFromLayer(entity, layerIndex) { + const layer = this.layers[layerIndex]; + if (!layer) { + return; + } + layer.removeEntity(entity); } - } - onEntityAddedToLayers(entity) { - this.emit('entityAdded', entity); - } + removeLayer(layer) { + const index = this.layers.indexOf(layer); + if (-1 === index) { + return; + } + layer.off('entityAdded', this.onEntityAddedToLayers); + layer.off('entityRemoved', this.onEntityRemovedFromLayers); + this.layers.splice(index, 1); + this.emit('layerRemoved', layer); + this.stopSynchronizing(layer); + } - onEntityRemovedFromLayers(entity) { - this.emit('entityRemoved', entity); - } - - packets(informed) { - const packets = []; - const updates = []; - for (let i = 0; i < this.layers.length; i++) { - const layerPackets = this.layers[i].packets(informed); - if (layerPackets.length > 0) { - updates.push({ - layerIndex: i, - layerPackets, - }); + tick(elapsed) { + for (let i = 0; i < this.layers.length; i++) { + this.layers[i].tick(elapsed); } } - if (updates.length > 0) { - packets.push([ - 'LayersUpdateLayer', - updates, - ]); + + toJSON() { + return this.layers.map((layer) => layer.toJSON()); } - return packets; - } - removeAllLayers() { - for (let i = 0; i < this.layers.length; i++) { - this.removeLayer(this.layers[i]); + toNetwork(informed) { + return this.layers.map((layer) => layer.toNetwork(informed)); } - } - removeEntityFromLayer(entity, layerIndex) { - const layer = this.layers[layerIndex]; - if (!layer) { - return; - } - layer.removeEntity(entity); - } - - removeLayer(layer) { - const index = this.layers.indexOf(layer); - if (-1 === index) { - return; - } - layer.off('entityAdded', this.onEntityAddedToLayers); - layer.off('entityRemoved', this.onEntityRemovedFromLayers); - this.layers.splice(index, 1); - this.emit('layerRemoved', layer); - } - - tick(elapsed) { - for (let i = 0; i < this.layers.length; i++) { - this.layers[i].tick(elapsed); - } - } - - toJSON() { - return this.layers.map((layer) => layer.toJSON()); - } - - toNetwork(informed) { - return this.layers.map((layer) => layer.toNetwork(informed)); - } - - visibleEntities(query) { - const entities = []; - for (let i = 0; i < this.layers.length; i++) { - const layerVisibleEntities = this.layers[i].visibleEntities(query); - for (let j = 0; j < layerVisibleEntities.length; j++) { - const layerVisibleEntity = layerVisibleEntities[j]; - entities.push(layerVisibleEntity); + visibleEntities(query) { + const entities = []; + for (let i = 0; i < this.layers.length; i++) { + const layerVisibleEntities = this.layers[i].visibleEntities(query); + for (let j = 0; j < layerVisibleEntities.length; j++) { + const layerVisibleEntity = layerVisibleEntities[j]; + entities.push(layerVisibleEntity); + } } + return entities; } - return entities; - } + }; }; diff --git a/packages/topdown/src/resources/room.js b/packages/topdown/src/resources/room.js index d7388fd..238e5d2 100644 --- a/packages/topdown/src/resources/room.js +++ b/packages/topdown/src/resources/room.js @@ -3,157 +3,125 @@ import {JsonResource} from '@avocado/resource'; import {Synchronized} from '@avocado/s13n'; import {compose, EventEmitter} from '@latus/core'; -const decorate = compose( - EventEmitter, - Synchronized, - Vector.Mixin('size', 'width', 'height', { - default: [0, 0], - }), -); - let s13nId = 1; -export default (latus) => class Room extends decorate(JsonResource) { +export default (latus) => { + const decorate = compose( + EventEmitter, + Vector.Mixin('size', 'width', 'height', { + default: [0, 0], + }), + Synchronized(latus), + ); + return class Room extends decorate(JsonResource) { - constructor() { - super(); - this._s13nId = s13nId++; - const {Layers} = latus.get('%resources'); - this.setLayers(new Layers()); - } - - acceptPacket(packet) { - // Layer updates. - if ('RoomUpdateLayers' === packet.constructor.type) { - const {layersPackets} = packet.data; - for (let i = 0; i < layersPackets.length; ++i) { - this.layers.acceptPacket(layersPackets[i]); - } + constructor() { + super(); + this._s13nId = s13nId++; + const {Layers} = latus.get('%resources'); + this.setLayers(new Layers()); } - } - addEntityToLayer(entity, layerIndex = 0) { - this.layers.addEntityToLayer(entity, layerIndex); - } - - cleanPackets() { - super.cleanPackets(); - this.layers.cleanPackets(); - } - - destroy() { - super.destroy(); - this.layers.destroy(); - this.layers.off('entityAdded', this.onEntityAddedToRoom); - this.layers.off('entityRemoved', this.onEntityRemovedFromRoom); - } - - get entities() { - return this.layers.entities; - } - - findEntity(uuid) { - return this.layers.findEntity(uuid); - } - - layer(index) { - return this.layers.layer(index); - } - - async load(json = {}) { - await super.load(json); - const {layers, size} = json; - if (size) { - this.size = size; + addEntityToLayer(entity, layerIndex = 0) { + this.layers.addEntityToLayer(entity, layerIndex); } - const {Layers} = latus.get('%resources'); - this.setLayers( - layers - ? await Layers.load(layers) - : new Layers(), - ); - } - onEntityAddedToRoom(entity) { - // eslint-disable-next-line no-param-reassign - entity.room = this; - entity.emit('addedToRoom'); - this.emit('entityAdded', entity); - } - - onEntityRemovedFromRoom(entity) { - // eslint-disable-next-line no-param-reassign - entity.room = null; - entity.emit('removedFromRoom', this); - this.emit('entityRemoved', entity); - } - - packets(informed) { - const payload = []; - // Layer updates. - const layersPackets = this.layers.packets(informed); - if (layersPackets.length > 0) { - payload.push([ - 'RoomUpdateLayers', - { - layersPackets, - }, - ]); - } - return payload; - } - - // eslint-disable-next-line class-methods-use-this - packetsAreIdempotent() { - return false; - } - - removeEntityFromLayer(entity, layerIndex) { - this.layers.removeEntityFromLayer(entity, layerIndex); - } - - setLayers(layers) { - if (this.layers) { - const entities = Object.values(this.layers.entities); - for (let i = 0; i < entities.length; i++) { - this.onEntityRemovedFromRoom(entities[i]); - } + destroy() { + super.destroy(); + this.layers.destroy(); this.layers.off('entityAdded', this.onEntityAddedToRoom); this.layers.off('entityRemoved', this.onEntityRemovedFromRoom); } - this.layers = layers; - this.layers.on('entityRemoved', this.onEntityRemovedFromRoom, this); - this.layers.on('entityAdded', this.onEntityAddedToRoom, this); - const entities = Object.values(this.layers.entities); - for (let i = 0; i < entities.length; i++) { - this.onEntityAddedToRoom(entities[i]); + + get entities() { + return this.layers.entities; } - } - s13nId() { - return this._s13nId; - } + findEntity(uuid) { + return this.layers.findEntity(uuid); + } - tick(elapsed) { - this.layers.tick(elapsed); - } + layer(index) { + return this.layers.layer(index); + } - toJSON() { - return { - layer: this.layers.toJSON(), - size: this.size, - }; - } + async load(json = {}) { + await super.load(json); + const {layers, size} = json; + if (size) { + this.size = size; + } + const {Layers} = latus.get('%resources'); + this.setLayers( + layers + ? await Layers.load(layers) + : new Layers(), + ); + } - toNetwork(informed) { - return { - layers: this.layers.toNetwork(informed), - size: this.size, - }; - } + onEntityAddedToRoom(entity) { + // eslint-disable-next-line no-param-reassign + entity.room = this; + entity.emit('addedToRoom'); + this.emit('entityAdded', entity); + } - visibleEntities(query) { - return this.layers.visibleEntities(query); - } + onEntityRemovedFromRoom(entity) { + // eslint-disable-next-line no-param-reassign + entity.room = null; + entity.emit('removedFromRoom', this); + this.emit('entityRemoved', entity); + } + removeEntityFromLayer(entity, layerIndex) { + this.layers.removeEntityFromLayer(entity, layerIndex); + } + + setLayers(layers) { + if (this.layers) { + this.stopSynchronizing(this.layers); + const entities = Object.values(this.layers.entities); + for (let i = 0; i < entities.length; i++) { + this.onEntityRemovedFromRoom(entities[i]); + } + this.layers.off('entityAdded', this.onEntityAddedToRoom); + this.layers.off('entityRemoved', this.onEntityRemovedFromRoom); + } + this.layers = layers; + this.layers.on('entityRemoved', this.onEntityRemovedFromRoom, this); + this.layers.on('entityAdded', this.onEntityAddedToRoom, this); + const entities = Object.values(this.layers.entities); + for (let i = 0; i < entities.length; i++) { + this.onEntityAddedToRoom(entities[i]); + } + this.startSynchronizing(this.layers); + } + + get s13nId() { + return this._s13nId; + } + + tick(elapsed) { + this.layers.tick(elapsed); + } + + toJSON() { + return { + layer: this.layers.toJSON(), + size: this.size, + }; + } + + toNetwork(informed) { + return { + layers: this.layers.toNetwork(informed), + size: this.size, + }; + } + + visibleEntities(query) { + return this.layers.visibleEntities(query); + } + + }; }; diff --git a/packages/topdown/src/resources/tiles.js b/packages/topdown/src/resources/tiles.js index 8fd4cd7..a89d006 100644 --- a/packages/topdown/src/resources/tiles.js +++ b/packages/topdown/src/resources/tiles.js @@ -1,122 +1,125 @@ import {Rectangle, Vector} from '@avocado/math'; import {Class, compose, EventEmitter} from '@latus/core'; +import {Synchronized} from '@avocado/s13n'; -const decorate = compose( - EventEmitter, - Vector.Mixin('size', 'width', 'height', { - default: [0, 0], - }), -); +export default (latus) => { + const decorate = compose( + EventEmitter, + Vector.Mixin('size', 'width', 'height', { + default: [0, 0], + }), + Synchronized(latus), + ); + return class Tiles extends decorate(Class) { -export default () => class Tiles extends decorate(Class) { - - constructor({data, size} = {}) { - super(); - this._packets = []; - if (size) { - super.size = size; - } - this.data = data && data.length > 0 ? data.concat() : Array(Vector.area(this.size)).fill(0); - } - - acceptPacket(packet) { - if ('TilesUpdate' === packet.constructor.type) { - this.setTileAt(packet.data.position, packet.data.tile); - } - } - - cleanPackets() { - this._packets = []; - } - - forEachTile(fn) { - let [x, y] = [0, 0]; - const [width, height] = this.size; - let i = 0; - for (let k = 0; k < height; ++k) { - for (let j = 0; j < width; ++j) { - fn(this.data[i], x, y, i); - ++i; - ++x; + constructor({data, size} = {}) { + super(); + this._packets = []; + if (size) { + super.size = size; } - x = 0; - ++y; + this.data = data && data.length > 0 ? data.concat() : Array(Vector.area(this.size)).fill(0); } - } - indexAt(position) { - return this.width * position[1] + position[0]; - } - - packets() { - return this._packets; - } - - get rectangle() { - return Rectangle.compose([0, 0], this.size); - } - - setTileAt(position, tile) { - const oldTile = this.tileAt(position); - if (oldTile === tile) { - return; - } - const index = this.indexAt(position); - if (index < 0 || index >= this.data.length) { - return; - } - this.data[index] = tile; - this._packets.push([ - 'TilesUpdate', - { - position, - tile, - }, - ]); - this.emit('dataChanged'); - } - - slice(rectangle) { - const tilesRectangle = this.rectangle; - // Get intersection. - if (!Rectangle.intersects(rectangle, tilesRectangle)) { - return []; - } - // eslint-disable-next-line prefer-const - let [x, y, sliceWidth, sliceHeight] = Rectangle.intersection(rectangle, tilesRectangle); - // No muls in the loop. - const ox = x; - let sliceRow = 0; - const dataWidth = this.width; - let dataRow = y * dataWidth; - // Copy slice. - const slice = new Array(sliceWidth * sliceHeight); - for (let j = 0; j < sliceHeight; ++j) { - for (let i = 0; i < sliceWidth; ++i) { - slice[sliceRow + (x - ox)] = this.data[dataRow + x]; - x++; + acceptPacket(packet) { + if ('TilesUpdate' === packet.constructor.type) { + this.setTileAt(packet.data.position, packet.data.tile); } - sliceRow += sliceWidth; - dataRow += dataWidth; - x -= sliceWidth; } - return slice; - } - tileAt(position) { - const index = this.indexAt(position); - return index < 0 || index >= this.data.length ? undefined : this.data[index]; - } + cleanPackets() { + this._packets = []; + } - toNetwork() { - return this.toJSON(); - } + forEachTile(fn) { + let [x, y] = [0, 0]; + const [width, height] = this.size; + let i = 0; + for (let k = 0; k < height; ++k) { + for (let j = 0; j < width; ++j) { + fn(this.data[i], x, y, i); + ++i; + ++x; + } + x = 0; + ++y; + } + } - toJSON() { - return { - size: this.size, - data: this.data, - }; - } + indexAt(position) { + return this.width * position[1] + position[0]; + } + packetsFor() { + return this._packets; + } + + get rectangle() { + return Rectangle.compose([0, 0], this.size); + } + + setTileAt(position, tile) { + const oldTile = this.tileAt(position); + if (oldTile === tile) { + return; + } + const index = this.indexAt(position); + if (index < 0 || index >= this.data.length) { + return; + } + this.data[index] = tile; + this._packets.push([ + 'TilesUpdate', + { + position, + tile, + }, + ]); + this.emit('dataChanged'); + } + + slice(rectangle) { + const tilesRectangle = this.rectangle; + // Get intersection. + if (!Rectangle.intersects(rectangle, tilesRectangle)) { + return []; + } + // eslint-disable-next-line prefer-const + let [x, y, sliceWidth, sliceHeight] = Rectangle.intersection(rectangle, tilesRectangle); + // No muls in the loop. + const ox = x; + let sliceRow = 0; + const dataWidth = this.width; + let dataRow = y * dataWidth; + // Copy slice. + const slice = new Array(sliceWidth * sliceHeight); + for (let j = 0; j < sliceHeight; ++j) { + for (let i = 0; i < sliceWidth; ++i) { + slice[sliceRow + (x - ox)] = this.data[dataRow + x]; + x++; + } + sliceRow += sliceWidth; + dataRow += dataWidth; + x -= sliceWidth; + } + return slice; + } + + tileAt(position) { + const index = this.indexAt(position); + return index < 0 || index >= this.data.length ? undefined : this.data[index]; + } + + toNetwork() { + return this.toJSON(); + } + + toJSON() { + return { + size: this.size, + data: this.data, + }; + } + + }; }; diff --git a/packages/topdown/src/traits/followed.js b/packages/topdown/src/traits/followed.js index b5b4701..17b8a8b 100644 --- a/packages/topdown/src/traits/followed.js +++ b/packages/topdown/src/traits/followed.js @@ -28,6 +28,7 @@ export default () => class Followed extends Trait { } destroy() { + super.destroy(); const {room} = this.entity; if (room) { room.off('sizeChanged', this.onRoomSizeChanged); diff --git a/packages/topdown/src/traits/layered.js b/packages/topdown/src/traits/layered.js index 17f3574..b45c60f 100644 --- a/packages/topdown/src/traits/layered.js +++ b/packages/topdown/src/traits/layered.js @@ -23,6 +23,7 @@ export default () => class Layered extends Trait { } destroy() { + super.destroy(); this.detachFromLayer(this.entity.layer); } diff --git a/packages/topdown/src/traits/tile-entity.js b/packages/topdown/src/traits/tile-entity.js index 4c32e2d..dfb67b5 100644 --- a/packages/topdown/src/traits/tile-entity.js +++ b/packages/topdown/src/traits/tile-entity.js @@ -26,6 +26,7 @@ export default () => class TileEntity extends decorate(Trait) { } destroy() { + super.destroy(); this.removeTileEntity(this.entity.tileIndex); } diff --git a/packages/traits/src/trait.js b/packages/traits/src/trait.js index edacf26..1586723 100644 --- a/packages/traits/src/trait.js +++ b/packages/traits/src/trait.js @@ -1,12 +1,6 @@ import {JsonResource} from '@avocado/resource'; -import {Synchronized} from '@avocado/s13n'; -import {compose} from '@latus/core'; -const decorate = compose( - Synchronized, -); - -export default class Trait extends decorate(JsonResource) { +export default class Trait extends JsonResource { #markedAsDirty = true; @@ -69,8 +63,9 @@ export default class Trait extends decorate(JsonResource) { return []; } - // eslint-disable-next-line class-methods-use-this - destroy() {} + destroy() { + this.#memoizedListeners = undefined; + } // eslint-disable-next-line class-methods-use-this hooks() { @@ -120,14 +115,18 @@ export default class Trait extends decorate(JsonResource) { return {}; } - // eslint-disable-next-line class-methods-use-this, no-unused-vars - packets(informed) { + // eslint-disable-next-line class-methods-use-this + packetsFor() { return []; } + static get resourceId() { + return this.id; + } + // eslint-disable-next-line class-methods-use-this - packetsAreIdempotent() { - return false; + get s13nId() { + return 0; } stateDifferences() { @@ -145,6 +144,10 @@ export default class Trait extends decorate(JsonResource) { return differences; } + toNetwork() { + return this.toJSON(); + } + toJSON() { return { params: this.params,