Source: elements/GameComposite.js

import {GameElement} from "./GameElement.js";
import {GameDrawable} from "../drawables/GameDrawable.js";
import {Game} from "../Game.js";
import {Point} from "../Misc.js";

/**
 * GameComposite class. Its children are elements.
 * Is used to manage multiple elements at once
 * @extends GameElement
 *
 * @property {Array<{
 *             element: GameElement,
 *             clickable: boolean,
 *             draggable: boolean,
 *             pressable: boolean
 *         }>} elements Elements instances + their original settings
 */
class GameComposite extends GameElement {

    elements = []

    set center(newCenter) {
        this._subtractPosition()
        super.center = newCenter
        this._addPosition()
    }
    get center() {
        return super.center
    }

    /**
     * GameComposite constructor
     * @param {Array<GameElement>} elements
     * @param {Object} attrs
     */
    constructor(elements,attrs) {
        super(new Point(0,0),[],attrs)

        for (const element of elements) {
            this.addElement(element)
        }
    }

    addHitbox(radius,dx,dy) {
        throw new Error('Incorrect method call in GameComposite "addHitbox"!')
    }
    addChild(child, sort = true) {
        throw new Error('Incorrect method call in GameComposite "addChild"!')
    }
    getChildByName(name) {
        throw new Error('Incorrect method call in GameComposite "getChildByName"!')
    }
    popChildByName(name) {
        throw new Error('Incorrect method call in GameComposite "popChildByName"!')
    }
    createText(text, attrs) {
        throw new Error('Incorrect method call in GameComposite "createText"!')
    }
    createShape(type, attrs) {
        throw new Error('Incorrect method call in GameComposite "createShape"!')
    }
    createImage(imageName, attrs) {
        throw new Error('Incorrect method call in GameComposite "createImage"!')
    }
    createGif(gifName, attrs) {
        throw new Error('Incorrect method call in GameComposite "createGif"!')
    }

    /**
     * Sorts elements by level
     */
    sortElements() {
        this.elements = this.elements.sort(((a, b) => a.element.level - b.element.level))
        this.elements
            .map(e=>e.element)
            .filter(e=>e instanceof GameComposite)
            .forEach(e=>e.sortElements())
    }

    /**
     * Adds element to composite, silences some of its functions
     * @param {GameElement} element
     */
    addElement(element) {
        if (!(element instanceof GameElement)) {
            throw new Error("Incorrect instance of element added to composite!")
        }
        this.game.removeElement(element)

        this.elements.push({
            element: element,
            clickable: element.clickable,
            draggable: element.draggable,
            pressable: element.pressable
        })

        element.clickable = false
        element.draggable = false
        element.pressable = false

        this.sortElements()
    }

    /**
     * Adds multiple elements to composite, some of its functions are turned off
     * @param {GameElement} elements
     */
    addElements(...elements) {
        for (const element of elements) {
            this.addElement(element)
        }
    }

    /**
     * Removes element from composite, returns its functions
     * @param {GameElement} element
     */
    removeElement(element) {
        if (!this.elements.map(obj=>obj.element).includes(element)) {
            this.elements
                .map(e => e.element)
                .filter(e => e instanceof GameComposite)
                .forEach(e => e.removeElement(element))
            return
        }

        const el = this.elements.filter(el=>el.element === element)[0]
        this.elements = this.elements.filter(el=>el.element !== element)

        element.clickable = el.clickable
        element.draggable = el.draggable
        element.pressable = el.pressable

        this.game.addElement(element)
    }

    /**
     * @protected
     * Subtracts position of composite from elements
     */
    _subtractPosition() {
        if (this.elements === undefined) {
            return
        }
        for (const element of this.elements.map(e=>e.element)) {
            element.center = element.center.subtract(this.center)
        }
    }

    /**
     * @protected
     * Adds position of composite to elements
     */
    _addPosition() {
        if (this.elements === undefined) {
            return
        }
        for (const element of this.elements.map(e=>e.element)) {
            element.center = element.center.add(this.center)
        }
    }

    /**
     * Sets center of composite without moving child elements
     * @param {number} x
     * @param {number} y
     */
    setCenter(x, y) {
        super.setPosition(x, y);
    }

    /**
     * Sets position of composite.
     * @param {number} x
     * @param {number} y
     */
    setPosition(x, y) {
        this._subtractPosition()
        super.setPosition(x, y);
        this._addPosition()
    }

    /**
     * Draws elements
     */
    draw(ctx) {
        for (const obj of this.elements) {
            const el = obj.element
            el.draw(ctx)
        }
    }

    /**
     * Animates elements
     */
    animate() {
        for (const obj of this.elements) {
            const el = obj.element
            el.animate()
        }
    }

    /**
     * Checks if mouse is inside any of the instance's child
     * @param {Point} mouse Mouse position
     * @returns {boolean} True when inside
     */
    isInside(mouse) {
        for (const element of this.elements.map(e=>e.element)) {
            const inside = element.isInside(mouse)
            if (inside) {
                return true
            }
        }
        return false
    }

    /**
     * Returns true on collision with the other element
     * @param {GameElement} other Element with which the collision is checked
     * @returns {boolean} True on colision else false
     */
    collidesWith(other) {
        for (const element of this.elements.map(e=>e.element)) {
            if (element.collidesWith(other)) {
                return true
            }
        }
        return false
    }

    /**
     * Removes all subElements from composite and returns their functions
     */
    reset() {
        for (const el of this.elements) {
            const element = el.element

            this.removeElement(element)
        }
        this.elements = []
        this.onClick = []
        this.onDrag = []
        this.onFinishDragging = []
        this.onKeyPress = []
        this.onKeyHold = []
    }

    /**
     * Rotates elements around a point
     * @param {Point} origin Point around which elements are rotated
     * @param {number} angle Angle in radians
     * @param {boolean} keepOrientation Elements don't change their rotation value on true
     */
    rotateElements(origin,angle,keepOrientation=false) {
        for (const element of this.elements.map(e=>e.element)) {
            element.center = element.center.rotateAround(origin,angle)
            if (!keepOrientation) {
                if (element instanceof GameComposite) {
                    element.rotateElements(element.center,angle,keepOrientation)
                } else {
                    element.rotation += angle
                }
            }
        }
    }

    /**
     * Returns true when it has elements
     * @returns {boolean} value
     */
    hasElements() {
        return this.elements.length > 0
    }

    /**
     * Returns attribute object with copies of elements
     * @returns {Object} Attribute object
     */
    getAttrs() {
        const attrs = Object.assign({
            elements: this.#copyOfIntactElements()
        },super.getAttrs())

        delete attrs["hitboxes"]
        delete attrs["hitboxVisible"]
        delete attrs["rotation"]
        delete attrs["children"]

        return attrs
    }

    /**
     * Creates a new copy of composite with all its elements. They aren't added to game.
     * Use game.copyElement() instead
     * @param {string} newName
     * @returns {GameComposite} New instance
     */
    copy(newName) {
        const attrs = this.getAttrs()
        return new GameComposite([...attrs.elements],attrs)
    }

    /**
     * Adds all elements to game
     * @param {Game} game
     */
    addToGame(game) {
        const elements = this.elements.map(el=>el.element)
        game.addElements(...elements)
    }

    /**
     * Returns array of copies of elements in composite
     * @returns {Array<GameElement>} Array of copied elements (that work)
     */
    #copyOfIntactElements() {
        const arr = []
        for (const el of this.elements) {
            const element = el.element

            element.clickable = el.clickable
            element.draggable = el.draggable
            element.pressable = el.pressable

            arr.push(element.copy())

            element.clickable = false
            element.draggable = false
            element.pressable = false
        }
        return arr
    }

}

export {GameComposite}