Files
2026-08-24 22:20:36 +02:00

553 lines
15 KiB
JavaScript

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;