lib/api/Map.js

const parser = require('../parser');
const Apply = require('./Apply');
const Decal = require('./Decal');
const Effect = require('./Effect');
const Entity = require('./Entity');
const Filler = require('./Filler');
const ObjectTiles = require('./ObjectTiles');
const Parallax = require('./Parallax');
const Room = require('./Room');
const Style = require('./Style');
const Tiles = require('./Tiles');
const Trigger = require('./Trigger');
const { attributes, children, findChildWithName } = require('../utils');

class Map {
	/**
	 * Create a Map instance
	 * @constructor
	 * @param {Object} [data={}] - An object defining the properties of the map
	 * @param {String} [data.package='lucid'] - The name of the mod package
	 * @param {Object} [data.rooms=[]] - An Array of {@link Room} instances
	 * @param {Style} [data.style=new Style()] - A {@link Style} instance
	 * @param {Object} [data.fillers=[]] - An Array of {@link Filler} instances
	 * @example
	 * // returns a new Map
	 * const { Map, Room } = require('lucid-dream');
	 * const map = new Map({
	 *   package: 'mypackage',
	 *   rooms: [ new Room([ 0, 0 ]) ]
	 * });
	 */
	constructor(data = {}) {
		Object.assign(this, {
			package: 'lucid',
			rooms: [],
			style: new Style(),
			fillers: []
		}, data);
	}

	/**
	 * Decodes a map from a `.bin` file
	 * @param {String} file - The path to the `.bin` file to decode
	 * @example
	 * // decodes a map
	 * const { Map } = require('lucid-dream');
	 * const map = new Map();
	 * const decodedMap = await map.decode('/path/to/my-map.bin');
	 * @returns {Object} The decoded map data
	 */
	async decode(file) {
		const data = await parser.bin.decode(file);
		const pkg = data._package;
		const roomsData = findChildWithName(data, 'levels');
		const style = findChildWithName(data, 'Style');
		const fgStyle = loadBackdrops(findChildWithName(style, 'Foregrounds'));
		const bgStyle = loadBackdrops(findChildWithName(style, 'Backgrounds'));
		const fillerRects = loadFillerRects(findChildWithName(data, 'Filler'));

		const rooms = children(roomsData).reduce((a,c) => {
			a.push(loadRoom(c));
			return a;
		}, []);

		// update the map with the decode data
		Object.assign(this, {
			package: pkg,
			rooms,
			style: new Style(fgStyle, bgStyle),
			fillers: fillerRects
		});

		// return the decoded data
		return data;
	}

	/**
	 * Encodes map data to a `.bin` file
	 * @param {Object} data - {@link Map} data
	 * @param {String} file - The path to the `.bin` file to which to encode
	 * @example
	 * // encodes a map to a .bin file
	 * const { Map } = require('lucid-dream');
	 * const map = new Map();
	 * await map.decode('/path/to/my-map.bin');
	 * // make your changes to the map
	 * await map.encode(map.data, '/path/to/my-new-map.bin');
	 * @returns {null}
	 */
	async encode(data, file) {
		await parser.bin.encode(this.toDict(), file);
	}

	/**
	 * Encode a map into a dictionary
	 * @example
	 * // returns encoded map dictionary
	 * const { Map } = require('lucid-dream');
	 * const map = new Map();
	 * map.toDict();
	 * @returns {Object} An object containing the encoded map dicitonary
	 */
	toDict() {
		return {
			_package: this.package,
			__name: 'Map',
			__children: [
				{
					__name: 'levels',
					__children: this.rooms.map(r => r.toDict())
				},
				{
					__name: 'Style',
					__children: this.style.toDict()
				},
				{
					__name: 'Filler',
					__children: this.fillers.map(f => f.toDict())
				}
			]
		};
	}

}

/*********************/
/* private functions */
/*********************/

function loadBackdrops(styleData) {
	const backdrops = [];

	for (let child of children(styleData)) {
		const styleType = child.__name;

		if (styleType === 'parallax') {
			backdrops.push(new Parallax(attributes(child)));
		} else if (styleType === 'apply') {
			const applyAttr = attributes(child);
			const parallax = [];

			for (let data of children(child)) {
				const typ = data.__name;
				if (typ === 'parallax') {
					parallax.push(new Parallax(attributes(data)));
				} else {
					parallax.push(new Effect(typ, attributes(data)));
				}
			}

			backdrops.push(new Apply(applyAttr, parallax));
		} else {
			backdrops.push(new Effect(styleType, attributes(child)));
		}
	}

	return backdrops;
}

function loadFillerRects(fillerData) {
	const fillers = [];

	for (let child of children(fillerData)) {
		fillers.push(new Filler(child.x, child.y, child.w, child.h));
	}

	return fillers;
}

function loadDecals(roomData, isFg = true) {
	const decals = [];
	const key = isFg ? 'fgdecals' : 'bgdecals';

	for (let child of children(findChildWithName(roomData, key))) {
		decals.push(new Decal(child.texture, {
			x: child.x,
			y: child.y,
			scaleX: child.scaleX,
			scaleY: child.scaleY
		}));
	}

	return decals;
}

function loadEntities(roomData, key) {
	const entities = [];
	const func = key === 'entities' ? Entity : Trigger;

	for (let child of (children(findChildWithName(roomData, key)))) {
		let id = child.id == null ? -1 : child.id;
		id = Number.isInteger(id) ? id : 0;
		delete child.id;

		const nodesRaw = children(child);

		if (nodesRaw && nodesRaw.length) {
			child.nodes = nodesRaw.reduce((a,c) => {
				a.push([c.x, c.y]);
				return a;
			}, []);
		}
		entities.push(new func(child.__name, attributes(child), id));
	}

	return entities;
}


function loadRoom(roomData) {
	const fgDecals = loadDecals(roomData, true);
	const bgDecals = loadDecals(roomData, false);

	const entities = loadEntities(roomData, 'entities');
	const triggers = loadEntities(roomData, 'triggers');

	const fgTilesRaw = findChildWithName(roomData, 'solids').innerText || '';
	const bgTilesRaw = findChildWithName(roomData, 'bg').innerText || '';
	const objTilesRaw = findChildWithName(roomData, 'objtiles').innerText || '';

	const fgTiles = new Tiles(fgTilesRaw);
	const bgTiles = new Tiles(bgTilesRaw);
	const objTiles = new ObjectTiles(objTilesRaw);

	const room = new Room([ roomData.x, roomData.y ], {
		name: roomData.name || 'lvl_1',
		size: [ roomData.width / 8, roomData.height / 8 ],
		entities,
		triggers,
		bgDecals,
		fgDecals,
		fgTiles,
		bgTiles,
		objTiles,
		musicLayer1: roomData.musicLayer1 || true,
		musicLayer2: roomData.musicLayer2 || true,
		musicLayer3: roomData.musicLayer3 || true,
		musicLayer4: roomData.musicLayer4 || true,
		musicProgress: roomData.musicProgress || '',
		ambienceProgress: roomData.ambienceProgress || '',
		dark: roomData.dark || false,
		space: roomData.space || false,
		underwater: roomData.underwater || false,
		whisper: roomData.whisper || false,
		disableDownTransition: roomData.disableDownTransition || false,
		delayAltMusicFade: roomData.delayAltMusicFade || false,
		music: roomData.music || 'music_oldsite_awake',
		altMusic: roomData.altMusic || '',
		windPattern: roomData.windPattern || 'None',
		color: roomData.c || 0,
		cameraOffsetX: roomData.cameraOffsetX || 0,
		cameraOffsetY: roomData.cameraOffsetY || 0
	});

	return room;
}

module.exports = Map;