diff --git a/sandbox/script.js b/sandbox/script.js index bbe71e4..a758fc9 100644 --- a/sandbox/script.js +++ b/sandbox/script.js @@ -3,13 +3,14 @@ import { FastEngine, PreciseEngine } from "../scripts/modules/generators.js"; import { Color } from "../scripts/modules/palette.js"; import { ArchiveManager } from "../scripts/modules/storage.js"; -import { CycleTypes, RenderEvent, Settings, board } from "../scripts/structure.js"; +import { Ability, CycleTypes, Elemental, RenderEvent, Settings } from "../scripts/structure.js"; //#region Initialize const canvas = await window.ensure(() => document.getElement(HTMLCanvasElement, `canvas#display`)); const context = await window.ensure(() => canvas.getContext(`2d`) ?? (() => { throw new EvalError(`Unable to get context`); })()); +const board = Elemental.Board.self; const inputTogglePlay = await window.ensure(() => document.getElement(HTMLInputElement, `input#toggle-play`)); const buttonReloadBoard = await window.ensure(() => document.getElement(HTMLButtonElement, `button#reload-board`)); const buttonCaptureCanvas = await window.ensure(() => document.getElement(HTMLButtonElement, `button#capture-canvas`)); @@ -28,14 +29,14 @@ window.addEventListener(`resize`, (event) => { }); window.dispatchEvent(new UIEvent(`resize`)); -await window.ensure(() => Promise.withSignal((signal, resolve, reject) => { +await window.load(Promise.withSignal((signal, resolve, reject) => window.ensure(() => { const scriptElements = document.createElement(`script`); scriptElements.type = `module`; scriptElements.src = `../scripts/elements.js`; scriptElements.addEventListener(`load`, (event) => resolve(undefined), { signal }); scriptElements.addEventListener(`error`, (event) => reject(event.error), { signal }); document.head.appendChild(scriptElements); -})); +}))); engine.limit = settings.FPSLimit; board.addEventListener(`generate`, (event) => { @@ -49,24 +50,26 @@ window.addEventListener(`resize`, (event) => { engine.addEventListener(`update`, (event) => { board.dispatchEvent(new RenderEvent(`render`, { context })); }); -engine.addEventListener(`update`, async (event) => { - if (!board.dispatchEvent(new Event(`execute`, { cancelable: true }))) { - engine.launched = false; - const repeat = await (async () => { - switch (settings.cycleType) { - case CycleTypes.break: return false; - case CycleTypes.ask: return await window.confirmAsync(`Elements have no more moves. Do you want to reload the board?`); - case CycleTypes.loop: return true; - default: throw new EvalError(`Invalid '${settings.cycleType}' cycle type`); - } - })(); - if (repeat) { - board.dispatchEvent(new Event(`generate`)); - engine.launched = true; +engine.addEventListener(`update`, (event) => window.ensure(async () => { + const invoker = new Ability.Invoker(); + invoker.observe(); + board.execute(invoker); + if (invoker.summarize()) return; + engine.launched = false; + const repeat = await (async () => { + switch (settings.cycleType) { + case CycleTypes.break: return false; + case CycleTypes.ask: return await window.confirmAsync(`Elements have no more moves. Do you want to reload the board?`); + case CycleTypes.loop: return true; + default: throw new EvalError(`Invalid '${settings.cycleType}' cycle type`); } - inputTogglePlay.checked = engine.launched; + })(); + if (repeat) { + board.dispatchEvent(new Event(`generate`)); + engine.launched = true; } -}); + inputTogglePlay.checked = engine.launched; +})); inputTogglePlay.addEventListener(`change`, (event) => { engine.launched = inputTogglePlay.checked; @@ -75,12 +78,12 @@ buttonReloadBoard.addEventListener(`click`, (event) => { board.dispatchEvent(new Event(`generate`)); }); -buttonCaptureCanvas.addEventListener(`click`, (event) => { +buttonCaptureCanvas.addEventListener(`click`, (event) => window.insure(() => { canvas.toBlob((blob) => { if (blob === null) throw new EvalError(`Unable to convert canvas for capture`); navigator.download(new File([blob], `${Date.now()}.png`, { type: `png` })); }); -}); +})); //#endregion //#region Aside divFPSCounter.hidden = !settings.showFPS; diff --git a/scripts/elements.js b/scripts/elements.js index 2240a28..5c9593b 100644 --- a/scripts/elements.js +++ b/scripts/elements.js @@ -6,477 +6,367 @@ import { Random } from "./modules/generators.js"; import { Point2D } from "./modules/measures.js"; import { Color } from "./modules/palette.js"; -import { Ability, Elemental, board } from "./structure.js"; +import { Ability, Elemental } from "./structure.js"; + +const random = Random.global; +const board = Elemental.Board.self; //#region Dirt class Dirt extends Elemental { - /** - * @readonly - * @returns {string} - */ - static get name() { - return `Dirt`; - } - /** - * @readonly - * @returns {Color} - */ - static get color() { - return Color.viaRGB(150, 100, 80); - } - /** - * @readonly - * @returns {Readonly} - */ - static get abilities() { - return Object.freeze([]); - } - /** - * @readonly - * @returns {Readonly} - */ - get abilities() { - return Object.freeze([]); - } + /** @type {string} */ + static #name = `Dirt`; + /** @readonly @returns {string} */ + static get name() { return Dirt.#name; } + + /** @type {Readonly} */ + static #color = Object.freeze(Color.viaRGB(150, 100, 80)); + /** @readonly @returns {Readonly} */ + static get color() { return Dirt.#color; } + + /** @type {Readonly} */ + static #metadata = Object.freeze([]); + /** @readonly @returns {Readonly} */ + static get metadata() { return Dirt.#metadata; } + + /** @type {Readonly} */ + #abilities = Object.freeze([]); + /** @readonly @returns {Readonly} */ + get abilities() { return this.#abilities; } } board.cases.set(Dirt, 90); //#endregion //#region Grass class Grass extends Elemental { - /** - * @readonly - * @returns {string} - */ - static get name() { - return `Grass`; - } - /** - * @readonly - * @returns {Color} - */ - static get color() { - return Color.viaRGB(0, 128, 0); - } + /** @type {string} */ + static #name = `Grass`; + /** @readonly @returns {string} */ + static get name() { return Grass.#name; } + + /** @type {Readonly} */ + static #color = Object.freeze(Color.viaRGB(0, 128, 0)); + /** @readonly @returns {Readonly} */ + static get color() { return Grass.#color; } + /** @type {AbilityMetadata} */ static #metaGrow = new Ability.Metadata(`Grow`, `Description`, 10); - /** - * @readonly - * @returns {Readonly} - */ - static get abilities() { - return Object.freeze([Grass.#metaGrow]); - } - constructor() { - super(); - const random = Random.global; - - this.addEventListener(`spawn`, (event) => { - const board = this.board; + /** @type {Readonly} */ + static #metadata = Object.freeze([Grass.#metaGrow]); + /** @readonly @returns {Readonly} */ + static get metadata() { return Grass.#metadata; } - this.#grow.addEventListener(`execute`, (event) => { - const positions = [ - new Point2D(this.position.x - 1, this.position.y - 1), - new Point2D(this.position.x, this.position.y - 1), - new Point2D(this.position.x + 1, this.position.y - 1), - new Point2D(this.position.x - 1, this.position.y), - new Point2D(this.position.x + 1, this.position.y), - new Point2D(this.position.x - 1, this.position.y + 1), - new Point2D(this.position.x, this.position.y + 1), - new Point2D(this.position.x + 1, this.position.y + 1) - ]; - const targets = board.getElementsOfType(positions, Dirt); - if (targets.length > 0) { - const target = random.item(targets); - board.spawnElementOfType(target.position, Grass); - } else event.preventDefault(); - }); - }); - } /** @type {Ability} */ - #grow = new Ability(Grass.#metaGrow); - /** - * @readonly - * @returns {Readonly} - */ - get abilities() { - return Object.freeze([this.#grow]); - } + #grow = new Ability(Grass.#metaGrow, (invoker) => { + const positions = [ + new Point2D(this.position.x - 1, this.position.y - 1), + new Point2D(this.position.x, this.position.y - 1), + new Point2D(this.position.x + 1, this.position.y - 1), + new Point2D(this.position.x - 1, this.position.y), + new Point2D(this.position.x + 1, this.position.y), + new Point2D(this.position.x - 1, this.position.y + 1), + new Point2D(this.position.x, this.position.y + 1), + new Point2D(this.position.x + 1, this.position.y + 1) + ]; + const targets = board.getElementsOfType(positions, Dirt); + if (targets.length > 0) { + const target = random.item(targets); + board.spawnElementOfType(target.position, Grass); + invoker.change(); + } + }); + /** @type {Readonly} */ + #abilities = Object.freeze([this.#grow]); + /** @readonly @returns {Readonly} */ + get abilities() { return this.#abilities; } } board.cases.set(Grass, 4); //#endregion //#region Fire class Fire extends Elemental { - /** - * @readonly - * @returns {string} - */ - static get name() { - return `Fire`; - } - /** - * @readonly - * @returns {Color} - */ - static get color() { - return Color.viaRGB(255, 150, 0); - } + /** @type {string} */ + static #name = `Fire`; + /** @readonly @returns {string} */ + static get name() { return Fire.#name; } + + /** @type {Readonly} */ + static #color = Object.freeze(Color.viaRGB(255, 150, 0)); + /** @readonly @returns {Readonly} */ + static get color() { return Fire.#color; } + /** @type {AbilityMetadata} */ static #metaBurn = new Ability.Metadata(`Burn`, `Description`, 4); /** @type {AbilityMetadata} */ static #metaFade = new Ability.Metadata(`Fade`, `Description`, 16); - /** - * @readonly - * @returns {Readonly} - */ - static get abilities() { - return Object.freeze([Fire.#metaBurn, Fire.#metaFade]); - } - constructor() { - super(); - const random = Random.global; + /** @type {Readonly} */ + static #metadata = Object.freeze([Fire.#metaBurn, Fire.#metaFade]); + /** @readonly @returns {Readonly} */ + static get metadata() { return Fire.#metadata; } - this.addEventListener(`spawn`, (event) => { - const board = this.board; - - this.#burn.addEventListener(`execute`, (event) => { - const positions = [ - new Point2D(this.position.x, this.position.y - 1), - new Point2D(this.position.x - 1, this.position.y), - new Point2D(this.position.x + 1, this.position.y), - new Point2D(this.position.x, this.position.y + 1), - ]; - const targets = board.getElementsOfType(positions, Grass); - if (targets.length > 0) { - const target = random.item(targets); - board.spawnElementOfType(target.position, Fire); - this.#fade.progress = 0; - } else event.preventDefault(); - }); - - this.#fade.addEventListener(`execute`, (event) => { - board.spawnElementOfType(this.position, Dirt); - event.preventDefault(); - }); - }); - } /** @type {Ability} */ - #burn = new Ability(Fire.#metaBurn); + #burn = new Ability(Fire.#metaBurn, (invoker) => { + const positions = [ + new Point2D(this.position.x, this.position.y - 1), + new Point2D(this.position.x - 1, this.position.y), + new Point2D(this.position.x + 1, this.position.y), + new Point2D(this.position.x, this.position.y + 1), + ]; + const targets = board.getElementsOfType(positions, Grass); + if (targets.length > 0) { + const target = random.item(targets); + board.spawnElementOfType(target.position, Fire); + this.#fade.progress = 0; + invoker.change(); + } + }); /** @type {Ability} */ - #fade = new Ability(Fire.#metaFade); - /** - * @readonly - * @returns {Readonly} - */ - get abilities() { - return Object.freeze([this.#burn, this.#fade]); - } + #fade = new Ability(Fire.#metaFade, (invoker) => { + board.spawnElementOfType(this.position, Dirt); + }); + /** @type {Readonly} */ + #abilities = Object.freeze([this.#burn, this.#fade]); + /** @readonly @returns {Readonly} */ + get abilities() { return this.#abilities; } } board.cases.set(Fire, 2); //#endregion //#region Water class Water extends Elemental { - /** - * @readonly - * @returns {string} - */ - static get name() { - return `Water`; - } - /** - * @readonly - * @returns {Color} - */ - static get color() { - return Color.viaRGB(0, 50, 255); - } + /** @type {string} */ + static #name = `Water`; + /** @readonly @returns {string} */ + static get name() { return Water.#name; } + + /** @type {Readonly} */ + static #color = Object.freeze(Color.viaRGB(0, 50, 255)); + /** @readonly @returns {Readonly} */ + static get color() { return Water.#color; } + /** @type {AbilityMetadata} */ static #metaFlow = new Ability.Metadata(`Flow`, `Description`, 8); /** @type {AbilityMetadata} */ static #metaEvaporate = new Ability.Metadata(`Evaporate`, `Description`, 8); - /** - * @readonly - * @returns {Readonly} - */ - static get abilities() { - return Object.freeze([Water.#metaFlow, Water.#metaEvaporate]); - } - constructor() { - super(); - const random = Random.global; - - this.addEventListener(`spawn`, (event) => { - const board = this.board; + /** @type {Readonly} */ + static #metadata = Object.freeze([Water.#metaFlow, Water.#metaEvaporate]); + /** @readonly @returns {Readonly} */ + static get metadata() { return Water.#metadata; } - this.#flow.addEventListener(`execute`, (event) => { - const positions = [ - new Point2D(this.position.x - 1, this.position.y - 1), - new Point2D(this.position.x, this.position.y - 1), - new Point2D(this.position.x + 1, this.position.y - 1), - new Point2D(this.position.x - 1, this.position.y), - new Point2D(this.position.x + 1, this.position.y), - new Point2D(this.position.x - 1, this.position.y + 1), - new Point2D(this.position.x, this.position.y + 1), - new Point2D(this.position.x + 1, this.position.y + 1) - ]; - const targets = board.getElementsOfType(positions, Dirt); - if (targets.length > 0) { - const target = random.item(targets); - board.spawnElementOfType(target.position, Water); - } else event.preventDefault(); - }); - - this.#evaporate.addEventListener(`execute`, (event) => { - const positions = [ - new Point2D(this.position.x, this.position.y - 1), - new Point2D(this.position.x - 1, this.position.y), - new Point2D(this.position.x + 1, this.position.y), - new Point2D(this.position.x, this.position.y + 1), - ]; - const targets = board.getElementsOfType(positions, Fire); - if (targets.length > 0) { - const target = random.item(targets); - board.spawnElementOfType(target.position, Dirt); - board.spawnElementOfType(this.position, Dirt); - } else event.preventDefault(); - }); - }); - } /** @type {Ability} */ - #flow = new Ability(Water.#metaFlow); + #flow = new Ability(Water.#metaFlow, (invoker) => { + const positions = [ + new Point2D(this.position.x - 1, this.position.y - 1), + new Point2D(this.position.x, this.position.y - 1), + new Point2D(this.position.x + 1, this.position.y - 1), + new Point2D(this.position.x - 1, this.position.y), + new Point2D(this.position.x + 1, this.position.y), + new Point2D(this.position.x - 1, this.position.y + 1), + new Point2D(this.position.x, this.position.y + 1), + new Point2D(this.position.x + 1, this.position.y + 1) + ]; + const targets = board.getElementsOfType(positions, Dirt); + if (targets.length > 0) { + const target = random.item(targets); + board.spawnElementOfType(target.position, Water); + invoker.change(); + } + }); /** @type {Ability} */ - #evaporate = new Ability(Water.#metaEvaporate); - /** - * @readonly - * @returns {Readonly} - */ - get abilities() { - return Object.freeze([this.#flow, this.#evaporate]); - } + #evaporate = new Ability(Water.#metaEvaporate, (invoker) => { + const positions = [ + new Point2D(this.position.x, this.position.y - 1), + new Point2D(this.position.x - 1, this.position.y), + new Point2D(this.position.x + 1, this.position.y), + new Point2D(this.position.x, this.position.y + 1), + ]; + const targets = board.getElementsOfType(positions, Fire); + if (targets.length > 0) { + const target = random.item(targets); + board.spawnElementOfType(target.position, Dirt); + board.spawnElementOfType(this.position, Dirt); + invoker.change(); + } + }); + /** @type {Readonly} */ + #abilities = Object.freeze([this.#flow, this.#evaporate]); + /** @readonly @returns {Readonly} */ + get abilities() { return this.#abilities; } } board.cases.set(Water, 2); //#endregion //#region Lava class Lava extends Elemental { - /** - * @readonly - * @returns {string} - */ - static get name() { - return `Lava`; - } - /** - * @readonly - * @returns {Color} - */ - static get color() { - return Color.viaRGB(255, 0, 0); - } + /** @type {string} */ + static #name = `Lava`; + /** @readonly @returns {string} */ + static get name() { return Lava.#name; } + + /** @type {Readonly} */ + static #color = Object.freeze(Color.viaRGB(255, 0, 0)); + /** @readonly @returns {Readonly} */ + static get color() { return Lava.#color; } + /** @type {AbilityMetadata} */ static #metaFlow = new Ability.Metadata(`Flow`, `Description`, 15); /** @type {AbilityMetadata} */ static #metaBurn = new Ability.Metadata(`Burn`, `Description`, 8); /** @type {AbilityMetadata} */ static #metaFade = new Ability.Metadata(`Fade`, `Description`, 4); - /** - * @readonly - * @returns {Readonly} - */ - static get abilities() { - return Object.freeze([Lava.#metaFlow, Lava.#metaBurn, Lava.#metaFade]); - } + /** @type {Readonly} */ + static #metadata = Object.freeze([Lava.#metaFlow, Lava.#metaBurn, Lava.#metaFade]); + /** @readonly @returns {Readonly} */ + static get metadata() { return Lava.#metadata; } /** @type {number} */ static #maxDensity = 3; - constructor() { - super(); - const random = Random.global; - - this.addEventListener(`spawn`, (event) => { - const board = this.board; - - this.#flow.addEventListener(`execute`, (event) => { - const positions = [ - new Point2D(this.position.x, this.position.y - 1), - new Point2D(this.position.x - 1, this.position.y), - new Point2D(this.position.x + 1, this.position.y), - new Point2D(this.position.x, this.position.y + 1), - ]; - const targets = board.getElementsOfType(positions, Dirt); - if (targets.length > 0 && this.#density > 0) { - const target = random.item(targets); - const descendant = board.spawnElementOfType(target.position, Lava); - descendant.#density = this.#density - 1; - } else event.preventDefault(); - }); - this.#burn.addEventListener(`execute`, (event) => { - const positions = [ - new Point2D(this.position.x, this.position.y - 1), - new Point2D(this.position.x - 1, this.position.y), - new Point2D(this.position.x + 1, this.position.y), - new Point2D(this.position.x, this.position.y + 1), - ]; - const targets = board.getElementsOfType(positions, Grass); - if (targets.length > 0) { - const target = random.item(targets); - board.spawnElementOfType(target.position, Fire); - } else event.preventDefault(); - }); - - this.#fade.addEventListener(`execute`, (event) => { - const positions = [ - new Point2D(this.position.x, this.position.y - 1), - new Point2D(this.position.x - 1, this.position.y), - new Point2D(this.position.x + 1, this.position.y), - new Point2D(this.position.x, this.position.y + 1), - ]; - const targets = board.getElementsOfType(positions, Water); - if (targets.length > 0) { - const target = random.item(targets); - board.spawnElementOfType(target.position, Dirt); - if (this.#density > 0) { - this.#density--; - } else { - board.spawnElementOfType(this.position, Fire); - } - } event.preventDefault(); - }); - }); - } - /** - * @readonly - * @returns {Color} - */ + /** @readonly @returns {Color} */ get color() { return Color.mix(Fire.color, Lava.color, this.#density / Lava.#maxDensity); } /** @type {number} */ #density = Lava.#maxDensity; /** @type {Ability} */ - #flow = new Ability(Lava.#metaFlow); + #flow = new Ability(Lava.#metaFlow, (invoker) => { + const positions = [ + new Point2D(this.position.x, this.position.y - 1), + new Point2D(this.position.x - 1, this.position.y), + new Point2D(this.position.x + 1, this.position.y), + new Point2D(this.position.x, this.position.y + 1), + ]; + const targets = board.getElementsOfType(positions, Dirt); + if (targets.length > 0 && this.#density > 1) { + const target = random.item(targets); + const descendant = board.spawnElementOfType(target.position, Lava); + descendant.#density = this.#density - 1; + invoker.change(); + } + }); /** @type {Ability} */ - #burn = new Ability(Lava.#metaBurn); + #burn = new Ability(Lava.#metaBurn, (invoker) => { + const positions = [ + new Point2D(this.position.x, this.position.y - 1), + new Point2D(this.position.x - 1, this.position.y), + new Point2D(this.position.x + 1, this.position.y), + new Point2D(this.position.x, this.position.y + 1), + ]; + const targets = board.getElementsOfType(positions, Grass); + if (targets.length > 0) { + const target = random.item(targets); + board.spawnElementOfType(target.position, Fire); + invoker.change(); + } + }); /** @type {Ability} */ - #fade = new Ability(Lava.#metaFade); - /** - * @readonly - * @returns {Readonly} - */ - get abilities() { - return Object.freeze([this.#flow, this.#burn, this.#fade]); - } + #fade = new Ability(Lava.#metaFade, (invoker) => { + const positions = [ + new Point2D(this.position.x, this.position.y - 1), + new Point2D(this.position.x - 1, this.position.y), + new Point2D(this.position.x + 1, this.position.y), + new Point2D(this.position.x, this.position.y + 1), + ]; + const targets = board.getElementsOfType(positions, Water); + if (targets.length > 0) { + const target = random.item(targets); + board.spawnElementOfType(target.position, Dirt); + if (this.#density > 0) { + this.#density--; + } else { + board.spawnElementOfType(this.position, Fire); + } + invoker.change(); + } + }); + /** @type {Readonly} */ + #abilities = Object.freeze([this.#flow, this.#burn, this.#fade]); + /** @readonly @returns {Readonly} */ + get abilities() { return this.#abilities; } } board.cases.set(Lava, 1); //#endregion //#region Ice class Ice extends Elemental { - /** - * @readonly - * @returns {string} - */ - static get name() { - return `Ice`; - } - /** - * @readonly - * @returns {Color} - */ - static get color() { - return Color.viaRGB(0, 200, 255); - } + /** @type {string} */ + static #name = `Ice`; + /** @readonly @returns {string} */ + static get name() { return Ice.#name; } + + /** @type {Readonly} */ + static #color = Object.freeze(Color.viaRGB(0, 200, 255)); + /** @readonly @returns {Readonly} */ + static get color() { return Ice.#color; } + /** @type {AbilityMetadata} */ static #metaFlow = new Ability.Metadata(`Flow`, `Description`, 12); /** @type {AbilityMetadata} */ static #metaMelt = new Ability.Metadata(`Melt`, `Description`, 4); /** @type {AbilityMetadata} */ static #metaEvaporate = new Ability.Metadata(`Evaporate`, `Description`, 4); - /** - * @readonly - * @returns {Readonly} - */ - static get abilities() { - return Object.freeze([Ice.#metaFlow, Ice.#metaMelt, Ice.#metaEvaporate]); - } + /** @type {Readonly} */ + static #metadata = Object.freeze([Ice.#metaFlow, Ice.#metaMelt, Ice.#metaEvaporate]); + /** @readonly @returns {Readonly} */ + static get metadata() { return Ice.#metadata; } /** @type {number} */ static #maxDensity = 3; - constructor() { - super(); - const random = Random.global; - - this.addEventListener(`spawn`, (event) => { - const board = this.board; - - this.#flow.addEventListener(`execute`, (event) => { - const positions = [ - new Point2D(this.position.x, this.position.y - 1), - new Point2D(this.position.x - 1, this.position.y), - new Point2D(this.position.x + 1, this.position.y), - new Point2D(this.position.x, this.position.y + 1), - ]; - const targets = board.getElementsOfType(positions, Dirt); - if (targets.length > 0 && this.#density > 0) { - const target = random.item(targets); - const descendant = board.spawnElementOfType(target.position, Ice); - descendant.#density = this.#density - 1; - } else event.preventDefault(); - }); - this.#melt.addEventListener(`execute`, (event) => { - const positions = [ - new Point2D(this.position.x, this.position.y - 1), - new Point2D(this.position.x - 1, this.position.y), - new Point2D(this.position.x + 1, this.position.y), - new Point2D(this.position.x, this.position.y + 1), - ]; - const targets = board.getElementsOfType(positions, Fire); - if (targets.length > 0) { - const target = random.item(targets); - board.spawnElementOfType(target.position, Dirt); - if (this.#density > 0) { - this.#density--; - } else { - board.spawnElementOfType(this.position, Water); - } - } event.preventDefault(); - }); - - this.#evaporate.addEventListener(`execute`, (event) => { - const positions = [ - new Point2D(this.position.x, this.position.y - 1), - new Point2D(this.position.x - 1, this.position.y), - new Point2D(this.position.x + 1, this.position.y), - new Point2D(this.position.x, this.position.y + 1), - ]; - const targets = board.getElementsOfType(positions, Lava); - if (targets.length > 0) { - const target = random.item(targets); - board.spawnElementOfType(target.position, Dirt); - board.spawnElementOfType(this.position, Dirt); - } event.preventDefault(); - }); - }); - } - /** - * @readonly - * @returns {Color} - */ + /** @readonly @returns {Color} */ get color() { return Color.mix(Water.color, Ice.color, this.#density / Ice.#maxDensity); } /** @type {number} */ #density = Ice.#maxDensity; /** @type {Ability} */ - #flow = new Ability(Ice.#metaFlow); + #flow = new Ability(Ice.#metaFlow, (invoker) => { + const positions = [ + new Point2D(this.position.x, this.position.y - 1), + new Point2D(this.position.x - 1, this.position.y), + new Point2D(this.position.x + 1, this.position.y), + new Point2D(this.position.x, this.position.y + 1), + ]; + const targets = board.getElementsOfType(positions, Dirt); + if (targets.length > 0 && this.#density > 1) { + const target = random.item(targets); + const descendant = board.spawnElementOfType(target.position, Ice); + descendant.#density = this.#density - 1; + invoker.change(); + } + }); /** @type {Ability} */ - #melt = new Ability(Ice.#metaMelt); + #melt = new Ability(Ice.#metaMelt, (invoker) => { + const positions = [ + new Point2D(this.position.x, this.position.y - 1), + new Point2D(this.position.x - 1, this.position.y), + new Point2D(this.position.x + 1, this.position.y), + new Point2D(this.position.x, this.position.y + 1), + ]; + const targets = board.getElementsOfType(positions, Fire); + if (targets.length > 0) { + const target = random.item(targets); + board.spawnElementOfType(target.position, Dirt); + if (this.#density > 0) { + this.#density--; + } else { + board.spawnElementOfType(this.position, Water); + } + invoker.change(); + } + }); /** @type {Ability} */ - #evaporate = new Ability(Ice.#metaEvaporate); - /** - * @readonly - * @returns {Readonly} - */ - get abilities() { - return Object.freeze([this.#flow, this.#melt, this.#evaporate]); - } + #evaporate = new Ability(Ice.#metaEvaporate, (invoker) => { + const positions = [ + new Point2D(this.position.x, this.position.y - 1), + new Point2D(this.position.x - 1, this.position.y), + new Point2D(this.position.x + 1, this.position.y), + new Point2D(this.position.x, this.position.y + 1), + ]; + const targets = board.getElementsOfType(positions, Lava); + if (targets.length > 0) { + const target = random.item(targets); + board.spawnElementOfType(target.position, Dirt); + board.spawnElementOfType(this.position, Dirt); + invoker.change(); + } + }); + /** @type {Readonly} */ + #abilities = Object.freeze([this.#flow, this.#melt, this.#evaporate]); + /** @readonly @returns {Readonly} */ + get abilities() { return this.#abilities; } } board.cases.set(Ice, 1); //#endregion diff --git a/scripts/structure.js b/scripts/structure.js index 78cfca1..6ef55a5 100644 --- a/scripts/structure.js +++ b/scripts/structure.js @@ -47,16 +47,19 @@ class RenderEvent extends Event { */ /** - * @typedef {Object} UncomposedAbilityEventMap - * @property {Event} execute - * - * @typedef {EventListener & UncomposedAbilityEventMap} AbilityEventMap + * @typedef {InstanceType} AbilityInvoker + */ + +/** + * @callback AbilityAction + * @param {AbilityInvoker} invoker + * @returns {void} */ /** * Represents an element ability. */ -class Ability extends EventTarget { +class Ability { //#region Metadata /** * Contains metadata about an ability. @@ -108,54 +111,69 @@ class Ability extends EventTarget { } }; //#endregion + //#region Invoker + /** + * Representing an ability invoker. + */ + static Invoker = class AbilityInvoker { + /** @type {boolean[]} */ + #stack = []; + /** + * Creates a new observation. + * @returns {void} + */ + observe() { + this.#stack.push(false); + } + /** + * Marks that changes have occurred to the last observation. + * @returns {void} + */ + change() { + for (let index = this.#stack.length - 1; index >= 0; index--) { + this.#stack[index] = true; + } + } + /** + * Summarizes changes of the last observation and removes it. + * @returns {boolean} Indicates whether any changes have occurred. + * @throws {EvalError} If there are no observable items. + */ + summarize() { + const observation = this.#stack.pop(); + if (observation === undefined) throw new EvalError(`There are no observable items`); + return observation; + } + }; + //#endregion /** * @param {AbilityMetadata} metadata Metadata for the ability. + * @param {AbilityAction} action Action of the ability. * @param {number} progress The initial progress of the ability. */ - constructor(metadata, progress = 0) { - super(); + constructor(metadata, action, progress = 0) { this.#preparation = metadata.preparation; + this.#action = action; this.#progress = progress; } + /** @type {AbilityAction} */ + #action; /** - * @template {keyof AbilityEventMap} K - * @param {K} type - * @param {(this: Ability, ev: AbilityEventMap[K]) => any} listener - * @param {boolean | AddEventListenerOptions} options + * Executes one frame for the ability. + * @param {AbilityInvoker} invoker The invoker that executes the ability. * @returns {void} */ - addEventListener(type, listener, options = false) { - // @ts-ignore - return super.addEventListener(type, listener, options); - } - /** - * @template {keyof AbilityEventMap} K - * @param {K} type - * @param {(this: Ability, ev: AbilityEventMap[K]) => any} listener - * @param {boolean | EventListenerOptions} options - * @returns {void} - */ - removeEventListener(type, listener, options = false) { - // @ts-ignore - return super.addEventListener(type, listener, options); - } - /** - * @param {Event} event - * @returns {boolean} - */ - dispatchEvent(event) { - if (event.type === `execute`) { - let moves = true; - if (this.#progress < this.#preparation) { - this.#progress++; - } else if (super.dispatchEvent(event)) { - this.#progress = 0; - } else { - moves = false; - } - return moves; - } else return super.dispatchEvent(event); + execute(invoker) { + if (this.#progress < this.#preparation) { + this.#progress++; + invoker.change(); + } else { + invoker.observe(); + this.#action(invoker); + if (!invoker.summarize()) return; + this.#progress = 0; + } } /** @type {number} */ #progress; @@ -196,26 +214,17 @@ class Ability extends EventTarget { /** * @typedef {Object} UncomposedElementalBoardEventMap * @property {Event} generate - * @property {Event} execute * @property {RenderEvent} render * @property {RenderEvent} repaint * * @typedef {EventListener & UncomposedElementalBoardEventMap} ElementalBoardEventMap */ -/** - * @typedef {Object} UncomposedElementalEventMap - * @property {Event} spawn - * @property {Event} execute - * - * @typedef {EventListener & UncomposedElementalEventMap} ElementalEventMap - */ - /** * Base class for all elements in board. * @abstract */ -class Elemental extends EventTarget { +class Elemental { /** * Gets the name of the element. * @abstract @@ -229,7 +238,7 @@ class Elemental extends EventTarget { * Gets the color of the element. * @abstract * @readonly - * @returns {Color} + * @returns {Readonly} */ static get color() { throw new ReferenceError(`Not implemented function`); @@ -240,7 +249,7 @@ class Elemental extends EventTarget { * @readonly * @returns {Readonly} */ - static get abilities() { + static get metadata() { throw new ReferenceError(`Not implemented function`); } @@ -249,81 +258,64 @@ class Elemental extends EventTarget { * Represents board for the elements. */ static Board = class ElementalBoard extends EventTarget { - + /** @type {ElementalBoard?} */ + static #self = null; + /** + * Gets the singleton instance of board. + * @readonly + * @returns {ElementalBoard} + * @throws {EvalError} If board isn't constructed yet. + */ + static get self() { + if (ElementalBoard.#self === null) throw new EvalError(`Board isn't constructed yet`); + return ElementalBoard.#self; + } /** * @param {Readonly} size The size of the board. + * @returns {void} + * @throws {EvalError} If board is already constructed. + */ + static construct(size) { + if (this.#self !== null) throw new EvalError(`Board is already constructed`); + ElementalBoard.#locked = false; + ElementalBoard.#self = new ElementalBoard(size); + ElementalBoard.#locked = true; + } + /** @type {boolean} */ + static #locked = true; + /** + * @param {Readonly} size The size of the board. + * @throws {TypeError} If the constructor is called directly. */ constructor(size) { super(); + if (ElementalBoard.#locked) throw new TypeError(`Illegal constructor`); this.#matrix = new Matrix(size, null); - const matrix = this.#matrix; const mapSearch = location.mapSearch; this.addEventListener(`generate`, (event) => window.ensure(() => { if (this.#cases.size === 0) throw new EvalError(`Unable to generate board. Cases map is empty`); + const random = Random.global; for (let y = 0; y < size.y; y++) { for (let x = 0; x < size.x; x++) { - const position = new Point2D(x, y); - const type = Random.global.case(this.#cases); - this.spawnElementOfType(position, type); + this.spawnElementOfType(new Point2D(x, y), random.case(this.#cases)); } } - if (!this.#isGenerated) { - this.#isGenerated = true; - } })); - this.addEventListener(`execute`, (event) => window.ensure(() => { - let moves = false; + this.addEventListener(`repaint`, ({ context }) => window.ensure(() => { + const canvas = context.canvas; + const scale = new Point2D(canvas.width / size.x, canvas.height / size.y); for (let y = 0; y < size.y; y++) { for (let x = 0; x < size.x; x++) { const position = new Point2D(x, y); - const element = matrix.get(position); - if (element !== null && element.dispatchEvent(new Event(`execute`, { cancelable: true }))) { - moves = true; - } + const element = this.#getElementAt(position); + context.fillStyle = element.color.toString(true); + context.fillRect(ceil(position.x * scale.x), ceil(position.y * scale.y), ceil(scale.x), ceil(scale.y)); } } - if (!moves) event.preventDefault(); })); - // /** - // * @param {CanvasRenderingContext2D} context - // * @param {Map>} blueprints - // */ - // function fill(context, blueprints) { - // const canvas = context.canvas; - // const scale = new Point2D(canvas.width / size.x, canvas.height / size.y); - // for (const [bluing, area] of blueprints) { - // context.fillStyle = bluing; - // context.beginPath(); - // for (const position of area) { - // context.rect(ceil(position.x * scale.x), ceil(position.y * scale.y), ceil(scale.x), ceil(scale.y)); - // } - // context.fill(); - // } - // } - - this.addEventListener(`repaint`, ({ context }) => { - this.dispatchEvent(new RenderEvent(`render`, { context })); - }); - - // this.addEventListener(`repaint`, ({ context }) => window.ensure(() => { - // /** @type {Map>} */ - // const blueprints = new Map(); - // for (let y = 0; y < size.y; y++) { - // for (let x = 0; x < size.x; x++) { - // const position = new Point2D(x, y); - // const element = this.#getElementAt(position); - // const bluing = element.color.toString(true); - // const area = blueprints.get(bluing) ?? (() => new Set())(); - // area.add(position); - // blueprints.set(bluing, area); - // } - // } - // fill(context, blueprints); - // })); - if (mapSearch.has(`render`)) { this.addEventListener(`render`, ({ context }) => { const canvas = context.canvas; @@ -341,30 +333,14 @@ class Elemental extends EventTarget { for (let x = 0; x < size.x; x++) { const position = new Point2D(x, y); const element = this.#getElementAt(position); - context.fillStyle = element.color.toString(true); - context.fillRect(ceil(position.x * scale.x), ceil(position.y * scale.y), ceil(scale.x), ceil(scale.y)); + if (element.#isRepainted) { + context.fillStyle = element.color.toString(true); + context.fillRect(ceil(position.x * scale.x), ceil(position.y * scale.y), ceil(scale.x), ceil(scale.y)); + element.#isRepainted = false; + } } } })); - - // this.addEventListener(`render`, ({ context }) => window.ensure(() => { - // /** @type {Map>} */ - // const blueprints = new Map(); - // for (let y = 0; y < size.y; y++) { - // for (let x = 0; x < size.x; x++) { - // const position = new Point2D(x, y); - // const element = this.#getElementAt(position); - // const bluing = element.color.toString(true); - // if (element.#isRepainted) { - // const area = blueprints.get(bluing) ?? (() => new Set())(); - // area.add(position); - // blueprints.set(bluing, area); - // element.#isRepainted = false; - // } - // } - // } - // fill(context, blueprints); - // })); } /** * @template {keyof ElementalBoardEventMap} K @@ -390,30 +366,20 @@ class Elemental extends EventTarget { } /** @type {Matrix} */ #matrix; - /** @type {boolean} */ - #isGenerated = false; /** - * Indicates whether the board has been generated. - * @readonly - * @returns {boolean} - */ - get isGenerated() { - return this.#isGenerated; - } - /** - * @param {Point2D} position + * @param {Readonly} position * @returns {boolean} * @throws {TypeError} If the coordinates of the position are not finite integer numbers. */ #outOfBounds(position) { const { x, y } = position; - const { x: xSize, y: ySize } = this.#matrix.size; if (!Number.isInteger(x)) throw new TypeError(`The x-coordinate of position ${position} must be finite integer number`); if (!Number.isInteger(y)) throw new TypeError(`The y-coordinate of position ${position} must be finite integer number`); + const { x: xSize, y: ySize } = this.#matrix.size; return (0 > x || x >= xSize || 0 > y || y >= ySize); } /** - * @param {Point2D} position + * @param {Readonly} position * @returns {Elemental} * @throws {TypeError} If the coordinates of the position are not finite integer numbers. * @throws {RangeError} If the coordinates of the position is out of bounds. @@ -425,7 +391,7 @@ class Elemental extends EventTarget { return element; } /** - * @param {Point2D} position + * @param {Readonly} position * @param {Elemental} element * @returns {void} * @throws {TypeError} If the coordinates of the position are not finite integer numbers. @@ -436,7 +402,7 @@ class Elemental extends EventTarget { } /** * Method to get the element at the specified position. - * @param {Point2D} position The position to get the element from. + * @param {Readonly} position The position to get the element from. * @returns {Elemental} The element at the specified position. * @throws {TypeError} If the coordinates of the position are not finite integer numbers. * @throws {RangeError} If the coordinates of the position is out of bounds. @@ -447,7 +413,7 @@ class Elemental extends EventTarget { } /** * Gets the elements at the specified positions. - * @param {Readonly} positions The positions to get the elements from. + * @param {Readonly[]} positions The positions to get the elements from. * @returns {Elemental[]} The elements at the specified positions. * @throws {TypeError} If the coordinates of the position are not finite integer numbers. * @throws {EvalError} If there is no element at the specified position. @@ -463,7 +429,7 @@ class Elemental extends EventTarget { /** * Gets the elements of the specified type at the specified positions. * @template {typeof Elemental} T - * @param {Readonly} positions The positions to get the elements from. + * @param {Readonly[]} positions The positions to get the elements from. * @param {T} type The type of elements to get. * @returns {InstanceType[]} The elements of the specified type at the specified positions. * @throws {TypeError} If the coordinates of the position are not finite integer numbers. @@ -483,7 +449,7 @@ class Elemental extends EventTarget { /** * Spawns an element of the specified type at the specified position. * @template {typeof Elemental} T - * @param {Point2D} position The position to spawn the element at. + * @param {Readonly} position The position to spawn the element at. * @param {T} type The type of element to spawn. * @returns {InstanceType} The spawned element. * @throws {TypeError} If the coordinates of the position are not finite integer numbers. @@ -493,15 +459,13 @@ class Elemental extends EventTarget { const element = Reflect.construct(type, []); this.#setElementAt(position, element); element.#position = position; - element.#board = this; element.#color = type.color; - element.dispatchEvent(new Event(`spawn`)); return (/** @type {InstanceType} */ (element)); } /** * Sets the element at the specified position. * @template {Elemental} T - * @param {Point2D} position The position to set the element at. + * @param {Readonly} position The position to set the element at. * @param {T} element The element to set. * @returns {T} The set element. * @throws {TypeError} If the coordinates of the position are not finite integer numbers. @@ -514,8 +478,8 @@ class Elemental extends EventTarget { } /** * Swaps the elements at the specified positions. - * @param {Point2D} position1 The first position. - * @param {Point2D} position2 The second position. + * @param {Readonly} position1 The first position. + * @param {Readonly} position2 The second position. * @returns {void} * @throws {TypeError} If the coordinates of the positions are not finite integer numbers. * @throws {RangeError} If the coordinates of the positions is out of bounds. @@ -537,75 +501,42 @@ class Elemental extends EventTarget { get cases() { return this.#cases; } - }; - //#endregion - - /** - * @template {keyof ElementalEventMap} K - * @param {K} type - * @param {(this: Elemental, ev: ElementalEventMap[K]) => any} listener - * @param {boolean | AddEventListenerOptions} options - * @returns {void} - */ - addEventListener(type, listener, options = false) { - // @ts-ignore - return super.addEventListener(type, listener, options); - } - /** - * @template {keyof ElementalEventMap} K - * @param {K} type - * @param {(this: Elemental, ev: ElementalEventMap[K]) => any} listener - * @param {boolean | EventListenerOptions} options - * @returns {void} - */ - removeEventListener(type, listener, options = false) { - // @ts-ignore - return super.addEventListener(type, listener, options); - } - /** - * @param {Event} event - * @returns {boolean} - */ - dispatchEvent(event) { - let result = false; - if (event.type === `execute`) { - for (const ability of this.abilities) { - if (ability.dispatchEvent(new Event(`execute`, { cancelable: true }))) { - result = true; + /** + * Executes one frame for the board. + * @param {AbilityInvoker} invoker The invoker that executes the board. + * @returns {void} + */ + execute(invoker) { + const size = this.#matrix.size; + for (let y = 0; y < size.y; y++) { + for (let x = 0; x < size.x; x++) { + const position = new Point2D(x, y); + const element = this.#matrix.get(position); + if (element === null) continue; + element.execute(invoker); } } } - return (super.dispatchEvent(event) && result); - } - /** @type {Point2D?} */ + }; + //#endregion + + /** @type {Readonly?} */ #position = null; /** * Gets the position of the element. * @readonly - * @returns {Point2D} + * @returns {Readonly} * @throws {EvalError} If the element has not been spawned yet. */ get position() { if (this.#position === null) throw new EvalError(`Element not spawned yet. Use it after 'spawn' event dispatched`); return this.#position; } - /** @type {ElementalBoard?} */ - #board = null; - /** - * Gets the board the element is on. - * @readonly - * @returns {ElementalBoard} - * @throws {EvalError} If the element has not been spawned yet. - */ - get board() { - if (this.#board === null) throw new EvalError(`Element not spawned yet. Use it after 'spawn' event dispatched`); - return this.#board; - } - /** @type {Color?} */ + /** @type {Readonly?} */ #color = null; /** * Gets the color of the element. - * @returns {Color} + * @returns {Readonly} * @throws {EvalError} If the element has not been spawned yet. */ get color() { @@ -616,7 +547,7 @@ class Elemental extends EventTarget { #isRepainted = true; /** * Sets the color of the element. - * @param {Color} value + * @param {Readonly} value * @returns {void} */ set color(value) { @@ -634,6 +565,16 @@ class Elemental extends EventTarget { get abilities() { throw new ReferenceError(`Not implemented function`); } + /** + * Executes one frame for the element. + * @param {AbilityInvoker} invoker The invoker that executes the element. + * @returns {void} + */ + execute(invoker) { + for (const ability of this.abilities) { + ability.execute(invoker); + } + } } //#endregion //#region Settings @@ -878,9 +819,6 @@ class Settings { //#endregion const settings = (await ArchiveManager.construct(`${navigator.dataPath}.Elements`, Settings)).data; -/** - * Current board object. - */ -const board = new Elemental.Board(Point2D.repeat(settings.boardSize)); +Elemental.Board.construct(Point2D.repeat(settings.boardSize)); -export { RenderEvent, Ability, Elemental, CycleTypes, Settings, board }; \ No newline at end of file +export { RenderEvent, Ability, Elemental, CycleTypes, Settings }; \ No newline at end of file