game_space_chunk_chunk.js

/**
 * @module Chunk
 * @fileOverview Contains Chunk class.
 */

/**
 * @class
 * Individual chunk for space partitioning.
 */
class Chunk {
    /**
     * Create a new chunk.
     * @param {Space} space Parent space
     * @param {number} x Index of chunk along x-axis
     * @param {number} y Index of chunk along y-axis
     * @constructor
     */
    constructor(space, x, y) {
        this._space = space;

        this._x = x;
        this._y = y;

        this.actors = new Map();
        this._nextActorID = 0;

        this._overlappingActors = new Set();

        this.units = [];

        this._persistent = false;

        this.cachedUnits = null;
        this.makeUnits();
    }

    //
    // Getters
    //

    /**
     * Get the parent space.
     * @returns {Space} Parent space
     */
    get space() {
        return this._space;
    }

    /**
     * Get the index along the x-axis.
     * @returns {number} Chunk x index
     */
    get x() {
        return this._x;
    }

    /**
     * Get the index along the y-axis
     * @returns {number} Chunk y index
     */
    get y() {
        return this._y;
    }

    /**
     * Check if the chunk, or any of its actors, are persistent.
     * @returns {boolean} Whether chunk is persistent
     */
    get persistent() {
        if (this._persistent)
            return true;

        for (const actor of this.actors.values())
            if (actor.persistent)
                return true;

        return false;
    }

    /**
     * Find and set the next actor ID to use.
     */
    getNextID() {
        while (this.actors.has(this._nextActorID))
            this._nextActorID++;
    }


    //
    // Setters
    //

    /**
     * Set the chunk itself to be persistent.
     * @param {boolean} val New persistence value
     */
    set persistent(val) {
        this._persistent = val;
    }


    //
    // Update & Draw
    //

    /**
     * Update the chunk and its actors.
     * @param {number} elapsed Time since last update cycle in seconds
     */
    update(elapsed) {
        for (const actor of this.actors.values())
            actor.update(elapsed);

        this._checkActors();
    }

    /**
     * Draw all units within the chunk.
     * @param {CanvasRenderingContext2D} ctx Canvas context to draw on
     */
    drawUnits(ctx) {
        const size = this.space.chunkSize * this.space.unitSize;

        ctx.save();
        ctx.translate(this.x * size, this.y * size);

        if (this.cachedUnits !== null) {
            ctx.drawImage(this.cachedUnits, 0, 0, size + 1, size + 1);

            return;
        }

        for (let x = 0; x < this.space.chunkSize; x++) {
            for (let y = 0; y < this.space.chunkSize; y++) {
                const unit = this.units[x][y];
                if (unit !== null)
                    unit.draw(ctx);
            }
        }

        ctx.restore();
    }

    /**
     * Draw all actors in the chunk.
     * @param {CanvasRenderingContext2D} ctx Canvas context to draw on
     */
    drawActors(ctx) {
        const top = [];
        for (const actor of this.actors.values()) {
            if (actor.alwaysOnTop)
                top.push(actor);
            else if (!actor.hasParent())
                actor.draw(ctx);
        }

        for (const actor of top)
            actor.draw(ctx);
    }

    /**
     * Draw all debugging information for the chunk and its actors.
     * @param {CanvasRenderingContext2D} ctx Canvas context to draw on
     */
    drawDebug(ctx) {
        if ($$.debug.chunks) {
            ctx.fillStyle = $$.colors.debug;
            ctx.strokeStyle = $$.colors.debug;

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

            ctx.lineWidth = 2 / zoom;

            const s = this.space.chunkSize * this.space.unitSize;
            const x = this.x * s;
            const y = this.y * s;
            ctx.strokeRect(x, y, s, s);

            if (this.space.camera !== null) {
                if (this.space.chunks.getChunkAt(this.space.camera.pos) === this) {
                    ctx.fillStyle = $$.colors.debug2;

                    ctx.strokeStyle = $$.colors.debug2;
                    ctx.strokeRect(x + 2, y + 2, s - 4, s - 4);
                }
            }

            ctx.shadowOffsetX = 1;
            ctx.shadowOffsetY = 1;
            ctx.shadowColor = "rgba(0,0,0,0.32)";

            if (this.space.camera.zoom > -3) {
                ctx.save();
                ctx.translate(x, y);
                ctx.scale(1 / zoom, 1 / zoom);

                ctx.textAlign = "left";
                ctx.font = "bold 20px " + $$.fonts.debug;
                const id = this.x + ", " + this.y;
                ctx.fillText(id, 8, 20);

                ctx.font = "16px " + $$.fonts.debug;
                let text = this.actors.size + " actors";
                if (this.actors.size === 1)
                    text = this.actors.size + " actor";
                ctx.fillText(text, 8, 40);

                ctx.shadowOffsetX = 0;
                ctx.shadowOffsetY = 0;
                ctx.shadowColor = 'transparent';

                ctx.restore();
            }
        }

        for (const actor of this.actors.values())
            actor.drawDebug(ctx);
    }


    //
    // Actor Management
    //

    /**
     * Add an actor to the chunk.
     * @param {Actor} actor Actor to add
     */
    addActor(actor) {
        this.actors.set(this._nextActorID, actor);
        actor.id = this._nextActorID;
        actor.space = this.space;

        this.getNextID();

        if (!actor.initialized)
            actor.init();
    }

    /**
     * Remove an actor from the chunk.
     * @param {number} id Chunk identifier of actor
     * @private
     */
    _removeActor(id) {
        if (this.actors.has(id)) {
            this.actors.delete(id);

            if (id < this._nextActorID)
                this._nextActorID = id;
        }
    }

    /**
     * Check collision for an actor with other actors.
     * @param {Actor} actor Actor to check collision for
     * @param {Vec2} pos Position to attempt to move to
     * @returns {Object} Object containing collision result data
     */
    checkCollision(actor, pos) {
        for (const other of this._overlappingActors.values()) {
            if (other === actor)
                continue;
            if (other.markedForDeletion)
                continue;

            for (const comp of actor.components.getCollisions()) {
                const result = other.checkCollision(comp, pos);
                if (result.result) {
                    return result;
                }
            }
        }

        return {
            result: false
        };
    }

    /**
     * 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) {
        const result = [];

        for (const actor of this.actors.values())
            if (actor !== ignore && pos.dist(actor.pos) <= radius)
                result.push(actor);

        return result;
    }

    /**
     * Transfer an actor to another chunk.
     * @param {Actor} actor Actor to transfer
     * @param {Chunk} to Chunk to transfer actor to
     * @private
     */
    _transferActor(actor, to) {
        this._removeActor(actor.id);
        actor.id = null;
        to.addActor(actor);
    }

    /**
     * Check the status of all actors within the chunk.
     * This includes checking if they are marked for deletion,
     * and if they should be moved to a different chunk.
     * @private
     */
    _checkActors() {
        for (const actor of this.actors.values()) {
            if (actor.markedForDeletion) {
                if (actor.id < this._nextActorID)
                    this._nextActorID = actor.id;
                this.actors.delete(actor.id);
                continue;
            }

            const c = this.space.chunks.valChunkAt(actor.globalPos);
            if (c !== this)
                this._transferActor(actor, c);
        }
    }

    /**
     * Add an actor that is overlapping the chunk.
     * @param {Actor} actor Actor to add as overlapping
     */
    addOverlapping(actor) {
        this._overlappingActors.add(actor);
    }

    /**
     * Remove an actor from overlapping with the chunk.
     * @param {Actor} actor Actor to remove as overlapping
     */
    removeOverlapping(actor) {
        this._overlappingActors.delete(actor);
    }


    //
    // Units
    //

    /**
     * Generate units within the chunk.
     */
    makeUnits() {
        for (let x = 0; x < this.space.chunkSize; x++) {
            this.units.push([]);
            for (let y = 0; y < this.space.chunkSize; y++)
                this.units[x].push(null);
        }
    }

    /**
     * Get a specific unit in the chunk.
     * @param {number} x Relative index along the x-axis
     * @param {number} y Relative index along the y-axis
     * @returns {ChunkUnit|null} Chunk unit at specified position, or null if there is none.
     */
    getUnit(x, y) {
        if (x < 0 || x >= this.space.chunkSize ||
            y < 0 || y >= this.space.chunkSize)
            return null;

        return this.units[Math.floor(x)][Math.floor(y)];
    }

    /**
     * Get a chunk unit at a specific position in the space.
     * @param {number} x Global position along the x-axis
     * @param {number} y Global position along the y-axis
     * @returns {ChunkUnit|null} Chunk unit at specified position, or null if there is none.
     */
    getUnitAt(x, y) {
        const size = this.space.unitSize * this.space.chunkSize;
        x -= this.x * size;
        y -= this.y * size;

        x = Math.floor(x / this.space.unitSize);
        y = Math.floor(y / this.space.unitSize);

        if (x < 0 || x >= this.space.chunkSize ||
            y < 0 || y >= this.space.chunkSize)
            return null;

        return this.units[x][y];
    }

    /**
     * Set a unit at a specific index.
     * @param {number} x Relative index along the x-axis
     * @param {number} y Relative index along the y-axis
     * @param {ChunkUnit|null} unit Unit to set at indices, or null to remove.
     */
    setUnit(x, y, unit) {
        if (x < 0 || x >= this.space.chunkSize ||
            y < 0 || y >= this.space.chunkSize)
            return;

        this.units[x][y] = unit;

        if (unit === null)
            return;

        unit.chunk = this;
        const ox = this.x * this.space.unitSize * this.space.chunkSize;
        const oy = this.y * this.space.unitSize * this.space.chunkSize;
        unit.x = x * this.space.unitSize + ox;
        unit.y = y * this.space.unitSize + oy;
    }
}

export default Chunk;