Source: World.js

import Sprite from "./Sprite";
class World {
    static defaultTypeTable = {
        MAP_FLOOR: 0,
        MAP_WALL: 1,
        MAP_WALL_SHADOW: 2,
        MAP_DOOR: 3,
        MAP_DOOR_FRAME: 4,
        MAP_PUSH_WALL: 5,
        MAP_CIRCULAR_COLUMN: 6,
        MAP_DIA_WALL_TR_BL: 7,
        MAP_DIA_WALL_TL_BR: 8,
        MAP_TRANSPARENT_WALL: 9,
        DOOR_CLOSED: 0,
        DOOR_OPENING: 1,
        DOOR_OPEN: 2,
        DOOR_CLOSING: 3,
    }

    static defaultSkyBox = {
        sky: "black",
        ground: "grey",
        front: null,
        middle: null,
        back: null,
    }

    /**
     * 
     * @param {number} [width = 24] integer indicating the width of the world (number of block)
     * @param {number} [height = 24] integer indicating the height of the world (number of block)
     * @param {string} [data=null] block data for the world, e.g. "0,1,0,0,0,0,1,2,0,0,0,0...", can be an array of integer instead]
     * @param {Map} [textureMap=null] [[0, texture], [1, texture], ...]
     * @param {object} [skyBox=World.defaultSkyBox] sky box
     * @param {string} skyBox.sky a string notation of color or a p5.Image object
     * @param {string} skyBox.ground a string notation of color or a p5.Image object
     * @param {p5.Image} skyBox.front can be a p5.Graphics object
     * @param {p5.Image} skyBox.middle can be a p5.Graphics object
     * @param {p5.Image} skyBox.back can be a p5.Graphics object
     * @param {object} [typeTable = World.defaultTypeTable] block type table to interpret the data, default to the default table
     * @param {object} [options=null]
     * @param {number} options.doorSpeed how fast the door open and close, default to 0.01;
     * @param {boolean} options.doorAutoClose will the door close by it self, default to false;
     * @param {number} options.doorClosingTime if doorAutoClose is true, define how long the door will be open
     */
    constructor(width = 24, height = 24, data = null, textureMap = null, skyBox = World.defaultSkyBox, typeTable = World.defaultTypeTable, options = null) {
        this.width = width;
        this.height = height;
        this.map = new Array(this.width * this.height).fill(0);
        this.table = typeTable;
        this.skyBox = skyBox;
        this.sprites = [];
        this.cameras = [];
        this.textureMap = textureMap;
        this.loadMap(data, options);
    }

    /**
     * 
     * @param {string} data block data for the world separated by comma, can be an array of integer instead
     * @param {object} [options = null]
     * @param {number} options.doorSpeed how fast the door open and close, default to 0.1;
     * @param {boolean} options.doorAutoClose will the door close by it self, default to false;
     * @param {number} options.doorClosingTime if doorAutoClose is true, define how long the door will be open
     */
    loadMap(data, options) {
        if (data && data.length) {
            if (typeof data === "string") {
                data.split(',').forEach((b, i) => { 
                    this.map[i] = parseInt(b);
                    if (this.map[i] % 10 === this.table.MAP_WALL_SHADOW) {
                        console.warn(`[p5RayCaster]: MAP_WALL_SHADOW ${this.map[i]} should not be used in the map, changed to MAP_WALL ${Math.floor(this.map[i] / 10) + this.table.MAP_WALL}`);
                        this.map[i] = Math.floor(this.map[i] / 10) + this.table.MAP_WALL;
                    }
                });
            } else if (typeof data[0] === "number") {
                data.forEach((b, i) => { 
                    this.map[i] = b;
                    if (this.map[i] % 10 === this.table.MAP_WALL_SHADOW) {
                        console.warn(`[p5RayCaster]: MAP_WALL_SHADOW ${this.map[i]} should not be used in the map, changed to MAP_WALL ${Math.floor(this.map[i] / 10) + this.table.MAP_WALL}`);
                        this.map[i] = Math.floor(this.map[i] / 10) + this.table.MAP_WALL;
                    }
                 });
            } else if (typeof data[0][0] === "number") {
                for (let y = 0; y < data.length; y++) {
                    for (let x = 0; x < data[y].length; x++) {
                        let idx = x + y * this.width;
                        this.map[idx] = data[y][x];
                        if (this.map[idx] % 10 === this.table.MAP_WALL_SHADOW) {
                            console.warn(`[p5RayCaster]: MAP_WALL_SHADOW ${this.map[idx]} should not be used in the map, changed to MAP_WALL ${Math.floor(this.map[idx] / 10) + this.table.MAP_WALL}`);
                            this.map[idx] = Math.floor(this.map[idx] / 10) + this.table.MAP_WALL;
                        }
                    }
                }
            }
        }
        this.doorOffsets = new Array(this.width * this.height).fill(0);
        this.doorStates = new Array(this.width * this.height).fill(0);
        this.doorSpeed = options && options.doorSpeed ? options.doorSpeed : 0.1;
        this.doorAutoClose = options && options.doorAutoClose ? options.doorAutoClose : false;
        this.doorClosingTime = options && options.doorClosingTime ? options.doorClosingTime : 0;
    }

    /**
     * 
     * @param {string} floorMapData or a 1D/2D array, or a single number indicating the texture for all blocks
     */
    loadFloor(floorMapData){
        if (typeof floorMapData === "number") {
            this.floor = floorMapData;
            return;
        }
        this.floor = new Array(this.width * this.height);
        if(typeof floorMapData === "string") {
            floorMapData.split(",").forEach(data => {
                this.floor.push(parseInt(data));
            });
            return;
        }
        if(Array.isArray(floorMapData) && floorMapData.length){
            if (typeof floorMapData[0] === "number") {
                floorMapData.forEach(data => this.floor.push(data));
                return;
            }
            if (typeof floorMapData[0][0] === "number") {
                for (let y = 0; y < floorMapData.length; y++) {
                    for (let x = 0; x < floorMapData[0].length; x++) {
                        let idx = x + y * this.width;
                        this.floor[idx] = floorMapData[y][x];
                    }
                }
            }
        }
    }

    /**
     * 
     * @param {string} ceilingMapData or a 1D/2D array, or a single number indicating the texture for all blocks
     */
    loadCeiling(ceilingMapData){
        if (typeof ceilingMapData === "number") {
            this.ceiling = ceilingMapData;
            return;
        }
        this.ceiling = new Array(this.width * this.height);
        if(typeof ceilingMapData === "string") {
            ceilingMapData.split(",").forEach(data => {
                this.ceiling.push(parseInt(data));
            });
            return;
        }
        if(Array.isArray(ceilingMapData) && ceilingMapData.length){
            if (typeof ceilingMapData[0] === "number") {
                ceilingMapData.forEach(data => this.ceiling.push(data));
                return;
            }
            if (typeof ceilingMapData[0][0] === "number") {
                for (let y = 0; y < ceilingMapData.length; y++) {
                    for (let x = 0; x < ceilingMapData[0].length; x++) {
                        let idx = x + y * this.width;
                        this.ceiling[idx] = ceilingMapData[y][x];
                    }
                }
            }
        }
    }

    /**
     * this function will not copy the Map
     * @param {Map} textureMap 
     */
    loadTextureMap(textureMap) {
        this.textureMap = textureMap;
    }

    /**
     * add a sprite to the world
     * @param {Sprite} sprite 
     */
    addSprite(sprite) {
        this.sprites.push(sprite);
        sprite.world = this;
        this.cameras.forEach((c) => {
            c.updateSpritesBuffers();
        })
    }

    /**
     * remove a sprite
     * @param {number} spriteIdx index of the sprite in the sprites array or the sprite itself
     */
    removeSprite(spriteIdx) {
        if (spriteIdx instanceof Sprite) {
            delete spriteIdx.world
            let idx = this.sprites.indexOf(spriteIdx)
            if (idx === -1) {
                console.warn("invalid remove: sprite is not in the world");
                return;
            }
            spriteIdx = idx;
        }
        delete this.sprites[spriteIdx].world;
        this.sprites.splice(spriteIdx, 1);
        this.cameras.forEach((c) => {
            c.updateSpritesBuffers();
        })
    }

    /**
     * update the state of the doors and push walls
     * @param {number} [frameRate=30]
     */
    update(frameRate = 30) {
        const deltaTime = 1 / frameRate;
        for (let i = 0; i < this.map.length; i++) {
            const block = this.map[i];
            if (block === this.table.MAP_DOOR) {
                if (this.doorStates[i] === this.table.DOOR_OPENING) {
                    this.doorOffsets[i] += deltaTime * this.doorSpeed;
                    if (this.doorOffsets[i] > 1) {
                        this.doorOffsets[i] = 1;
                        this.doorStates[i] = this.table.DOOR_OPEN;
                        if (this.doorAutoClose) {
                            setTimeout((i) => { this.doorStates[i] = this.table.DOOR_CLOSING }, this.doorClosingTime, i);
                        }
                    }
                } else if (this.doorStates[i] === this.table.DOOR_CLOSING) {
                    this.doorOffsets[i] -= deltaTime * this.doorSpeed;
                    if (this.doorOffsets[i] < 0) {
                        this.doorOffsets[i] = 0;
                        this.doorStates[i] = this.table.DOOR_CLOSED;
                    }
                }
            } else if (block === this.table.MAP_PUSH_WALL) {
                if (this.doorStates[i] === this.table.DOOR_OPENING) {
                    this.doorOffsets[i] += deltaTime * this.doorSpeed;
                    if (this.doorOffsets[i] > 1) {
                        this.doorOffsets[i] = 1;
                        this.doorStates[i] = this.table.DOOR_OPEN;
                    }
                }
            }
        }
    }

    /**
     * 
     * @param {number} x x coordinate of the block
     * @param {number} y y coordinate of the block
     */
    openDoor(x, y) {
        let idx = arguments.length === 1 ? x : x + y * this.width;
        if (this.map[idx] === this.table.MAP_DOOR || this.map[idx] === this.table.MAP_PUSH_WALL) {
            if (this.doorStates[idx] === this.table.DOOR_CLOSED) {
                this.doorStates[idx] = this.table.DOOR_OPENING;
            }
        }
    }

    /**
     * 
     * @param {number} x x coordinate of the block
     * @param {number} y y coordinate of the block
     */
    closeDoor(x, y) {
        let idx = arguments.length === 1 ? x : x + y * this.width;
        if (this.map[idx] === this.table.MAP_DOOR) {
            if (this.doorStates[idx] === this.table.DOOR_OPEN) {
                this.doorStates[idx] = this.table.DOOR_CLOSING;
            }
        }
    }

     /**
     * 
     * @param {number} x x coordinate of the block
     * @param {number} y y coordinate of the block
     */
    moveDoor(x, y) {
        let idx = arguments.length === 1 ? x : x + y * this.width;
        if (this.map[idx] === this.table.MAP_DOOR) {
            if (this.doorStates[idx] === this.table.DOOR_OPEN) {
                this.doorStates[idx] = this.table.DOOR_CLOSING;
            } else if (this.doorStates[idx] === this.table.DOOR_CLOSED) {
                this.doorStates[idx] = this.table.DOOR_OPENING;
            }
        }

        if (this.map[idx] === this.table.MAP_PUSH_WALL) {
            if (this.doorStates[idx] === this.table.DOOR_CLOSED) {
                this.doorStates[idx] = this.table.DOOR_OPENING;
            }
        }
    }
}

export default World