const configThreatCeiling = { regular: 1, rare: 0.5 }; module.exports = { type: 'aggro', range: 7, cascadeRange: 5, faction: null, physics: null, list: [], ignoreList: [], threatDecay: 0.9, threatCeiling: 1, //Certain summoned minions need to despawn when they lose their last target dieOnAggroClear: false, init: function (blueprint) { this.physics = this.obj.instance.physics; blueprint = blueprint || {}; if (blueprint.faction) this.faction = blueprint.faction; //TODO: Why don't we move if faction is null? if (!this.has('faction')) return; if (this.physics.width > 0) this.move(); }, calcThreatCeiling: function (mobType) { this.threatCeiling = configThreatCeiling[mobType]; }, events: { beforeRezone: function () { this.die(); } }, simplify: function (self) { return { type: 'aggro', faction: this.faction }; }, //If we send through a proxy, we know it's for cascading threat move: function (proxy) { let obj = proxy || this.obj; let aggro = obj.aggro; if (obj.dead) return; let result = { success: true }; obj.fireEvent('beforeAggro', result); if (!result.success) return; //If we're attacking something, don't try and look for more trouble. SAVE THE CPU! // this only counts for mobs, players can have multiple attackers let list = aggro.list; if (obj.mob) { let lLen = list.length; for (let i = 0; i < lLen; i++) { let l = list[i]; let lThreat = l.obj.aggro.getHighest(); if (lThreat) { l.obj.aggro.list.forEach(function (a) { a.obj.aggro.unIgnore(lThreat); }); } l.obj.aggro.unIgnore(obj); if (l.threat > 0) return; } } else { let lLen = list.length; for (let i = 0; i < lLen; i++) { let targetAggro = list[i].obj.aggro; //Maybe the aggro component has been removed? if (targetAggro) targetAggro.unIgnore(obj); } } let x = this.obj.x; let y = this.obj.y; //Find mobs in range let range = proxy ? aggro.cascadeRange : aggro.range; let inRange = this.physics.getArea(x - range, y - range, x + range, y + range, c => ( c.aggro && !c.dead && ( !c.player || !obj.player ) && c.aggro.willAutoAttack(obj) && !list.some(l => l.obj === c) )); if (!inRange.length) return; let iLen = inRange.length; for (let i = 0; i < iLen; i++) { let enemy = inRange[i]; if (!this.physics.hasLos(x, y, enemy.x, enemy.y)) continue; else if (enemy.aggro.tryEngage(obj)) aggro.tryEngage(enemy, 0); } }, canAttack: function (target) { let obj = this.obj; if (target === obj) return false; else if ((target.player) && (obj.player)) { let hasButcher = (obj.prophecies.hasProphecy('butcher')) && (target.prophecies.hasProphecy('butcher')); if ((!target.social.party) || (!obj.social.party)) return hasButcher; else if (target.social.partyLeaderId !== obj.social.partyLeaderId) return hasButcher; return false; } else if ((target.follower) && (target.follower.master.player) && (obj.player)) return false; else if (obj.player) return true; else if (target.aggro.faction !== obj.aggro.faction) return true; else if (!!target.player !== !!obj.player) return true; }, willAutoAttack: function (target) { if (this.obj === target) return false; let faction = target.aggro.faction; if (!faction || !this.faction) return false; let rep = this.obj.reputation; if (!rep) { let targetRep = target.reputation; if (!targetRep) return false; return (targetRep.getTier(this.faction) < 3); } return (rep.getTier(faction) < 3); }, ignore: function (obj) { this.ignoreList.spliceWhere(o => o === obj); this.ignoreList.push(obj); }, unIgnore: function (obj) { this.ignoreList.spliceWhere(o => o === obj); }, tryEngage: function (source, amount, threatMult = 1) { let obj = this.obj; //Don't aggro yourself, stupid if (source === obj) return; let result = { success: true }; obj.fireEvent('beforeAggro', result); if (!result.success) return false; //Mobs shouldn't aggro players that are too far from their home let mob = obj.mob || source.mob; if (mob) { let notMob = source.mob ? obj : source; if (!mob.canChase(notMob)) return false; } let oId = source.id; let list = this.list; amount = (amount || 0); let threat = (amount / obj.stats.values.hpMax) * threatMult; let exists = list.find(l => l.obj.id === oId); if (!exists) { exists = { obj: source, damage: 0, threat: 0 }; list.push(exists); //Cascade threat if (obj.mob) this.move(source); } exists.damage += amount; exists.threat += threat; if (exists.threat > this.threatCeiling) exists.threat = this.threatCeiling; return true; }, getFirstAttacker: function () { let first = this.list.find(l => ((l.obj.player) && (l.damage > 0))); if (first) return first.obj; return null; }, reset: function () { let list = this.list; let lLen = list.length; for (let i = 0; i < lLen; i++) { let l = list[i]; if (!l) { lLen--; continue; } //Maybe the aggro component was removed? let targetAggro = l.obj.aggro; if (targetAggro) { targetAggro.unAggro(this.obj); i--; lLen--; } } this.list = []; }, die: function () { this.reset(); }, unAggro: function (obj, amount) { let list = this.list; let lLen = list.length; for (let i = 0; i < lLen; i++) { let l = list[i]; if (l.obj !== obj) continue; if (!amount) { list.splice(i, 1); obj.aggro.unAggro(this.obj); break; } else { l.threat -= amount; if (l.threat <= 0) { list.splice(i, 1); obj.aggro.unAggro(this.obj); break; } } } if (list.length !== lLen && this.dieOnAggroClear) this.obj.destroyed = true; this.ignoreList.spliceWhere(o => o === obj); //Stuff like cocoons don't have spellbooks if (this.obj.spellbook) this.obj.spellbook.unregisterCallback(obj.id, true); if ((this.list.length === 0) && (this.obj.mob) && (!this.obj.follower)) this.obj.stats.resetHp(); }, sortThreat: function () { this.list.sort(function (a, b) { return (b.threat - a.threat); }); }, getHighest: function () { if (this.list.length === 0) return null; let list = this.list; let lLen = list.length; let highest = null; let closest = 99999; let thisObj = this.obj; let x = thisObj.x; let y = thisObj.y; for (let i = 0; i < lLen; i++) { let l = list[i]; let obj = l.obj; if (this.ignoreList.some(o => o === obj)) continue; if (!highest || l.threat > highest.threat) { highest = l; closest = Math.max(Math.abs(x - obj.x), Math.abs(y - obj.y)); } else if (l.threat === highest.threat && l.threat !== 0) { //Don't chase a closer target if both targets are at 0 threat because // this means that neither of them have attacked the target. // This stops people from griefing other players by pulling mobs to them. let distance = Math.max(Math.abs(x - obj.x), Math.abs(y - obj.y)); if (distance < closest) { highest = l; closest = distance; } } } if (highest) return highest.obj; return null; }, getFurthest: function () { let furthest = null; let distance = 0; let list = this.list; let lLen = list.length; let thisObj = this.obj; let x = thisObj.x; let y = thisObj.y; for (let i = 0; i < lLen; i++) { let l = list[i]; let obj = l.obj; if (this.ignoreList.some(o => o === obj)) continue; let oDistance = Math.max(Math.abs(x - obj.x), Math.abs(y - obj.y)); if (oDistance > distance) { furthest = l; distance = oDistance; } } return furthest.obj; }, getRandom: function () { let useList = this.list.filter(l => (!this.ignoreList.some(o => (o === l.obj)))); return useList[~~(Math.random() * useList.length)]; }, hasAggroOn: function (obj) { return ( this.list.find(l => l.obj === obj) && !this.ignoreList.find(l => l === obj) ); }, update: function () { let list = this.list; let lLen = list.length; for (let i = 0; i < lLen; i++) { let l = list[i]; if (l.obj.destroyed) { this.unAggro(l.obj); i--; lLen--; } else if (l.threat > 0) l.threat *= this.threatDecay; } }, clearIgnoreList: function () { this.ignoreList = []; }, isInCombat: function () { return this.list.length > 0; } };