node_Node.js

"use strict";

const vec = require("../geometry/vec.js");
const Common = require("../core/Common.js");

/**
 * A generic node object
 * ## Events
 * | Name | Description | Arguments |
 * | ---- | ----------- | --------- |
 * | add | Node is added to the world | None |
 * | delete | Node is removed from the world | None |
 */
class Node {
	static id = 0;
	/**
	 * Generates a unique id for nodes
	 * @return {number} A unique integer id
	*/
	static getUniqueId() {
		return ++Node.id;
	}
	
	/**
	 * Type of node, ie `Node` or `RigidBody`
	 * @readonly
	 */
	nodeType = "Node";

	/**
	 * Position of the node
	 * @type {vec}
	 * @readonly
	 * @todo Implement getPosition method and make this private
	 */
	position = new vec(0, 0);
	/**
	 * Angle, in radians
	 * @type {number}
	 * @readonly
	 * @todo Implement getAngle method and make this private
	 */
	angle = 0;
	/**
	 * Children of the node
	 * To modify, use `addChild` or `removeChild`.
	 * @readonly
	 * @type {Set}
	 */
	children = new Set();
	/**
	 * If the node is added to the game world. 
	 * @private
	 * @type {boolean}
	 */
	#added = false;
	
	/**
	 * Creates a Node with the given position
	 */
	constructor(position = new vec(0, 0)) {
		this.id = Node.getUniqueId();
		this.position = new vec(position);
	}
	
	/**
	 * Adds this node and its children, triggering the `add` event
	 * @returns {Node} `this`
	 */
	add() {
		if (!this.#added) {
			this.trigger("add");
			this.#added = true;

			for (let child of this.children) {
				child.add();
			}
		}
		return this;
	}
	/**
	 * Removes this node and its children, triggering the `delete` event
	 * @returns {Node} `this`
	 */
	delete() {
		if (this.#added) {
			this.trigger("delete");
			this.#added = false;
	
			for (let child of this.children) {
				child.delete();
			}
		}
		return this;
	}

	/**
	 * Gets if the node is added
	 * @returns {Boolean} if the node is added
	 */
	isAdded() {
		return this.#added;
	}

	/**
	 * Adds all `children` to this node's children
	 * @param {...Node} children - Children added
	 * @example
	 * let parentNode = new Node();
	 * let childNode = new Node();
	 * node.addChild(childNode);
	 */
	addChild(...children) {
		for (let child of children) {
			this.children.add(child);
		}
	}
	/**
	 * Removes all `children` from this node's children
	 * @param {...Node} children - Children removed
	 * @example
	 * let parentNode = new Node();
	 * let childNode = new Node();
	 * node.addChild(childNode); // node.children: Set {childNode}
	 * node.removeChild(childNode); // node.children: Set {}
	 */
	removeChild(...children) {
		for (let child of children) {
			this.children.delete(child);
		}
	}
	
	/**
	 * Sets this node's position to `position`
	 * @example
	 * node.setPosition(new vec(100, 100)); // Sets node's position to (100, 100) 
	 * @param {vec} position - Position the node should be set to
	*/
	setPosition(position) {
		if (!position instanceof vec) throw new Error("position must be a vec");
		let delta = position.sub(this.position);
		this.translate(delta);
	}
	/**
	 * Shifts this node's position by `positionDelta`
	 * @param {vec} positionDelta - Amount to shift the position
	 */
	translate(positionDelta) {
		if (!positionDelta instanceof vec) throw new Error("positionDelta must be a vec");
		this.position.add2(positionDelta);
		for (let child of this.children) {
			child.translate(positionDelta);
		}
	}
	
	/**
	 * Sets the node's angle to `angle`
	 * @param {number} angle - Angle body should be in radians
	 * @example
	 * node.setAngle(Math.PI); // Sets node's angle to Pi radians, or 180 degrees
	 */
	setAngle(angle, pivot = this.position) {
		if (isNaN(angle)) return;
		if (angle !== this.angle) {
			let delta = Common.angleDiff(angle, this.angle);
			this.translateAngle(delta, pivot);
		}
	}
	
	/**
	 * Rotates the body by `angle`- Relative
	 * @param {number} angle -Amount the body should be rotated, in radians
	 */
	translateAngle(angle, pivot = this.position, pivotPosition = true) {
		if (isNaN(angle)) return;

		this.angle += angle;

		if (pivotPosition) {
			let sin = Math.sin(angle);
			let cos = Math.cos(angle);
			let dist = this.position.sub(pivot);
			let newPosition = new vec((dist.x * cos - dist.y * sin), (dist.x * sin + dist.y * cos)).add(pivot);
			this.setPosition(newPosition);
		}

		for (let child of this.children) {
			child.translateAngle?.(angle, pivot);
		}
	}

	
	#events = {
		delete: [],
		add: [],
	}
	/**
	 * Bind a callback to an event
	 * @param {string} event - Name of the event
	 * @param {Function} callback - Callback run when event is fired
	 */
	on(event, callback) {
		if (this.#events[event]) {
			this.#events[event].push(callback);
		}
		else {
			console.warn(event + " is not a valid event");
		}
	}
	/**
	 * Unbinds a callback from an event
	 * @param {string} event - Name of the event
	 * @param {Function} callback - Function to unbind
	 */
	off(event, callback) {
		let events = this.#events[event];
		if (events.includes(callback)) {
			events.splice(events.indexOf(callback), 1);
		}
	}
	/**
	 * Triggers an event, firing all bound callbacks
	 * @param {string} event - Name of the event
	 * @param {...*} args - Arguments passed to callbacks
	 */
	trigger(event, ...args) {
		// Trigger each event
		if (this.#events[event]) {
			this.#events[event].forEach(callback => {
				callback(...args);
			});
		}
	}
}
module.exports = Node;