Initial commit

This commit is contained in:
2026-08-24 22:20:36 +02:00
commit 62f7b1278a
251 changed files with 16670 additions and 0 deletions
+511
View File
@@ -0,0 +1,511 @@
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;