refactor: synchronization
This commit is contained in:
parent
8afa52fc2e
commit
6bdbe70fec
|
@ -118,6 +118,7 @@ export default (latus) => class Behaved extends decorate(Trait) {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
this.#context.destroy();
|
this.#context.destroy();
|
||||||
this.#currentRoutine = undefined;
|
this.#currentRoutine = undefined;
|
||||||
this.#routines = undefined;
|
this.#routines = undefined;
|
||||||
|
|
|
@ -85,8 +85,8 @@ export default (Trait, latus) => class DialogInitiator extends Trait {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
packets(informed) {
|
packetsFor(informed) {
|
||||||
return super.packets(informed).concat(
|
return super.packetsFor(informed).concat(
|
||||||
informed === this.entity
|
informed === this.entity
|
||||||
? this.#dialogs.map((dialog) => ['OpenDialog', dialog])
|
? this.#dialogs.map((dialog) => ['OpenDialog', dialog])
|
||||||
: [],
|
: [],
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const blacklistedAccessorKeys = [
|
const blacklistedAccessorKeys = [
|
||||||
|
's13nId',
|
||||||
'state',
|
'state',
|
||||||
'uri',
|
'uri',
|
||||||
'instanceUuid',
|
'instanceUuid',
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
|
@ -2,300 +2,272 @@ import {compile, Context} from '@avocado/behavior';
|
||||||
import {compose, EventEmitter} from '@latus/core';
|
import {compose, EventEmitter} from '@latus/core';
|
||||||
import {QuadTree, Rectangle} from '@avocado/math';
|
import {QuadTree, Rectangle} from '@avocado/math';
|
||||||
import {JsonResource} from '@avocado/resource';
|
import {JsonResource} from '@avocado/resource';
|
||||||
import {Serializer} from '@avocado/s13n';
|
import {Synchronized} from '@avocado/s13n';
|
||||||
|
|
||||||
const decorate = compose(
|
export default (latus) => {
|
||||||
EventEmitter,
|
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();
|
async acceptPacket(packet) {
|
||||||
|
await super.acceptPacket(packet);
|
||||||
#serializer = new Serializer();
|
const {s13nType} = packet;
|
||||||
|
if ('create' === s13nType) {
|
||||||
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;
|
|
||||||
const {Entity} = latus.get('%resources');
|
const {Entity} = latus.get('%resources');
|
||||||
this.#serializer.create(uuid, Entity.load(packet.data.spec));
|
const {id} = packet.data.synchronized;
|
||||||
this.#serializer.later(uuid, (entity) => {
|
this.addEntity(this.synchronized(Entity.resourceId, id));
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Send updates.
|
|
||||||
this.#informedEntities.set(informed, visibleEntities);
|
async addEntity(entity) {
|
||||||
if (updates.length > 0) {
|
const uuid = entity.instanceUuid;
|
||||||
packets.push([
|
// Already exists?
|
||||||
'EntityListUpdateEntity',
|
if (this.#entities[uuid]) {
|
||||||
updates,
|
return;
|
||||||
]);
|
|
||||||
}
|
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
}
|
this.#entities[uuid] = entity;
|
||||||
return packets;
|
this.#flatEntities.push(entity);
|
||||||
}
|
this.#entityTickers.push(entity.tick);
|
||||||
|
|
||||||
get quadTree() {
|
|
||||||
return this.#quadTree;
|
|
||||||
}
|
|
||||||
|
|
||||||
queryEntities(query, condition, context) {
|
|
||||||
if (!context) {
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
// 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);
|
cleanPackets() {
|
||||||
const fails = [];
|
super.cleanPackets();
|
||||||
for (let i = 0; i < candidates.length; ++i) {
|
for (let i = 0; i < this.#flatEntities.length; i++) {
|
||||||
const entity = candidates[i];
|
this.#flatEntities[i].cleanPackets();
|
||||||
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) {
|
destroy() {
|
||||||
return this.queryEntities(Rectangle.centerOn(query, [1, 1]), condition, context);
|
for (let i = 0; i < this.#flatEntities.length; i++) {
|
||||||
}
|
this.#flatEntities[i].destroy();
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let i = 0; i < finishedTickers.length; ++i) {
|
|
||||||
const ticker = finishedTickers[i];
|
get entities() {
|
||||||
const index = this.#afterDestructionTickers.indexOf(ticker);
|
return this.#entities;
|
||||||
this.#afterDestructionTickers.splice(index, 1);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
tickEntities(elapsed) {
|
findEntity(uuid) {
|
||||||
for (let i = 0; i < this.#entityTickers.length; i++) {
|
return uuid in this.#entities
|
||||||
this.#entityTickers[i](elapsed);
|
? this.#entities[uuid]
|
||||||
|
: undefined;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
async load(json = []) {
|
||||||
const json = [];
|
await super.load(json);
|
||||||
for (let i = 0; i < this.#flatEntities.length; i++) {
|
const {Entity} = latus.get('%resources');
|
||||||
const entity = this.#flatEntities[i];
|
const entityInstances = await Promise.all(json.map((entity) => Entity.load(entity)));
|
||||||
json.push(entity.mergeDiff(entity.toJSON()));
|
for (let i = 0; i < entityInstances.length; i++) {
|
||||||
}
|
this.addEntity(entityInstances[i]);
|
||||||
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) {
|
onEntityDestroying(entity) {
|
||||||
return this.visibleEntities(query).filter((entity) => entity.uri === uri);
|
this.removeEntity(entity);
|
||||||
}
|
// In the process of destroying, allow entities to specify tickers that
|
||||||
|
// must live on past destruction.
|
||||||
async waitForEntity(uuid) {
|
const tickers = entity.invokeHookFlat('afterDestructionTickers');
|
||||||
const entity = this.findEntity(uuid);
|
for (let i = 0; i < tickers.length; i++) {
|
||||||
if (entity) {
|
const ticker = tickers[i];
|
||||||
return entity;
|
this.#afterDestructionTickers.push(ticker);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return new Promise((resolve) => {
|
|
||||||
const find = () => {
|
packetsFor(informed) {
|
||||||
const entity = this.findEntity(uuid);
|
const packets = [];
|
||||||
if (entity) {
|
const {Entity} = latus.get('%resources');
|
||||||
this.off('entityAdded', find);
|
// Visible entities.
|
||||||
resolve(entity);
|
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));
|
||||||
}
|
}
|
||||||
};
|
else {
|
||||||
this.on('entityAdded', find);
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,428 +14,412 @@ import BaseEntity from '../base-entity';
|
||||||
|
|
||||||
const debug = D('@avocado/entity');
|
const debug = D('@avocado/entity');
|
||||||
|
|
||||||
const decorate = compose(
|
|
||||||
EventEmitter,
|
|
||||||
Synchronized,
|
|
||||||
);
|
|
||||||
|
|
||||||
let numericUid = 'client' !== process.env.SIDE ? 1 : 1000000000;
|
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() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.once('destroyed', () => {
|
this.once('destroyed', () => {
|
||||||
this.removeAllTraits();
|
this.removeAllTraits();
|
||||||
});
|
});
|
||||||
// Bind to prevent lookup overhead.
|
// Bind to prevent lookup overhead.
|
||||||
this.tick = this.tick.bind(this);
|
this.tick = this.tick.bind(this);
|
||||||
// Fast props.
|
// Fast props.
|
||||||
this.elapsed = 0;
|
this.elapsed = 0;
|
||||||
this.list = null;
|
this.list = null;
|
||||||
this.numericUid = numericUid++;
|
this.numericUid = numericUid++;
|
||||||
this.position = [0, 0];
|
this.position = [0, 0];
|
||||||
this.room = null;
|
this.room = null;
|
||||||
this.uptime = 0;
|
this.uptime = 0;
|
||||||
this.visibleAabb = [0, 0, 0, 0];
|
this.visibleAabb = [0, 0, 0, 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
acceptPacket(packet) {
|
addTickingPromise(tickingPromise) {
|
||||||
if ('EntityUpdateTrait' === packet.constructor.type) {
|
const ticker = tickingPromise.tick.bind(tickingPromise);
|
||||||
const {traits} = packet.data;
|
this.#tickingPromisesTickers.push(ticker);
|
||||||
for (let i = 0; i < traits.length; i++) {
|
return tickingPromise.then(() => {
|
||||||
const {type, packets} = traits[i];
|
const index = this.#tickingPromisesTickers.indexOf(ticker);
|
||||||
for (let j = 0; j < packets.length; j++) {
|
if (-1 !== index) {
|
||||||
this.#traits[type].acceptPacket(packets[j]);
|
this.#tickingPromisesTickers.splice(index, 1);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
addTickingPromise(tickingPromise) {
|
async _addTrait(type, json = {}) {
|
||||||
const ticker = tickingPromise.tick.bind(tickingPromise);
|
const {[type]: Trait} = latus.get('%traits');
|
||||||
this.#tickingPromisesTickers.push(ticker);
|
if (!Trait) {
|
||||||
return tickingPromise.then(() => {
|
// eslint-disable-next-line no-console
|
||||||
const index = this.#tickingPromisesTickers.indexOf(ticker);
|
console.error(`Tried to add trait "${type}" which isn't registered!`);
|
||||||
if (-1 !== index) {
|
return undefined;
|
||||||
this.#tickingPromisesTickers.splice(index, 1);
|
|
||||||
}
|
}
|
||||||
});
|
if (this.is(type)) {
|
||||||
}
|
await this.#traits[type].load({
|
||||||
|
entity: this,
|
||||||
async _addTrait(type, json = {}) {
|
...json,
|
||||||
const {[type]: Trait} = latus.get('%traits');
|
});
|
||||||
if (!Trait) {
|
this.emit('traitAdded', type, this.#traits[type]);
|
||||||
// eslint-disable-next-line no-console
|
return undefined;
|
||||||
console.error(`Tried to add trait "${type}" which isn't registered!`);
|
}
|
||||||
return undefined;
|
// Ensure dependencies.
|
||||||
}
|
const dependencies = Trait.dependencies();
|
||||||
if (this.is(type)) {
|
const allTypes = this.traitTypes();
|
||||||
await this.#traits[type].load({
|
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,
|
entity: this,
|
||||||
...json,
|
...json,
|
||||||
});
|
});
|
||||||
this.emit('traitAdded', type, this.#traits[type]);
|
// Proxy properties.
|
||||||
return undefined;
|
defineTraitAccessors(Trait.prototype, this, trait);
|
||||||
}
|
// Attach listeners.
|
||||||
// Ensure dependencies.
|
const listeners = Object.entries(trait.memoizedListeners());
|
||||||
const dependencies = Trait.dependencies();
|
for (let i = 0; i < listeners.length; i++) {
|
||||||
const allTypes = this.traitTypes();
|
const [event, listener] = listeners[i];
|
||||||
const lacking = without(dependencies, ...allTypes);
|
this.on(event, listener);
|
||||||
if (lacking.length > 0) {
|
}
|
||||||
// eslint-disable-next-line no-console
|
// Proxy methods.
|
||||||
console.error(
|
const methods = Object.entries(trait.methods());
|
||||||
`Tried to add trait "${type}" but lack one or more dependents: "${lacking.join('", "')}"!`,
|
for (let i = 0; i < methods.length; i++) {
|
||||||
);
|
const [key, method] = methods[i];
|
||||||
return undefined;
|
this[key] = method;
|
||||||
}
|
}
|
||||||
// Instantiate.
|
// Register hook listeners.
|
||||||
const trait = await Trait.load({
|
const hooks = Object.entries(trait.hooks());
|
||||||
entity: this,
|
for (let i = 0; i < hooks.length; i++) {
|
||||||
...json,
|
const [key, fn] = hooks[i];
|
||||||
});
|
this.#hooks[key] = this.#hooks[key] || [];
|
||||||
// Proxy properties.
|
this.#hooks[key].push({
|
||||||
defineTraitAccessors(Trait.prototype, this, trait);
|
fn,
|
||||||
// Attach listeners.
|
type,
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!reorganized[type]) {
|
// Track trait.
|
||||||
reorganized[type] = traits[type] || {};
|
this.#traits[type] = trait;
|
||||||
|
this.#traitsFlat.push(trait);
|
||||||
|
if ('tick' in trait) {
|
||||||
|
this.#traitTickers.push(trait.tick);
|
||||||
}
|
}
|
||||||
};
|
if ('renderTick' in trait) {
|
||||||
Object.keys(traits).forEach(add);
|
this.#traitRenderTickers.push(trait.renderTick);
|
||||||
const entries = Object.entries(reorganized);
|
}
|
||||||
const instances = {};
|
if ('acceptPacket' in trait) {
|
||||||
for (let i = 0; i < entries.length; i++) {
|
this.#traitsAcceptingPackets.push(trait);
|
||||||
const [type, json] = entries[i];
|
}
|
||||||
// eslint-disable-next-line no-await-in-loop
|
this.startSynchronizing(trait);
|
||||||
instances[type] = await this._addTrait(type, json);
|
this.emit('traitAdded', type, trait);
|
||||||
|
return trait;
|
||||||
}
|
}
|
||||||
return instances;
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanPackets() {
|
async addTrait(type, json) {
|
||||||
if (!this.#markedAsDirty) {
|
return this.addTraits({[type]: json});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
for (let i = 0; i < this.#traitsFlat.length; i++) {
|
|
||||||
this.#traitsFlat[i].cleanPackets();
|
|
||||||
}
|
|
||||||
this.#markedAsDirty = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async destroy() {
|
async addTraits(traits) {
|
||||||
if (this.#isDestroying) {
|
const Traits = latus.get('%traits');
|
||||||
return;
|
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) {
|
cleanPackets() {
|
||||||
const results = {};
|
super.cleanPackets();
|
||||||
if (!(hook in this.#hooks)) {
|
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;
|
return results;
|
||||||
}
|
}
|
||||||
const values = Object.values(this.#hooks[hook]);
|
|
||||||
for (let i = 0; i < values.length; i++) {
|
invokeHookFlat(hook, ...args) {
|
||||||
const {fn, type} = values[i];
|
return Object.values(this.invokeHook(hook, ...args));
|
||||||
results[type] = fastApply(null, fn, args);
|
|
||||||
}
|
}
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
invokeHookFlat(hook, ...args) {
|
invokeHookReduced(hook, ...args) {
|
||||||
return Object.values(this.invokeHook(hook, ...args));
|
return this.invokeHookFlat(hook, ...args)
|
||||||
}
|
.reduce((r, result) => ({...r, ...result}), {});
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
this.instanceUuid = instanceUuid || this.numericUid;
|
|
||||||
await this.addTraits(traits);
|
|
||||||
}
|
|
||||||
|
|
||||||
markAsDirty() {
|
is(type) {
|
||||||
this.#markedAsDirty = true;
|
return type in this.#traits;
|
||||||
}
|
}
|
||||||
|
|
||||||
packets(informed) {
|
async load(json = {}) {
|
||||||
const packets = [];
|
await super.load(json);
|
||||||
const updates = [];
|
const {instanceUuid, traits = {}} = json;
|
||||||
const traits = Object.entries(this.#traits);
|
if (!this.#originalJson) {
|
||||||
for (let i = 0; i < traits.length; i++) {
|
this.#originalJson = json;
|
||||||
const [type, trait] = traits[i];
|
}
|
||||||
const traitPackets = trait.packets(informed);
|
this.instanceUuid = instanceUuid || this.numericUid;
|
||||||
if (traitPackets.length > 0) {
|
await this.addTraits(traits);
|
||||||
updates.push({
|
}
|
||||||
type,
|
|
||||||
packets: traitPackets,
|
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) {
|
removeAllTraits() {
|
||||||
for (let i = 0; i < this.#traitRenderTickers.length; i++) {
|
const types = this.traitTypes();
|
||||||
this.#traitRenderTickers[i](elapsed);
|
this.removeTraits(types);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
removeAllTraits() {
|
removeTrait(type) {
|
||||||
const types = this.traitTypes();
|
if (!this.is(type)) {
|
||||||
this.removeTraits(types);
|
debug(`Tried to remove trait "${type}" when it doesn't exist!`);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
removeTrait(type) {
|
// Destroy instance.
|
||||||
if (!this.is(type)) {
|
const instance = this.#traits[type];
|
||||||
debug(`Tried to remove trait "${type}" when it doesn't exist!`);
|
this.stopSynchronizing(instance);
|
||||||
return;
|
// 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) {
|
removeTraits(types) {
|
||||||
types.forEach((type) => this.removeTrait(type));
|
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);
|
|
||||||
}
|
}
|
||||||
for (let i = 0; i < this.#tickingPromisesTickers.length; i++) {
|
|
||||||
this.#tickingPromisesTickers[i](elapsed);
|
get s13nId() {
|
||||||
|
return this.instanceUuid;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
tick(elapsed) {
|
||||||
const json = {};
|
this.elapsed = elapsed;
|
||||||
const traits = Object.entries(this.#traits);
|
this.uptime += elapsed;
|
||||||
for (let i = 0; i < traits.length; i++) {
|
for (let i = 0; i < this.#traitTickers.length; i++) {
|
||||||
const [type, trait] = traits[i];
|
this.#traitTickers[i](elapsed);
|
||||||
json[type] = trait.toJSON();
|
}
|
||||||
|
for (let i = 0; i < this.#tickingPromisesTickers.length; i++) {
|
||||||
|
this.#tickingPromisesTickers[i](elapsed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
traits: json,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
toNetwork(informed) {
|
toJSON() {
|
||||||
const pristine = {traits: {}};
|
const json = {};
|
||||||
const json = {
|
const traits = Object.entries(this.#traits);
|
||||||
traits: {},
|
for (let i = 0; i < traits.length; i++) {
|
||||||
};
|
const [type, trait] = traits[i];
|
||||||
const traits = Object.entries(this.#traits);
|
json[type] = trait.toJSON();
|
||||||
for (let i = 0; i < traits.length; i++) {
|
}
|
||||||
const [type, trait] = traits[i];
|
return {
|
||||||
pristine.traits[type] = trait.constructor.withDefaults(
|
traits: json,
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
trait(type) {
|
toNetwork(informed) {
|
||||||
return this.#traits[type];
|
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() {
|
trait(type) {
|
||||||
return this.#traits;
|
return this.#traits[type];
|
||||||
}
|
}
|
||||||
|
|
||||||
traitTypes() {
|
get traits() {
|
||||||
return Object.keys(this.#traits);
|
return this.#traits;
|
||||||
}
|
}
|
||||||
|
|
||||||
static withDefaults(json = {}) {
|
traitTypes() {
|
||||||
const Traits = latus.get('%traits');
|
return Object.keys(this.#traits);
|
||||||
return {
|
}
|
||||||
...json,
|
|
||||||
traits: Object.entries(json.traits)
|
|
||||||
.reduce((r, [type, traitJson]) => ({
|
|
||||||
...r,
|
|
||||||
[type]: Traits[type]
|
|
||||||
? Traits[type].withDefaults(traitJson)
|
|
||||||
: traitJson,
|
|
||||||
}), {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static withoutDefaults(json) {
|
static withDefaults(json = {}) {
|
||||||
const Traits = latus.get('%traits');
|
const Traits = latus.get('%traits');
|
||||||
const without = {
|
return {
|
||||||
...json,
|
...json,
|
||||||
traits: Object.entries(json.traits)
|
traits: Object.entries(json.traits)
|
||||||
.reduce((r, [type, traitJson]) => ({
|
.reduce((r, [type, traitJson]) => ({
|
||||||
...r,
|
...r,
|
||||||
[type]: Traits[type]
|
[type]: Traits[type]
|
||||||
? Traits[type].withoutDefaults(traitJson)
|
? Traits[type].withDefaults(traitJson)
|
||||||
: traitJson,
|
: traitJson,
|
||||||
}), {}),
|
}), {}),
|
||||||
};
|
};
|
||||||
return without;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -112,6 +112,7 @@ export default (latus) => class Alive extends decorate(Trait) {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
this.#context.destroy();
|
this.#context.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,7 +194,7 @@ export default (latus) => class Alive extends decorate(Trait) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
packets() {
|
packetsFor() {
|
||||||
const packets = this.#packets.concat();
|
const packets = this.#packets.concat();
|
||||||
const {life, maxLife} = this.stateDifferences();
|
const {life, maxLife} = this.stateDifferences();
|
||||||
if (life || maxLife) {
|
if (life || maxLife) {
|
||||||
|
|
|
@ -90,7 +90,7 @@ export default () => class Directional extends decorate(Trait) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
packets() {
|
packetsFor() {
|
||||||
const {direction} = this.stateDifferences();
|
const {direction} = this.stateDifferences();
|
||||||
if (direction) {
|
if (direction) {
|
||||||
return [[
|
return [[
|
||||||
|
|
|
@ -37,6 +37,7 @@ export default () => class DomNode extends decorate(Trait) {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
super.parentNode?.removeChild(this.entity.node);
|
super.parentNode?.removeChild(this.entity.node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -224,7 +224,7 @@ export default () => class Mobile extends decorate(Trait) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
packets() {
|
packetsFor() {
|
||||||
const {isMobile, speed} = this.stateDifferences();
|
const {isMobile, speed} = this.stateDifferences();
|
||||||
if (isMobile || speed) {
|
if (isMobile || speed) {
|
||||||
return [[
|
return [[
|
||||||
|
|
|
@ -75,6 +75,7 @@ export default () => class Positioned extends decorate(Trait) {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
this.off('trackedPositionChanged', this.ontrackedPositionChanged);
|
this.off('trackedPositionChanged', this.ontrackedPositionChanged);
|
||||||
if ('client' === process.env.SIDE) {
|
if ('client' === process.env.SIDE) {
|
||||||
this.off('serverPositionChanged', this.onServerPositionChanged);
|
this.off('serverPositionChanged', this.onServerPositionChanged);
|
||||||
|
@ -107,7 +108,7 @@ export default () => class Positioned extends decorate(Trait) {
|
||||||
this.serverPositionDirty = true;
|
this.serverPositionDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
packets() {
|
packetsFor() {
|
||||||
const {x, y} = this.stateDifferences();
|
const {x, y} = this.stateDifferences();
|
||||||
if (x || y) {
|
if (x || y) {
|
||||||
return [[
|
return [[
|
||||||
|
|
|
@ -143,6 +143,7 @@ export default (latus) => class Spawner extends decorate(Trait) {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
while (this.#children.length > 0) {
|
while (this.#children.length > 0) {
|
||||||
const child = this.#children.pop();
|
const child = this.#children.pop();
|
||||||
if (child) {
|
if (child) {
|
||||||
|
|
|
@ -112,7 +112,7 @@ describe('Alive', () => {
|
||||||
it('generates and accepts life packets', async () => {
|
it('generates and accepts life packets', async () => {
|
||||||
entity.life = 80;
|
entity.life = 80;
|
||||||
entity.maxLife = 90;
|
entity.maxLife = 90;
|
||||||
const packets = entity.trait('Alive').packets();
|
const packets = entity.trait('Alive').packetsFor();
|
||||||
expect(packets).to.have.lengthOf(1);
|
expect(packets).to.have.lengthOf(1);
|
||||||
expect(packets[0][0]).to.equal('TraitUpdateAlive');
|
expect(packets[0][0]).to.equal('TraitUpdateAlive');
|
||||||
expect(packets[0][1]).to.deep.equal({life: 80, maxLife: 90});
|
expect(packets[0][1]).to.deep.equal({life: 80, maxLife: 90});
|
||||||
|
@ -124,7 +124,7 @@ describe('Alive', () => {
|
||||||
it('generates and accepts death packets', async () => {
|
it('generates and accepts death packets', async () => {
|
||||||
entity.life = 0;
|
entity.life = 0;
|
||||||
entity.tick();
|
entity.tick();
|
||||||
const packets = entity.trait('Alive').packets();
|
const packets = entity.trait('Alive').packetsFor();
|
||||||
expect(packets).to.have.lengthOf(2);
|
expect(packets).to.have.lengthOf(2);
|
||||||
expect(packets[0][0]).to.equal('Died');
|
expect(packets[0][0]).to.equal('Died');
|
||||||
expect(packets[1][0]).to.equal('TraitUpdateAlive');
|
expect(packets[1][0]).to.equal('TraitUpdateAlive');
|
||||||
|
|
|
@ -42,7 +42,7 @@ describe('Directional', () => {
|
||||||
});
|
});
|
||||||
it('generates and accepts direction packets', async () => {
|
it('generates and accepts direction packets', async () => {
|
||||||
entity.direction = 2;
|
entity.direction = 2;
|
||||||
const packets = entity.trait('Directional').packets();
|
const packets = entity.trait('Directional').packetsFor();
|
||||||
expect(packets).to.have.lengthOf(1);
|
expect(packets).to.have.lengthOf(1);
|
||||||
expect(packets[0][0]).to.equal('TraitUpdateDirectionalDirection');
|
expect(packets[0][0]).to.equal('TraitUpdateDirectionalDirection');
|
||||||
expect(packets[0][1]).to.equal(2);
|
expect(packets[0][1]).to.equal(2);
|
||||||
|
|
|
@ -30,7 +30,7 @@ describe('Positioned', () => {
|
||||||
if ('client' !== process.env.SIDE) {
|
if ('client' !== process.env.SIDE) {
|
||||||
it('generates and accepts movement packets', async () => {
|
it('generates and accepts movement packets', async () => {
|
||||||
entity.setPosition([1, 1]);
|
entity.setPosition([1, 1]);
|
||||||
const packets = entity.trait('Positioned').packets();
|
const packets = entity.trait('Positioned').packetsFor();
|
||||||
expect(packets).to.have.lengthOf(1);
|
expect(packets).to.have.lengthOf(1);
|
||||||
expect(packets[0][0]).to.equal('TraitUpdatePositionedPosition');
|
expect(packets[0][0]).to.equal('TraitUpdatePositionedPosition');
|
||||||
expect(packets[0][1]).to.deep.equal([1, 1]);
|
expect(packets[0][1]).to.deep.equal([1, 1]);
|
||||||
|
|
|
@ -62,6 +62,7 @@ export default (latus) => class Pictured extends decorate(Trait) {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
if (this.#sprites) {
|
if (this.#sprites) {
|
||||||
const sprites = Object.entries(this.#sprites);
|
const sprites = Object.entries(this.#sprites);
|
||||||
for (let i = 0; i < sprites.length; i++) {
|
for (let i = 0; i < sprites.length; i++) {
|
||||||
|
|
|
@ -35,6 +35,7 @@ export default () => class Rastered extends Trait {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
if (this.#container) {
|
if (this.#container) {
|
||||||
this.#container.destroy();
|
this.#container.destroy();
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,6 +98,7 @@ export default () => class Visible extends decorate(Trait) {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
this.removeFromQuadTree(this.entity.list);
|
this.removeFromQuadTree(this.entity.list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,7 +168,7 @@ export default () => class Visible extends decorate(Trait) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
packets() {
|
packetsFor() {
|
||||||
const {isVisible, opacity, rotation} = this.stateDifferences();
|
const {isVisible, opacity, rotation} = this.stateDifferences();
|
||||||
if (isVisible || opacity || rotation) {
|
if (isVisible || opacity || rotation) {
|
||||||
return [[
|
return [[
|
||||||
|
|
|
@ -26,6 +26,7 @@ export default () => class Controllable extends Trait {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
this.#actionRegistry.stopListening();
|
this.#actionRegistry.stopListening();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ export default (latus) => class Interactive extends decorate(Trait) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
packets() {
|
packetsFor() {
|
||||||
const {isInteractive} = this.stateDifferences();
|
const {isInteractive} = this.stateDifferences();
|
||||||
if (isInteractive) {
|
if (isInteractive) {
|
||||||
return [[
|
return [[
|
||||||
|
|
|
@ -126,6 +126,7 @@ export default (latus) => class Collider extends decorate(Trait) {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
this.releaseAllCollisions();
|
this.releaseAllCollisions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -322,7 +322,7 @@ export default (latus) => class Emitter extends decorate(Trait) {
|
||||||
/* eslint-enable no-param-reassign */
|
/* eslint-enable no-param-reassign */
|
||||||
}
|
}
|
||||||
|
|
||||||
packets() {
|
packetsFor() {
|
||||||
return this.#emitting.length > 0
|
return this.#emitting.length > 0
|
||||||
? [['EmitParticles', this.#emitting]]
|
? [['EmitParticles', this.#emitting]]
|
||||||
: [];
|
: [];
|
||||||
|
|
|
@ -93,6 +93,7 @@ export default () => class Physical extends decorate(Trait) {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
this.world = undefined;
|
this.world = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ export default () => class Shaped extends decorate(Trait) {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
this.#shape.destroy();
|
this.#shape.destroy();
|
||||||
if (this.#shapeView) {
|
if (this.#shapeView) {
|
||||||
this.#shapeView.destroy();
|
this.#shapeView.destroy();
|
||||||
|
|
|
@ -4,19 +4,14 @@ import {
|
||||||
SynchronizedUpdatePacket,
|
SynchronizedUpdatePacket,
|
||||||
} from './packets';
|
} from './packets';
|
||||||
|
|
||||||
export * from './packets';
|
export {default as Synchronized} from './synchronized';
|
||||||
|
|
||||||
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 {
|
export default {
|
||||||
hooks: {
|
hooks: {
|
||||||
'@latus/socket/packets': () => ({
|
'@latus/socket/packets': (latus) => ({
|
||||||
SynchronizedCreate: SynchronizedCreatePacket,
|
SynchronizedCreate: SynchronizedCreatePacket,
|
||||||
SynchronizedDestroy: SynchronizedDestroyPacket,
|
SynchronizedDestroy: SynchronizedDestroyPacket,
|
||||||
SynchronizedUpdate: SynchronizedUpdatePacket,
|
SynchronizedUpdate: SynchronizedUpdatePacket(latus),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,29 @@
|
||||||
import SynchronizedPacket from './synchronized';
|
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() {
|
static get s13nType() {
|
||||||
return 'update';
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
|
@ -16,6 +16,10 @@ export default class SynchronizedPacket extends Packet {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get s13nType() {
|
||||||
|
return this.constructor.s13nType;
|
||||||
|
}
|
||||||
|
|
||||||
static get s13nType() {
|
static get s13nType() {
|
||||||
return 'none';
|
return 'none';
|
||||||
}
|
}
|
||||||
|
|
|
@ -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] || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -7,13 +7,6 @@ export default class Serializer {
|
||||||
|
|
||||||
#promises = new Map();
|
#promises = new Map();
|
||||||
|
|
||||||
cancelIfPending(id) {
|
|
||||||
const pending = this.#pending.get(id);
|
|
||||||
if (pending) {
|
|
||||||
pending.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
create(id, creator) {
|
create(id, creator) {
|
||||||
const promise = creator.then(async (resource) => {
|
const promise = creator.then(async (resource) => {
|
||||||
if (!this.#pending.has(id)) {
|
if (!this.#pending.has(id)) {
|
||||||
|
@ -27,13 +20,18 @@ export default class Serializer {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.#pending.set(id, {
|
this.#pending.set(id, () => this.#pending.delete(id));
|
||||||
cancel: () => this.#pending.delete(id),
|
|
||||||
});
|
|
||||||
this.#promises.set(id, promise);
|
this.#promises.set(id, promise);
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroy(id) {
|
||||||
|
// Cancel...
|
||||||
|
this.#pending.get(id)?.();
|
||||||
|
this.#pending.delete(id);
|
||||||
|
this.#promises.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
later(id, fn) {
|
later(id, fn) {
|
||||||
const promise = this.#promises.get(id);
|
const promise = this.#promises.get(id);
|
||||||
if (!promise) {
|
if (!promise) {
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
107
packages/s13n/src/synchronized/client.js
Normal file
107
packages/s13n/src/synchronized/client.js
Normal file
|
@ -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];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
4
packages/s13n/src/synchronized/index.js
Normal file
4
packages/s13n/src/synchronized/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import Client from './client';
|
||||||
|
import Server from './server';
|
||||||
|
|
||||||
|
export default process.env.SIDE === 'client' ? Client : Server;
|
123
packages/s13n/src/synchronized/server.js
Normal file
123
packages/s13n/src/synchronized/server.js
Normal file
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
|
@ -68,6 +68,7 @@ export default (latus) => class Audible extends Trait {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
Object.values(this.#sounds).forEach((sound) => {
|
Object.values(this.#sounds).forEach((sound) => {
|
||||||
Promise.resolve(sound).then((sound) => {
|
Promise.resolve(sound).then((sound) => {
|
||||||
sound.destroy();
|
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}]);
|
return Object.keys(this.#playing).map((key) => ['PlaySound', {sound: key}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -89,6 +89,7 @@ export default (latus) => class Animated extends decorate(Trait) {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
if (this.#animationViews) {
|
if (this.#animationViews) {
|
||||||
const animationViews = Object.entries(this.#animationViews);
|
const animationViews = Object.entries(this.#animationViews);
|
||||||
for (let i = 0; i < animationViews.length; i++) {
|
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;
|
return this.params.animations[key].offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
packets() {
|
packetsFor() {
|
||||||
const {currentAnimation, isAnimating} = this.stateDifferences();
|
const {currentAnimation, isAnimating} = this.stateDifferences();
|
||||||
if (this.#forceUpdate || currentAnimation || isAnimating) {
|
if (this.#forceUpdate || currentAnimation || isAnimating) {
|
||||||
this.#forceUpdate = false;
|
this.#forceUpdate = false;
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,230 +1,215 @@
|
||||||
import {Property} from '@avocado/core';
|
import {Property} from '@avocado/core';
|
||||||
import {JsonResource} from '@avocado/resource';
|
import {JsonResource} from '@avocado/resource';
|
||||||
|
import {Synchronized} from '@avocado/s13n';
|
||||||
import {compose, EventEmitter} from '@latus/core';
|
import {compose, EventEmitter} from '@latus/core';
|
||||||
|
|
||||||
const decorate = compose(
|
export default (latus) => {
|
||||||
EventEmitter,
|
const decorate = compose(
|
||||||
Property('tileset', {
|
EventEmitter,
|
||||||
track: true,
|
Property('tileset', {
|
||||||
}),
|
track: true,
|
||||||
);
|
}),
|
||||||
|
Synchronized(latus),
|
||||||
|
);
|
||||||
|
return class Layer extends decorate(JsonResource) {
|
||||||
|
|
||||||
export default (latus) => class Layer extends decorate(JsonResource) {
|
#s13nId;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.tileEntities = {};
|
this.tileEntities = {};
|
||||||
this.tileGeometry = [];
|
this.tileGeometry = [];
|
||||||
const {EntityList, Tiles} = latus.get('%resources');
|
const {EntityList, Tiles} = latus.get('%resources');
|
||||||
this.setEntityList(new EntityList());
|
this.setEntityList(new EntityList());
|
||||||
this.setTiles(new Tiles());
|
this.setTiles(new Tiles());
|
||||||
}
|
|
||||||
|
|
||||||
acceptPacket(packet) {
|
|
||||||
const {constructor: {s13nType}} = packet;
|
|
||||||
switch (s13nType) {
|
|
||||||
case 'create':
|
|
||||||
case 'destroy':
|
|
||||||
this.entityList.acceptPacket(packet);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
}
|
||||||
switch (packet.constructor.type) {
|
|
||||||
case 'EntityListUpdateEntity':
|
addEntity(entity) {
|
||||||
this.entityList.acceptPacket(packet);
|
this.entityList.addEntity(entity);
|
||||||
break;
|
|
||||||
case 'TilesUpdate':
|
|
||||||
this.tiles.acceptPacket(packet);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
addEntity(entity) {
|
addTileEntity(entity, index) {
|
||||||
this.entityList.addEntity(entity);
|
if (!this.tileEntities[index]) {
|
||||||
}
|
this.tileEntities[index] = [];
|
||||||
|
}
|
||||||
addTileEntity(entity, index) {
|
if (-1 !== this.tileEntities[index].indexOf(entity)) {
|
||||||
if (!this.tileEntities[index]) {
|
return;
|
||||||
this.tileEntities[index] = [];
|
}
|
||||||
|
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() {
|
destroy() {
|
||||||
this.entityList.cleanPackets();
|
this.entityList.destroy();
|
||||||
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);
|
|
||||||
this.entityList.off('entityAdded', this.onEntityAddedToLayer);
|
this.entityList.off('entityAdded', this.onEntityAddedToLayer);
|
||||||
}
|
this.entityList.off('entityRemoved', this.onEntityRemovedFromLayer);
|
||||||
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.tiles.off('dataChanged', this.onTileDataChanged);
|
this.tiles.off('dataChanged', this.onTileDataChanged);
|
||||||
|
if (this.tileset) {
|
||||||
|
this.tileset.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.tiles = tiles;
|
|
||||||
this.tiles.on('dataChanged', this.onTileDataChanged, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
tick(elapsed) {
|
get entities() {
|
||||||
this.entityList.tick(elapsed);
|
return this.entityList.entities;
|
||||||
}
|
|
||||||
|
|
||||||
tileAt(position) {
|
|
||||||
return this.tiles.tileAt(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
tileEntitiesAt(tilePosition) {
|
|
||||||
const index = this.indexAt(tilePosition);
|
|
||||||
if (!this.tileEntities[index]) {
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
return this.tileEntities[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
findEntity(uuid) {
|
||||||
return {
|
return this.entityList.findEntity(uuid);
|
||||||
entities: this.entityList.toJSON(),
|
}
|
||||||
tilesetUri: this.tileset.uri,
|
|
||||||
tiles: this.tiles.toJSON(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
toNetwork(informed) {
|
hasTileEntityWithUriAt(tilePosition, uri) {
|
||||||
return {
|
const tileEntities = this.tileEntitiesAt(tilePosition);
|
||||||
entities: this.entityList.toNetwork(informed),
|
if (0 === tileEntities.length) {
|
||||||
tilesetUri: this.tileset.uri,
|
return false;
|
||||||
tiles: this.tiles.toNetwork(informed),
|
}
|
||||||
};
|
const entitiesWithUri = tileEntities.filter((entity) => entity.uri === uri);
|
||||||
}
|
return entitiesWithUri.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
visibleEntities(query) {
|
indexAt(position) {
|
||||||
return this.entityList.visibleEntities(query);
|
return this.tiles.indexAt(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
// visibleEntitiesWithUri(query, uri) {
|
async load(json = {}) {
|
||||||
// return this.entityList.visibleEntitiesWithUri(query, uri);
|
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);
|
||||||
|
// }
|
||||||
|
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,171 +1,147 @@
|
||||||
import {compose, EventEmitter} from '@latus/core';
|
import {compose, EventEmitter} from '@latus/core';
|
||||||
import {JsonResource} from '@avocado/resource';
|
import {JsonResource} from '@avocado/resource';
|
||||||
|
import {Synchronized} from '@avocado/s13n';
|
||||||
|
|
||||||
const decorate = compose(
|
export default (latus) => {
|
||||||
EventEmitter,
|
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() {
|
addEntityToLayer(entity, layerIndex) {
|
||||||
super();
|
const layer = this.layers[layerIndex];
|
||||||
this.layers = [];
|
if (!layer) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
layer.addEntity(entity);
|
||||||
|
}
|
||||||
|
|
||||||
acceptPacket(packet) {
|
addLayer(layer) {
|
||||||
if ('LayersUpdateLayer' === packet.constructor.type) {
|
// eslint-disable-next-line no-param-reassign
|
||||||
for (let i = 0; i < packet.data.length; i++) {
|
layer.s13nId = this.layers.length;
|
||||||
const {layerIndex, layerPackets} = packet.data[i];
|
this.startSynchronizing(layer);
|
||||||
for (let j = 0; j < layerPackets.length; j++) {
|
layer.on('entityAdded', this.onEntityAddedToLayers, this);
|
||||||
this.layers[layerIndex].acceptPacket(layerPackets[j]);
|
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) {
|
findEntity(uuid) {
|
||||||
const layer = this.layers[layerIndex];
|
for (let i = 0; i < this.layers.length; i++) {
|
||||||
if (!layer) {
|
const foundEntity = this.layers[i].findEntity(uuid);
|
||||||
return;
|
if (foundEntity) {
|
||||||
|
return foundEntity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
layer.addEntity(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
addLayer(layer) {
|
layer(index) {
|
||||||
layer.on('entityAdded', this.onEntityAddedToLayers, this);
|
return this.layers[index];
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
async load(json = []) {
|
||||||
for (let i = 0; i < this.layers.length; i++) {
|
await super.load(json);
|
||||||
const layer = this.layers[i];
|
this.removeAllLayers();
|
||||||
this.removeLayer(layer);
|
const {Layer} = latus.get('%resources');
|
||||||
layer.destroy();
|
const layers = await Promise.all(json.map((layer) => Layer.load(layer)));
|
||||||
}
|
for (let i = 0; i < layers.length; i++) {
|
||||||
}
|
this.addLayer(layers[i]);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
findEntity(uuid) {
|
onEntityAddedToLayers(entity) {
|
||||||
for (let i = 0; i < this.layers.length; i++) {
|
this.emit('entityAdded', entity);
|
||||||
const foundEntity = this.layers[i].findEntity(uuid);
|
}
|
||||||
if (foundEntity) {
|
|
||||||
return foundEntity;
|
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) {
|
removeEntityFromLayer(entity, layerIndex) {
|
||||||
return this.layers[index];
|
const layer = this.layers[layerIndex];
|
||||||
}
|
if (!layer) {
|
||||||
|
return;
|
||||||
async load(json = []) {
|
}
|
||||||
await super.load(json);
|
layer.removeEntity(entity);
|
||||||
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]);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
onEntityAddedToLayers(entity) {
|
removeLayer(layer) {
|
||||||
this.emit('entityAdded', entity);
|
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) {
|
tick(elapsed) {
|
||||||
this.emit('entityRemoved', entity);
|
for (let i = 0; i < this.layers.length; i++) {
|
||||||
}
|
this.layers[i].tick(elapsed);
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (updates.length > 0) {
|
|
||||||
packets.push([
|
toJSON() {
|
||||||
'LayersUpdateLayer',
|
return this.layers.map((layer) => layer.toJSON());
|
||||||
updates,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
return packets;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAllLayers() {
|
toNetwork(informed) {
|
||||||
for (let i = 0; i < this.layers.length; i++) {
|
return this.layers.map((layer) => layer.toNetwork(informed));
|
||||||
this.removeLayer(this.layers[i]);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
removeEntityFromLayer(entity, layerIndex) {
|
visibleEntities(query) {
|
||||||
const layer = this.layers[layerIndex];
|
const entities = [];
|
||||||
if (!layer) {
|
for (let i = 0; i < this.layers.length; i++) {
|
||||||
return;
|
const layerVisibleEntities = this.layers[i].visibleEntities(query);
|
||||||
}
|
for (let j = 0; j < layerVisibleEntities.length; j++) {
|
||||||
layer.removeEntity(entity);
|
const layerVisibleEntity = layerVisibleEntities[j];
|
||||||
}
|
entities.push(layerVisibleEntity);
|
||||||
|
}
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
return entities;
|
||||||
}
|
}
|
||||||
return entities;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,157 +3,125 @@ import {JsonResource} from '@avocado/resource';
|
||||||
import {Synchronized} from '@avocado/s13n';
|
import {Synchronized} from '@avocado/s13n';
|
||||||
import {compose, EventEmitter} from '@latus/core';
|
import {compose, EventEmitter} from '@latus/core';
|
||||||
|
|
||||||
const decorate = compose(
|
|
||||||
EventEmitter,
|
|
||||||
Synchronized,
|
|
||||||
Vector.Mixin('size', 'width', 'height', {
|
|
||||||
default: [0, 0],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
let s13nId = 1;
|
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() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this._s13nId = s13nId++;
|
this._s13nId = s13nId++;
|
||||||
const {Layers} = latus.get('%resources');
|
const {Layers} = latus.get('%resources');
|
||||||
this.setLayers(new Layers());
|
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]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
addEntityToLayer(entity, layerIndex = 0) {
|
addEntityToLayer(entity, layerIndex = 0) {
|
||||||
this.layers.addEntityToLayer(entity, layerIndex);
|
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;
|
|
||||||
}
|
}
|
||||||
const {Layers} = latus.get('%resources');
|
|
||||||
this.setLayers(
|
|
||||||
layers
|
|
||||||
? await Layers.load(layers)
|
|
||||||
: new Layers(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onEntityAddedToRoom(entity) {
|
destroy() {
|
||||||
// eslint-disable-next-line no-param-reassign
|
super.destroy();
|
||||||
entity.room = this;
|
this.layers.destroy();
|
||||||
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]);
|
|
||||||
}
|
|
||||||
this.layers.off('entityAdded', this.onEntityAddedToRoom);
|
this.layers.off('entityAdded', this.onEntityAddedToRoom);
|
||||||
this.layers.off('entityRemoved', this.onEntityRemovedFromRoom);
|
this.layers.off('entityRemoved', this.onEntityRemovedFromRoom);
|
||||||
}
|
}
|
||||||
this.layers = layers;
|
|
||||||
this.layers.on('entityRemoved', this.onEntityRemovedFromRoom, this);
|
get entities() {
|
||||||
this.layers.on('entityAdded', this.onEntityAddedToRoom, this);
|
return this.layers.entities;
|
||||||
const entities = Object.values(this.layers.entities);
|
|
||||||
for (let i = 0; i < entities.length; i++) {
|
|
||||||
this.onEntityAddedToRoom(entities[i]);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
s13nId() {
|
findEntity(uuid) {
|
||||||
return this._s13nId;
|
return this.layers.findEntity(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
tick(elapsed) {
|
layer(index) {
|
||||||
this.layers.tick(elapsed);
|
return this.layers.layer(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
async load(json = {}) {
|
||||||
return {
|
await super.load(json);
|
||||||
layer: this.layers.toJSON(),
|
const {layers, size} = json;
|
||||||
size: this.size,
|
if (size) {
|
||||||
};
|
this.size = size;
|
||||||
}
|
}
|
||||||
|
const {Layers} = latus.get('%resources');
|
||||||
|
this.setLayers(
|
||||||
|
layers
|
||||||
|
? await Layers.load(layers)
|
||||||
|
: new Layers(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
toNetwork(informed) {
|
onEntityAddedToRoom(entity) {
|
||||||
return {
|
// eslint-disable-next-line no-param-reassign
|
||||||
layers: this.layers.toNetwork(informed),
|
entity.room = this;
|
||||||
size: this.size,
|
entity.emit('addedToRoom');
|
||||||
};
|
this.emit('entityAdded', entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
visibleEntities(query) {
|
onEntityRemovedFromRoom(entity) {
|
||||||
return this.layers.visibleEntities(query);
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,122 +1,125 @@
|
||||||
import {Rectangle, Vector} from '@avocado/math';
|
import {Rectangle, Vector} from '@avocado/math';
|
||||||
import {Class, compose, EventEmitter} from '@latus/core';
|
import {Class, compose, EventEmitter} from '@latus/core';
|
||||||
|
import {Synchronized} from '@avocado/s13n';
|
||||||
|
|
||||||
const decorate = compose(
|
export default (latus) => {
|
||||||
EventEmitter,
|
const decorate = compose(
|
||||||
Vector.Mixin('size', 'width', 'height', {
|
EventEmitter,
|
||||||
default: [0, 0],
|
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();
|
||||||
constructor({data, size} = {}) {
|
this._packets = [];
|
||||||
super();
|
if (size) {
|
||||||
this._packets = [];
|
super.size = size;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
x = 0;
|
this.data = data && data.length > 0 ? data.concat() : Array(Vector.area(this.size)).fill(0);
|
||||||
++y;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
indexAt(position) {
|
acceptPacket(packet) {
|
||||||
return this.width * position[1] + position[0];
|
if ('TilesUpdate' === packet.constructor.type) {
|
||||||
}
|
this.setTileAt(packet.data.position, packet.data.tile);
|
||||||
|
|
||||||
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++;
|
|
||||||
}
|
}
|
||||||
sliceRow += sliceWidth;
|
|
||||||
dataRow += dataWidth;
|
|
||||||
x -= sliceWidth;
|
|
||||||
}
|
}
|
||||||
return slice;
|
|
||||||
}
|
|
||||||
|
|
||||||
tileAt(position) {
|
cleanPackets() {
|
||||||
const index = this.indexAt(position);
|
this._packets = [];
|
||||||
return index < 0 || index >= this.data.length ? undefined : this.data[index];
|
}
|
||||||
}
|
|
||||||
|
|
||||||
toNetwork() {
|
forEachTile(fn) {
|
||||||
return this.toJSON();
|
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() {
|
indexAt(position) {
|
||||||
return {
|
return this.width * position[1] + position[0];
|
||||||
size: this.size,
|
}
|
||||||
data: this.data,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -28,6 +28,7 @@ export default () => class Followed extends Trait {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
const {room} = this.entity;
|
const {room} = this.entity;
|
||||||
if (room) {
|
if (room) {
|
||||||
room.off('sizeChanged', this.onRoomSizeChanged);
|
room.off('sizeChanged', this.onRoomSizeChanged);
|
||||||
|
|
|
@ -23,6 +23,7 @@ export default () => class Layered extends Trait {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
this.detachFromLayer(this.entity.layer);
|
this.detachFromLayer(this.entity.layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ export default () => class TileEntity extends decorate(Trait) {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
this.removeTileEntity(this.entity.tileIndex);
|
this.removeTileEntity(this.entity.tileIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
import {JsonResource} from '@avocado/resource';
|
import {JsonResource} from '@avocado/resource';
|
||||||
import {Synchronized} from '@avocado/s13n';
|
|
||||||
import {compose} from '@latus/core';
|
|
||||||
|
|
||||||
const decorate = compose(
|
export default class Trait extends JsonResource {
|
||||||
Synchronized,
|
|
||||||
);
|
|
||||||
|
|
||||||
export default class Trait extends decorate(JsonResource) {
|
|
||||||
|
|
||||||
#markedAsDirty = true;
|
#markedAsDirty = true;
|
||||||
|
|
||||||
|
@ -69,8 +63,9 @@ export default class Trait extends decorate(JsonResource) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
destroy() {
|
||||||
destroy() {}
|
this.#memoizedListeners = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
hooks() {
|
hooks() {
|
||||||
|
@ -120,14 +115,18 @@ export default class Trait extends decorate(JsonResource) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this, no-unused-vars
|
// eslint-disable-next-line class-methods-use-this
|
||||||
packets(informed) {
|
packetsFor() {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get resourceId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
packetsAreIdempotent() {
|
get s13nId() {
|
||||||
return false;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
stateDifferences() {
|
stateDifferences() {
|
||||||
|
@ -145,6 +144,10 @@ export default class Trait extends decorate(JsonResource) {
|
||||||
return differences;
|
return differences;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toNetwork() {
|
||||||
|
return this.toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
params: this.params,
|
params: this.params,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user