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
+39
View File
@@ -0,0 +1,39 @@
class AdmiralCounter {
constructor(configuration) {
this.entities = Object.entries(configuration);
this.admiralCounters = {};
}
increaseCounter(ids) {
const { typeID, parentEntityTypeID } = ids;
const id = `${typeID}_${parentEntityTypeID}`;
if (!this.admiralCounters[id]) this.admiralCounters[id] = { childKilledCount: 1, admiralAliveCount: 0 };
else this.admiralCounters[id].childKilledCount++;
const thresholds = this.entities.find(e => parseInt(e[0]) === typeID);
if (!thresholds) return;
const threshold = thresholds[1][parentEntityTypeID];
if (!threshold) return;
const counter = this.admiralCounters[id];
if (counter.childKilledCount === threshold.childToBeKilledCount) {
counter.childKilledCount = 0;
if (counter.admiralAliveCount < threshold.maximumAdmiralCount) {
counter.admiralAliveCount++;
return true;
};
};
return false;
}
descreaseAdmiralCount(id) {
const counter = this.admiralCounters[id];
if (counter) counter.admiralAliveCount--;
}
};
module.exports = AdmiralCounter;
+71
View File
@@ -0,0 +1,71 @@
const EventEmitter = require("events");
const constants = require("../Data/constants");
const { CATEGORIES } = constants;
const { CATEGORY_CANNON } = CATEGORIES;
const eventsMapped = {
1: 97,
2: 98,
3: 99,
4: 100,
5: 101,
6: 102,
7: 103,
8: 104,
9: 105,
10: 106
};
class Cannon extends EventEmitter {
constructor(info) {
super();
this.id = info.id;
this.categoryID = CATEGORY_CANNON;
this.eventID = eventsMapped[this.id];
this.damage = info.damage;
this.range = info.range;
this.reload = info.reload;
this.scatter = info.scatter;
this.amount = info.amount;
this.amountEquipped = info.amountEquipped || 0;
this.lastShotTickTimestamp = 0;
this.resourceSharedLimiter = info.resourceSharedLimiter;;
};
equip(quantity) {
if (this.amountEquipped + quantity > this.amount) return
const isTypeQunatityExhausted = this.resourceSharedLimiter.count(quantity);
if (!isTypeQunatityExhausted) return
this.amountEquipped += quantity;
return true;
}
unequip(quantity) {
if (this.amountEquipped - quantity < 0) return
const isTypeQunatityExhausted = this.resourceSharedLimiter.count(-quantity);
if (!isTypeQunatityExhausted) return
this.amountEquipped -= quantity;
return true;
}
getProperties() {
return {
typeID: 1,
id: this.id,
amount: this.amount,
amountEquipped: this.amountEquipped
};
}
};
module.exports = Cannon;
+70
View File
@@ -0,0 +1,70 @@
const Entity = require("./Entity");
const Reward = require("./Reward");
const constants = require("../Data/constants");
const { TYPES, TIMING_EVENTS } = constants;
const { ENTITY_TYPE_COLLECTABLE } = TYPES;
const { TIMING_EVENT_RESPAWN } = TIMING_EVENTS;
class Collectable extends Entity {
constructor(info) {
super({
type: ENTITY_TYPE_COLLECTABLE,
id: info.id,
map: info.map,
isAlive: true,
position: info.position
});
this.respawnTime = 10000;
this.entityTypeID = info.entityTypeID;
this.dieCallback = info.dieCallback;
this.rewardCallback = info.rewardCallback;
this.reward = new Reward({
distributionType: 2,
baseReward: info.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;
}, {})
});
this.map.collectables.set(this.id, this);
}
die(deathEvent) {
super.die();
const die = {
typeID: this.typeID,
entityTypeID: this.entityTypeID,
id: this.id,
authorID: deathEvent.authorID,
targetEntityTypeID: this.entityTypeID
};
this.dieCallback(die);
this.setSingleEvent(TIMING_EVENT_RESPAWN, this.respawnTime);
}
getProperties() {
const { position, type, typeID, entityTypeID, id } = this;
return { position, type, typeID, entityTypeID, id };
}
};
module.exports = Collectable;
+106
View File
@@ -0,0 +1,106 @@
const EventScheduler = require("./EventScheduler");
const constants = require("../Data/constants");
const { TYPES, TIMING_EVENTS } = constants;
const { ENTITY_TYPE_PLAYER } = TYPES;
const { TIMING_EVENT_RESPAWN } = TIMING_EVENTS;
class Entity extends EventScheduler {
constructor(info) {
const id = info.type === ENTITY_TYPE_PLAYER ? info.id : Entity.composeID(info.type - 1, info.id);
super(info.type, id, info.map.id);
this.isAlive = info.isAlive ?? true;
this.chunk = null;
this.type = null;
this.map = info.map;
this.typeID = info.type;
this.id = id;
const { map } = this;
if (info.position) {
const cell = info.map.grid.getCell(info.position.x, info.position.y);
const coordinate = info.map.grid.getCoords(cell.xw, cell.xh)
this.position = coordinate;
} else this.position = map.grid.randomNode(10);
if (info.type !== ENTITY_TYPE_PLAYER) {
const currentChunk = this.getCurrentChunk();
map[currentChunk].addItem(id, this);
this.chunk = currentChunk;
};
};
static composeID(entityType, entityID) {
return ((entityType & 0xFF) << 24) | (entityID & 0xFFFFFF);
}
static getType(compositeID) {
return (compositeID >> 24) & 0xFF;
}
static getID(compositeID) {
return compositeID & 0xFFFFFF;
}
getCurrentChunk() {
return `chunk_${Math.floor(this.position.x / (10 * 120))}-${Math.floor(this.position.y / (10 * 90))}`;
}
removeFromChunk() {
if (!this.chunk) return;
this.map[this.chunk].removeItem(this.id);
this.chunk = null;
}
updateChunk() {
const { chunk, map, id } = this;
const currentChunk = this.getCurrentChunk();
if (chunk !== currentChunk) {
this.removeFromChunk();
map[currentChunk].addItem(id, this);
this.chunk = currentChunk;
this.chunkIndex = [Math.floor(this.position.x / (10 * 120)), Math.floor(this.position.y / (10 * 90))];
return true;
};
return false;
}
die() {
if (!this.isAlive) return;
this.isAlive = false;
this.removeFromChunk();
const players = this.map.getLivingEntities([ENTITY_TYPE_PLAYER]);
players.forEach(p => delete p.inDistance[this.id]); // this is not efficient, should update
}
respawn(respawn = {}) {
if (this.isAlive) return;
this.isAlive = true;
this.position = respawn.position ? respawn.position : this.map.grid.randomNode(10);
this.updateChunk();
this.removeEvent(TIMING_EVENT_RESPAWN);
}
markActive() {
this.map[`chunkTest_${Math.floor(this.position.x / (10 * 120))}-${Math.floor(this.position.y / (10 * 90))}`].addItem(this.id, this);
}
};
module.exports = Entity;
+96
View File
@@ -0,0 +1,96 @@
const DoubleLinkedList = require("../Utility/DoubleLinkedList");
class EntityGroup {
constructor(info) {
this.id = info.id;
this.mapIDs = Array.isArray(info.mapIDs) ? info.mapIDs : [];
this.structure = info.structure;
this.entitySettings = info.entitySettings;
this.members = new DoubleLinkedList();
this.leaders = new DoubleLinkedList();
this.isSummonPossible = true;
this.predefinedArea = info.predefinedArea ? info.predefinedArea : {
x: 1000,
y: 1000,
width: 750,
height: 750
};
};
setAreaPosition(position) {
this.predefinedArea.x = position.x;
this.predefinedArea.y = position.y;
}
getEntitySetting(groupMemberTypeID) {
const entitySetting = this.entitySettings[groupMemberTypeID];
return entitySetting ? entitySetting : {
speed: 600,
respawnTime: 0
};
}
cancelRespawn() {
if (!this.isSummonPossible) return;
this.members.each((key, value) => {
value.cancelEvent(8);
return true;
});
this.isSummonPossible = false;
}
default() {
this.members.clearEach(() => null);
this.leaders.clearEach(() => null);
this.isSummonPossible = true;
}
isDefeated() {
let deadEntitiesCount = 0;
this.members.each((key, value) => {
if (!value.isAlive) deadEntitiesCount++;
return true;
});
this.leaders.each((key, value) => {
if (!value.isAlive) deadEntitiesCount++;
return true;
});
return this.structure.length === deadEntitiesCount || (this.members.isEmpty() && this.leaders.isEmpty());
}
addMember(entity) {
this.members.addItem(entity.id, entity);
}
addLeader(entity) {
this.leaders.addItem(entity.id, entity);
}
isLeader(id) {
return this.leaders.hasElement(id);
}
isLeaderAlive() {
let isAlive = false;
this.leaders.each(function (key, value) {
isAlive = value.isAlive;
return !value.isAlive;
});
return isAlive;
}
};
module.exports = EntityGroup;
+20
View File
@@ -0,0 +1,20 @@
class EquipmentLimiter {
constructor(info) {
this.quantity = {
current: 0,
maximum: info.maximum
};
};
count(quantity) {
if (!quantity) return false;
if (this.quantity.current + quantity > this.quantity.maximum || this.quantity.current + quantity < 0) return false;
this.quantity.current += quantity;
return true;
}
};
module.exports = EquipmentLimiter;
+154
View File
@@ -0,0 +1,154 @@
const EventEmitter = require("events");
const Game = require("./Game");
class EventScheduler extends EventEmitter {
constructor(typeID, id, mapID) {
super();
this.entity = {
typeID,
id
};
this.events = {};
this.mapID = mapID;
};
setLocalization(mapID) {
this.mapID = mapID;
}
getLocalization = () => this.mapID;
setReusableSingleEvent(id, time, data) {
if (!this.events[id] || this.events[id][0].isOver) {
const previousTick = this.getEventTick(id);
const relativeTo = previousTick ? previousTick : performance.now();
const tickAt = relativeTo + (time > 0 ? time : 0);
const event = {
id,
entity: this.entity,
isCancelled: false,
index: 0,
tickAt,
data,
getLocalization: this.getLocalization,
isSingle: true,
isOver: false
};
Game.events.add(tickAt, event);
this.events[id] = [event];
} else {
this.events[id][0].isCancelled = false;
};
}
setSingleEvent(id, time, data) {
const tickAt = performance.now() + time;
const event = {
id,
entity: this.entity,
isCancelled: false,
index: 0,
tickAt,
data,
getLocalization: this.getLocalization
};
if (!this.events[id]) {
Game.events.add(tickAt, event);
this.events[id] = [event];
};
}
setEventTest2(id, time, data) {
const previousTick = this.getEventTick(id);
const relativeTo = previousTick ? previousTick : performance.now();
const tickAt = relativeTo + time;
const event = {
id,
entity: this.entity,
isCancelled: false,
index: this.events[id] ? this.events[id].length - 1 : 0,
tickAt,
data,
getLocalization: this.getLocalization
};
Game.events.add(tickAt, event);
this.events[id] = [event];
}
setEvent(id, time, data) {
const previousTick = this.getEventTick(id);
const relativeTo = previousTick ? previousTick : performance.now();
const tickAt = relativeTo + time;
const event = {
id,
entity: this.entity,
isCancelled: false,
index: this.events[id] ? this.events[id].length - 1 : 0,
tickAt,
data,
getLocalization: this.getLocalization
};
Game.events.add(tickAt, event);
if (this.events[id]) this.events[id].push(event);
else this.events[id] = [event];
}
setIndependentEvent(id, time, data) {
const tickAt = performance.now() + time;
const event = {
id,
entity: this.entity,
isCancelled: false,
index: this.events[id] ? this.events[id].length : 0,
tickAt,
data,
getLocalization: this.getLocalization
};
Game.events.add(tickAt, event);
if (this.events[id]) this.events[id].push(event);
else this.events[id] = [event];
}
getEventTick(id, index) {
const event = this.events[id];
if (!event) return 0;
if (!index) index = event.length - 1;
const element = event[index];
return element && !element.isOver && element.tickAt;
}
removeEvent(id) {
delete this.events[id];
}
cancelEvent(id, index) {
if (this.events[id]) {
if (index == null) {
for (let i = 0; i < this.events[id].length; i++) this.events[id][i].isCancelled = true;
} else if (index === -1) {
for (let i = 0; i < this.events[id].length - 1; i++) this.events[id][i].isCancelled = true;
} else {
this.events[id][index].isCancelled = true;
};
};
}
};
module.exports = EventScheduler;
+43
View File
@@ -0,0 +1,43 @@
const DoubleLinkedList = require("../Utility/DoubleLinkedList");
const PQ = require("../Utility/Q");
const maps = [];
for (let i = 0; i < 20; i++) maps.push(require(`../Maps/Map_${i + 1}`));
const game = {
players: {},
packets: new DoubleLinkedList(),
guilds: {},
guildIslands: [],
events: new PQ(),
eventsFixed: new PQ(),
maps,
getTopic: function (topicName) {
const topic = this.packets.getElement(topicName);
if (topic) return topic;
return this.packets.addItem(topicName, []);
},
addPlayer: function (player) {
this.players[player.id] = player;
},
/* testDestroyAll: function() {
this.maps.forEach(map => map.npcs.forEach(npc => npc))
},*/
disconnectAll: function() {
this.state = 0;
this.maps.forEach(map => map.players.forEach(player => player.disconnect(() => {
socket.sockets[player.sessionID].end(2);
delete this.players[player.id];
delete socket.sockets[player.sessionID];
}, true)))
// socket.us_listen_socket_close(this.listeningServer);
// this.listeningServer = null;
},
listeningServer: null,
state: 1
};
module.exports = game;
+125
View File
@@ -0,0 +1,125 @@
class Grid {
constructor(config) {
this.width = config.width;
this.height = config.height;
this.collision = config.collision ?? {};
}
getCoords(tileX, tileY) {
const x = 0 + tileX * 24 + !(tileY % 2) * (24 / 2)
const y = 0 + tileY * (18 / 2);
return {
x,
y
};
}
getCell(roundedX, roundedY) {
const x = roundedX - 0;
const y = roundedY - 0;
const isEven = ((Math.floor(y / 9) * 9 - 9) / 18) % 1 !== 0;
const xw = isEven ? Math.floor(x / 24) : Math.floor((x + 12) / 24);
const xh = Math.floor(y / (18 / 2));
return {
xw,
xh
};
}
isValidNode(node) {
const blockedTile = this.collision[`${node.xw}-${node.xh}`];
return (!blockedTile && node.xw > 0 && node.xh > 0 && node.xw < this.width / 24 && node.xh < (this.height * 2) / 18);
}
randomNodeNear(nearToX, nearToY, radius) {
let r = radius * Math.sqrt(Math.random());
let a = 2 * Math.PI * Math.random();
let nearX = Math.round(r * Math.cos(a) + nearToX);
let nearY = Math.round(r * Math.sin(a) + nearToY);
let triedNode = this.getCell(nearX, nearY);
const { xw, xh } = triedNode;
let { x, y } = this.getCoords(xw, xh);
while (!this.isValidNode(triedNode) || (x === nearToX && y === nearToY) || Math.pow(x - nearToX, 2) + Math.pow(y - nearToY, 2) < Math.pow(500, 2)) {
r = radius * Math.sqrt(Math.random());
a = 2 * Math.PI * Math.random();
nearX = Math.round(r * Math.cos(a) + nearToX);
nearY = Math.round(r * Math.sin(a) + nearToY);
triedNode = this.getCell(nearX, nearY);
const { xw, xh } = triedNode;
const coords = this.getCoords(xw, xh);
x = coords.x;
y = coords.y;
};
return {
x,
y
};
}
randomNodeInArea(area) {
let xw = 0;
let xh = 0;
const minimumWidth = area.x - area.width / 2;
const maximumWidth = area.x + area.width / 2;
const minimumHeight = area.y - area.height / 2;
const maximumHeight = area.y + area.height / 2;
do {
xw = Math.floor((Math.random() * (maximumWidth - minimumWidth + 1) + minimumWidth) / 24);
xh = Math.floor((Math.random() * (maximumHeight - minimumHeight + 1) + minimumHeight) / (18 / 2));
} while (!this.isValidNode({ xw, xh }));
return this.getCoords(xw, xh);
}
randomNode(percentage) {
let xw = 0;
let xh = 0;
if (!percentage) percentage = 0;
percentage /= 200;
const minimumWidth = 0 + this.width * percentage;
const maximumWidth = this.width * (1 - percentage);
const minimumHeight = 0 + this.height * percentage;
const maximumHeight = this.height * (1 - percentage);
do {
xw = Math.floor((Math.random() * (maximumWidth - minimumWidth + 1) + minimumWidth) / 24);
xh = Math.floor((Math.random() * (maximumHeight - minimumHeight + 1) + minimumHeight) / (18 / 2));
} while (!this.isValidNode({ xw, xh }));
return this.getCoords(xw, xh);
}
getNodeNeighbours(node) {
const { xw, xh } = node;
let rightdown, leftdown, rightup, leftup;
if (node.xh % 2 !== 0) {
rightdown = { xw, xh: xh + 1 };
leftdown = { xw: xw - 1, xh: xh + 1 };
rightup = { xw, xh: xh - 1 };
leftup = { xw: xw - 1, xh: xh - 1 };
} else {
rightdown = { xw: xw + 1, xh: xh + 1 };
leftdown = { xw: xw, xh: xh + 1 };
rightup = { xw: xw + 1, xh: xh - 1 };
leftup = { xw: xw, xh: xh - 1 };
};
return [rightup, leftup, leftdown, rightdown];
}
};
module.exports = Grid;
+15
View File
@@ -0,0 +1,15 @@
const Grid = require("./Grid");
const data = require("../Data/maps");
const grids = [];
data.forEach(d => {
const grid = new Grid({
width: d.size.width,
height: d.size.height,
collision: d.collision
});
grids.push(grid);
})
module.exports = grids;
+143
View File
@@ -0,0 +1,143 @@
const InventoryList = require("./InventoryList");
const LEVELS = require("../Data/levels");
const GUILD_MAXIMUM_REQUESTS_SIZE = 100;
const ECONOMY = [1, 2];
const globalRequests = {};
class Guild {
constructor(info) {
this.id = info.id;
this.topic = `g${info.id}`;
this.name = info.name;
this.tag = info.tag;
this.description = info.description;
this.ownerID = info.ownerID;
this.bank = new GuildBank();
this.taxRates = info.taxRates;
this.experiencePoints = info.experiencePoints ?? 0;
this.level = LEVELS.guildLevel(this.experiencePoints) ?? 1;
this.size = 0;
this.sizeMaximum = 30;
this.members = [];
this.requests = [];
this.diplomacy = {};
this.timestamps = info.timestamps;
this.bank.depositTax({
categoryID: 1,
id: 1,
amount: 0
});
this.bank.depositTax({
categoryID: 1,
id: 2,
amount: 0
});
};
levelUpdate(amount) {
this.experiencePoints += amount;
if (this.level >= 25) return -1;
const newLevel = LEVELS.guildLevel(this.experiencePoints);
if (newLevel !== this.level) {
if (newLevel > 25) this.level = 25;
else this.level = newLevel;
return this.level;
};
return -1;
}
isLeader(playerID) {
return this.ownerID === playerID;
}
hasRequested(playerID) {
const isRequestPresent = this.requests.some(r => r.playerID === playerID);
return isRequestPresent;
}
setPermission(playerID, permission) {
const member = this.members.find(m => m.id === playerID);
if (!member) return;
member.permission = permission;
}
addRequest(playerID, message) {
//if (this.requestSize >= GUILD_MAXIMUM_REQUESTS_SIZE) return;
if (globalRequests[playerID] >= 8) return;
console.info(globalRequests[playerID], "request count for player", playerID);
if (globalRequests[playerID]) globalRequests[playerID]++;
else globalRequests[playerID] = 1;
const request = {
playerID,
message
};
this.requests.push(request);
return true;
}
removeRequest(playerID) {
if (globalRequests[playerID]) globalRequests[playerID]--;
this.requests = this.requests.filter(r => r.playerID !== playerID);
}
isMember(playerID) {
return this.members.find(member => member.id === playerID) ? true : false;
}
getMember(playerID) {
return this.members.find(member => member.id === playerID);
}
addMember(member) {
if (this.size >= this.sizeMaximum) return;
if (this.isMember(member.id)) return;
this.removeRequest(member.id);
this.members.push(member);
delete globalRequests[member.id];
this.size++;
return true;
}
removeMember(playerID) {
this.size--;
this.members = this.members.filter(member => member.id !== playerID);
}
};
class GuildBank {
constructor(info) {
this.list = new InventoryList();
this.deposits = [];
};
depositTax(resource) {
if (!ECONOMY.includes(resource.id)) return;
this.list.accumulateItem(resource.id, resource);
}
};
module.exports = Guild;
+69
View File
@@ -0,0 +1,69 @@
const DoubleLinkedList = require("../Utility/DoubleLinkedList");
const constants = require("../Data/constants");
const { TIMING_EVENTS } = constants;
const { TIMING_EVENT_RESPAWN } = TIMING_EVENTS;
const Game = require("./Game");
const { guilds } = Game;
class GuildIsland {
constructor(info) {
this.id = info.id;
this.locationMapID = info.locationMapID;
this.index = info.index;
this.position = info.position;
this.guild = info.guild;
this.towers = [];
this.totalDamageReceived = new DoubleLinkedList();
};
updateReceivedDamage(guildID, damage) {
this.totalDamageReceived.accumulateItem(guildID, damage);
}
getTower(id) {
const tower = this.towers.find(t => t.id === id);
return tower;
}
addTower(tower) {
this.towers.push(tower);
}
updateOwnership() {
const towersAlive = this.towers.filter(t => t.hp.current);
if (towersAlive.length) return null;
let maximumDamage = 0;
let newGuildID = 0;
this.totalDamageReceived.clearEach((guildID, damage) => {
if (damage > maximumDamage) {
maximumDamage = damage;
newGuildID = guildID;
};
});
this.towers.forEach(tower => tower.setEvent(TIMING_EVENT_RESPAWN, 10000));
const topicOld = this.guild.topic;
const guildOld = guilds[this.guild.id];
const guildNew = newGuildID ? guilds[newGuildID] : { tag: new ArrayBuffer(0), id: 0 };
const topicNew = guildNew.topic;
this.guild = guildNew;
return {
guildOld,
guildNew,
topicOld,
topicNew
};
}
};
module.exports = GuildIsland;
+63
View File
@@ -0,0 +1,63 @@
const EventEmitter = require("events");
const constants = require("../Data/constants");
const { CATEGORIES } = constants;
const { CATEGORY_HARPOONER } = CATEGORIES;
const eventsMapped = {
1: 197,
2: 198,
3: 199,
4: 200,
5: 201,
6: 202
};
class Harpooner extends EventEmitter {
constructor(info) {
super();
this.id = info.id;
this.categoryID = CATEGORY_HARPOONER;
this.eventID = eventsMapped[this.id];
this.damage = info.damage;
this.range = info.range;
this.reload = info.reload;
this.amount = info.amount;
this.amountEquipped = info.amountEquipped || 0;
this.resourceSharedLimiter = info.resourceSharedLimiter;
}
equip(quantity) {
if (this.amountEquipped + quantity > this.amount) return
const isTypeQunatityExhausted = this.resourceSharedLimiter.count(quantity);
if (!isTypeQunatityExhausted) return
this.amountEquipped += quantity;
return true;
}
unequip(quantity) {
if (this.amountEquipped - quantity < 0) return
const isTypeQunatityExhausted = this.resourceSharedLimiter.count(-quantity);
if (!isTypeQunatityExhausted) return
this.amountEquipped -= quantity;
return true;
}
getProperties() {
return {
typeID: 2,
id: this.id,
amount: this.amount,
amountEquipped: this.amountEquipped
};
}
};
module.exports = Harpooner;
+303
View File
@@ -0,0 +1,303 @@
const Item = require("./Item");
const Cannon = require("./Cannon");
const Harpooner = require("./Harpooner");
const InventoryList = require("../Game/InventoryList");
const schemeCannons = require("../Data/cannons");
const schemeHarpooners = require("../Data/harpooners");
const ammunitions = require("../Data/ammunitions");
const harpoons = require("../Data/harpoons");
const itemTable = require("../Data/items");
const constants = require("../Data/constants");
const { CATEGORIES, AFFECTS } = constants;
const { CATEGORY_ITEM, CATEGORY_AMMUNITION, CATEGORY_HARPOON, CATEGORY_DESIGN, CATEGORY_ECONOMY, CATEGORY_STATUS, CATEGORY_CANNON, CATEGORY_HARPOONER } = CATEGORIES;
const { AFFECT_CANNON_DAMAGE, AFFECT_HARPOONER_DAMAGE, AFFECT_PROTECTION, AFFECT_SAILING_SPEED, AFFECT_HITPOINT } = AFFECTS;
const AFFECT_TABLE = {
1: [1, 2, 3, 4],
[AFFECT_PROTECTION]: [5, 6, 7, 8]
};
class Inventory {
constructor(info) {
this.selectedAmmunition = 0;
this.selectedHarpoon = 0;
this.resourceSharedLimiter = info.resourceSharedLimiter;
this[CATEGORY_AMMUNITION] = new InventoryList();
this[CATEGORY_HARPOON] = new InventoryList();
this[CATEGORY_ITEM] = new InventoryList();
this[CATEGORY_STATUS] = new InventoryList();
this[CATEGORY_ECONOMY] = new InventoryList();
this[CATEGORY_DESIGN] = new InventoryList();
this[CATEGORY_CANNON] = new InventoryList();
this[CATEGORY_HARPOONER] = new InventoryList();
this[CATEGORY_DESIGN].addItem(1, {
categoryID: CATEGORY_DESIGN,
id: 1,
amount: 1
}); // default design
}
refreshSingle(element) {
switch (element.categoryID) {
case CATEGORY_ECONOMY:
this[CATEGORY_ECONOMY].accumulateItem(element.id, element);
break;
case CATEGORY_ITEM:
const inventoryItem = this[CATEGORY_ITEM].getElement(element.id);
if (!inventoryItem) return this.addItem(element);
inventoryItem.amount += element.amount;
break;
case CATEGORY_AMMUNITION:
if (!element.amount) return;
this[CATEGORY_AMMUNITION].accumulateItem(element.id, element);
if (!this.selectedAmmunition) this.setSelectedAmmunition(element.id);
break;
case CATEGORY_HARPOON:
if (!element.amount) return;
this[CATEGORY_HARPOON].accumulateItem(element.id, element);
if (!this.selectedHarpoon) this.setSelectedHarpoon(element.id);
break;
case CATEGORY_DESIGN:
this[CATEGORY_DESIGN].accumulateItem(element.id, element);
break;
case CATEGORY_STATUS:
this[CATEGORY_STATUS].accumulateItem(element.id, element);
break;
case CATEGORY_CANNON:
let cannon = this[CATEGORY_CANNON].getElement(element.id);
if (!cannon) {
const schemeCannon = schemeCannons[element.id];
if (!schemeCannon) return;
cannon = new Cannon({
id: element.id,
damage: schemeCannon.damage,
range: schemeCannon.range,
reload: schemeCannon.reload,
scatter: schemeCannon.scatter,
amount: element.amount,
amountEquipped: element.amountEquipped,
resourceSharedLimiter: this.resourceSharedLimiter.cannon
});
this[CATEGORY_CANNON].accumulateItem(cannon.id, cannon);
} else this[CATEGORY_CANNON].accumulateItem(cannon.id, element);
break;
case CATEGORY_HARPOONER:
let harpooner = this[CATEGORY_HARPOONER].getElement(element.id);
if (!harpooner) {
const schemeHarpooner = schemeHarpooners[element.id];
if (!schemeHarpooner) return;
harpooner = new Harpooner({
id: element.id,
damage: schemeHarpooner.damage,
range: schemeHarpooner.range,
reload: schemeHarpooner.reload,
scatter: schemeHarpooner.scatter,
amount: element.amount,
amountEquipped: element.amountEquipped,
resourceSharedLimiter: this.resourceSharedLimiter.harpooner
});
this[CATEGORY_HARPOONER].accumulateItem(harpooner.id, harpooner);
} else this[CATEGORY_HARPOONER].accumulateItem(element.id, element);
break;
};
}
use(categoryID, id, amount) {
const category = this[categoryID];
if (!category) return
const item = category[id];
if (!item || !item.amount) return
const itemScheme = itemTable[id];
if (!itemScheme && categoryID === CATEGORY_ITEM) return
let removedCount = -1;
if (itemScheme.isDurable) item.isActive = true;
else {
if (categoryID === CATEGORY_ITEM) item.nextUseAt = performance.now() + itemScheme.cooldown;
removedCount = itemScheme.useOnImpact || categoryID !== CATEGORY_ITEM ? Math.min(amount, item.amount) : 0;
item.amount -= removedCount;
item.usedAmount = removedCount;
};
return item;
}
disable(id) {
const item = this[CATEGORY_ITEM].getElement(id);
if (!item) return;
item.isActive = false;
}
alterAmount(categoryID, id, amount) {
const category = this[categoryID];
if (!category) return
const element = category.getElement(id);
if (!element || !element.amount) return
const usedAmount = Math.min(amount, element.amount);
element.amount -= usedAmount;
return usedAmount;
}
setSelectedAmmunition(id) {
if (!id) return this.selectedAmmunition = 0;
if (!ammunitions[id]) return;
const ammunition = this[CATEGORY_AMMUNITION].hasElement(id);
if (!ammunition) return this.selectedAmmunition = 0;
console.info(ammunition)
return this.selectedAmmunition = id;
}
getSelectedAmmunition() {
const selectedAmmunition = this[CATEGORY_AMMUNITION].getElement(this.selectedAmmunition);
if (!selectedAmmunition) return;
return selectedAmmunition;
}
setSelectedHarpoon(id) {
if (!id) return this.selectedHarpoon = 0;
if (!harpoons[id]) return;
const harpoon = this[CATEGORY_HARPOON].hasElement(id);
if (!harpoon) return this.selectedHarpoon = 0;
return this.selectedHarpoon = id;
}
getSeletedHarpoon() {
const selectedHarpoon = this[CATEGORY_HARPOON].getElement(this.selectedHarpoon);
if (!selectedHarpoon) return;
return selectedHarpoon;
}
addItem(entry) {
const itemScheme = itemTable[entry.id];
if (!itemScheme) return;
if (!entry.amount) return;
const { time, scale, affects } = itemScheme;
const item = new Item({
id: entry.id,
amount: entry.amount,
time,
scale,
affects
});
this[CATEGORY_ITEM].addItem(item.id, item);
}
getItemsByAffect(affect) {
const items = Object.values(this[CATEGORY_ITEM]);
return items.filter(item => item.affects.includes(affect));
}
useAvailableItemsByAffect(affect, cb) {
AFFECT_TABLE[affect]
.forEach(itemID => {
const item = this[CATEGORY_ITEM].getElement(itemID);
if (item && item.isActive && item.amount) {
const use = item.use();
cb(use.scale, itemID, use.statusID, use.isStatusActive);
};
});
}
getPosessions() {
return {
[CATEGORY_AMMUNITION]: this[CATEGORY_AMMUNITION],
[CATEGORY_HARPOON]: this[CATEGORY_HARPOON],
[CATEGORY_DESIGN]: this[CATEGORY_DESIGN]
};
}
getDesignIDs() {
const designIDs = [];
this[CATEGORY_DESIGN].each(key => designIDs.push(key));
return designIDs;
}
getEconomy() {
const economy = [];
this[CATEGORY_ECONOMY].each((key, value) => economy.push(value));
return economy;
}
getAmmunitions() {
const ammunitions = [];
this[CATEGORY_AMMUNITION].each((key, value) => ammunitions.push(value));
return ammunitions;
}
getHarpoons() {
const harpoons = [];
this[CATEGORY_HARPOON].each((key, value) => harpoons.push(value));
return harpoons;
}
getItems() {
const items = [];
this[CATEGORY_ITEM].each((key, value) => items.push(value));
return items;
}
getCannons() {
const cannons = [];
this[CATEGORY_CANNON].each((key, value) => cannons.push(value));
return cannons;
}
getHarpooners() {
const harpooners = [];
this[CATEGORY_HARPOONER].each((key, value) => harpooners.push(value));
return harpooners;
}
hasDesign(id) {
return this[CATEGORY_DESIGN].hasElement(id);
}
getItem(id) {
return this[CATEGORY_ITEM].getElement(id);
}
};
module.exports = Inventory;
+41
View File
@@ -0,0 +1,41 @@
const LinkedList = require("../Utility/LinkedList");
class LinkedListNode {
constructor(key, value) {
this.key = key;
this.value = value;
this.next = null;
};
};
class InventoryList extends LinkedList {
constructor() {
super();
};
accumulateItem(key, value) {
const entry = this.map.get(key);
const updatedValue = entry ? entry.value.amount + value.amount : value;
if (entry) entry.value.amount = updatedValue;
else this.addItem(key, updatedValue); /// eeeh
}
addItem(key, value) {
if (!key || !value || !value.categoryID || !value.id || isNaN(value.amount)) return console.info("addItem", key, value);
const temp = new LinkedListNode(key, value);
if (!this.head) {
this.head = temp;
this.tail = temp;
} else {
this.tail.next = temp;
this.tail = temp;
};
this.map.set(key, temp);
}
};
module.exports = InventoryList;
+61
View File
@@ -0,0 +1,61 @@
const itemTable = require("../Data/items");
const constants = require("../Data/constants");
const { CATEGORIES } = constants;
const { CATEGORY_ITEM } = CATEGORIES;
class Item {
constructor(config) {
this.categoryID = CATEGORY_ITEM;
this.id = config.id;
this.amount = config.amount;
this.affects = config.affects;
this.scale = config.scale;
this.isDurable = config.isDurable;
this.time = config.time;
this.isActive = false;
}
use(amount) {
const itemScheme = itemTable[this.id];
if (!itemScheme) return;
if (!this.amount) return global.logger.send({ event: "itemUsage", reason: "item amount is 0" });
if (!amount) amount = 1;
const removedAmount = Math.min(amount, this.amount);
this.amount -= removedAmount;
if (!this.amount && itemScheme.isDurable) this.isActive = false;
return {
scale: this.scale,
statusID: itemScheme.statusID,
isStatusActive: this.amount && itemScheme.isDurable
};
}
activate(amount) {
const itemScheme = itemTable[this.id];
if (!itemScheme) return;
if (!this.amount) return;
if (!amount) amount = 1;
const removedAmount = itemScheme.useOnImpact ? Math.min(amount, this.amount) : 0;
if (itemScheme.isDurable) this.isActive = true;
else {
this.nextUseAt = performance.now() + itemScheme.cooldown;
this.amount -= removedAmount;
};
return {
...this,
removedAmount
};
}
};
module.exports = Item;
+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;
+7
View File
@@ -0,0 +1,7 @@
const maps = [];
for (let i = 0; i < 19; i++) {
const map = require(`../Maps/Map_${i + 1}`);
maps.push(map);
};
module.exports = maps;
+147
View File
@@ -0,0 +1,147 @@
const Entity = require("./Entity");
const constants = require("../Data/constants");
const { TYPES, EVENTS, TIMING_EVENTS, MAXIMUM } = constants;
const { ENTITY_TYPE_PLAYER, ENTITY_TYPE_MONSTER } = TYPES;
const { TIMING_EVENT_RESPAWN } = TIMING_EVENTS;
const { EVENT_DIE } = EVENTS;
const { MAX_SAFE_INTEGER } = MAXIMUM;
class Monster extends Entity {
constructor(info) {
super({
type: ENTITY_TYPE_MONSTER,
id: info.id,
map: info.map,
isAlive: true,
position: info.position
});
this.hp = {
maximum: info.maximumHitpoint,
current: info.currentHitpoint
};
this.entityTypeID = info.entityTypeID;
this.parentEntityTypeID = info.parentEntityTypeID ?? 0;
this.respawnTime = info.respawnTime * 1000;
this.isAdmiral = info.isAdmiral ?? false;
this.attackers = [];
this.totalDamageReceived = 0;
this.damageReceived = {};
this.dieCallback = info.dieCallback;
this.attackCallback = info.attackCallback;
this.rewardCallback = info.rewardCallback;
this.reward = info.reward;
const { map, id } = this;
map.monsters.set(id, this);
const nearbyPlayersID = map.getNearbyPlayersGameID(this);
for (let i = 0; i < nearbyPlayersID.length; i++) {
const player = map.getEntityFromSubgroup(nearbyPlayersID[i]);
player.inDistance[id] = 0x01;
};
}
increaseCurrentHitpoint(amount) {
this.hp.current = Math.min(this.hp.maximum, this.hp.current + amount);
return this.hp.current;
}
decreaseCurrentHitpoint(amount, author) {
const { authorID } = author;
amount = Math.abs(amount);
const damage = Math.min(this.hp.current, amount);
if (this.damageReceived[authorID]) this.damageReceived[authorID] += damage;
else this.damageReceived[authorID] = damage;
this.totalDamageReceived += damage;
this.hp.current -= damage;
this.attackCallback({
typeID: this.typeID,
entityTypeID: this.entityTypeID,
authorID,
damage
});
const isOverSafeLimit = this.totalDamageReceived >= MAX_SAFE_INTEGER;
if (isOverSafeLimit) this.totalDamageReceived = MAX_SAFE_INTEGER;
if (this.hp.current === 0 || isOverSafeLimit) {
const die = {
typeID: this.typeID,
lastShotTypeID: 1,
lastShotID: author.authorID,
targetID: this.id,
targetEntityTypeID: this.entityTypeID
};
this.die(die);
// this.setSingleEvent(11, 0, die);
};
return {
normalizedAmount: damage,
currentHitpoint: this.hp.current
};
}
isAtMaximumHitpoints() {
return this.hp.current === this.hp.maximum;
}
die(die) {
super.die();
this.hp.current = 0;
this.attackers.forEach(data => {
const attackerEntity = this.map.getEntityFromSubgroup(data.attackerEntry);
attackerEntity.wipeAttack();
});
this.attackers.length = 0;
this.emit(EVENT_DIE, die, this.id);
const admiralProperties = {
typeID: this.typeID,
entityTypeID: this.entityTypeID,
parentEntityTypeID: this.parentEntityTypeID,
id: this.id,
isAdmiral: this.isAdmiral,
authorID: die.lastShotID
};
this.dieCallback(admiralProperties);
this.removeEvent(11);
this.damageReceived = {};
if (!this.isAdmiral) this.setEvent(TIMING_EVENT_RESPAWN, this.respawnTime);
}
respawn() {
if (this.isAlive) return;
this.hp.current = this.hp.maximum;
super.respawn();
this.removeEvent(TIMING_EVENT_RESPAWN);
}
getProperties() {
const { position, id, hp, type, typeID, isAdmiral, entityTypeID } = this;
return { position, id, hp, type, typeID, isAdmiral, entityTypeID };
}
};
module.exports = Monster;
+763
View File
@@ -0,0 +1,763 @@
const Entity = require("./Entity");
const Utility = require("./Utility");
const Cannon = require("./Cannon");
const ammunitions = require("../Data/ammunitions");
const cannonsToEquip = require("../Data/cannonsSchemeNPC");
const cannons = require("../Data/cannons");
const OVERTIME_HEALTH_EFFECTS = require("../Data/overtime");
const constants = require("../Data/constants");
const { EVENTS, TIMING_EVENTS, TYPES, EFFECTS_TERMINATE_MOVEMENT, EVENTS_BY_TIMING_EVENTS_ID, AFFECTS, MAXIMUM } = constants;
const { EVENT_DESTINATION_REACHED, EVENT_DESTINATION_NEW, EVENT_SPEED_UPDATE, EVENT_ATTACK_STARTED, EVENT_DIE, EVENT_ATTACKING, EVENT_EFFECT_ADD, EVENT_ATTACK_BAR, EVENT_HEALTH_CHANGED } = EVENTS;
const { ENTITY_TYPE_PLAYER, ENTITY_TYPE_NPC } = TYPES;
const { TIMING_EVENT_MOVE, TIMING_EVENT_RESPAWN } = TIMING_EVENTS;
const { AFFECT_CANNON_DAMAGE } = AFFECTS;
const { MAX_SAFE_INTEGER } = MAXIMUM;
const EVENTS_REMOVE_DEATH_NPC = [9, 11, 17];
class NPC extends Entity {
constructor(info) {
super({
type: ENTITY_TYPE_NPC,
id: info.id,
map: info.map,
isAlive: true,
position: info.position
});
this.hp = {
maximum: info.maximumHitpoint,
current: info.currentHitpoint
};
this.entityTypeID = info.entityTypeID ?? 0;
this.parentEntityTypeID = info.parentEntityTypeID ?? 0;
this.respawnTime = info.respawnTime * 1000;
this.isAdmiral = info.isAdmiral ?? false;
this.speed = info.speed;
this.size = info.size ?? 128;
this.cannonRange = 0;
this.i = 0;
this.path = [];
this.attackers = [];
this.isSpeedLowered = false;
this.damageReceived = {};
// this.behaviour = info.behaviour;
this.totalDamageReceived = 0;
this.lastShotTickTimestamp = 0;
this.activeOvertimeHealthEffects = {};
this.rewardCallback = info.rewardCallback;
this.attackCallback = info.attackCallback;
this.dieCallback = info.dieCallback;
this.reward = info.reward;
this.activeEffects = {};
this.cannonTypesOutOfRange = [];
this.searchedTarget = null;
this.group = info.group;
this.inventory = {
cannons: [],
selectedAmmunition: 1,
equipCannons() {
const cannonsEquippedScheme = cannonsToEquip[info.entityTypeID];
if (!cannonsEquippedScheme) return;
for (let i = 0, length = cannonsEquippedScheme.length; i < length; i++) {
const [id, amount] = cannonsEquippedScheme[i];
const cannonScheme = cannons[id];
if (this.cannonRange < cannonScheme.range) this.cannonRange = cannonScheme.range;
const cannon = new Cannon({
id,
damage: cannonScheme.damage,
range: cannonScheme.range,
reload: cannonScheme.reload,
scatter: 0,
amount,
amountEquipped: amount
});
this.cannons.push(cannon);
};
},
getCannons() {
return this.cannons;
}
};
this.inventory.equipCannons();
const cannonsStatistics = this.getCannonsStatistics();
this.cannonRange = cannonsStatistics.maximumCannonRange;
this.cannonTypesInUse = cannonsStatistics.distinctCannonTypes;
this.behaviour = {
stopIfAttacked: !!this.parentEntityTypeID,
attackIfAttacked: true,
isAggressive: !this.parentEntityTypeID,
isChaser: false,
stopAttackIfAbandoned: true,
selectTargetType: 1
};
this.behaviourBits = Object.values(this.behaviour).reduce((prev, cur, i) => cur ? prev + Math.pow(2, i) : prev + 0, 0);
if (this.behaviour.isChaser) {
this.chasingBehaviour = {
range: 750,
recalculateAccumulator: 0,
position: {
x: 0,
y: 0
},
target: null
};
};
const { map, id } = this;
map.npcs.set(id, this);
const nearbyPlayersID = map.getNearbyPlayersGameID(this);
for (let i = 0; i < nearbyPlayersID.length; i++) {
const player = map.getEntityFromSubgroup(nearbyPlayersID[i]);
player.inDistance[id] = true;
};
}
trackOvertimeHealthEffect(id) {
const effect = this.activeOvertimeHealthEffects[id];
if (effect) effect.count++;
else this.activeOvertimeHealthEffects[id] = { count: 0 };
return effect ? effect.count : 0;
}
overtimeDecreaseCurrentHitpoint(event) {
if (!event) return;
const { id, authorTypeID, authorID, damage, iid } = event;
const { amount, ticks, time } = OVERTIME_HEALTH_EFFECTS[id];
const decrease = this.decreaseCurrentHitpoint(damage * amount, {
authorTypeID,
authorID
});
if (!decrease) return;
const currentCount = this.trackOvertimeHealthEffect(id);
if (currentCount < ticks && decrease.currentHitpoint) this.setIndependentEvent(iid, time, event);
else delete this.activeOvertimeHealthEffects[id];
const data = {
targetTypeID: this.typeID,
targetID: this.id,
amount: decrease.normalizedAmount,
type: 0,
isCritical: false
};
this.emit(EVENT_HEALTH_CHANGED, data, this.id);
}
effectAdd(effectID) {
this.activeEffects[effectID] = true;
this.emit(EVENT_EFFECT_ADD, {
typeID: this.typeID,
id: this.id,
effectID
});
}
effectRemove(effectID) {
this.activeEffects[effectID] = false;
this.emit(EVENT_EFFECT_ADD, {
typeID: this.typeID,
id: this.id,
effectID
});
this.removeEvent(17);
switch (effectID) {
case 1:
this.determineMovementState();
break;
};
}
isAtMaximumHitpoints() {
return this.hp.current === this.hp.maximum;
}
increaseCurrentHitpoint(amount) {
amount = Math.abs(amount);
const normalizedAmount = this.hp.current + amount > this.hp.maximum ? amount - (this.hp.current + amount - this.hp.maximum) : amount;
this.hp.current += normalizedAmount;
if (this.isSpeedLowered && !this.isHPBelowPercentageThreshold(35)) {
this.changeSpeed(-this.speed / 2);
this.isSpeedLowered = false;
};
return {
normalizedAmount,
currentHitpoint: this.hp.current
};
}
decreaseCurrentHitpoint(amount, author) {
if (this.hp.current === 0) return;
const { authorID } = author;
amount = Math.abs(amount);
const damage = Math.min(this.hp.current, amount);
if (this.damageReceived[authorID]) this.damageReceived[authorID] += damage;
else this.damageReceived[authorID] = damage
this.totalDamageReceived += damage;
this.hp.current -= damage;
if (!this.isSpeedLowered && this.isHPBelowPercentageThreshold(35)) {
this.changeSpeed(this.speed);
this.isSpeedLowered = true;
};
this.attackCallback({
typeID: this.typeID,
entityTypeID: this.entityTypeID,
authorID,
damage
});
const isOverSafeLimit = this.totalDamageReceived >= MAX_SAFE_INTEGER;
if (isOverSafeLimit) this.totalDamageReceived = MAX_SAFE_INTEGER;
if (this.hp.current === 0 || isOverSafeLimit) {
const die = {
typeID: this.typeID,
lastShotTypeID: 1,
lastShotID: author.authorID,
targetID: this.id,
targetEntityTypeID: this.entityTypeID
};
this.die(die);
//this.setSingleEvent(11, 0, die);
};
return {
normalizedAmount: damage,
currentHitpoint: this.hp.current
};
}
isHPBelowPercentageThreshold(percentageThreshold) {
return this.hp.maximum / 100 * percentageThreshold > this.hp.current;
}
changeSpeed(speed) {
this.speed += speed;
const eSpeed = {
typeID: this.typeID,
id: this.id,
speed: this.speed
};
this.emit(EVENT_SPEED_UPDATE, eSpeed, this.id);
}
die(eventDeath) {
if (!this.isAlive) return;
this.emit(EVENT_DIE, eventDeath, this.id);
super.die();
this.hp.current = 0;
this.attackers.forEach(data => {
const attackerEntity = this.map.getEntityFromSubgroup(data.attackerEntry);
attackerEntity.wipeAttack();
});
this.attackers.length = 0;
if (this.group)
this.emit("miniMapDotRemoveTest", {
id: this.id
});
const admiralProperties = {
typeID: this.typeID,
entityTypeID: this.entityTypeID,
parentEntityTypeID: this.parentEntityTypeID,
id: this.id,
isAdmiral: this.isAdmiral,
authorID: eventDeath.lastShotID
};
this.stopMovement();
this.abortAttack();
this.untrackChasedTarget();
this.dieCallback(admiralProperties);
this.damageReceived = {};
this.activeEffects = {};
EVENTS_REMOVE_DEATH_NPC.forEach(e => this.removeEvent(e));
if (this.group && !this.group.isLeaderAlive()) this.group.cancelRespawn();
if ((!this.isAdmiral && !this.group) || (this.group && !this.group.isLeader(this.id) && this.group.isLeaderAlive())) this.setEvent(TIMING_EVENT_RESPAWN, this.respawnTime);
}
respawn() {
if (this.isAlive) return;
this.hp.current = this.hp.maximum;
this.isSpeedLowered = false;
this.speed /= 2;
super.respawn({
position: this.group ? this.map.grid.randomNodeInArea(this.group.predefinedArea) : null
});
this.determineMovementState();
this.removeEvent(TIMING_EVENT_RESPAWN);
}
haltMovement() {
this.isDirty = this.isDirty || this.isMoving && this.behaviour.stopIfAttacked;
}
stopMovement() {
this.isDirty = false;
this.isMoving = false;
this.path = [];
this.i = 0;
this.removeEvent(TIMING_EVENT_MOVE);
}
startMovement(path) {
if (!Array.isArray(path)) return;
if (!path.length) return;
this.path = path;
this.isMoving = true;
const [x, y] = this.path[this.i];
this.emit(EVENT_DESTINATION_NEW, {
id: this.id,
typeID: this.typeID,
mapID: this.map.id,
x,
y
});
this.setEvent(TIMING_EVENT_MOVE, this.speed);
}
getCannonsStatistics() {
const cannons = this.inventory.getCannons();
let maximumCannonRange = 0;
const distinctCannonTypes = cannons.reduce((acc, cur) => {
const alreadyAddedType = acc.find(c => c.id === cur.id);
if (!alreadyAddedType && cur.amountEquipped) {
if (cur.range > maximumCannonRange) maximumCannonRange = cur.range;
acc.push({
id: cur.id,
reload: cur.reload,
range: cur.range,
eventID: cur.eventID
});
};
return acc;
}, []);
return {
distinctCannonTypes,
maximumCannonRange
};
}
calculateCannonDamage(id) {
const { inventory } = this;
const ammunitionScheme = ammunitions[inventory.selectedAmmunition];
let scaleSum = 1;
let damage = 0;
let k = 0;
/*inventory.useAvailableItemsByAffect(AFFECT_CANNON_DAMAGE, (scale, itemID, statusID, isStatusActive) => {
scaleSum += scale;
if (!isStatusActive) this.disableStatusEffect(statusID);
});*/
const cannons = this.inventory.getCannons();
for (let i = 0, length = cannons.length; i < length; i++) {
const cannonType = cannons[i];
for (let j = 0, length = cannonType.amountEquipped; j < length; j++) {
const minimum = ((cannonType.damage * ((100 + cannonType.scatter) / 100) + 1) * scaleSum) - 1;
const maximum = cannonType.damage * scaleSum;
if (id === cannonType.id) {
damage += Math.ceil((Math.random() * (maximum - minimum) + minimum) * ammunitionScheme.damage);
k += cannonType.amountEquipped;
};
};
};
return {
usedCannonCount: k,
value: damage,
critical: false,
usedAmmunition: ammunitionScheme
};
}
initializeAttackWithCannon(target) {
if (!target) return;
if (this.isAttacking()) return false;
const isInShootingRange = Utility.IsInDistance(this.position, target.position, this.cannonRange);
if (!isInShootingRange) return false;
if (target.id === this.id) return false;
if (!target.isAlive) return false;
if (this.hp.current <= 0) return false;
if (!this.behaviour.attackIfAttacked) return false;
if (!this.inventory.getCannons().some(c => c.amountEquipped)) return false;
if (target.typeID === ENTITY_TYPE_PLAYER) target.cancelDisconnect();
this.haltMovement();
this.target = target;
this.searchedTarget = null;
const attackerInformation = {
attackerEntry: this.id
};
target.attackers.push(attackerInformation);
const eventAttackBar = {
authorID: this.id,
authorTypeID: this.typeID,
isTarget: false,
source: 1
};
target.emit(EVENT_ATTACK_BAR, eventAttackBar);
this.cannonTypesInUse.forEach(c => this.setReusableSingleEvent(c.eventID, 0, { id: c.id }));
}
flush(timingEvent) {
const currentFlush = this.map.eventBuffer[this.id];
if (!currentFlush) return
this.emit("attacking", currentFlush.data, currentFlush.author);
delete this.map.eventBuffer[this.id];
}
attackWithCannon(event) {
if (!this.isAlive) return;
const { target } = this;
const cannonType = this.cannonTypesInUse.find(c => c.id === event.id);
const recheckShootingRange = Utility.IsInDistance(this.position, target.position, cannonType.range);
if (!recheckShootingRange) {
this.cannonTypesOutOfRange.push({
cannonID: cannonType.id,
lastFiredAtTimestamp: performance.now(),
event
});
if (this.cannonRange === cannonType.range) {
this.abortAttack();
} else {
this.setReusableSingleEvent(cannonType.eventID, cannonType.reload, event);
};
return true;
};
if (this.behaviour.stopAttackIfAbandoned && !this.behaviour.isAggressive && !this.attackers.length) return this.abortAttack();
if (target.isRepairing()) target.cancelRepair();
const damage = this.calculateCannonDamage(cannonType.id);
this.lastShotTickTimestamp = performance.now();
const healthUpdate = target.decreaseCurrentHitpoint(damage.value, {
authorTypeID: this.typeID,
authorID: this.id
});
// this.emit("attacking", attacking, this.id);
const flushBuffer = this.map.eventBuffer[this.id];
if (flushBuffer) flushBuffer.data.amount += healthUpdate.normalizedAmount;
else {
const attacking = {
authorID: this.id,
authorTypeID: this.typeID,
targetID: target.id,
targetTypeID: target.typeID,
amount: healthUpdate.normalizedAmount,
isCritical: damage.critical
};
this.map.eventBuffer[this.id] = {
data: attacking,
author: target.id,
target: this.id
};
this.map.eventAuthors.push(this.id);
/*this.flushBuffer[EVENT_ATTACKING] = {
data: attacking,
author: target.id
};*/
};
//this.flush(1);
if (healthUpdate.currentHitpoint <= 0) return;
this.setReusableSingleEvent(cannonType.eventID, cannonType.reload, event);
return true;
}
wipeAttack() {
const { target } = this;
if (!target) return
this.target = null;
this.untrackChasedTarget();
this.stopMovement();
this.determineMovementState();
this.cannonTypesInUse.forEach(c => this.cancelEvent(c.eventID));
}
abortAttack() {
const { target } = this;
if (!target) return;
if (this.behaviour.isChaser) {
this.chasingBehaviour.target = this.target;
this.chasingBehaviour.position = Object.assign({}, this.target.position);
};
this.target.attackers = target.attackers.filter(data => data.attackerEntry !== this.id);
this.target = null;
this.cannonTypesInUse.forEach(c => this.cancelEvent(c.eventID));
this.determineMovementState();
this.markActive();
this.map.lookingForTarget.push(this);
}
determineMovementState(preSelectedDestinationNode) {
if (!this.isAlive) return;
if (this.isDirty) return;
if (this.isMoving) return;
if (this.behaviour.stopIfAttacked && this.isAttacking()) return;
if (!this.behaviour.stopIfAttacked && this.isMoving) return;
if (this.behaviour.stopIfAttacked && this.attackers.length) return;
if (EFFECTS_TERMINATE_MOVEMENT.some(effect => this.activeEffects[effect])) return;
if (!this.isDirty && this.chasingBehaviour && this.chasingBehaviour.target
&& this.chasingBehaviour.position.x === this.position.x && this.chasingBehaviour.position.y === this.position.y) {
this.chasingBehaviour.position = {
x: this.chasingBehaviour.target.position.x,
y: this.chasingBehaviour.target.position.y
};
};
let destination = null;
if (this.group) destination = this.map.grid.randomNodeInArea(this.group.predefinedArea);
else if (preSelectedDestinationNode) destination = preSelectedDestinationNode;
else if (this.chasingBehaviour && this.chasingBehaviour.target) destination = this.chasingBehaviour.position;
else destination = null;
const predefinedArea = this.group ? this.group.predefinedArea : null;
this.emit(EVENT_DESTINATION_REACHED, {
destination,
predefinedArea
});
}
seekEnemy(searchedTarget) {
if (!searchedTarget) return;
if (!this.isAlive) return;
if (this.isAttacking()) return;
if (!this.behaviour.isAggressive) return;
if (!this.searchedTarget) return this.searchedTarget = searchedTarget;
let isBehaviourSatisfied = false;
switch (this.behaviour.selectTargetType) {
case 1:
isBehaviourSatisfied = searchedTarget.hp.current - this.searchedTarget.hp.current <= 0;
break;
case 2:
isBehaviourSatisfied = this.searchedTarget.hp.current - searchedTarget.hp.current <= 0;
break;
case 3:
isBehaviourSatisfied = searchedTarget.hp.maximum - this.searchedTarget.hp.maximum < 0;
break;
case 4:
isBehaviourSatisfied = this.searchedTarget.hp.maximum - searchedTarget.hp.maximum < 0;
break;
case 5:
isBehaviourSatisfied = searchedTarget.speed - this.searchedTarget.speed < 0;
break;
case 6:
isBehaviourSatisfied = this.searchedTarget.speed - searchedTarget.speed < 0;
break;
case 7:
isBehaviourSatisfied = searchedTarget.sight - this.searchedTarget.sight < 0;
break;
case 8:
isBehaviourSatisfied = this.searchedTarget.sight - searchedTarget.sight < 0;
break;
};
const isInShootingRange = Utility.IsInDistance(this.position, searchedTarget.position, this.cannonRange);
const isSelectedInShootingRange = Utility.IsInDistance(this.position, this.searchedTarget.position, this.cannonRange);
if ((isBehaviourSatisfied && isInShootingRange) || !isSelectedInShootingRange) return this.searchedTarget = searchedTarget;
}
trackChasedTarget() {
if (!this.chasingBehaviour || !this.chasingBehaviour.target) return;
const isInRange = Utility.IsInDistance(this.position, this.chasingBehaviour.target.position, this.chasingBehaviour.range);
if (!isInRange) {
this.untrackChasedTarget();
return this.isDirty = true;
};
const chasedPosition = this.chasingBehaviour.target.position;
if (chasedPosition.x === this.chasingBehaviour.position.x && chasedPosition.y === this.chasingBehaviour.position.y) return;
const threshold = Math.ceil((1000 - this.speed) / 150);
if (++this.chasingBehaviour.recalculateAccumulator === threshold) {
const isChasedTargetInShootingRange = Utility.IsInDistance(this.chasingBehaviour.position, chasedPosition, this.range);
if (!isChasedTargetInShootingRange) {
this.chasingBehaviour.position = {
x: chasedPosition.x,
y: chasedPosition.y
};
this.isDirty = true;
};
this.chasingBehaviour.recalculateAccumulator = 0;
};
}
untrackChasedTarget() {
if (!this.chasingBehaviour || !this.chasingBehaviour.target) return;
this.chasingBehaviour.target = null;
this.chasingBehaviour.position.x = 0;
this.chasingBehaviour.position.y = 0;
this.chasingBehaviour.recalculateAccumulator = 0;
}
move() {
const { position } = this;
const [currentX, currentY] = this.path[this.i];
const dx = currentX - position.x;
const dy = currentY - position.y;
position.x += dx;
position.y += dy;
if (this.group) {
this.emit("miniMapMove", {
id: this.id,
position: this.position
});
};
this.updateChunk();
this.markActive();
if (this.searchedTarget) {
const target = this.map.getEntityFromSubgroup(this.searchedTarget.id);
if (target) this.initializeAttackWithCannon(target);
else this.searchedTarget = null;
};
if (this.i === this.path.length - 1 || this.isDirty) {
this.untrackChasedTarget();
this.stopMovement();
this.determineMovementState();
} else {
const nextNode = this.path[++this.i];
const [x, y] = nextNode;
this.trackChasedTarget();
this.setEvent(TIMING_EVENT_MOVE, this.speed);
this.emit(EVENT_DESTINATION_NEW, {
id: this.id,
typeID: this.typeID,
x,
y
});
};
}
isAttacking = () => this.target ? true : false;
getProperties() {
const { hp, position, id, speed, size, typeID, entityTypeID, path, behaviourBits, isMoving, isAdmiral, isSpeedLowered } = this;
const activeEffectsBits = Object.values(this.activeEffects).reduce((prev, cur, i) => cur ? prev + Math.pow(2, i) : prev + 0, 0);
return { hp, position, id, speed, size, typeID, entityTypeID, path: path.length ? path[this.i] : [], behaviourBits, activeEffectsBits, isMoving, isAdmiral, isSpeedLowered };
}
};
module.exports = NPC;
+21
View File
@@ -0,0 +1,21 @@
class Limiter {
constructor(info) {
this.countThreshold = info.countThreshold;
this.timeThreshold = info.timeThreshold;
this.nextTime = 0;
this.time = 0;
this.count = 0;
};
update() {
if (performance.now() - this.time < this.timeThreshold) this.count++;
else {
this.time = performance.now();
this.count = 1;
};
return this.count > this.countThreshold;
}
};
module.exports = Limiter;
+1536
View File
File diff suppressed because it is too large Load Diff
+39
View File
@@ -0,0 +1,39 @@
class PlayerBehaviour {
constructor(info) {
this.id = info.id;
this.size = info.size ?? 32;
this.entries = Array.from({ length: this.size }, () => ({ isProcessed: false, timestamp: Infinity, entry: {}, id: 0 }));
this.accumulator = 0;
this.buffer = info.buffer;
};
add(entry) {
const index = this.accumulator++ % this.size;
this.entries[index] = {
timestamp: Date.now(),
entry,
id: this.id,
isProcessed: false
};
}
save(threshold) {
const now = Date.now();
for (let i = 0; i < this.size; i++) {
const entry = this.entries[i];
if (entry.id === 0) continue;
if (entry.isProcessed) continue;
if (entry.timestamp + threshold < now) continue;
entry.isProcessed = true;
// require("../Miscellaneous/winston").error(entry)
this.buffer.push(entry.entry); // entry.entry
};
return this.buffer;
}
};
module.exports = PlayerBehaviour;
+68
View File
@@ -0,0 +1,68 @@
const EventEmitter = require("events");
const Reward = require("./Reward");
class Quest extends EventEmitter {
constructor(configuration, doneCallback) {
super();
this.id = configuration.id;
this.currentComplete = configuration.completedAmount;
this.maximumComplete = configuration.maximumComplete;
this.task = JSON.parse(JSON.stringify(configuration.task));
this.reward = new Reward({
distributionType: 2,
baseReward: configuration.reward
});
this.name = configuration.name || "Quest without a name";
this.finishedCount = 0;
this.totalTasksCount = Object.values(this.task).reduce((pre, cur) => pre + cur.length, 0);
this.doneCallback = doneCallback;
}
adjustTask(events, amount) {
events.forEach(type => {
const tasksInCategory = this.task[type.taskCategoryID];
if (!tasksInCategory) return
tasksInCategory.forEach(task => {
if (task.isFinished) return
if (type.typeID === task.typeID && type.entityTypeID === task.entityTypeID) {
task.amount += amount;
if (task.requiredAmount <= task.amount) {
task.amount = task.requiredAmount;
task.isFinished = true;
this.finishedCount++;
};
};
});
});
if (this.finishedCount === this.totalTasksCount) {
this.doneCallback(this.id);
};
}
getTasks() {
const taskTypes = Object.keys(this.task);
return taskTypes.map(t => this.task[t]);
}
getProgression() {
const taskTypes = Object.keys(this.task);
const progression = [];
taskTypes.forEach(t => this.task[t].forEach((c, i) => {
progression.push({ amount: c.amount, index: i, type: t });
}));
return progression;
}
};
module.exports = Quest;
+86
View File
@@ -0,0 +1,86 @@
const Quest = require("./Quest");
const quests = require("../Data/quests");
class Quests {
constructor(quests, doneCallback) {
let currentQuest = quests[0];
this.completedQuests = [];
this.finishedQuests = {};
this.completedQuestsAmount = {};
this.doneCallback = doneCallback;
this.list = {};
this.list = quests.reduce((result, entry) => {
this.completedQuestsAmount[entry.questID] = entry.completedAmount;
if (entry.state === 1 || entry.state === 2) return result;
const quest = this.add(entry.questID);
if (!quest) return result;
if (currentQuest.id !== entry.questID) {
result[entry.questID] = quest;
currentQuest = quest;
};
if (!entry.amount) return result;
const { typeID, entityTypeID } = result[entry.questID].task[entry.taskType][entry.taskIndex];
currentQuest.adjustTask([{
taskCategoryID: entry.taskType,
typeID,
entityTypeID
}], entry.amount);
return result;
}, {});
};
add(id) {
const questScheme = quests[id - 1];
if (!questScheme) return;
const isAcceptable = !questScheme.maximumComplete || questScheme.maximumComplete && questScheme.maximumComplete > this.completedQuestsAmount[id] || !this.completedQuestsAmount[id] && questScheme.maximumComplete;
if (!isAcceptable) return;
const isQuestAlreadyAdded = this.list[id];
if (isQuestAlreadyAdded) return true;
const quest = new Quest(questScheme, completedQuestID => {
if (questScheme.maximumComplete !== -1) this.completedQuests.push(completedQuestID);
if (!this.completedQuestsAmount[completedQuestID]) this.completedQuestsAmount[completedQuestID] = 1;
else this.completedQuestsAmount[completedQuestID]++;
const completedQuest = this.list[completedQuestID];
this.doneCallback(completedQuest);
this.remove(completedQuestID);
});
delete this.finishedQuests[id];
this.list[id] = quest;
return quest;
}
remove(id) {
const questScheme = quests[id - 1];
if (!questScheme) return;
this.finishedQuests[id] = true;
delete this.list[id];
}
has(id) {
return this.list[id] ? true : false;
}
refresh(pirateLevel) {
for (const id in this.list) {
const questScheme = quests[+id - 1];
if (questScheme.maximumLevel < pirateLevel || questScheme.minimumLevel > pirateLevel) this.remove(+id);
};
}
};
module.exports = Quests;
+134
View File
@@ -0,0 +1,134 @@
const Utility = require("./Utility");
const chooseWeightedRandom = require("../Utility/chooseWeightedRandom");
const uniqueCategories = {
5: true
};
const uniqueItems = {};
class Reward {
constructor(information) {
this.distributionType = information.distributionType;
this.baseReward = information.baseReward;
this.damage = information.damage;
};
single(amountOfItems, inventory, cb) {
if (this.distributionType !== 2) return;
const { baseReward } = this;
const allRewardCategories = Object.entries(baseReward);
const rewardChances = [];
const rewardEntries = [];
const finalRewards = [];
allRewardCategories.forEach(category => {
const [categoryName, categoryEntries] = category;
Object.entries(categoryEntries).forEach(subEntry => {
const [id, entry] = subEntry;
const isUniqueReward = uniqueCategories[categoryName] || uniqueItems[categoryName] && uniqueItems[categoryName][id];
const isAlreadyAcquired = inventory[categoryName] && inventory[categoryName].hasElement(+id);
if (isUniqueReward && isAlreadyAcquired) return;
const rewardEntry = {
categoryID: +categoryName,
id: +id,
amount: entry.amountMaximum ? Utility.RandomIntegerBetween(entry.amountMinimum, entry.amountMaximum) : entry.amountMinimum,
index: rewardEntries.length
};
if (entry.chance === 100) finalRewards.push(rewardEntry);
else {
rewardChances.push(entry.chance);
rewardEntries.push(rewardEntry);
};
});
});
const batchedRewards = {};
if (rewardEntries.length) {
for (let i = 0; i < amountOfItems; i++) {
const chosenItem = chooseWeightedRandom(rewardEntries, rewardChances);
const batchedEntryIndex = batchedRewards[`${chosenItem.categoryID}-${chosenItem.id}`];
if (batchedEntryIndex !== undefined) {
finalRewards[batchedEntryIndex].amount += chosenItem.amount;
} else {
batchedRewards[`${chosenItem.categoryID}-${chosenItem.id}`] = finalRewards.length;
finalRewards.push(Object.assign({}, chosenItem));
const isUniqueReward = uniqueCategories[chosenItem.categoryID] || uniqueItems[chosenItem.categoryID] && uniqueItems[chosenItem.categoryID][chosenItem.id];
if (isUniqueReward) rewardChances[chosenItem.index] = 0;
};
};
};
cb(finalRewards);
}
multiple(data, cb) {
if (this.distributionType !== 1) return;
const { baseReward } = this;
const { playerInventory, damage, maximum, lastShotAuthorID } = data;
const allRewardCategories = Object.keys(baseReward);
const totalParticipants = Object.keys(damage);
totalParticipants.forEach(playerID => {
playerID = parseInt(playerID);
const rewards = [];
allRewardCategories.forEach(category => {
category = parseInt(category);
const rew = baseReward[category];
Object.keys(rew).forEach(id => {
id = parseInt(id);
const isUniqueReward = uniqueCategories[category] || uniqueItems[category] && uniqueItems[category][id];
const isAlreadyAcquired = playerInventory[category] && playerInventory[category].hasElement(id);
if (isUniqueReward && isAlreadyAcquired) return;
if (rew[id].lastShot && playerID !== lastShotAuthorID) return;
if (rew[id].chance < Math.random() * 100) return;
const baseRewardAmount = rew[id].amountMaximum ? Utility.RandomIntegerBetween(rew[id].amountMinimum, rew[id].amountMaximum) : rew[id].amountMinimum;
const amountOfReward = rew[id].lastShot ? baseRewardAmount : Math.round(baseRewardAmount / 100 * (damage[playerID] / maximum * 100));
if (amountOfReward <= 0) return;
rewards.push({
categoryID: category,
id,
amount: amountOfReward
});
});
});
cb({ id: playerID }, rewards);
});
}
dynamic(cb) {
if (this.distributionType !== 3) return;
for (let i = 0, length = this.baseReward.length; i < length; i++) {
const reward = this.baseReward[i];
const baseRewardAmount = reward.amountMaximum ? Utility.RandomIntegerBetween(reward.amountMinimum, reward.amountMaximum) : reward.amountMinimum;
cb([{
categoryID: reward.categoryID,
id: reward.id,
amount: baseRewardAmount
}]);
};
}
};
module.exports = Reward;
+15
View File
@@ -0,0 +1,15 @@
class SessionManager {
constructor() {
this.startAt = Date.now();
this.endedAt = null;
this.currency = [0, 0, 0, 0]
this.kills = [0, 0, 0, 0, 0];
this.experience = 0;
this.sunkCount = 0;
this.packets = {};
this.ammunition = {};
this.harpoon = {};
};
};
module.exports = SessionManager;
+552
View File
@@ -0,0 +1,552 @@
const Entity = require("./Entity");
const Utility = require("./Utility");
const OVERTIME_HEALTH_EFFECTS = require("../Data/overtime");
const GUILD_TOWER_SCHEME = require("../Data/schemeTower");
const constants = require("../Data/constants");
const { EVENTS, TIMING_EVENTS, TYPES, MAXIMUM } = constants;
const { ENTITY_TYPE_TOWER, ENTITY_TYPE_PLAYER } = TYPES;
const { EVENT_ATTACK_STARTED, EVENT_DIE, EVENT_CANNONS_USED, EVENT_ATTACK_BAR, EVENT_HEALTH_CHANGED, EVENT_HEALTH_CHANGED_AUTHORLESS, EVENT_GUILD_ISLAND_CAPTURE } = EVENTS;
const { TIMING_EVENT_RESPAWN, TIMING_EVENT_REPAIR } = TIMING_EVENTS;
const { MAX_SAFE_INTEGER } = MAXIMUM;
const EVENTS_REMOVE_DEATH_TOWER = [9, 11, 12, 13, 17];
class Tower extends Entity {
constructor(info) {
super({
type: ENTITY_TYPE_TOWER,
id: info.id,
map: info.map,
isAlive: info.currentHitpoint !== 0,
position: info.position
});
if (!info.island) return;
this.order = info.order;
this.behaviour = {
attackIfAttacked: true,
isAggressive: true,
stopAttackIfAbandoned: false,
selectTargetType: 1
};
this.hp = {
maximum: info.maximumHitpoint,
current: info.currentHitpoint
};
this.island = info.island;
this.island.addTower(this);
this.entityTypeID = info.entityTypeID;
this.attackers = [];
this.totalDamageReceived = 0;
this.damageReceived = {};
this.activeEffects = {};
this.activeOvertimeHealthEffects = {};
this.dieCallback = info.dieCallback;
this.attackCallback = info.attackCallback;
this.map.towers.set(this.id, this);
setTimeout(() => {
// this.repair();
}, 15000);
};
trackOvertimeHealthEffect(id) {
const effect = this.activeOvertimeHealthEffects[id];
if (effect) effect.count++;
else this.activeOvertimeHealthEffects[id] = { count: 0 };
return effect ? effect.count : 0;
}
overtimeDecreaseCurrentHitpoint(event) {
if (!event) return;
const { id, authorTypeID, authorID, authorGuildID, damage, iid } = event;
const { amount, ticks, time } = OVERTIME_HEALTH_EFFECTS[id];
const decrease = this.decreaseCurrentHitpoint(damage * amount, {
authorTypeID,
authorID,
authorGuildID
});
if (!decrease) return;
const currentCount = this.trackOvertimeHealthEffect(id);
if (currentCount < ticks && decrease.currentHitpoint) this.setIndependentEvent(iid, time, event);
else delete this.activeOvertimeHealthEffects[id];
const data = {
targetTypeID: this.typeID,
targetID: this.id,
amount: decrease.normalizedAmount,
type: 0,
isCritical: false
};
this.emit(EVENT_HEALTH_CHANGED_AUTHORLESS, data, this.id);
}
initializeRepair() {
if (!this.isAlive) return;
if (this.isAtMaximumHitpoints()) return;
this.repair();
}
repair(doneIndex) {
const { id } = this;
this.lastRepairTickTimestamp = performance.now();
const healthUpdate = this.increaseCurrentHitpoint(Math.floor(Math.random() * 2000));
if (!healthUpdate.normalizedAmount) return console.info("aaa", healthUpdate);
const eventRepair = {
targetTypeID: ENTITY_TYPE_TOWER,
targetID: id,
amount: healthUpdate.normalizedAmount,
type: 1,
isCritical: false,
};
this.emit(EVENT_HEALTH_CHANGED_AUTHORLESS, eventRepair, this.id);
if (healthUpdate.currentHitpoint >= this.hp.maximum) return this.cancelRepair();
this.setReusableSingleEvent(TIMING_EVENT_REPAIR, 1000, doneIndex);
return true;
}
cancelRepair() {
if (!this.isRepairing()) return;
this.cancelEvent(TIMING_EVENT_REPAIR);
}
increaseCurrentHitpoint(amount) {
amount = Math.abs(amount);
const normalizedAmount = this.hp.current + amount <= this.hp.maximum ? amount : this.hp.maximum - this.hp.current;
this.hp.current += normalizedAmount;
const eventGuildIslandTowerHealthChanged = {
id: this.id,
entityTypeID: this.entityTypeID,
guildIslandID: this.island.id,
currentHitpoint: this.hp.current
};
this.emit("guildDataIslandTowerHealthChanged", eventGuildIslandTowerHealthChanged, this.id);
return {
normalizedAmount,
currentHitpoint: this.hp.current
};
}
decreaseCurrentHitpoint(amount, author) {
if (this.hp.current === 0) return console.info("couldn't decrease tower");
const { authorID, authorGuildID } = author;
amount = Math.abs(amount);
const damage = Math.min(this.hp.current, amount);
this.island.updateReceivedDamage(authorGuildID, damage);
if (this.damageReceived[authorID]) this.damageReceived[authorID] += damage;
else this.damageReceived[authorID] = damage;
this.totalDamageReceived += damage;
this.hp.current -= damage;
this.attackCallback({
typeID: this.typeID,
entityTypeID: this.entityTypeID,
authorID,
damage
});
const eventGuildIslandDamage = {
guildIslandID: this.island.id,
guildID: authorGuildID,
damage
};
const eventGuildIslandTowerHealthChanged = {
id: this.id,
entityTypeID: this.entityTypeID,
guildIslandID: this.island.id,
currentHitpoint: this.hp.current
};
this.emit("guildDataIslandTowerHealthChanged", eventGuildIslandTowerHealthChanged, this.id);
this.emit("_", eventGuildIslandDamage);
const isOverSafeLimit = this.totalDamageReceived >= MAX_SAFE_INTEGER;
if (isOverSafeLimit) this.totalDamageReceived = MAX_SAFE_INTEGER;
if (this.hp.current === 0 || isOverSafeLimit) {
const death = {
typeID: this.typeID,
lastShotTypeID: 1,
lastShotID: author.authorID,
targetID: this.id
};
this.die(death);
// this.setSingleEvent(11, 0, die);
};
return {
normalizedAmount: damage,
currentHitpoint: this.hp.current
};
}
initializeAttackWithCannon(selectedTarget) {
//const cannonsStatistics = this.getCannonsStatistics();
const isInShootingRange = Utility.IsInDistance(this.position, selectedTarget.position, 600);
//if (UNATTACKABLE_ENTITY_TYPES.includes(selectedTarget.typeID)) return
if (!isInShootingRange) return
//if (this.isRepairing()) return
// if (selectedTarget.typeID === ENTITY_TYPE_PLAYER && this.map.type === MAP_PVE) return
//const selected = this.inventory.getSelectedAmmunition();
//if (!selected || selected.amount <= 0) return
//if (ammunitions[this.inventory.selectedAmmunition].damage < 0 && selectedTarget.isAtMaximumHitpoints()) return
if (this.target && this.target.id === selectedTarget.id) return
if (selectedTarget.id === this.id) return
if (!selectedTarget.isAlive) return
if (!this.isAlive) return
//if (selectedTarget.typeID === ENTITY_TYPE_PLAYER && selectedTarget.guild.id === this.island.guild.id) return
//if (!this.inventory.getCannons().some(c => c.amountEquipped)) return
if (this.target && this.target.id !== selectedTarget.id) this.abortAttack();
const attackInformation = {
attackerEntry: this.id
};
selectedTarget.attackers.push(attackInformation);
this.target = selectedTarget;
this.searchedTarget = null;
/*if (selectedTarget.typeID === ENTITY_TYPE_PLAYER) {
const attacking = {
authorID: this.id,
authorTypeID: this.typeID,
};
selectedTarget.emit(EVENT_ATTACK_STARTED, attacking, this.id);
};*/
const attacking = {
authorID: this.id,
authorTypeID: this.typeID,
isTarget: false,
source: 1
};
this.emit(EVENT_ATTACK_STARTED, attacking, this.id);
selectedTarget.emit(EVENT_ATTACK_BAR, attacking, selectedTarget.id);
this.setReusableSingleEvent(97, 0, { id: 1 });
//this.cannonTypesInUse = cannonsStatistics.distinctCannonTypes;
//this.maximumCannonRange = cannonsStatistics.maximumCannonRange;
//cannonsStatistics.distinctCannonTypes.forEach(c => this.setReusableSingleEvent(c.eventID, 0, { id: c.id }));
}
attackWithCannon(event) {
const { target } = this;
if (!target) return;
const recheckShootingRange = Utility.IsInDistance(this.position, target.position, 600);
if (!recheckShootingRange) {
this.abortAttack();
return true;
};
this.lastShotTickTimestamp = performance.now();
const attackingAnimation = {
authorSessionID: this.sessionID,
authorID: this.id,
authorTypeID: this.typeID,
targetID: target.id,
targetTypeID: target.typeID,
entityTypeID: target.entityTypeID
};
const damage = //this.calculateCannonDamage(1);
{ value: 2000 }
const attacking = {
authorID: this.id,
authorTypeID: this.typeID,
targetID: target.id,
targetTypeID: target.typeID,
amount: 0,
isCritical: false,
type: damage.value <= 0
};
const healthUpdate = damage.value >= 0 ?
target.decreaseCurrentHitpoint(damage.value, {
authorTypeID: this.typeID,
authorID: this.id,
authorGuildID: this.guild ? this.guild.id : 0
}) : target.increaseCurrentHitpoint(damage.value);
attacking.amount = healthUpdate.normalizedAmount;
attacking.isCritical = damage.critical;
const flushBuffer = this.map.eventBuffer[this.id];
if (flushBuffer) flushBuffer.data[0].amount += attacking.amount;
else {
const data = [attacking, attackingAnimation];
this.map.eventBuffer[this.id] = {
data,
author: target.id,
target: this.id
};
this.map.eventAuthors.push(this.id);
};
this.emit(EVENT_CANNONS_USED, {
amount: damage.usedCannonCount,
type: 0
}, this.id);
if (healthUpdate.currentHitpoint <= 0) return;
this.setReusableSingleEvent(97, 2000, event);
return true;
}
removeTarget() {
if (!this.target) return;
this.target.attackers = this.target.attackers.filter(data => data.attackerEntry !== this.id);
this.target = null;
}
wipeAttack() {
const { target } = this;
if (!target) return;
this.cancelEvent(97);
this.target = null;
}
abortAttack() {
if (!this.isAttacking()) return;
if (!this.target) return;
this.cancelEvent(97);
const target = {
id: this.target.id,
typeID: this.target.typeID
};
this.removeTarget();
return target;
}
die(eventDeath) {
super.die();
console.trace("tower death")
this.hp.current = 0;
this.attackers.forEach(data => {
const attackerEntity = this.map.getEntityFromSubgroup(data.attackerEntry);
attackerEntity.wipeAttack();
});
this.attackers.length = 0;
const eventGuildIslandTowerDestroyed = {
id: this.id,
entityTypeID: this.entityTypeID,
guildIslandID: this.island.id
};
this.emit(EVENT_DIE, [eventDeath, eventGuildIslandTowerDestroyed], this.id);
const admiralProperties = {
typeID: this.typeID,
entityTypeID: this.entityTypeID,
parentEntityTypeID: this.parentEntityTypeID,
id: this.id,
isAdmiral: this.isAdmiral,
authorID: eventDeath.lastShotID
};
this.abortAttack();
this.dieCallback(admiralProperties);
EVENTS_REMOVE_DEATH_TOWER.forEach(e => this.removeEvent(e));
const updatedOwnership = this.island.updateOwnership();
if (updatedOwnership) {
const { guildOld, guildNew, topicOld, topicNew } = updatedOwnership;
const eventGuildIslandCaptured = {
topicOld: topicOld,
topicNew: topicNew,
packet: {
tag: guildNew.tag
},
database: {
newID: guildNew.id,
oldID: guildOld.id
}
};
this.emit(EVENT_GUILD_ISLAND_CAPTURE, eventGuildIslandCaptured, this.id);
};
this.damageReceived = {};
}
seekEnemy(searchedTarget) {
if (!searchedTarget) return;
if (!this.isAlive) return;
if (this.isAttacking()) return;
if (!this.behaviour.isAggressive) return;
if (this.island.guild.id && this.island.guild.id === searchedTarget.guild.id) return;
if (!this.searchedTarget) return this.searchedTarget = searchedTarget;
let isBehaviourSatisfied = false;
switch (this.behaviour.selectTargetType) {
case 1:
isBehaviourSatisfied = searchedTarget.hp.current - this.searchedTarget.hp.current <= 0;
break;
case 2:
isBehaviourSatisfied = this.searchedTarget.hp.current - searchedTarget.hp.current <= 0;
break;
case 3:
isBehaviourSatisfied = searchedTarget.hp.maximum - this.searchedTarget.hp.maximum < 0;
break;
case 4:
isBehaviourSatisfied = this.searchedTarget.hp.maximum - searchedTarget.hp.maximum < 0;
break;
case 5:
isBehaviourSatisfied = searchedTarget.speed - this.searchedTarget.speed < 0;
break;
case 6:
isBehaviourSatisfied = this.searchedTarget.speed - searchedTarget.speed < 0;
break;
case 7:
isBehaviourSatisfied = searchedTarget.sight - this.searchedTarget.sight < 0;
break;
case 8:
isBehaviourSatisfied = this.searchedTarget.sight - searchedTarget.sight < 0;
break;
};
const isInShootingRange = Utility.IsInDistance(this.position, searchedTarget.position, 600);
const isSelectedInShootingRange = Utility.IsInDistance(this.position, this.searchedTarget.position, 600);
if ((isBehaviourSatisfied && isInShootingRange) || !isSelectedInShootingRange) return this.searchedTarget = searchedTarget;
}
respawn() {
if (this.isAlive) return;
this.entityTypeID = 1;
const baseScheme = GUILD_TOWER_SCHEME[this.entityTypeID];
if (!baseScheme) return;
this.hp.maximum = baseScheme.maximumHitpoints;
this.hp.current = this.hp.maximum;
super.respawn({
position: this.position
});
const eventTowerBuild = {
id: this.id,
entityTypeID: this.entityTypeID,
guildIslandID: this.island.id
};
this.emit("guildDataIslandTowerBuilt", eventTowerBuild, this.id);
console.info("guildDataIslandTowerBuilt")
this.markActive();
}
upgrade(entityTypeID) {
if (!this.isAlive) {
this.respawn();
return true;
};
const baseScheme = GUILD_TOWER_SCHEME[entityTypeID];
if (!baseScheme) return false;
const isNextLevelTarget = entityTypeID - this.entityTypeID === 1;
if (isNextLevelTarget) {
this.hp.maximum = baseScheme.maximumHitpoints;
this.hp.current = baseScheme.maximumHitpoints;
this.entityTypeID = entityTypeID;
} else return false;
const eventTowerBuild = {
id: this.id,
entityTypeID: this.entityTypeID,
guildIslandID: this.island.id
};
this.emit("guildDataIslandTowerBuilt", eventTowerBuild, this.id);
return true;
}
getProperties() {
const { position, id, hp, type, typeID, isAdmiral, entityTypeID, island, order } = this;
return { position, id, hp, type, typeID, isAdmiral, entityTypeID, guildTag: island.guild ? island.guild.tag : null, order };
}
isAtMaximumHitpoints() {
return this.hp.current === this.hp.maximum;
}
isRepairing() {
const event = this.events[TIMING_EVENT_REPAIR];
return event && !event[event.length - 1].isCancelled ? true : false;
}
isAttacking = () => this.target ? true : false;
};
module.exports = Tower;
+11
View File
@@ -0,0 +1,11 @@
class Utility {
static RandomIntegerBetween(minimum, maximum) {
return Math.floor(Math.random() * (maximum - minimum + 1) + minimum);
}
static IsInDistance(basePoint, targetPoint, range) {
return (basePoint.x - targetPoint.x) ** 2 + (basePoint.y - targetPoint.y) ** 2 <= range ** 2;
}
};
module.exports = Utility;