Initial commit
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user