ChiefOfGxBxL/WC3MapTranslator

View on GitHub
lib/translators/TerrainTranslator.ts

Summary

Maintainability
B
6 hrs
Test Coverage
import { HexBuffer } from '../HexBuffer';
import { W3Buffer } from '../W3Buffer';
import { WarResult, JsonResult } from '../CommonInterfaces'

interface Terrain {
    tileset: string;
    customTileset: boolean;
    tilePalette: string[];
    cliffTilePalette: string[];
    map: Map;
    // "Masks"
    groundHeight: number[],
    waterHeight: number[],
    boundaryFlag: boolean[],
    flags: number[],
    groundTexture: number[],
    groundVariation: number[],
    cliffVariation: number[],
    cliffTexture: number[],
    layerHeight: number[]
}

interface Map {
    width: number;
    height: number;
    offset: Offset;
}

interface Offset {
    x: number;
    y: number;
}

function splitLargeArrayIntoWidthArrays(array: any[], width: number) {
    const rows = [];
    for(let i = 0; i < array.length / width; i++) {
        rows.push(array.slice(i * width, (i+1) * width));
    }
    return rows;
}

export abstract class TerrainTranslator {

    public static jsonToWar(terrainJson: Terrain): WarResult {
        const outBufferToWar = new HexBuffer();

        /*
         * Header
         */
        outBufferToWar.addChars('W3E!'); // file id
        outBufferToWar.addInt(11); // file version
        outBufferToWar.addChar(terrainJson.tileset); // base tileset
        outBufferToWar.addInt(+terrainJson.customTileset); // 1 = using custom tileset, 0 = not

        /*
         * Tiles
         */
        outBufferToWar.addInt(terrainJson.tilePalette.length);
        terrainJson.tilePalette.forEach((tile) => {
            outBufferToWar.addChars(tile);
        });

        /*
         * Cliffs
         */
        outBufferToWar.addInt(terrainJson.cliffTilePalette.length);
        terrainJson.cliffTilePalette.forEach((cliffTile) => {
            outBufferToWar.addChars(cliffTile);
        });

        /*
         * Map size data
         */
        outBufferToWar.addInt(terrainJson.map.width + 1);
        outBufferToWar.addInt(terrainJson.map.height + 1);

        /*
         * Map offset
         */
        outBufferToWar.addFloat(terrainJson.map.offset.x);
        outBufferToWar.addFloat(terrainJson.map.offset.y);

        /*
         * Tile points
         */
        // Partition the terrainJson masks into "chunks" (i.e. rows) of (width+1) length,
        // reverse that list of rows (due to vertical flipping), and then write the rows out
        const rows = {
            groundHeight: splitLargeArrayIntoWidthArrays(terrainJson.groundHeight, terrainJson.map.width + 1),
            waterHeight: splitLargeArrayIntoWidthArrays(terrainJson.waterHeight, terrainJson.map.width + 1),
            boundaryFlag: splitLargeArrayIntoWidthArrays(terrainJson.boundaryFlag, terrainJson.map.width + 1),
            flags: splitLargeArrayIntoWidthArrays(terrainJson.flags, terrainJson.map.width + 1),
            groundTexture: splitLargeArrayIntoWidthArrays(terrainJson.groundTexture, terrainJson.map.width + 1),
            groundVariation: splitLargeArrayIntoWidthArrays(terrainJson.groundVariation, terrainJson.map.width + 1),
            cliffVariation: splitLargeArrayIntoWidthArrays(terrainJson.cliffVariation, terrainJson.map.width + 1),
            cliffTexture: splitLargeArrayIntoWidthArrays(terrainJson.cliffTexture, terrainJson.map.width + 1),
            layerHeight: splitLargeArrayIntoWidthArrays(terrainJson.layerHeight, terrainJson.map.width + 1)
        };

        rows.groundHeight.reverse();
        rows.waterHeight.reverse();
        rows.boundaryFlag.reverse();
        rows.flags.reverse();
        rows.groundTexture.reverse();
        rows.groundVariation.reverse();
        rows.cliffVariation.reverse();
        rows.cliffTexture.reverse();
        rows.layerHeight.reverse();

        for(let i = 0; i < rows.groundHeight.length; i++) {
            for(let j = 0; j < rows.groundHeight[i].length; j++) {
                // these bit operations are based off documentation from https://github.com/stijnherfst/HiveWE/wiki/war3map.w3e-Terrain
                const groundHeight = rows.groundHeight[i][j];
                const waterHeight = rows.waterHeight[i][j];
                const boundaryFlag = rows.boundaryFlag[i][j];
                const flags = rows.flags[i][j];
                const groundTexture = rows.groundTexture[i][j];
                const groundVariation = rows.groundVariation[i][j];
                const cliffVariation = rows.cliffVariation[i][j];
                const cliffTexture = rows.cliffTexture[i][j];
                const layerHeight = rows.layerHeight[i][j];

                const hasBoundaryFlag = boundaryFlag ? 0x4000 : 0;

                outBufferToWar.addShort(groundHeight);
                outBufferToWar.addShort(waterHeight | hasBoundaryFlag);
                outBufferToWar.addByte(flags | groundTexture);
                outBufferToWar.addByte(groundVariation | cliffVariation);
                outBufferToWar.addByte(cliffTexture | layerHeight);
            }
        }

        return {
            errors: [],
            buffer: outBufferToWar.getBuffer()
        };
    }

    public static warToJson(buffer: Buffer): JsonResult<Terrain> {
        // create buffer
        const result: Terrain = {
            tileset: '',
            customTileset: false,
            tilePalette: [],
            cliffTilePalette: [],
            map: {
                width: 1,
                height: 1,
                offset: {
                    x: 0,
                    y: 0
                }
            },
            groundHeight: [],
            waterHeight: [],
            boundaryFlag: [],
            flags: [],
            groundTexture: [],
            groundVariation: [],
            cliffVariation: [],
            cliffTexture: [],
            layerHeight: []
        };
        const outBufferToJSON = new W3Buffer(buffer);

        /**
         * Header
         */
        const w3eHeader = outBufferToJSON.readChars(4); // W3E!
        const version = outBufferToJSON.readInt(); // 0B 00 00 00
        const tileset = outBufferToJSON.readChars(1); // tileset
        const customTileset = (outBufferToJSON.readInt() === 1);

        result.tileset = tileset;
        result.customTileset = customTileset;

        /**
         * Tiles
         */
        const numTilePalettes = outBufferToJSON.readInt();
        const tilePalettes = [];
        for (let i = 0; i < numTilePalettes; i++) {
            tilePalettes.push(outBufferToJSON.readChars(4));
        }

        result.tilePalette = tilePalettes;

        /**
         * Cliffs
         */
        const numCliffTilePalettes = outBufferToJSON.readInt();
        const cliffPalettes = [];
        for (let i = 0; i < numCliffTilePalettes; i++) {
            const cliffPalette = outBufferToJSON.readChars(4);
            cliffPalettes.push(cliffPalette);
        }

        result.cliffTilePalette = cliffPalettes;

        /**
         * map dimensions
         */
        const width = outBufferToJSON.readInt() - 1;
        const height = outBufferToJSON.readInt() - 1;
        result.map = { width, height, offset: { x: 0, y: 0 } };

        const offsetX = outBufferToJSON.readFloat();
        const offsetY = outBufferToJSON.readFloat();
        result.map.offset = { x: offsetX, y: offsetY };

        /**
         * map tiles
         */
        const arr_groundHeight = [];
        const arr_waterHeight = [];
        const arr_boundaryFlag = [];
        const arr_flags = [];
        const arr_groundTexture = [];
        const arr_groundVariation = [];
        const arr_cliffVariation = [];
        const arr_cliffTexture = [];
        const arr_layerHeight = [];

        while(!outBufferToJSON.isExhausted()) {
            const groundHeight = outBufferToJSON.readShort();
            const waterHeightAndBoundary = outBufferToJSON.readShort();
            const flagsAndGroundTexture = outBufferToJSON.readByte();
            const groundAndCliffVariation = outBufferToJSON.readByte();
            const cliffTextureAndLayerHeight = outBufferToJSON.readByte();

            // parse out different bits (based on documentation from https://github.com/stijnherfst/HiveWE/wiki/war3map.w3e-Terrain)
            const waterHeight = waterHeightAndBoundary & 32767;
            const boundaryFlag = (waterHeightAndBoundary & 0x4000) === 0x4000;
            const flags = flagsAndGroundTexture & 240;
            const groundTexture = flagsAndGroundTexture & 15;
            const groundVariation = groundAndCliffVariation & 248;
            const cliffVariation = groundAndCliffVariation & 7;
            const cliffTexture = cliffTextureAndLayerHeight & 240;
            const layerHeight = cliffTextureAndLayerHeight & 15;

            arr_groundHeight.push(groundHeight);
            arr_waterHeight.push(waterHeight);
            arr_boundaryFlag.push(boundaryFlag);
            arr_flags.push(flags);
            arr_groundTexture.push(groundTexture);
            arr_groundVariation.push(groundVariation);
            arr_cliffVariation.push(cliffVariation);
            arr_cliffTexture.push(cliffTexture);
            arr_layerHeight.push(layerHeight);
        }

        function convertArrayOfArraysIntoFlatArray(arr) {
            return arr.reduce((a, b) => {
                return [...a, ...b]
            });
        }

        // The map was read in "backwards" because wc3 maps have origin (0,0)
        // at the bottom left instead of top left as we desire. Flip the rows
        // vertically to fix this.
        result.groundHeight = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arr_groundHeight, result.map.width + 1).reverse())
        result.waterHeight = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arr_waterHeight, result.map.width + 1).reverse())
        result.boundaryFlag = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arr_boundaryFlag, result.map.width + 1).reverse())
        result.flags = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arr_flags, result.map.width + 1).reverse())
        result.groundTexture = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arr_groundTexture, result.map.width + 1).reverse())
        result.groundVariation = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arr_groundVariation, result.map.width + 1).reverse())
        result.cliffVariation = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arr_cliffVariation, result.map.width + 1).reverse())
        result.cliffTexture = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arr_cliffTexture, result.map.width + 1).reverse())
        result.layerHeight = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arr_layerHeight, result.map.width + 1).reverse())

        return {
            errors: [],
            json: result
        };
    }
}