game_space_space.js

import ChunkMgr from "./chunk/chunk_mgr.js";
import Vec2 from "../vector/vec2.js";
import ChunkUnit from "./chunk/unit/chunk_unit.js";
import PathBuilder from "./path/path_builder.js";
import Color from "../../util/color/color.js";
import Bounds from "../vector/bounds.js";
import EventHandler from "../../event/event_handler.js";

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

/**
 * Default fill color for new units.
 * @type {Color}
 */
const FILL_COLOR = new Color(250,47,77);

/**
 * @class
 * Root class for holding all chunks and actors.
 */
class Space {
    /**
     * Create a new space.
     * @param {App} app Parent application
     * @param {number} chunkSize Number of units per chunk
     * @param {number} unitSize Number of pixels per unit
     * @constructor
     */
    constructor(app, chunkSize = 16, unitSize = 16) {
        this.app = app;

        this.id = "";

        this.width = $$.width;
        this.height = $$.height;

        this._camera = null;

        /// The background color of the space
        this.background = "rgb(100, 150, 255)";

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

        this.chunks = new ChunkMgr(this, chunkSize, unitSize);

        this._globalIDs = new Map();

        this._events = new EventHandler();
    }


    //
    // Getters
    //

    /**
     * Get the space's event handler.
     * @returns {EventHandler} Event handler
     */
    get events() {
        return this._events;
    }

    /**
     * Get the camera of the space.
     * @returns {Camera|null} Current camera
     */
    get camera() {
        return this._camera;
    }

    /**
     * Get the mouse position relative to the camera and its zoom.
     * @returns {Vec2} Mouse position in space
     */
    getMousePos() {
        let pos = $$.input.mouse.pos.copy().divv(this.camera.calcZoom());
        if (this.camera !== null)
            pos = pos.add(this.camera.getOffsetWithZoom());

        return pos;
    }


    //
    // Setters
    //

    /**
     * Set the camera of the space.
     * @param {Camera|null} camera New current camera
     */
    set camera(camera) {
        this._camera = camera;
        this.addActor(camera);
    }


    //
    // Update & Draw
    //

    /**
     * Update the space and its actors.
     * @param {number} elapsed Time since last update
     */
    update(elapsed) {
        this.chunks.update(elapsed);
    }

    /**
     * Draw the space and its actors.
     * @param {CanvasRenderingContext2D} ctx The canvas context to draw on
     */
    draw(ctx) {
        this.drawBackground(ctx);

        if (this.camera !== null) {
            this.chunks.draw(ctx);
            this.camera.drawFilter(ctx);
        }
    }

    /**
     * Draw the background color of the space.
     * @param {CanvasRenderingContext2D} ctx The canvas context to draw on
     */
    drawBackground(ctx) {
        if (this.background instanceof Color)
            ctx.fillStyle = this.background.get();
        else
            ctx.fillStyle = this.background;
        ctx.fillRect(0, 0, $$.width, $$.height);
    }


    //
    // Actors
    //

    /**
     * Add an actor to the space.
     * @param {Actor} actor The actor to add
     */
    addActor(actor) {
        this.chunks.addActor(actor);
        return actor;
    }

    /**
     * Add an actor's global identifier to the space (do not call manually).
     * @param {Actor} actor Actor to register identifier for
     */
    addGlobalID(actor) {
        this._globalIDs.set(actor.globalID, actor);
    }

    /**
     * Get an actor with specified global identifier.
     * @param {string} id Identifier to search for
     * @returns {Actor|null} Actor with specified identifier, or null if it does not exist.
     */
    find(id) {
        if (!this._globalIDs.has(id))
            return null;

        return this._globalIDs.get(id);
    }

    /**
     * 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) {
        return this.chunks.getNearbyActors(ignore, pos, radius);
    }

    checkUnitCollision(actor, pos) {
        let bounds = new Bounds();
        for (const comp of actor.components.getCollisions())
            if (comp.layers.has("default"))
                bounds = bounds.merge(comp.bounds);

        bounds = bounds.scale(actor.scale).add(actor.pos);

        for (const unit of this.getUnitsIn(bounds, true)) {
            const check = unit.checkCollision(actor, pos);
            if (check.result)
                return check;
        }

        return {
            result: false
        };
    }

    /**
     * Check collision with all other actors.
     * @param {Actor} actor The actor to check collision for
     * @param {Vec2} pos The position to attempt to move to
     * @returns {Object} Object containing collision result data
     */
    checkCollision(actor, pos) {
        const actors = this.getNearbyActors(actor, pos, 256);

        for (const other of actors) {
            if (other.collision === null)
                continue;

            const result = actor.checkCollision(other.collision, pos);
            if (result.result) {
                return result;
            }
        }

        return {
            result: false
        };
    }

    /**
     * Generate a new path in the space.
     * @param {Vec2} start Starting position
     * @param {Vec2} end Final position
     * @returns {Path|null} Generated path, or null if unsuccessful.
     */
    generatePath(start, end) {
        const path = new PathBuilder(this, start, end);
        return path.get();
    }


    //
    // Chunks
    //

    /**
     * Handle a chunk just being added.
     * @param {Chunk} chunk Chunk that was just added
     */
    chunkAdded(chunk) {}


    //
    // Units
    //

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

        const chunk = this.chunks.valChunk(Math.floor(x / size), Math.floor(y / size));
        return chunk.getUnitAt(x, y);
    }

    getUnitsIn(bounds, filledOnly = false) {
        const units = [];

        const width = Math.floor(bounds.width / this.unitSize) + 1;
        const height = Math.floor(bounds.height / this.unitSize) + 1;

        for (let x = -1; x <= width + 1; x++) {
            for (let y = -1; y <= height + 1; y++) {
                const pos = bounds.min.addv(x * this.unitSize, y * this.unitSize);
                const unit = this.getUnitAt(pos.x, pos.y);

                if (unit === null)
                    continue;

                if (filledOnly && unit.canWalkOn)
                    continue;

                units.push(unit);
            }
        }

        return units;
    }

    /**
     * Get a unit at specified indices.
     * @param {number} x Index along x-axis
     * @param {number} y Index along y-axis
     * @returns {ChunkUnit|null} Chunk unit at specified indices, or null if there is none.
     */
    getUnit(x, y) {
        const chunk = this.chunks.valChunk(Math.floor(x / this.chunkSize), Math.floor(y / this.chunkSize));
        return chunk.getUnit(x - (this.chunkSize * chunk.x), y - (this.chunkSize * chunk.y));
    }

    /**
     * Set a unit at a specified position.
     * @param {number} x Position along x-axis
     * @param {number} y Position along y-axis
     * @param {ChunkUnit} unit Chunk unit to set at specified position
     * @param {boolean} regenCache Whether to regenerate cached units image
     * @private
     */
    _setUnit(x, y, unit, regenCache = true) {
        const chunk = this.chunks.valChunk(Math.floor(x / this.chunkSize), Math.floor(y / this.chunkSize));
        chunk.setUnit(x - (this.chunkSize * chunk.x), y - (this.chunkSize * chunk.y), unit);

        chunk.cachedUnits = null;

        if (regenCache && $$.flags.chunks.cache) {
            $$.createImage(ctx => {
                for (let x = 0; x < chunk.units.length; x++) {
                    for (let y = 0; y < chunk.units[x].length; y++) {
                        const unit = chunk.units[x][y];
                        if (unit !== null)
                            unit.draw(ctx);
                    }
                }
            }, image => chunk.cachedUnits = image, this.chunkSize, this.chunkSize);
        }
    }

    /**
     * Fill a single unit at a specified position.
     * @param {number} x Position along x-axis
     * @param {number} y Position along y-axis
     * @param {boolean} fill Whether to fill unit or clear it
     * @param {Color|string|Sprite} color Color, sprite, or sprite identifier for unit
     * @param {boolean} walkOn Whether to allow walking on unit
     * @param {boolean} persistent Whether to make parent chunk persistent
     * @param {boolean} regenCache Whether to regenerate cached units image
     */
    fillUnit(x, y, fill = true, color = FILL_COLOR, walkOn = false,
             persistent = true, regenCache = true) {
        const size = this.chunkSize * this.unitSize;
        const chunk = this.chunks.valChunkAt(new Vec2(x * size, y * size));
        let unit = null;
        if (fill) {
            unit = new ChunkUnit({
                chunk: chunk,
                x: x,
                y: y,
                sprite: color
            });

            chunk.persistent = persistent;

            unit.canWalkOn = walkOn;
        }

        this._setUnit(x, y, unit, regenCache);
    }

    /**
     *
     * @param {number} x Position along x-axis
     * @param {number} y Position along y-axis
     * @param {string} pattern Pattern to use for generation
     * @param {Color|string|Sprite} color Color, sprite, or sprite identifier for unit
     * @param {string} unfilled Character to use for unfilled units
     * @param {string} filled Character to use for filled units
     */
    setUnitPattern(x = 0, y = 0, pattern = "", color= FILL_COLOR,
                   unfilled = '0', filled = '1') {
        if (pattern.length === 0)
            return;

        const lines = pattern.split("\n");
        let yi = 0;
        for (const line of lines) {
            let xi = 0;
            for (const c of line) {
                if (c === filled) {
                    const pos = new Vec2(x + xi, y + yi);
                    const ind = this.chunks.getIndex(pos);
                    const chunk = this.chunks.valChunk(ind.x, ind.y);
                    const unit = new ChunkUnit({
                        chunk: chunk,
                        x: pos.x,
                        y: pos.y,
                        sprite: color
                    });
                    unit.canWalkOn = false;
                    this._setUnit(pos.x, pos.y, unit);
                }
                xi++;
            }

            yi++;
        }
    }

    /**
     * Fill an area at a specified position.
     * @param {number} x1 First position along x-axis
     * @param {number} y1 First position along y-axis
     * @param {number} x2 Second position along x-axis
     * @param {number} y2 Second position along y-axis
     * @param {boolean} fill Whether to fill unit or clear it
     * @param {Color|string|Sprite} color Color, sprite, or sprite identifier for unit
     * @param {boolean} walkOn Whether to allow walking on unit
     */
    fillUnits(x1, y1, x2, y2, fill = true,
              color = FILL_COLOR, walkOn = false) {
        for (let x = x1; x < x2; x++) {
            for (let y = y1; y < y2; y++) {
                const pos = new Vec2(x, y);
                const ind = this.chunks.getIndex(pos);
                const chunk = this.chunks.valChunk(ind.x, ind.y);
                let unit = null;
                if (fill) {
                    unit = new ChunkUnit({
                        chunk: chunk,
                        x: pos.x,
                        y: pos.y,
                        sprite: color
                    });

                    unit.canWalkOn = walkOn;
                }
                this._setUnit(pos.x, pos.y, unit);
            }
        }
    }

    /**
     * Fill walls around an area at a specified position.
     * @param {number} x1 First position along x-axis
     * @param {number} y1 First position along y-axis
     * @param {number} x2 Second position along x-axis
     * @param {number} y2 Second position along y-axis
     * @param {boolean} fill Whether to fill unit or clear it
     * @param {Color|string|Sprite} color Color, sprite, or sprite identifier for unit
     * @param {boolean} walkOn Whether to allow walking on unit
     */
    fillUnitsWalls(x1, y1, x2, y2, fill = true,
                   color = FILL_COLOR, walkOn = false) {
        for (let x = x1; x < x2; x++) {
            for (let y = y1; y < y2; y++) {
                if ((y !== y1 && y !== y2 - 1) && (x !== x1 && x !== x2 - 1))
                    continue;

                const pos = new Vec2(x, y);
                const ind = this.chunks.getIndex(pos.mulv(this.unitSize));
                const chunk = this.chunks.valChunk(ind.x, ind.y);
                chunk.persistent = true;
                let unit = null;
                if (fill) {
                    unit = new ChunkUnit({
                        chunk: chunk,
                        x: pos.x,
                        y: pos.y,
                        sprite: color
                    });

                    unit.canWalkOn = walkOn;
                }
                this._setUnit(pos.x, pos.y, unit);
            }
        }
    }

    /**
     * Fill a line from one point to another.
     * @param {number} x1 First position along x-axis
     * @param {number} y1 First position along y-axis
     * @param {number} x2 Second position along x-axis
     * @param {number} y2 Second position along y-axis
     * @param {boolean} fill Whether to fill unit or clear it
     * @param {Color|string|Sprite} color Color, sprite, or sprite identifier for unit
     * @param {boolean} walkOn Whether to allow walking on unit
     */
    fillUnitsLine(x1, y1, x2, y2, fill = true,
                  color = FILL_COLOR, walkOn = false) {
        const p1 = new Vec2(x1, y1);
        const p2 = new Vec2(x2, y2);
        const norm = p2.sub(p1).norm();

        const length = Math.floor(p1.dist(p2));

        for (let i = 0; i < length; i++) {
            const pos = p1.add(norm.mulv(i));
            const ind = this.chunks.getIndex(pos);
            const chunk = this.chunks.valChunk(ind.x, ind.y);
            chunk.persistent = true;
            let unit = null;
            if (fill) {
                unit = new ChunkUnit({
                    chunk: chunk,
                    x: pos.x,
                    y: pos.y,
                    sprite: color
                });
                unit.canWalkOn = false;
            }
            this._setUnit(pos.x, pos.y, unit);
        }
    }

    placePrefab(id, x = 0, y = 0) {
        x = Math.floor(x);
        y = Math.floor(y);

        $$.reg.prefab.get(id).place(this, x, y);
    }
}

export default Space;