511 lines
16 KiB
JavaScript
511 lines
16 KiB
JavaScript
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; |