553 lines
15 KiB
JavaScript
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;
|