const EventEmitter = require("events"); const Utility = require("./Utility"); const Reward = require("./Reward"); const EntityGroup = require("./EntityGroup"); const mapMatrix = require("../Data/mapMatrix"); const constants = require("../Data/constants"); const DoubleLinkedList = require("../Utility/DoubleLinkedList"); const grids = require("./Grids"); const { TYPES, EVENTS } = constants; const { ENTITY_TYPE_PLAYER } = TYPES; const { EVENT_SPAWN_ADMIRAL } = EVENTS; const COORDINATE_REGIONS = ["A", "B", "C"]; const COORDINATE_SECTIONS = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]; function getType(compositeID) { return (compositeID >> 24) & 0xFF; }; class GameMap extends EventEmitter { constructor(config) { super(); this.w = config.size.width; this.h = config.size.height; this.cw = config.size.chunk.width; this.ch = config.size.chunk.height; this.chunkRows = config.size.height / config.size.chunk.height; this.chunkColumns = config.size.width / config.size.chunk.width; this.name = config.name; this.type = config.type; this.id = config.id; this.neighbours = config.neighbours; this.collision = config.collision ?? {}; this.level = config.level; this.spawnedEntitiesTotalCount = 0; this.availableIdentifiers = []; this.players = new Map(); this.npcs = new Map(); this.monsters = new Map(); this.towers = new Map(); this.collectables = new Map(); this.entities = [this.players, this.npcs, this.monsters, this.towers, this.collectables]; this.entityGroups = Array.isArray(config.entityGroups) ? config.entityGroups.map(e => new EntityGroup(e)) : []; this.chunksData = []; this.networkPacketsGlobal = []; this.networkPacketsLocal = []; this.networkPacketsInvidiual = {}; this.testPackets = []; this.eventBuffer = {}; this.eventAuthors = []; this.lookingForTarget = []; this.dirtyChunks = []; this.grid = grids[this.id - 1]; for (let i = 0; i < this.chunkColumns; i++) { for (let j = 0; j < this.chunkRows; j++) { const m = new DoubleLinkedList(); this[`chunkTest_${i}-${j}`] = m; this[`chunk_${i}-${j}`] = new DoubleLinkedList(); this.dirtyChunks.push(m); this.chunksData.push({ x: i * config.size.chunk.width, y: j * config.size.chunk.height, name: `chunk_${i}-${j}`, nameTest: `chunkTest_${i}-${j}` }); }; }; } registerIndividualNetworkPacket(identifier, packets, metadata = []) { if (!(packets instanceof Array)) packets = [packets]; if (!(metadata instanceof Array)) metadata = [metadata]; if (!this.networkPacketsInvidiual[identifier]) this.networkPacketsInvidiual[identifier] = []; packets.forEach(packet => this.networkPacketsInvidiual[identifier].push({ index: this.networkPacketsLocal.length, packet, metadata })); } registerGlobalNetworkPacket(identifier, packets) { if (!(packets instanceof Array)) packets = [packets]; const entity = this.getEntityFromSubgroup(identifier); packets.forEach(p => this.networkPacketsGlobal.push({ position: entity.position, authorID: entity.id, packet: p })); } registerNetworkPacket(identifier, packets, headers = []) { if (!(packets instanceof Array)) packets = [packets]; const entity = this.getEntityFromSubgroup(identifier); if (headers[0] === 59) packets.forEach((p, i) => this.testPackets.push({ position: entity.position, authorID: entity.id, packet: p, header: headers[i] ? headers[i] : 59 })); else packets.forEach((p, i) => this.networkPacketsLocal.push({ position: entity.position, authorID: entity.id, packet: p, header: headers[i] ? headers[i] : 0 })); } getNeighbourMapsInRadius(radius) { const maps = []; for (let i = 0; i < mapMatrix.length; i++) { const mapRow = mapMatrix[i]; for (let j = 0; j < mapRow.length; j++) { if (this.id === mapRow[j]) { const startFromColumn = Math.max(0, j - radius); const startFromRow = Math.max(0, i - radius); const rawColumnBoundary = (j + radius) + 1; const rawRowBoundary = (i + radius) + 1; const columnBoundary = rawColumnBoundary > mapRow.length - 1 ? mapRow.length : rawColumnBoundary; const rowBoundary = rawRowBoundary > mapMatrix.length - 1 ? mapMatrix.length : rawRowBoundary; for (let l = startFromRow; l < rowBoundary; l++) for (let k = startFromColumn; k < columnBoundary; k++) maps.push(mapMatrix[l][k]); break; }; }; }; return maps; } spawnEntityGroups(cb, NPC, attackCallback, dieCallback) { for (let i = 0, length = this.entityGroups.length; i < length; i++) { const group = this.entityGroups[i]; if (!group.isDefeated()) continue; group.default(); const randomPosition = this.grid.randomNode(20); group.setAreaPosition(randomPosition); for (let i = 0, length = group.structure.length; i < length; i++) { const { typeID, entityTypeID, groupMemberTypeID } = group.structure[i]; const entityTypeSetting = group.getEntitySetting(groupMemberTypeID); const preSelectedDestinationNode = this.grid.randomNodeInArea(group.predefinedArea); const spawnData = { id: this.spawnedEntitiesTotalCount, speed: entityTypeSetting.speed, respawnTime: entityTypeSetting.respawnTime, size: entityTypeSetting.size, entityTypeID, parentEntityTypeID: 0, maximumHitpoint: 4500, currentHitpoint: 4500, map: this, group, position: preSelectedDestinationNode, attackCallback, dieCallback }; const spawn = this.registerNonPlayerCharacter(spawnData, NPC); if (groupMemberTypeID === 1) group.addLeader(spawn.npc); else group.addMember(spawn.npc); cb(this, spawn); }; }; } setupNonPlayerCharacterSpawn(spawn, cb, globalDieCallback, globalRewardCallback, globalAttackCallback, NPC) { spawn.forEach(data => { const { id, parentEntityTypeId, speed, minHealth, maxHealth, amount, respawnTime, rewardEntries } = data; if (!parentEntityTypeId) return; const admiralNPC = spawn.find(parent => parent.id === parentEntityTypeId); const reward = new Reward({ distributionType: 1, baseReward: rewardEntries.reduce((result, entry) => { if (result[entry.categoryId]) result[entry.categoryId][entry.itemId] = { amountMinimum: entry.min, amountMaximum: entry.max, chance: entry.chance, lastShot: entry.isLastShot }; else result[entry.categoryId] = { [entry.itemId]: { amountMinimum: entry.min, amountMaximum: entry.max, chance: entry.chance, lastShot: entry.isLastShot } }; return result; }, {}) }); for (let i = 0; i < amount; i++) { const spareID = this.availableIdentifiers.pop(); const hitpoint = Math.floor(Math.random() * (maxHealth - minHealth + 1) + minHealth); const npcSpawnData = { id: spareID ? spareID : this.spawnedEntitiesTotalCount, speed, respawnTime, reward, entityTypeID: id, parentEntityTypeID: parentEntityTypeId, maximumHitpoint: hitpoint, currentHitpoint: hitpoint, map: this, behaviour: { stopIfAttacked: true, aggressive: false }, attackCallback: globalAttackCallback, rewardCallback: globalRewardCallback }; const newSpawn = this.registerNonPlayerCharacter(npcSpawnData, NPC); newSpawn.npc.dieCallback = data => { const isAdmiralReady = globalDieCallback.call(newSpawn.npc, data); if (isAdmiralReady && admiralNPC) { const { id, speed, minHealth, maxHealth, rewardEntries } = admiralNPC; const reward = new Reward({ distributionType: 1, baseReward: rewardEntries.reduce((result, entry) => { if (result[entry.categoryId]) result[entry.categoryId][entry.itemId] = { amountMinimum: entry.min, amountMaximum: entry.max, chance: entry.chance, lastShot: entry.isLastShot }; else result[entry.categoryId] = { [entry.itemId]: { amountMinimum: entry.min, amountMaximum: entry.max, chance: entry.chance, lastShot: entry.isLastShot } }; return result; }, {}) }); const spareID = this.availableIdentifiers.pop(); const hitpoint = Math.floor(Math.random() * (maxHealth - minHealth + 1) + minHealth); const admiralSpawnData = { id: spareID ? spareID : this.spawnedEntitiesTotalCount, isAdmiral: true, speed, reward, size: 192, entityTypeID: id, position: newSpawn.npc.position, maximumHitpoint: hitpoint, currentHitpoint: hitpoint, map: this, behaviour: { stopIfAttacked: true, aggressive: false }, dieCallback: globalDieCallback, attackCallback: globalAttackCallback, rewardCallback: globalRewardCallback }; const emittingTo = this.getNeighbourMapsInRadius(1); const notificationData = { mapID: this.id, typeID: newSpawn.npc.typeID, entityTypeID: newSpawn.npc.entityTypeID }; this.emit(EVENT_SPAWN_ADMIRAL, notificationData, emittingTo); const admiral = this.registerNonPlayerCharacter(admiralSpawnData, NPC); cb(this, admiral); }; }; cb(this, newSpawn); }; }); } registerNonPlayerCharacter(data, NPC) { const npc = new NPC(data); if (!this.availableIdentifiers.length) this.spawnedEntitiesTotalCount++; const npcSpawnProperties = npc.getProperties(); return { npc, npcSpawnProperties }; } setupMonsterSpawn(spawn, cb, globalDieCallback, globalRewardCallback, globalAttackCallback, Monster) { spawn.forEach(data => { const { id, parentEntityTypeId, minHealth, maxHealth, amount, respawnTime, rewardEntries } = data; if (!parentEntityTypeId) return; const bossMonster = spawn.find(b => b.id === parentEntityTypeId); const reward = new Reward({ distributionType: 1, baseReward: rewardEntries.reduce((result, entry) => { if (result[entry.categoryId]) result[entry.categoryId][entry.itemId] = { amountMinimum: entry.min, amountMaximum: entry.max, chance: entry.chance }; else result[entry.categoryId] = { [entry.itemId]: { amountMinimum: entry.min, amountMaximum: entry.max, chance: entry.chance } }; return result; }, {}) }); for (let i = 0; i < amount; i++) { const spareID = this.availableIdentifiers.pop(); const hitpoint = Math.floor(Math.random() * (maxHealth - minHealth + 1) + minHealth); const monsterData = { id: spareID ? spareID : this.spawnedEntitiesTotalCount, entityTypeID: id, reward, respawnTime, parentEntityTypeID: parentEntityTypeId, maximumHitpoint: hitpoint, currentHitpoint: hitpoint, map: this, rewardCallback: globalRewardCallback, attackCallback: globalAttackCallback }; const newSpawn = this.registerMonster(monsterData, Monster); newSpawn.monster.dieCallback = data => { const isAdmiralReady = globalDieCallback.call(newSpawn.monster, data); if (isAdmiralReady && bossMonster) { const { id, minHealth, maxHealth, reward } = bossMonster; const spareID = this.availableIdentifiers.pop(); const hitpoint = Math.floor(Math.random() * (maxHealth - minHealth + 1) + minHealth); const emittingTo = this.getNeighbourMapsInRadius(1); const notificationData = { mapID: this.id, typeID: newSpawn.monster.typeID, entityTypeID: newSpawn.monster.entityTypeID }; this.emit(EVENT_SPAWN_ADMIRAL, notificationData, emittingTo); const bossSpawnData = { id: spareID ? spareID : this.spawnedEntitiesTotalCount, isAdmiral: true, reward, entityTypeID: id, position: newSpawn.monster.position, maximumHitpoint: hitpoint, currentHitpoint: hitpoint, map: this, dieCallback: globalDieCallback, attackCallback: globalAttackCallback, rewardCallback: globalRewardCallback }; const boss = this.registerMonster(bossSpawnData, Monster); cb(boss); }; }; cb(newSpawn); }; }); } registerMonster(data, Monster) { const monster = new Monster(data); if (!this.availableIdentifiers.length) this.spawnedEntitiesTotalCount++; const monsterSpawnProperties = monster.getProperties(); return { monster, monsterSpawnProperties }; } registerCollectable(spawn, cb, globalDieCallback, globalRewardCallback, Collectable) { spawn.forEach(data => { const { id, amount, respawnTime, reward } = data; const newCollectables = []; for (let i = 0; i < amount; i++) { const collectable = new Collectable({ id: this.spawnedEntitiesTotalCount++, entityTypeID: id, respawnTime, map: this, rewardEntries: reward, dieCallback: globalDieCallback, rewardCallback: globalRewardCallback }); const collectableSpawnProperties = collectable.getProperties(); newCollectables.push({ collectable, collectableSpawnProperties }); }; cb(newCollectables); }); } removeNPC(id) { this.npcs.delete(id); this.availableIdentifiers.push(id); } removeMonster(id) { this.monsters.delete(id); this.availableIdentifiers.push(id); } getNearbyPlayersGameID(objectToCheck) { const nearbyPlayers = []; this.players.forEach(player => { if (!player.hasUnexpectedlyLeft && player.isAlive && Math.pow(objectToCheck.position.x - player.position.x, 2) + Math.pow(objectToCheck.position.y - player.position.y, 2) < Math.pow(player.sight, 2)) nearbyPlayers.push(player.id); }); return nearbyPlayers; } getLivingEntities(types) { if (!types.length) return; const aliveEntities = []; for (let i = 0; i < types.length; i++) { const type = types[i]; const entityGroup = this.entities[type - 1]; entityGroup.forEach(entity => entity.isAlive ? aliveEntities.push(entity) : null); }; return aliveEntities; } getAliveEntityFromSubgroup(type, id) { const subgroup = this.entities[type - 1]; if (!subgroup) return null; const entity = subgroup.get(+id); return entity && entity.isAlive ? entity : null; } getEntityFromSubgroup(id) { const type = getType(id); return this.entities[type] ? this.entities[type].get(id) : null; } removePlayer = id => this.players.delete(id); addPlayer = player => this.players.set(player.id, player); switchPlayer(player, map) { this.removePlayer(player.id); map.players.set(player.id, player); player.map = map; } getGameCoordinates(spiedEntity) { const vertical = Math.round(spiedEntity.position.y / 100) - 1; const verticalRest = vertical % COORDINATE_SECTIONS.length; const index = Math.floor(Math.max(vertical, 0) / COORDINATE_SECTIONS.length); const letters = `${COORDINATE_REGIONS[index]}${COORDINATE_SECTIONS[verticalRest]}`; const numbers = Math.floor(spiedEntity.position.x / 100) < 10 ? `0${Math.floor(spiedEntity.position.x / 100)}` : Math.floor(spiedEntity.position.x / 100); return { name: spiedEntity.name, id: spiedEntity.id, map: spiedEntity.map.name, x: numbers, y: letters }; } }; module.exports = GameMap;