Source: level.js

import { LevelObject, Block } from "./block.js";
import { Dimension, Coordinate, Coordinate2D, Coordinate3D } from "./coordinate.js";

/**
 * Represents a LevelZ Level.
 * @abstract
 * @classdesc Abstract Level for representation purposes
 */
export class Level {

    #dimension;

    /**
     * The headers of the level.
     * @type {Map<string, any>}
     */
    headers = new Map();

    /**
     * The blocks in the level.
     * @type {Set<LevelObject>}
     */
    blocks = new Set();

    constructor(dimension) {
        if (new.target === Level)
            throw new TypeError('Cannot instantiate abstract class')
        
        this.#dimension = dimension
    }

    /**
     * The dimension of the level.
     * @type {Dimension}
     */
    get dimension() {
        return this.#dimension
    }

    /**
     * Gets the spawn point of the level.
     * @returns {Coordinate} The spawn point of the level.
     */
    get spawn() {
        return this.headers.get('spawn')
    }

    /**
     * Gets all the coordinates of the blocks in the level.
     * @returns {Coordinate[]} The coordinates of the blocks in the level.
     */
    get coordinates() {
        return Array.from(this.blocks).map(block => block.coordinate)
    }

    /**
     * Gets all the raw blocks in the level.
     * @returns {Block[]} The raw blocks in the level.
     */
    get rawBlocks() {
        return Array.from(this.blocks).map(block => block.block)
    }

    /**
     * Gets the string representation of the level.
     * @returns {string} The string representation of the level.
     */
    toString() {
        let str = `@type ${this.#dimension}`
        for (const [key, value] of this.headers) {
            str += `\n@${key} ${value}`
        }

        if (this.blocks.size !== 0) {
            str += '\n---'
            for (const block of this.blocks) {
                str += `\n${block}`
            }
            str += '\nend'
        }
        return str
    }

}

// 2D

/**
 * Represents a 2D Level.
 * @classdesc 2D Level in LevelZ
 * @extends Level
 */
export class Level2D extends Level {

    /**
     * Constructs a 2D Level.
     * @constructs Level2D
     * @param {Map<string, any>|Object.<string, any>} [headers] The headers of the level 
     * @param {Set<LevelObject>|LevelObject[]} [blocks] The blocks in the level
     */
    constructor(headers, blocks) {
        super(Dimension.TWO)

        if (headers) {
            if (headers instanceof Map) 
                headers.forEach((value, key) => { this.headers.set(key, value) })
            else for (const [key, value] of Object.entries(headers)) 
                this.headers.set(key, value)
        }
        if (blocks) {
            if (blocks instanceof Set)
                this.blocks = blocks
            else 
                this.blocks = new Set(blocks)
        }
    }

    /**
     * Returns the scroll direction of this 2D Level.
     * @returns {Scroll} The scroll direction
     */
    get scroll() {
        return this.headers.get('scroll')
    }

    /**
     * Sets the scroll direction of this 2D Level.
     * @param {Scroll} value The scroll direction
     * @throws Invalid scroll direction
     */
    set scroll(value) {
        if (value !== Scroll.NONE && value !== Scroll.HORIZONTAL_LEFT && value !== Scroll.HORIZONTAL_RIGHT && value !== Scroll.VERTICAL_UP && value !== Scroll.VERTICAL_DOWN)
            throw new SyntaxError('Invalid scroll direction')
        this.headers.set('scroll', value)
    }

    /**
     * Gets the spawn point of the level.
     * @returns {Coordinate2D} The spawn point of the level.
     */
    get spawn() {
        return super.spawn
    }

    /**
     * Sets the spawn point for the level.
     * @param {Coordinate2D} coordinate The coordinate to spawn the object at.
     */
    set spawn(coordinate) {
        if (coordinate instanceof Array) {
            if (coordinate.length !== 2)
                throw new SyntaxError('Invalid coordinate; must be 2D')
            this.headers.set('spawn', new Coordinate2D(coordinate[0], coordinate[1]))
            return
        }

        if (!(coordinate instanceof Coordinate2D))
            throw new SyntaxError('Invalid coordinate; must be 2D')

        this.headers.set('spawn', coordinate)
    }

}

/**
 * The auto-scroll type in a 2D Level.
 * @typedef {'none'|'horizontal-left'|'horizontal-right'|'vertical-up'|'vertical-down'} Scroll
 */

/**
 * Represents the scroll direction of a 2D Level.
 */
export const Scroll = {
    /**
     * No Scrolling
     * @type {Scroll}
     */
    NONE: "none",

    /**
     * Horizontal Scrolling moving Left
     * @type {Scroll}
     */
    HORIZONTAL_LEFT: "horizontal-left",

    /**
     * Horizontal Scrolling moving Right
     * @type {Scroll}
     */
    HORIZONTAL_RIGHT: "horizontal-right",

    /**
     * Vertical Scrolling moving Up
     * @type {Scroll}
     */
    VERTICAL_UP: "vertical-up",

    /**
     * Vertical Scrolling moving Down
     * @type {Scroll}
     */
    VERTICAL_DOWN: "vertical-down"
}

// 3D

/**
 * Represents a 3D Level.
 * @classdesc 3D Level in LevelZ
 * @extends Level
 */
export class Level3D extends Level {

    /**
     * Constructs a 3D Level.
     * @constructs Level3D
     * @param {Map<string, any>|Object.<string, any>} [headers] The headers of the level 
     * @param {Set<LevelObject>|LevelObject[]} [blocks] The blocks in the level
     */
    constructor(headers, blocks) {
        super(Dimension.THREE)

        if (headers) {
            if (headers instanceof Map) 
                headers.forEach((value, key) => { this.headers.set(key, value) })
            else for (const [key, value] of Object.entries(headers)) 
                this.headers.set(key, value)
        }
        if (blocks) 
            if (blocks instanceof Set)
                this.blocks = blocks
            else
                this.blocks = new Set(blocks)
    }

    /**
     * Gets the spawn point of the level.
     * @returns {Coordinate3D} The spawn point of the level.
     */
    get spawn() {
        return super.spawn
    }

    /**
     * Sets the spawn point for the level.
     * @param {Coordinate3D|number[]} coordinate The coordinate to spawn the object at.
     */
    set spawn(coordinate) {
        if (coordinate instanceof Array) {
            if (coordinate.length !== 3)
                throw new SyntaxError('Invalid coordinate; must be 3D')
            this.headers.set('spawn', new Coordinate3D(coordinate[0], coordinate[1], coordinate[2]))
            return
        }

        if (!(coordinate instanceof Coordinate3D))
            throw new SyntaxError('Invalid coordinate; must be 3D')

        this.headers.set('spawn', coordinate)
    }

}