core_Ticker.js

const Common = require("../core/Common.js");
const Animation = require("../other/Animation.js");

/**
 * A game ticker that handles updating the engine every frame.
 * 
 * ## Events
 * | Name | Description | Arguments |
 * | ---- | ----------- | --------- |
 * | beforeTick | Triggered at the start of every frame | None |
 * | afterTick | Triggered at the end of every frame | None |
 */
class Ticker {
	static defaultOptions = {
		enabled: true,
		pauseOnFreeze: true,
		freezeThreshold: 0.3,
	}

	#enabled = true;

	/**
	 * Creates a ticker that updates [Game](./Game.html) every frame.
	 * @param {Game} Game - Game ticker should be run on
	 * @param {Object} options - Options object
	 * @param {boolean} [options.enabled=true] - If the ticker runs. To start the ticker again, use `ticker.run()`
	 * @param {boolean} [options.pauseOnFreeze=true] - If the ticker should pause when the game freezes. Helps prevent jumping when user switches tabs.
	 * @param {number} [options.freezeThreshold=0.3] - The threshold before the game pauses **between 0 and 1**. Higher values means the fps doesn't have to dip as low for the ticker to pause.
	 */
	constructor(Game, options = {}) {
		let defaults = { ...Ticker.defaultOptions };
		Common.merge(defaults, options, 1);
		options = defaults;
		
		this.Game = Game;
		this.#enabled = options.enabled;
		this.pauseOnFreeze   = options.pauseOnFreeze;
		this.freezeThreshold = options.freezeThreshold;

		this.tick = this.tick.bind(this);
		if (this.#enabled) {
			window.addEventListener("load", this.tick);
		}
	}
	/**
	 * Starts a stopped ticker
	 */
	start() {
		if (this.#enabled) return;
		this.#enabled = true;
		this.tick();
	}
	/**
	 * Stops a running ticker. This will stop physics, performance, and animation updates, but will not stop the renderer.
	 */
	stop() {
		if (!this.#enabled) return;
		this.#enabled = false;
	}
	tick() {
		if (!this.#enabled) return;

		this.trigger("beforeTick");

		const { Engine } = this.Game;
		const { Performance } = Engine;
		if (this.pauseOnFreeze && Performance.fps / Math.max(1, Performance.history.avgFps) < this.freezeThreshold) {
			Performance.update();
		}
		else {
			Engine.update();
			// animations.run();
		}

		Animation.update();
		this.trigger("afterTick");
		requestAnimationFrame(this.tick);
		// setTimeout(this.tick, 16);
	}
	
	#events = {
		beforeTick: [],
		afterTick: [],
	}
	/**
	 * Binds a function to an event
	 * @param {("beforeTick"|"afterTick")} event - Name of the event
	 * @param {function} callback - Function called when event fires
	 */
	on(event, callback) {
		if (this.#events[event]) {
			this.#events[event].push(callback);
		}
		else {
			console.warn(event + " is not a valid event");
		}
	}
	/**
	 * Unbinds a function from an event
	 * @param {("beforeTick"|"afterTick")} event - Name of the event
	 * @param {function} callback - Function bound to event
	 */
	off(event, callback) {
		event = this.#events[event];
		if (event.includes(callback)) {
			event.splice(event.indexOf(callback), 1);
		}
	}
	/**
	 * Fires an event
	 * @param {("beforeTick"|"afterTick")} event - Name of the event
	 */
	trigger(event) {
		// Trigger each event
		if (this.#events[event]) {
			this.#events[event].forEach(callback => {
				callback();
			});
		}
	}
}
module.exports = Ticker;