You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
195 lines
6.2 KiB
195 lines
6.2 KiB
/* typehints:start */
|
|
import { Component } from "./component";
|
|
import { GameRoot } from "./root";
|
|
import { Entity } from "./entity";
|
|
/* typehints:end */
|
|
|
|
import { GameSystem } from "./game_system";
|
|
import { arrayDelete, arrayDeleteValue } from "../core/utils";
|
|
import { DrawParameters } from "../core/draw_parameters";
|
|
import { globalConfig } from "../core/config";
|
|
import { Math_floor, Math_ceil } from "../core/builtins";
|
|
|
|
export class GameSystemWithFilter extends GameSystem {
|
|
/**
|
|
* Constructs a new game system with the given component filter. It will process
|
|
* all entities which have *all* of the passed components
|
|
* @param {GameRoot} root
|
|
* @param {Array<typeof Component>} requiredComponents
|
|
*/
|
|
constructor(root, requiredComponents) {
|
|
super(root);
|
|
this.requiredComponents = requiredComponents;
|
|
this.requiredComponentIds = requiredComponents.map(component => component.getId());
|
|
|
|
/**
|
|
* All entities which match the current components
|
|
* @type {Array<Entity>}
|
|
*/
|
|
this.allEntities = [];
|
|
|
|
this.root.signals.entityAdded.add(this.internalPushEntityIfMatching, this);
|
|
this.root.signals.entityGotNewComponent.add(this.internalReconsiderEntityToAdd, this);
|
|
this.root.signals.entityComponentRemoved.add(this.internalCheckEntityAfterComponentRemoval, this);
|
|
this.root.signals.entityQueuedForDestroy.add(this.internalPopEntityIfMatching, this);
|
|
|
|
this.root.signals.postLoadHook.add(this.internalPostLoadHook, this);
|
|
}
|
|
|
|
/**
|
|
* Calls a function for each matching entity on the screen, useful for drawing them
|
|
* @param {DrawParameters} parameters
|
|
* @param {function} callback
|
|
*/
|
|
forEachMatchingEntityOnScreen(parameters, callback) {
|
|
const cullRange = parameters.visibleRect.toTileCullRectangle();
|
|
if (this.allEntities.length < 100) {
|
|
// So, its much quicker to simply perform per-entity checking
|
|
|
|
for (let i = 0; i < this.allEntities.length; ++i) {
|
|
const entity = this.allEntities[i];
|
|
if (cullRange.containsRect(entity.components.StaticMapEntity.getTileSpaceBounds())) {
|
|
callback(parameters, entity);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
const top = cullRange.top();
|
|
const right = cullRange.right();
|
|
const bottom = cullRange.bottom();
|
|
const left = cullRange.left();
|
|
|
|
const border = 1;
|
|
const minY = top - border;
|
|
const maxY = bottom + border;
|
|
const minX = left - border;
|
|
const maxX = right + border - 1;
|
|
|
|
const map = this.root.map;
|
|
|
|
let seenUids = new Set();
|
|
|
|
const chunkStartX = Math_floor(minX / globalConfig.mapChunkSize);
|
|
const chunkStartY = Math_floor(minY / globalConfig.mapChunkSize);
|
|
|
|
const chunkEndX = Math_ceil(maxX / globalConfig.mapChunkSize);
|
|
const chunkEndY = Math_ceil(maxY / globalConfig.mapChunkSize);
|
|
|
|
const requiredComponents = this.requiredComponentIds;
|
|
|
|
// Render y from top down for proper blending
|
|
for (let chunkX = chunkStartX; chunkX <= chunkEndX; ++chunkX) {
|
|
for (let chunkY = chunkStartY; chunkY <= chunkEndY; ++chunkY) {
|
|
const chunk = map.getChunk(chunkX, chunkY, false);
|
|
if (!chunk) {
|
|
continue;
|
|
}
|
|
|
|
// BIG TODO: CULLING ON AN ENTITY BASIS
|
|
|
|
const entities = chunk.containedEntities;
|
|
entityLoop: for (let i = 0; i < entities.length; ++i) {
|
|
const entity = entities[i];
|
|
|
|
// Avoid drawing twice
|
|
if (seenUids.has(entity.uid)) {
|
|
continue;
|
|
}
|
|
seenUids.add(entity.uid);
|
|
|
|
for (let i = 0; i < requiredComponents.length; ++i) {
|
|
if (!entity.components[requiredComponents[i]]) {
|
|
continue entityLoop;
|
|
}
|
|
}
|
|
callback(parameters, entity);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Entity} entity
|
|
*/
|
|
internalPushEntityIfMatching(entity) {
|
|
for (let i = 0; i < this.requiredComponentIds.length; ++i) {
|
|
if (!entity.components[this.requiredComponentIds[i]]) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
assert(this.allEntities.indexOf(entity) < 0, "entity already in list: " + entity);
|
|
this.internalRegisterEntity(entity);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Entity} entity
|
|
*/
|
|
internalCheckEntityAfterComponentRemoval(entity) {
|
|
if (this.allEntities.indexOf(entity) < 0) {
|
|
// Entity wasn't interesting anyways
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < this.requiredComponentIds.length; ++i) {
|
|
if (!entity.components[this.requiredComponentIds[i]]) {
|
|
// Entity is not interesting anymore
|
|
arrayDeleteValue(this.allEntities, entity);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Entity} entity
|
|
*/
|
|
internalReconsiderEntityToAdd(entity) {
|
|
for (let i = 0; i < this.requiredComponentIds.length; ++i) {
|
|
if (!entity.components[this.requiredComponentIds[i]]) {
|
|
return;
|
|
}
|
|
}
|
|
if (this.allEntities.indexOf(entity) >= 0) {
|
|
return;
|
|
}
|
|
this.internalRegisterEntity(entity);
|
|
}
|
|
|
|
refreshCaches() {
|
|
this.allEntities.sort((a, b) => a.uid - b.uid);
|
|
}
|
|
|
|
/**
|
|
* Recomputes all target entities after the game has loaded
|
|
*/
|
|
internalPostLoadHook() {
|
|
this.refreshCaches();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Entity} entity
|
|
*/
|
|
internalRegisterEntity(entity) {
|
|
this.allEntities.push(entity);
|
|
|
|
if (this.root.gameInitialized) {
|
|
// Sort entities by uid so behaviour is predictable
|
|
this.allEntities.sort((a, b) => a.uid - b.uid);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Entity} entity
|
|
*/
|
|
internalPopEntityIfMatching(entity) {
|
|
const index = this.allEntities.indexOf(entity);
|
|
if (index >= 0) {
|
|
arrayDelete(this.allEntities, index);
|
|
}
|
|
}
|
|
}
|