let objects = require('../objects/objects'); let physics = require('./physics'); let spawners = require('./spawners'); let resourceSpawner = require('./resourceSpawner'); let globalZone = require('../config/zoneBase'); let randomMap = require('./randomMap/randomMap'); const generateMappings = require('./randomMap/generateMappings'); let events = require('../misc/events'); const mapObjects = require('./map/mapObjects'); const canPathFromPos = require('./map/canPathFromPos'); let mapFile = null; let mapScale = null; let padding = null; const objectifyProperties = oldProperties => { if (!oldProperties || !oldProperties.push) return oldProperties || {}; let newProperties = {}; oldProperties.forEach(p => { newProperties[p.name] = p.value; }); return newProperties; }; module.exports = { name: null, path: null, layers: [], mapFile: null, //The size of the base map, before mods are applied originalSize: { w: 0, h: 0 }, //The size of the map after mods are applied size: { w: 0, h: 0 }, custom: null, collisionMap: null, clientMap: null, oldLayers: { tiles: null, walls: null, doodads: null }, objBlueprints: [], spawn: { x: 0, y: 0 }, rooms: [], hiddenRooms: [], hiddenWalls: null, hiddenTiles: null, zone: null, init: function (args) { this.name = args.name; this.path = args.path; try { this.zone = require('../' + this.path + '/' + this.name + '/zone'); } catch (e) { this.zone = globalZone; } events.emit('onAfterGetZone', this.name, this.zone); let chats = null; try { chats = require('../' + this.path + '/' + this.name + '/chats'); } catch (e) {} if (chats) this.zone.chats = chats; let dialogues = null; try { dialogues = require('../' + this.path + '/' + this.name + '/dialogues'); } catch (e) {} events.emit('onBeforeGetDialogue', this.name, dialogues); if (dialogues) this.zone.dialogues = dialogues; this.zone = extend({}, globalZone, this.zone); let resources = this.zone.resources || {}; for (let r in resources) resourceSpawner.register(r, resources[r]); mapFile = require('../' + this.path + '/' + this.name + '/map'); this.mapFile = mapFile; //Fix for newer versions of Tiled this.mapFile.properties = objectifyProperties(this.mapFile.properties); mapScale = mapFile.tilesets[0].tileheight; this.custom = mapFile.properties.custom; if (mapFile.properties.spawn) { this.spawn = JSON.parse(mapFile.properties.spawn); if (!this.spawn.push) this.spawn = [this.spawn]; } }, create: function () { this.getMapFile(); this.clientMap = { zoneId: -1, map: this.layers, collisionMap: this.collisionMap, clientObjects: this.objBlueprints, padding: padding, hiddenRooms: this.hiddenRooms }; }, getMapFile: function () { this.build(); this.randomMap = extend({}, randomMap); this.oldMap = extend([], this.layers); this.randomMap.templates = extend([], this.rooms); generateMappings(this.randomMap, this); if (!mapFile.properties.isRandom) { for (let i = 0; i < this.size.w; i++) { let row = this.layers[i]; for (let j = 0; j < this.size.h; j++) { let cell = row[j]; if (!cell) continue; cell = cell.split(','); let cLen = cell.length; let newCell = ''; for (let k = 0; k < cLen; k++) { let c = cell[k]; let newC = c; //Randomize tile const msgBeforeRandomizePosition = { success: true, x: i, y: j, map: this.name }; events.emit('onBeforeRandomizePosition', msgBeforeRandomizePosition); if (msgBeforeRandomizePosition.success) newC = this.randomMap.randomizeTile(c); newCell += newC; //Wall? if ((c >= 160) && (c <= 352) && (newC === 0)) this.collisionMap[i][j] = 0; if (k < cLen - 1) newCell += ','; } let fakeContents = []; const hiddenWall = this.hiddenWalls[i][j]; const hiddenTile = this.hiddenTiles[i][j]; if (hiddenTile) fakeContents.push(-this.randomMap.randomizeTile(hiddenTile)); if (hiddenWall) fakeContents.push(-this.randomMap.randomizeTile(hiddenWall)); if (fakeContents.length) newCell += ',' + fakeContents.join(','); row[j] = newCell; } } } //Fix for newer versions of Tiled this.randomMap.templates .forEach(r => { r.properties = objectifyProperties(r.properties); }); this.randomMap.templates .filter(r => r.properties.mapping) .forEach(function (m) { let x = m.x; let y = m.y; let w = m.width; let h = m.height; for (let i = x; i < x + w; i++) { let row = this.layers[i]; for (let j = y; j < y + h; j++) row[j] = ''; } }, this); physics.init(this.collisionMap); padding = mapFile.properties.padding; mapFile = null; }, build: function () { const mapSize = { w: mapFile.width, h: mapFile.height }; this.originalSize = { w: mapFile.width, h: mapFile.height }; events.emit('onBeforeGetMapSize', this.name, mapSize); this.size.w = mapSize.w; this.size.h = mapSize.h; const { w: oldW, h: oldH } = this.originalSize; const { w, h } = this.size; this.layers = _.get2dArray(w, h, null); this.hiddenWalls = _.get2dArray(w, h, null); this.hiddenTiles = _.get2dArray(w, h, null); this.oldLayers.tiles = _.get2dArray(w, h, 0); this.oldLayers.walls = _.get2dArray(w, h, 0); this.oldLayers.doodads = _.get2dArray(w, h, 0); let builders = { tile: this.builders.tile.bind(this), object: this.builders.object.bind(this) }; this.collisionMap = _.get2dArray(w, h); const layers = [...mapFile.layers.filter(l => l.objects), ...mapFile.layers.filter(l => !l.objects)]; //Rooms need to be ahead of exits const layerRooms = layers.find(l => l.name === 'rooms') || {}; layerRooms.objects.sort((a, b) => { const isExitA = a?.properties?.some(p => p.name === 'exit'); const isExitB = b?.properties?.some(p => p.name === 'exit'); if (isExitA && !isExitB) return 1; else if (!isExitA && isExitB) return -1; return 0; }); for (let i = 0; i < layers.length; i++) { let layer = layers[i]; let layerName = layer.name; if (!layer.visible) continue; let data = layer.data || layer.objects; if (layer.objects) { let info = { map: this.name, layer: layerName, objects: data, mapScale, size: this.size }; events.emit('onAfterGetLayerObjects', info); } if (layer.objects) { let len = data.length; for (let j = 0; j < len; j++) { let cell = data[j]; builders.object(layerName, cell, j); } } else { for (let x = 0; x < w; x++) { for (let y = 0; y < h; y++) { let index = (y * oldW) + x; const msgBuild = { map: this.name, layer: layerName, sheetName: null, cell: 0, x, y }; if (x < oldW && y < oldH) msgBuild.cell = data[index]; events.emit('onBeforeBuildLayerTile', msgBuild); builders.tile(msgBuild); events.emit('onAfterBuildLayerTile', msgBuild); } } } } }, getOffsetCellPos: function (sheetName, cell) { const { config: { atlasTextureDimensions, atlasTextures } } = clientConfig; const indexInAtlas = atlasTextures.indexOf(sheetName); let offset = 0; for (let i = 0; i < indexInAtlas; i++) { const dimensions = atlasTextureDimensions[atlasTextures[i]]; offset += (dimensions.width / 8) * (dimensions.height / 8); } return cell + offset; }, getCellInfo: function (gid, x, y, layerName) { const cellInfoMsg = { mapName: this.name, x, y, layerName, tilesets: mapFile.tilesets, sheetName: null }; events.emit('onBeforeGetCellInfo', cellInfoMsg); const tilesets = cellInfoMsg.tilesets; let flipX = null; if ((gid ^ 0x80000000) > 0) { flipX = true; gid = gid ^ 0x80000000; } let firstGid = 0; let sheetName = cellInfoMsg.sheetName; if (!sheetName) { for (let s = 0; s < tilesets.length; s++) { let tileset = tilesets[s]; if (tileset.firstgid <= gid) { sheetName = tileset.name; firstGid = tileset.firstgid; } } gid = gid - firstGid + 1; } return { cell: gid, sheetName, flipX }; }, builders: { tile: function (info) { let { x, y, cell, layer: layerName, sheetName } = info; if (cell === 0) { if (layerName === 'tiles') this.collisionMap[x][y] = 1; return; } let cellInfo = this.getCellInfo(cell, x, y, layerName); if (!sheetName) { info.sheetName = cellInfo.sheetName; sheetName = cellInfo.sheetName; } const offsetCell = this.getOffsetCellPos(sheetName, cellInfo.cell); const isHiddenLayer = layerName.indexOf('hidden') === 0; if (isHiddenLayer) this[layerName][x][y] = offsetCell; else { const layer = this.layers; if (this.oldLayers[layerName]) this.oldLayers[layerName][x][y] = offsetCell; layer[x][y] = (layer[x][y] === null) ? offsetCell : layer[x][y] + ',' + offsetCell; if (layerName.indexOf('walls') > -1) this.collisionMap[x][y] = 1; else if (clientConfig.config.blockingTileIndices.includes(offsetCell)) this.collisionMap[x][y] = 1; } }, object: function (layerName, cell) { //Fixes for newer versions of tiled cell.properties = objectifyProperties(cell.properties); cell.polyline = cell.polyline || cell.polygon; const x = cell.x / mapScale; const y = (cell.y / mapScale) - 1; let clientObj = (layerName === 'clientObjects'); let cellInfo = this.getCellInfo(cell.gid, x, y, layerName); let name = (cell.name || ''); let objZoneName = name; if (name.indexOf('|') > -1) { let split = name.split('|'); name = split[0]; objZoneName = split[1]; } let blueprint = { id: cell.properties.id, clientObj: clientObj, sheetName: cell.has('sheetName') ? cell.sheetName : cellInfo.sheetName, cell: cell.has('cell') ? cell.cell : cellInfo.cell - 1, x, y, name: name, properties: cell.properties || {}, layerName: layerName }; if (objZoneName !== name) blueprint.objZoneName = objZoneName; if (this.zone) { if ((this.zone.objects) && (this.zone.objects[objZoneName.toLowerCase()])) extend(blueprint, this.zone.objects[objZoneName.toLowerCase()]); else if ((this.zone.objects) && (this.zone.mobs[objZoneName.toLowerCase()])) extend(blueprint, this.zone.mobs[objZoneName.toLowerCase()]); } if (blueprint.blocking) this.collisionMap[blueprint.x][blueprint.y] = 1; if ((blueprint.properties.cpnNotice) || (blueprint.properties.cpnLightPatch) || (layerName === 'rooms') || (layerName === 'hiddenRooms')) { blueprint.y++; blueprint.width = cell.width / mapScale; blueprint.height = cell.height / mapScale; } else if (cell.width === 24) blueprint.x++; if (cell.polyline) mapObjects.polyline(this.size, blueprint, cell, mapScale); if (layerName === 'rooms') { if (blueprint.properties.exit) { let room = this.rooms.find(function (r) { return (!( (blueprint.x + blueprint.width < r.x) || (blueprint.y + blueprint.height < r.y) || (blueprint.x >= r.x + r.width) || (blueprint.y >= r.y + r.height) )); }); room.exits.push(blueprint); } else if (blueprint.properties.resource) resourceSpawner.register(blueprint.properties.resource, blueprint); else { blueprint.exits = []; blueprint.objects = []; this.rooms.push(blueprint); } } else if (layerName === 'hiddenRooms') { blueprint.fog = (cell.properties || {}).fog; blueprint.interior = (cell.properties || {}).interior; blueprint.discoverable = (cell.properties || {}).discoverable; blueprint.layer = ~~((cell.properties || {}).layer || 0); if (!mapFile.properties.isRandom) this.hiddenRooms.push(blueprint); else { let room = this.rooms.find(r => { return !( blueprint.x < r.x || blueprint.y < r.y || blueprint.x >= r.x + r.width || blueprint.y >= r.y + r.height ); }); room.objects.push(blueprint); } } else if (!clientObj) { if (!mapFile.properties.isRandom) spawners.register(blueprint, blueprint.spawnCd || mapFile.properties.spawnCd); else { let room = this.rooms.find(r => { return !( blueprint.x < r.x || blueprint.y < r.y || blueprint.x >= r.x + r.width || blueprint.y >= r.y + r.height ); }); room.objects.push(blueprint); } } else { if ((cell.width) && (!cell.polyline)) { blueprint.width = cell.width / mapScale; blueprint.height = cell.height / mapScale; } let obj = objects.buildObjects([blueprint], true).getSimple(true); this.objBlueprints.push(obj); } } }, getSpawnPos: function (obj) { let stats = obj.components.find(c => (c.type === 'stats')); let level = stats.values.level; let spawns = this.spawn.filter(s => (((s.maxLevel) && (s.maxLevel >= level)) || (!s.maxLevel))); return spawns[0]; }, //Find if any spawns can path to a position. This is important for when maps change and players // log in on tiles that aren't blocking but not able to reach anywhere useful canPathFromPos: function (pos) { return canPathFromPos(this, pos); } };