import GameEntity from '../domain/entities/game/GameEntity'; class Rectangle { private readonly x: number; private readonly y: number; private readonly width: number; private readonly height: number; constructor(x: number, y: number, width: number, height: number) { this.x = x; this.y = y; this.width = width; this.height = height; } contains(x: number, y: number) { return x >= this.x && x < this.x + this.width && y >= this.y && y < this.y + this.height; } } export default class QuadTree { private x: number; private y: number; private width: number; private height: number; private capacity: number; private bounds: Rectangle; private subdivided: boolean; private entities: Map; private _nw: QuadTree | null; private _ne: QuadTree | null; private _sw: QuadTree | null; private _se: QuadTree | null; constructor(x: number, y: number, width: number, height: number, capacity: number) { this.x = x; this.y = y; this.width = width; this.height = height; this.capacity = capacity; this.bounds = new Rectangle(x, y, width, height); this.subdivided = false; this.entities = new Map(); this._nw = null; this._ne = null; this._sw = null; this._se = null; } getEntities() { return this.entities; } isSubdivided() { return this.subdivided; } insert(entity: GameEntity): boolean { if (!this.bounds.contains(entity.getPositionX(), entity.getPositionY())) { return false; } if (this.entities.size < this.capacity && !this.subdivided) { this.entities.set(entity.getVirtualId(), entity); return true; } if (!this.subdivided) { this.subdivide(); } return (this._nw?.insert(entity) || this._ne?.insert(entity) || this._sw?.insert(entity) || this._se?.insert(entity))!; } remove(entity: GameEntity): boolean { if (this.subdivided) { return (this._nw?.remove(entity) || this._ne?.remove(entity) || this._sw?.remove(entity) || this._se?.remove(entity))!; } return this.entities.delete(entity.getVirtualId()); } queryAround(x: number, y: number, radius: number, filter: number | null = null) { const entities = new Map(); this.queryAroundInternal(entities, x, y, radius, filter); return entities; } queryAroundInternal( entities: Map, x: number, y: number, radius: number, filter: number | null = null, ) { if (!this.circleIntersects(x, y, radius)) { return; } if (this.subdivided) { this._ne?.queryAroundInternal(entities, x, y, radius, filter); this._nw?.queryAroundInternal(entities, x, y, radius, filter); this._se?.queryAroundInternal(entities, x, y, radius, filter); this._sw?.queryAroundInternal(entities, x, y, radius, filter); } else { for (const entity of this.entities.values()) { if (filter !== null && entity.getEntityType() !== filter) { continue; } const dx = entity.getPositionX() - x; const dy = entity.getPositionY() - y; if (dx * dx + dy * dy <= radius * radius) { entities.set(entity.getVirtualId(), entity); } } } } circleIntersects(x: number, y: number, radius: number) { const halfWidth = this.width / 2; const halfHeight = this.height / 2; const centerX = this.x + halfWidth; const centerY = this.y + halfHeight; const xDist = Math.abs(centerX - x); const yDist = Math.abs(centerY - y); const edges = (xDist - halfWidth) ** 2 + (yDist - halfHeight) ** 2; if (xDist > radius + halfWidth || yDist > radius + halfHeight) { return false; } if (xDist <= halfWidth || yDist <= halfHeight) { return true; } return edges <= radius * radius; } subdivide() { const halfWidth = this.width / 2; const halfHeight = this.height / 2; this._nw = new QuadTree(this.x, this.y, halfWidth, halfHeight, this.capacity); this._ne = new QuadTree(this.x, this.y + halfHeight, halfWidth, halfHeight, this.capacity); this._sw = new QuadTree(this.x + halfWidth, this.y, halfWidth, halfHeight, this.capacity); this._se = new QuadTree(this.x + halfWidth, this.y + halfHeight, halfWidth, halfHeight, this.capacity); this.subdivided = true; for (const entity of this.entities.values()) { this._nw.insert(entity) || this._ne.insert(entity) || this._sw.insert(entity) || this._se.insert(entity); } this.entities.clear(); } updatePosition(entity: GameEntity) { this.remove(entity); this.insert(entity); } }