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;