255 lines
6.9 KiB
JavaScript
255 lines
6.9 KiB
JavaScript
import {compose, EventEmitter} from '@avocado/core';
|
|
import {QuadTree, Rectangle, Vector} from '@avocado/math';
|
|
import {
|
|
SynchronizedCreatePacket,
|
|
SynchronizedDestroyPacket,
|
|
} from '@avocado/net';
|
|
|
|
import {EntityListUpdateEntityPacket} from '../packets/entity-list-update-entity.packet';
|
|
import {Entity} from '../entity.synchronized';
|
|
|
|
const decorate = compose(
|
|
EventEmitter,
|
|
);
|
|
|
|
export class EntityList extends decorate(class {}) {
|
|
|
|
constructor() {
|
|
super();
|
|
this._afterDestructionTickers = [];
|
|
this._entities = {};
|
|
this._entityTickers = []
|
|
this._flatEntities = [];
|
|
this._informedEntities = new Map();
|
|
this._quadTree = new QuadTree();
|
|
}
|
|
|
|
*[Symbol.iterator]() {
|
|
for (const uuid in this._entities) {
|
|
const entity = this._entities[uuid];
|
|
yield entity;
|
|
}
|
|
}
|
|
|
|
acceptPacket(packet) {
|
|
if (packet instanceof EntityListUpdateEntityPacket) {
|
|
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]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (packet instanceof SynchronizedCreatePacket) {
|
|
Entity.loadOrInstance(packet.data.spec).then((entity) => {
|
|
this.addEntity(entity);
|
|
});
|
|
}
|
|
if (packet instanceof SynchronizedDestroyPacket) {
|
|
const uuid = packet.data.synchronized.id;
|
|
if (this._entities[uuid]) {
|
|
this._entities[uuid].destroy();
|
|
}
|
|
}
|
|
}
|
|
|
|
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 (AVOCADO_SERVER) {
|
|
this._informedEntities.set(entity, []);
|
|
}
|
|
entity.setIntoList(this);
|
|
entity.once('destroy', () => {
|
|
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);
|
|
}
|
|
});
|
|
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();
|
|
}
|
|
}
|
|
|
|
findEntity(uuid) {
|
|
if (uuid in this._entities) {
|
|
return this._entities[uuid];
|
|
}
|
|
}
|
|
|
|
fromJSON(json) {
|
|
for (let i = 0; i < json.length; i++) {
|
|
const entityJSON = json[i];
|
|
if (entityJSON.uri) {
|
|
Entity.read(entityJSON.uri).then((readJSON) => {
|
|
this.addEntity(new Entity(readJSON, entityJSON));
|
|
});
|
|
}
|
|
else {
|
|
this.addEntity(new Entity(json[i]));
|
|
}
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
// Still visible entity.
|
|
else {
|
|
const entityPackets = entity.packets(informed);
|
|
if (entityPackets.length > 0) {
|
|
updates.push({
|
|
uuid: entity.instanceUuid,
|
|
packets: entityPackets,
|
|
});
|
|
}
|
|
previousVisibleEntities.splice(index, 1);
|
|
}
|
|
}
|
|
// Send updates.
|
|
this._informedEntities.set(informed, visibleEntities);
|
|
if (updates.length > 0) {
|
|
packets.push(new EntityListUpdateEntityPacket(updates));
|
|
}
|
|
// Send destroys.
|
|
for (let i = 0; i < previousVisibleEntities.length; i++) {
|
|
const entity = previousVisibleEntities[i];
|
|
// Newly removed entity.
|
|
if (-1 === visibleEntities.indexOf(entity)) {
|
|
packets.push(entity.destroyPacket(informed));
|
|
}
|
|
}
|
|
return packets;
|
|
}
|
|
|
|
get quadTree() {
|
|
return this._quadTree;
|
|
}
|
|
|
|
removeEntity(entity) {
|
|
const uuid = entity.instanceUuid;
|
|
if (!(uuid in this._entities)) {
|
|
return;
|
|
}
|
|
if (AVOCADO_SERVER) {
|
|
this._informedEntities.delete(entity);
|
|
}
|
|
entity.removeFromList();
|
|
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);
|
|
}
|
|
}
|
|
|
|
toNetwork(informed) {
|
|
const {areaToInform} = informed;
|
|
const visibleEntities = this.visibleEntities(areaToInform);
|
|
// Mark as notified.
|
|
this._informedEntities.set(informed, visibleEntities);
|
|
return visibleEntities.map((entity) => {
|
|
return entity.mergeDiff();
|
|
});
|
|
}
|
|
|
|
toJSON() {
|
|
const json = [];
|
|
for (let i = 0; i < this._flatEntities.length; i++) {
|
|
json.push(this._flatEntities[i].mergeDiff());
|
|
}
|
|
return json;
|
|
}
|
|
|
|
visibleEntities(query) {
|
|
const entities = [];
|
|
const entitiesChecked = [];
|
|
const quadTree = this._quadTree;
|
|
const nodes = quadTree.search(query);
|
|
// Check all nodes.
|
|
for (let i = 0; i < nodes.length; ++i) {
|
|
const node = nodes[i];
|
|
const entity = node.data[2];
|
|
const aabb = node.data[3];
|
|
if (-1 === entitiesChecked.indexOf(entity)) {
|
|
entitiesChecked.push(entity);
|
|
// Make sure the AABB is actually in the query due to expansion.
|
|
if (entity.isVisible && Rectangle.intersects(query, aabb)) {
|
|
entities.push(entity);
|
|
}
|
|
}
|
|
}
|
|
return entities;
|
|
}
|
|
|
|
visibleEntitiesWithUri(query, uri) {
|
|
return this.visibleEntities(query).filter((entity) => {
|
|
return entity.uri === uri;
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
export {EntityListView} from './view';
|