game_space_chunk_chunk_mgr.js

import Chunk from "./chunk.js";

/**
 * @module ChunkMgr
 * @fileoverview Contains ChunkMgr class.
 */

/**
 * @class
 * Manager of chunks for a space.
 */
class ChunkMgr {
    /**
     * Create a new chunk manager.
     * @param {Space} space Parent space
     * @param {number} chunkSize Number of units per chunk
     * @param {number} unitSize Number of pixels per unit
     * @constructor
     */
    constructor(space, chunkSize, unitSize) {
        this.space = space;

        this.chunkSize = chunkSize;
        this.unitSize = unitSize;

        this.rx = Math.ceil($$.width / this.chunkSize / this.unitSize / 2);
        this.ry = Math.ceil($$.height / this.chunkSize / this.unitSize / 2);

        this.chunks = new Map();

        this.count = 0;
        this.counter = document.getElementById("chunk-count");

        this.cleanUpTimer = 0;
        this.cleanUpTimerStop = 4;
    }


    //
    // Update & Draw
    //

    /**
     * Update all chunks.
     * @param {number} elapsed Time since last update cycle in seconds
     */
    update(elapsed) {
        if (this.space.camera === null)
            return;

        const zoom = this.space.camera.calcZoom();
        const chunks = this.getNearby(this.space.camera.pos,
            Math.ceil(this.rx / zoom), Math.ceil(this.ry / zoom));
        //for (const chunk of chunks)
        //    chunk.update(elapsed);
        //for (const chunk of chunks)
        //    chunk.addAllQueue();

        this.forAll(chunk => chunk.update(elapsed));

        this.cleanUpTimer += elapsed;
        if (this.cleanUpTimer >= this.cleanUpTimerStop) {
            this.cleanUpTimer = 0;

            this._replaceAll(chunks);
        }

        if (this.counter)
            this.counter.innerText = this.count;
    }

    /**
     * Draw all chunks.
     * @param {CanvasRenderingContext2D} ctx Canvas context to draw on
     */
    draw(ctx) {
        if (this.space.camera === null)
            return;

        const offset = this.space.camera.getOffsetWithZoom();
        const zoom = this.space.camera.calcZoom();

        ctx.save();
        ctx.scale(zoom, zoom);
        ctx.translate(-Math.floor(offset.x), -Math.floor(offset.y));

        const chunks = this.getNearby(this.space.camera.pos,
            Math.ceil(this.rx / zoom) + 1, Math.ceil(this.ry / zoom) + 1);
        for (const chunk of chunks)
            chunk.drawUnits(ctx);
        for (const chunk of chunks)
            chunk.drawActors(ctx);

        if ($$.debug.chunks)
            for (const chunk of chunks)
                chunk.drawDebug(ctx);

        this.space.events.do("draw", ctx);

        ctx.restore();
    }


    //
    // Actors
    //

    /**
     * Add an actor to the space.
     * @param {Actor} actor Actor to add
     */
    addActor(actor) {
        const index = this.getIndex(actor.pos);
        this.valChunk(index.x, index.y);

        this.valChunk(index.x, index.y).addActor(actor);
    }

    /**
     * Get all actors near a specified position and within a radius.
     * @param {Actor} ignore Actor to ignore
     * @param {Vec2} pos Point to find actors around
     * @param {number} radius Radius to find actors within
     * @returns {Actor[]} Array of actors near point within at radius
     */
    getNearbyActors(ignore, pos, radius) {
        let result = [];

        const r = Math.ceil(radius / (this.unitSize * this.chunkSize)) + 1;

        const ind = this.getIndex(pos);

        for (let x = -r; x <= r; x++) {
            for (let y = -r; y <= r; y++) {
                const chunk = this.valChunk(ind.x + x, ind.y + y);
                if (chunk !== null)
                    result.push(...chunk.getNearbyActors(ignore, pos, radius));
            }
        }

        return result;
    }


    //
    // Chunks - Access
    //

    /**
     * Check if a chunk exists.
     * @param {number} x Index along x-axis
     * @param {number} y Index along y-axis
     * @returns {boolean} Whether chunk exists
     */
    exists(x, y) {
        if (!this.chunks.has(x))
            return false;

        return this.chunks.get(x).has(y);
    }

    /**
     * Get the chunk indices at a specific position.
     * @param {Vec2} pos Position to check at
     * @returns {{x: number, y: number}} Chunk indices
     */
    getIndex(pos) {
        let xt = Math.floor(pos.x / (this.unitSize * this.chunkSize));
        let yt = Math.floor(pos.y / (this.unitSize * this.chunkSize));
        return {x: xt, y: yt};
    }

    /**
     * Get the chunk at specified indices.
     * @param {number} x Index along x-axis
     * @param {number} y Index along y-axis
     * @returns {Chunk|null} Chunk at indices, or null if it does not exist
     */
    getChunk(x, y) {
        if (!this.chunks.has(x))
            return null;
        if (!this.chunks.get(x).has(y))
            return null;

        return this.chunks.get(x).get(y);
    }

    /**
     * Get a chunk at a specified position.
     * @param {Vec2} pos Position to get chunk at
     * @returns {Chunk|null} Chunk at position, or null if it does not exist
     */
    getChunkAt(pos) {
        let xt = Math.floor(pos.x / (this.unitSize * this.chunkSize));
        let yt = Math.floor(pos.y / (this.unitSize * this.chunkSize));

        if (!this.chunks.has(xt))
            return null;
        if (!this.chunks.get(xt).has(yt))
            return null;

        return this.chunks.get(xt).get(yt);
    }

    /**
     * Get all chunks within an area. Will not include null chunks.
     * @param {Vec2} pos Center of area
     * @param {number} rx Width of area
     * @param {number} ry Height of area
     * @returns {Chunk[]} Chunks within area
     */
    getNearby(pos, rx, ry) {
        pos = this.getIndex(pos);

        const chunks = [];

        for (let xi = -rx; xi <= rx; xi++) {
            for (let yi = -ry; yi <= ry; yi++) {
                this.valChunk(pos.x + xi, pos.y + yi);
                chunks.push(this.chunks.get(pos.x + xi).get(pos.y + yi));
            }
        }

        return chunks;
    }

    /**
     * Run a function for each chunk.
     * @param {function} func Function to run
     */
    forAll(func = chunk => {}) {
        for (const x of this.chunks.keys())
            for (const chunk of this.chunks.get(x).values())
                func(chunk);
    }

    /**
     * Get all chunks within specified bounds.
     * @param {Bounds} bounds Bounding box
     * @returns {Chunk[]} Chunks within bounds
     */
    getInBounds(bounds) {
        return this.getFromTo(bounds.min, bounds.max);
    }

    /**
     * Get all chunks within two corner points.
     * @param {Vec2} from Minimum corner
     * @param {Vec2} to Maximum corner
     * @returns {Chunk[]} Chunks within corners
     */
    getFromTo(from, to) {
        const min = this.getIndex(from);
        const max = this.getIndex(to);

        const chunks = [];

        for (let x = min.x; x <= max.x + 1; x++) {
            for (let y = min.y; y <= max.y + 1; y++) {
                const chunk = this.genChunk(x, y);

                if (chunk !== null)
                    chunks.push(chunk);
            }
        }

        return chunks;
    }


    //
    // Chunks - Generation & Validation
    //

    /**
     * Generate a chunk at specified indices.
     * @param {number} x Index along x-axis
     * @param {number} y Index along y-axis
     * @returns {Chunk} Chunk that was generated or already existed
     */
    genChunk(x, y) {
        if (!this.chunks.has(x))
            this.chunks.set(x, new Map());
        if (!this.chunks.get(x).has(y)) {
            const chunk = new Chunk(this.space, x, y)
            this.chunks.get(x).set(y, chunk);

            this.space.chunkAdded(chunk);

            this.count++;

            return chunk;
        }

        return this.getChunk(x, y);
    }

    /**
     * Get a chunk at specified indices, and create it if it does not yet exist.
     * @param {number} x Index along x-axis
     * @param {number} y Index along y-axis
     * @returns {Chunk} Chunk that was generated or already existed
     */
    valChunk(x, y) {
        if (this.chunks.has(x))
            if (this.chunks.get(x).has(y))
                return this.chunks.get(x).get(y);

        return this.genChunk(x, y);
    }

    /**
     * Get a chunk at a specified position, and create it if it does not yet exist.
     * @param {Vec2} pos Position to get chunk at
     * @returns {Chunk} Chunk that was generated or already existed
     */
    valChunkAt(pos) {
        let x = Math.floor(pos.x / (this.unitSize * this.chunkSize));
        let y = Math.floor(pos.y / (this.unitSize * this.chunkSize));

        return this.valChunk(x, y);
    }

    /**
     * Set a chunk at specific indices.
     * @param {number} x Index along x-axisIndex along y-axis
     * @param {number} y Index along x-axisIndex along y-axis
     * @param {Chunk} chunk Chunk to set at indices
     * @private
     */
    _setChunk(x, y, chunk) {
        if (!this.chunks.has(x))
            this.chunks.set(x, new Map());
        this.chunks.get(x).set(y, chunk);
    }

    /**
     * Replace all chunks currently being stored. Used for routine cleanup.
     * @param {Chunk[]} chunks Chunks to replace with
     * @private
     */
    _replaceAll(chunks) {
        for (const x of this.chunks.keys())
            for (const c of this.chunks.get(x).values())
                if (c.persistent || c.actors.size !== 0)
                    chunks.push(c);

        this.chunks = new Map();

        this.count = chunks.length;
        if (this.counter)
            this.counter.innerText = this.count;

        for (const c of chunks)
            this._setChunk(c.x, c.y, c);
    }
}

export default ChunkMgr;