Source: base/2d/DrawTiledLayer.js

import { AnimationEvent } from "../AnimationEvent.js";
import { DrawShapeObject } from "./DrawShapeObject.js";
import { ImageTempStorage } from "../Temp/ImageTempStorage.js";
import { TiledLayerTempStorage } from "../Temp/TiledLayerTempStorage.js";
/**
 * A render object represents a layer from tiled editor
 * @see {@link DrawObjectFactory} should be created with factory method
 */
export class DrawTiledLayer {
    #layerKey;
    #tileMapKey;
    #tilemap;
    #tilesets;
    /**
     * @type {string}
     */
    #DELIMITER = "-#-";
    #tilesetImages;
    /**
     * @type {Array<ImageTempStorage>}
     */
    #textureStorages;
    #layerData;
    #setBoundaries;
    #drawBoundaries;
    #attachedMaskId;
    /**
     * @type {number}
     */
    #sortIndex = 0;
    /**
     * @type {Map<string, AnimationEvent>}
     */
    #animations = new Map();
    #isOffsetTurnedOff;

    /**
     * @hideconstructor
     */
    constructor(layerKey, tileMapKey, tilemap, tilesets, tilesetImages, layerData, setBoundaries = false, shapeMask) {
        this.#layerKey = layerKey;
        this.#tileMapKey = tileMapKey;
        this.#tilemap = tilemap;
        this.#tilesets = tilesets;
        this.#textureStorages = [];
        this.#tilesetImages = tilesetImages;
        this.#layerData = layerData;
        
        this.#setBoundaries = setBoundaries;
        this.#drawBoundaries = setBoundaries ? setBoundaries : false;
        if (shapeMask) {
            this.setMask(shapeMask);
        }
        this.#processData(tilesets, layerData);
    }

    /**
     * A layer name.
     * @type {string}
     */
    get layerKey() {
        return this.#layerKey;
    }

    /**
     * A tilemap layer key, should match key from the tilemap.
     * @type {string}
     */
    get tileMapKey() {
        return this.#tileMapKey;
    }

    get tilemap() {
        return this.#tilemap;
    }
    
    get tilesets() {
        return this.#tilesets;
    }

    get tilesetImages() {
        return this.#tilesetImages;
    }

    get layerData() {
        return this.#layerData;
    }
    /**
     * Should the layer borders used as boundaries, or not
     * Can be set in GameStage.addRenderLayer() method.
     * @type {boolean}
     */
    get setBoundaries() {
        return this.#setBoundaries;
    }

    /**
     * Should draw a boundaries helper, or not
     * Can be set in SystemSettings.
     * @type {boolean}
     */
    get drawBoundaries() {
        return this.#drawBoundaries;
    }

    set drawBoundaries(value) {
        this.#drawBoundaries = value;
    }

    /**
     * @ignore
     */
    get _maskId() {
        return this.#attachedMaskId;
    }
    /**
     * 
     * @param {DrawShapeObject} mask 
     */
    setMask(mask) {
        mask._isMask = true;
        this.#attachedMaskId = mask.id;
    }

    removeMask() {
        this.#attachedMaskId = null;
    }

    /**
     * @type {number}
     */
    get sortIndex () {
        return this.#sortIndex;
    }

    set sortIndex(value) {
        this.#sortIndex = value;
    }

    get isOffsetTurnedOff() {
        return this.#isOffsetTurnedOff;
    }
    turnOffOffset() {
        this.#isOffsetTurnedOff = true;
    }

    /**
     * Determines if image is animated or not
     * @type {boolean}
     */
    get hasAnimations() {
        return this.#animations.size > 0;
    }

    /**
     * @ignore
     */
    get _textureStorages() {
        return this.#textureStorages;
    }

    /**
     * @ignore
     */
    _setTextureStorage(index, value) {
        this.#textureStorages[index] = value;
    }

    /**
     * Tilesets has a property tiles, which could contain tile animations
     * or object boundaries, this is workaround for split this and add
     * additional properties for use in draw phase:
     * _hasAnimations
     * _animations - Map<id:activeSprite>
     * _hasBoundaries
     * _boundaries - Map<id:objectgroup>
     * @param {*} tilesets
     */
    #processData(tilesets, layerData) {
        // границы для слоя создаются одни, даже если они высчитываются с разных тайлсетов
        // поэтому суммируем и находим максимальное их количество
        let ellipseBLen = 0,
            pointBLen = 0,
            polygonBLen = 0;
        tilesets.forEach((tileset, idx) => {
            const tiles = tileset.data.tiles,
                name = tileset.data.name,
                firstgid = tileset.firstgid,
                nextTileset = this.tilesets[idx + 1],
                nextgid = nextTileset ? nextTileset.firstgid : 1_000_000_000;
                
            if (tiles) {
                for (let tile of tiles) {
                    const animation = tile.animation,
                        objectgroup = tile.objectgroup,
                        id = tile.id;
                    if (animation) {
                        const eventName = name + this.#DELIMITER + id, 
                            animationIndexes = this.#fixAnimationsItems(animation),
                            animationEvent = new AnimationEvent(eventName, animationIndexes, true);

                        this.#animations.set(eventName, animationEvent);
                        // add additional properties
                        if (!tileset.data._hasAnimations) {
                            tileset.data._hasAnimations = true;
                            tileset.data._animations = new Map();
                            //
                            tileset.data._animations.set(id, animationIndexes[0][0]);
                        }
                        this.#activateAnimation(animationEvent);
                    }
                    if (objectgroup && this.#setBoundaries) {
                        if (tileset.data._hasBoundaries) {
                            tileset.data._boundaries.set(id, objectgroup);
                        } else {
                            // add additional properties
                            tileset.data._hasBoundaries = true;
                            tileset.data._boundaries = new Map();
                            tileset.data._boundaries.set(id, objectgroup);
                        }
                        objectgroup.objects.forEach((object) => {
                            if (object.ellipse) {
                                const cellsWithB = layerData.data.filter((tile) => tile === id + firstgid).length;
                                ellipseBLen += (4 * cellsWithB); // (x, y, wRad, hRad) * layer items
                            } else if (object.point) {
                                const cellsWithB = layerData.data.filter((tile) => tile === id + firstgid).length;
                                pointBLen += (2 * cellsWithB); // (x, y) * layer items
                            } else if (object.polygon) {
                                const cellsWithB = layerData.data.filter((tile) => tile === id + firstgid).length;
                                polygonBLen += (object.polygon.length * 2 * cellsWithB); // (each point * 2(x,y) ) * layer items
                            } else { // rect object
                                const cellsWithB = layerData.data.filter((tile) => tile === id + firstgid).length;
                                polygonBLen += (16 * cellsWithB); // (4 faces * 4 cords for each one) * layer items
                            }
                        });
                    }
                }
            }
            
            const nonEmptyCells = layerData.data.filter((tile) => ((tile >= firstgid) && (tile < nextgid))).length,
                cells = layerData.data.length;

            if (this.#setBoundaries) {
                polygonBLen+=(nonEmptyCells * 16); // potential boundaries also nonEmptyCells
            }
            // создаем вспомогательный объект для расчетов и хранения данных отрисовки
            // help class for draw calculations
            tileset._temp = new TiledLayerTempStorage(cells, nonEmptyCells);
        });
        
        // save boundaries max possible lengths
        layerData.ellipseBoundariesLen = ellipseBLen;
        layerData.pointBoundariesLen = pointBLen;
        layerData.polygonBoundariesLen = polygonBLen;
    }

    /**
     * 
     * @param {Array<{duration:number, tileid:number}>} animation 
     * @returns {Array<{duration:number, id:number}>}
     */
    #fixAnimationsItems(animation) {
        return animation.map((animation_item) => ({duration:animation_item.duration, id: animation_item.tileid}));
    }
    /**
     * @ignore
     */
    _processActiveAnimations() {
        for (let animationEvent of this.#animations.values()) {
            if (animationEvent.isActive) {
                animationEvent.iterateAnimationIndex();
                this.#switchCurrentActiveSprite(animationEvent);
            }
        }
    }

    #activateAnimation = (animationEvent) => {
        animationEvent.activateAnimation();
        this.#switchCurrentActiveSprite(animationEvent);
    }; 

    #switchCurrentActiveSprite = (animationEvent) => {
        const [tilesetKey, animationId] = animationEvent.name.split(this.#DELIMITER),
            tilesetIndex = this.#tilesets.findIndex(tileset => tileset.data.name === tilesetKey),
            tileset = this.#tilesets[tilesetIndex];
            
        tileset.data._animations.set(parseInt(animationId), animationEvent.currentSprite);
    };

    /**
     *
     * @param {string} eventName - animation name
     */
    stopRepeatedAnimation (eventName) {
        this.#animations.get(eventName).deactivateAnimation();
    }

    /**
     * Removes animations
     */
    removeAllAnimations() {
        for (let [eventName, animationEvent] of this.#animations.entries()) {
            this.removeEventListener(eventName, animationEvent.activateAnimation);
            animationEvent.deactivateAnimation();
        }
        this.#animations.clear();
        this.#animations = undefined;
    }

    destroy() {
        this.removeAllAnimations();
        super.destroy();
    }
}