node_World.js

const Node = require("../node/Node.js");
const Common = require("../core/Common.js")
const Grid = require("../geometry/Grid.js");
const vec = require("../geometry/vec.js");
const RigidBody = require("../physics/RigidBody.js");
const CollisionShape = require("../physics/CollisionShape.js");

/**
 * The game world
 * @extends Node
 */
class World extends Node {
	static defaultOptions = {
		gravity: new vec(0, 500),
		gridSize: 500,
	}
	
	gravity = new vec(0, 0);
	timescale = 1;
	time = 0;

	rigidBodies = new Set();
	constraints = new Set();
	pairs = {};

	dynamicGrid;
	staticGrid;
	
	globalPoints = [];
	globalVectors = [];
	
	/**
	 * 
	 * @param {object} options - World options
	 * @param {vec} [options.gravity=vec(0, 500)] - Gravity in pixels / second
	 * @param {number} [options.gridSize=500] - Size of broadphase grid in pixels
	 */
	constructor(options = {}) {
		super();
		let defaults = { ...World.defaultOptions };
		Common.merge(defaults, options, 1);
		options = defaults;

		let { gravity, gridSize } = options;
		this.gravity = new vec(gravity);
		this.dynamicGrid = new Grid(gridSize);
		this.staticGrid = new Grid(gridSize);
	}

	canCollide(filterA, filterB) {
		let { layer: layerA, mask: maskA } = filterA;
		let { layer: layerB, mask: maskB } = filterB;

		let canA = (maskA & layerB) !== 0;
		let canB = (maskB & layerA) !== 0;

		return canA || canB;
	}
	#getPairs(bodies) {
		let pairs = [];
		let canCollide = this.canCollide;

		for (let i = 0; i < bodies.length - 1; i++) {
			let bodyA = bodies[i];
			if (!bodyA.parentNode.hasCollisions)
				continue;
			
			for (let j = i + 1; j < bodies.length; j++) {
				// Do AABB collision test
				let bodyB = bodies[j];
				if (!bodyB.parentNode.hasCollisions || bodyA.parentNode === bodyB.parentNode)
					continue;
				if (!canCollide(bodyA.parentNode.collisionFilter, bodyB.parentNode.collisionFilter))
					continue;
				

				const boundsA = bodyA.bounds;
				const boundsB = bodyB.bounds;

				if (boundsA.min.x <= boundsB.max.x &&
					boundsA.max.x >= boundsB.min.x &&
					boundsA.min.y <= boundsB.max.y &&
					boundsA.max.y >= boundsB.min.y) {
					pairs.push([ bodyA, bodyB ]);
				}
			}
		}

		return pairs;
	}
	get collisionPairs() {
		let canCollide = this.canCollide;
		let dynamicGrid = this.dynamicGrid;
		let staticGrid = this.staticGrid;
		let pair = Common.pairCommon;
		let pairIds = new Set();
		let pairs = [];

		let dynamicBuckets = dynamicGrid.grid;
		let staticBuckets = staticGrid.grid;
		let bucketIds = dynamicGrid.gridIds;

		for (let id of bucketIds) {
			let curDynamicBucket = dynamicBuckets[id];
			let curStaticBucket = staticBuckets[id];
			let curPairs = this.#getPairs(curDynamicBucket); // pair dynamic bodies

			// add static bodies
			if (curStaticBucket) {
				for (let j = 0; j < curDynamicBucket.length; j++) {
					let bodyA = curDynamicBucket[j];
					if (!bodyA.parentNode.hasCollisions)
						continue;
					for (let k = 0; k < curStaticBucket.length; k++) {
						let bodyB = curStaticBucket[k];

						if (!bodyB.parentNode.hasCollisions || bodyA.parentNode.isStatic && bodyB.parentNode.isStatic || bodyA.parentNode === bodyB.parentNode)
							continue;
						if (!canCollide(bodyA.parentNode.collisionFilter, bodyB.parentNode.collisionFilter))
							continue;
	
	
						const boundsA = bodyA.bounds;
						const boundsB = bodyB.bounds;
						
						if (boundsA.min.x <= boundsB.max.x &&
							boundsA.max.x >= boundsB.min.x &&
							boundsA.min.y <= boundsB.max.y &&
							boundsA.max.y >= boundsB.min.y) {
							curPairs.push([ bodyA, bodyB ]);
						}
					}
				}
			}

			for (let j = 0; j < curPairs.length; j++) {
				let curPair = curPairs[j];
				let n = pair(curPair[0].id, curPair[1].id);
				if (!pairIds.has(n)) {
					pairIds.add(n);
					pairs.push(curPair);
				}
			}
		}

		return pairs;
	}

	addChild(...children) {
		super.addChild(...children);

		for (let child of children) {
			// Add to engine
			if (child instanceof RigidBody) {
				this.rigidBodies.add(child);

				for (let rigidChild of child.children) {
					if (rigidChild instanceof CollisionShape) {
						// Add to grids
						if (child.isStatic) {
							this.staticGrid.addBody(rigidChild);
						}
						else {
							this.dynamicGrid.addBody(rigidChild);
						}
					}
				}
			}
		}
	}
	removeChild(...children) {
		super.removeChild(...children);

		for (let child of children) {
			// Remove from engine
			if (child instanceof RigidBody) {
				this.rigidBodies.delete(child);

				for (let child2 of child.children) {
					// Remove from grids
					if (child2._Grids) {
						if (child2._Grids[this.staticGrid.id]) {
							this.staticGrid.removeBody(child2);
						}
						if (child2._Grids[this.dynamicGrid.id]) {
							this.dynamicGrid.removeBody(child2);
						}
					}
				}
			}
			
		}
	}
}
module.exports = World;